diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27c7a32..042b1ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 10 env: + BILLING_TEST_MONGODB_URI: mongodb://127.0.0.1:27017/billing_test_ci TEST_MONGODB_URI: mongodb://127.0.0.1:27017/dependency_test PERFORMANCE_MONGO_URI: mongodb://127.0.0.1:27017/perf_test_ci PERFORMANCE_REDIS_PORT: 6379 @@ -53,3 +54,48 @@ jobs: - run: npm run build - run: npm audit --audit-level=low - run: npm test + + review-consent: + name: Review consent transactions + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - run: npx tsc --noEmit + - name: Start disposable MongoDB replica set + run: | + docker run -d --name review-consent-test --network host --memory=1g --cpus=1 --tmpfs /data/db:rw,noexec,nosuid,size=512m mongo:7.0@sha256:0e145625e78b94224d16222ff2609c4621ff6e2c390300e4e6bf698305596792 mongod --bind_ip 127.0.0.1 --port 28743 --replSet review-consent-test --oplogSize 16 + for attempt in $(seq 1 40); do + if docker exec review-consent-test mongosh --quiet --port 28743 --eval 'db.adminCommand({ping:1})' > /dev/null 2>&1; then break; fi + sleep 1 + done + docker exec review-consent-test mongosh --quiet --port 28743 --eval 'rs.initiate({_id:"review-consent-test",members:[{_id:0,host:"127.0.0.1:28743"}]})' + for attempt in $(seq 1 40); do + if docker exec review-consent-test mongosh --quiet --port 28743 --eval 'if (!db.hello().isWritablePrimary) quit(1)' > /dev/null 2>&1; then exit 0; fi + sleep 1 + done + exit 1 + - name: Verify consent authority, concurrency and replay cost + env: + TEST_REVIEW_CONSENT_MONGO: mongodb://127.0.0.1:28743/?replicaSet=review-consent-test + TEST_REVIEW_CONSENT_PERF_REPORT: review-consent-perf.json + TEST_REVIEW_COMPLETION_HTTP_PERF: review-completion-http-perf.json + TEST_REVIEW_COMPLETION_PERF_REPORT: review-completion-perf.json + TEST_REVIEW_CONSENT_HTTP_PERF: review-consent-http-perf.json + run: npx mocha test/review-owner-consent.test.js test/review-consent-http.test.js test/review-completion-http.test.js + - uses: actions/upload-artifact@v4 + with: + name: review-consent-performance + path: | + review-consent-perf.json + review-consent-http-perf.json + review-completion-perf.json + review-completion-http-perf.json + - name: Remove disposable database + if: always() + run: docker rm -f review-consent-test || true diff --git a/.gitignore b/.gitignore index b877c57..bbabd09 100644 --- a/.gitignore +++ b/.gitignore @@ -124,10 +124,12 @@ temp/ # GitHub App signing key /secrets/github-app.pem -/secrets/github-app-beta.pem +# Generated local benchmark reports +/benchmark-results/ + +/secrets/github-app-beta.pem # Local beta deployment credentials /.env.beta - # Isolated Go shadow secrets /.env.go-shadow diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..aad07ee --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,26 @@ +{ + "detector": { + "ignoreRules": [], + "ignoreFiles": [], + "ignoreValues": [ + { + "rule": "broken-image", + "value": "*", + "files": [ + "public/partials/anonymize.htm" + ], + "createdAt": "2026-11-02T06:04:45.274Z", + "reason": "Agent reviewed: existing avatar images use Vue :src bindings to safeUrl, not missing image sources." + }, + { + "rule": "broken-image", + "value": "*", + "files": [ + "public/partials/profile.htm" + ], + "createdAt": "2026-11-02T21:13:31.062Z", + "reason": "False positive: avatar uses Vue :src=\"safeUrl(user.photo)\" and v-if=\"user?.photo\", not a missing static image source." + } + ] + } +} diff --git a/Dockerfile b/Dockerfile index ed36b4f..7008d68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ COPY backend ./ RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /anonymous-server ./cmd/server FROM debian:bookworm-slim +LABEL science.4open.owner-alert-protocol="recipient-v2" RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* && useradd --uid 10001 --create-home anonymous WORKDIR /app COPY --from=build /anonymous-server /usr/local/bin/anonymous-server diff --git a/README.md b/README.md index 577d353..f90bd3b 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,20 @@ A free public instance is available at **[anonymous.4open.science](https://anony Just open **[anonymous.4open.science](https://anonymous.4open.science/)**, paste your repository URL, add the terms to hide, and share the generated link. +### Git submodules + +Direct public GitHub submodules are included in the file browser and ZIP downloads +at the commit recorded by the parent repository. Their paths and contents use the +parent's anonymization settings. `.gitmodules` files are omitted from shared GitHub +content because they contain source repository URLs. + +The service expands up to 20 direct submodules and 10,000 combined child tree entries. +Private, non-GitHub, nested, missing, oversized, and truncated submodules remain empty +folders with a warning. Child files are fetched anonymously on demand; child tree +lookups use GitHub's unauthenticated API quota. ZIP downloads fetch public child archives. +Existing cached repositories with `.gitmodules` rebuild their file trees on the next +listing or ZIP download. No manual database migration is required. + ### CLI Anonymize a repository locally and produce an anonymized zip: @@ -65,8 +79,8 @@ Local installation requires Node.js 22.15 or newer in the 22.x series, For the November 2026 dependency update, install from the committed lockfile with `npm ci`, rebuild with `npm run build`, and restart the API and streamer services together. Docker installations should rebuild the application image. -The Node dependency update alone does not change MongoDB documents, Redis keys, -or queue formats. Switching to Go requires the separate cutover procedure linked above. Existing credential +This dependency update does not change MongoDB documents, Redis keys, or queue +formats, so it requires no production data migration. Existing credential migration requirements still apply when upgrading from an older release. The [`glob`](https://github.com/advisories/GHSA-3g3m-2fjr-6pw4) and @@ -131,6 +145,96 @@ the [MongoDB replication guide](docs/mongodb-replication.md). +### Owner email alerts + +Set `RESEND_API_KEY` and `EMAIL_FROM` on the web server and download workers to +send access alerts through [Resend](https://resend.com/docs/api-reference/emails/send-email). +Use a sender address on a domain verified in Resend. `APP_HOSTNAME` sets the +host used for the connections link in emails. + +Registration saves a verified GitHub email address, preferring the primary address. +OAuth requests `user:email`; GitHub App sign-in uses the read-only email permission +described in the setup guide. Later sign-ins also fill missing addresses. Saved +addresses and custom alert addresses are preserved. GitHub privacy placeholder +addresses are not imported. + +Signed-in owners without an email address see an email form in a popup. +“Remind me later” dismisses it for the current login session; “Never” saves a +permanent preference on the account. Existing addresses are reused, and owners +can add or change their address on the connections page at any time. +Alerts cover invalid GitHub credentials and repository access errors encountered +while reading or refreshing content. No periodic access scan is performed. +Emails contain no repository names, anonymous URLs, or credentials. + +An atomic recipient claim allows one email attempt per normalized address across +repositories, pull requests, gists and accounts. Suppression has no time expiry. +Sign-in, reconnecting, successful access recovery, resource removal or expiration, +and disabling the original account retain suppression for that address. Inactive +resources cannot trigger new alerts. Changing to an address that has never been +notified permits its first alert. + +Failed deliveries and interrupted sends whose outcome is unknown also stay +blocked to avoid duplicate delivery. Unsent claims can be cancelled by confirmed recovery before dispatch. Bookkeeping failures +release claims when no provider request has started. Pending claims interrupted +before dispatch can be reclaimed after a 60-second lease during startup or a new +notification attempt. Claim identity and state checks prevent a paused sender +from dispatching after another instance reclaims its claim. Cancellation is saved +before cleanup; matching legacy fields are cleared before the guard is released. +Interrupted cleanup resumes on startup or a later notification. Legacy fields +carry an attempt ID so unrelated deliveries and newer generations are preserved. +A resource revision prevents delayed preparation from recreating a reclaimed +claim. All asynchronous preparation stays pending. A sending reservation is +reclaimable until dispatch commits atomically on the owner document, against its +reset revision and lease, immediately before contacting the provider. Cancellation +competes with that same commitment, so a paused sender cannot dispatch afterward. +Owner dispatch records distinguish known-unsent reservations from interrupted +committed sends. Completed records are removed; expired uncommitted records are +pruned on later attempts. Persistent reset revisions also fence attempts created +while a reset is in progress, even when no recipient guard existed at its start. +Recovery recorded during an active reset is retried when reset finishes or its +lease expires, with the original source and revision checks. +Provider idempotency keys +and redacted attempt records support auditing without storing recipient addresses +or repository names in the audit log. Records include owner and resource IDs, +resource kind, generation and a categorized access failure to identify the +trigger without exposing the address. Attempt records are saved before dispatch, +with completion recorded separately. Existing resource claims are migrated to +recipient guards before new application instances serve requests using an indexed +scan with resumable checkpoints. Other instances wait while its lease is healthy, +and take over if the lease expires. +Every existing resource claim is retained behind the recipient guard. Recovery +marks claims recovered without authorizing another email. Completed provider +results are recorded before guard updates, with bounded retries and reconciliation +on startup or a later notification. The upgrade restores missing recipient guards +from immutable provider completion records, including guards previously released +by recovery or resource removal. This history scan uses an index and resumable +checkpoints and does not send email. Its `recipient-alerts-v2` database marker is +independent of the v1 marker, so cold starts also restore missing suppression. +Resource lifecycle mutations advance the owner dispatch fence and block new +attempts until the status write completes. Concurrent mutations keep separate +reservations. Failed or ambiguous status writes retain suppression for that +owner; run the offline reconciliation command below after stopping all writers +to clear interrupted reservations safely. + +Use `scripts/deploy.sh` when upgrading recipient alert suppression. The new image +has the `science.4open.owner-alert-protocol=recipient-v2` label. When running +application containers have an older or missing protocol label, the script builds +the replacement, stops and verifies all old API and streamer containers, then prepares a full claim +reconciliation and starts the new streamers and API. Stopped legacy containers +from a rollback also trigger reconciliation, so claims written after an earlier +migration are included. This path also runs when retrying with an already-built +image and `FORCE=0`. Once writers are stopped, preparation releases all known +unsent pending claims and clears their matching legacy fields before backfill. +This upgrade pauses traffic while claims migrate and allows up to 15 +minutes for startup, configurable with `MIGRATION_HEALTH_TIMEOUT`. Subsequent +deployments keep the usual rolling streamer replacement. Manual upgrades must +stop all application writers, run +`docker compose run --rm --no-deps --entrypoint node anonymous_github ./build/scripts/reconcile-legacy-owner-alerts.js` +using the new image, and then start the new application instances. Run this +preparation after legacy writers have operated, even if their containers were +already removed. Ordinary restarts preserve confirmed recoveries. +Leaving either Resend setting empty disables delivery and the email prompt. + ## Scope of anonymization In double-anonymous review, the boundary of anonymization is **the paper plus its online appendix — and only that**. Googling part of the paper or appendix to reveal authorship is considered a deliberate attempt to break anonymity ([explanation](https://www.monperrus.net/martin/open-science-double-blind)). @@ -174,3 +278,7 @@ diagram is encountered. These libraries use hashed URLs and load once per tab. The Docker build compiles the same bundles and copies them, the asset manifest, and document-worker assets into the runtime image. The Compose app serves these assets directly; no frontend development server is needed. + +### Paid hosting + +Optional Stripe billing supports monthly and annual author subscriptions, repository review passes, and prepaid conference packages. Billing is disabled by default. See the [setup and rollout guide](docs/paid-hosting.md) for prices, capacities, webhooks, migration behavior, and test-mode verification. diff --git a/docs/github-app-setup.md b/docs/github-app-setup.md index 5000a73..0aa41e2 100644 --- a/docs/github-app-setup.md +++ b/docs/github-app-setup.md @@ -19,7 +19,10 @@ Configure: - Repository permissions: **Contents: read-only**, **Metadata: read-only**, **Pull requests: read-only**, **Pages: read-only**. Do not grant write permissions. Pull requests read also permits [reading PR issue comments](https://docs.github.com/en/rest/issues/comments#list-issue-comments); Issues permission is unnecessary. -- Keep user access token expiration enabled. Private email permission is unnecessary. +- User permissions: **Email addresses: read-only** to save a verified address at + sign-in, including addresses hidden from the public GitHub profile. If this + permission is unavailable, owners can enter an address in the email form. +- Keep user access token expiration enabled. - Leave **Request user authorization (OAuth) during installation** unchecked. Anonymous GitHub authorizes the user before opening installation; the setup redirect must remain available afterward. @@ -105,6 +108,14 @@ Public reads use a [repository-scoped App user token](https://docs.github.com/en Each API request renews through its own repository binding, rechecking identity and public visibility while keeping the same user quota. Existing installation bindings retain their installation checks. + +If a previously public source becomes private, its existing public-only binding +continues to reject private reads. Select the repository in the App installation, +then open the anonymization's Edit page and click **Repair GitHub access**. This +verifies installation access to the same GitHub repository ID and published commit +before replacing the binding. It preserves the anonymous URL, snapshot and settings. +Signing in again alone does not change a resource's binding. The Connections page +also provides **Reconnect read-only access** for this purpose. This uses GitHub's documented [public resource access for App user tokens](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). The explicit **Use existing OAuth access** choice handles repositories not yet diff --git a/docs/markdown-compatibility.md b/docs/markdown-compatibility.md index f2ab732..6bbe2f4 100644 --- a/docs/markdown-compatibility.md +++ b/docs/markdown-compatibility.md @@ -17,6 +17,10 @@ GitHub service features such as user mentions, issue-status badges, commit refer The sanitizer runs on all Markdown-produced HTML. Math and diagram renderers insert their own generated markup through unpredictable, per-document markers after sanitization. TeX runs with `trust: false`; Mermaid runs with `securityLevel: strict`. Source HTML cannot opt into this bypass. Task inputs are always disabled checkboxes. -Run `npm run build:ui` before `npx mocha test/markdown-compatibility.test.js test/markdown-sanitization.test.js test/mermaid-security.test.js test/asset-build.test.js`. The browser tests exercise the built bundle, not a copy of the renderer. +Run `npm run build:ui` before `npx mocha test/markdown-compatibility.test.js test/markdown-sanitization.test.js test/markdown-security.test.js test/mermaid-security.test.js test/asset-build.test.js`. The browser tests exercise the built bundle, not a copy of the renderer. References: [GitHub basic syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax), [math](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions), [diagrams](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams), and the [GFM specification](https://github.github.com/gfm/). + +Diagram inputs are limited to one million characters and 60,000 coordinates. Maps also enforce nesting and TopoJSON expansion limits before conversion. Rejected diagrams display as code. + +Mermaid, DOMPurify and KaTeX are npm dependencies so dependency audits cover their versions. After upgrading them, run `npm run vendor:markdown` followed by `npm run build:ui` and commit the refreshed assets. Security tests require the vendored files to match the installed packages. diff --git a/docs/paid-hosting.md b/docs/paid-hosting.md new file mode 100644 index 0000000..3ce64f4 --- /dev/null +++ b/docs/paid-hosting.md @@ -0,0 +1,271 @@ +# Paid hosting + +Billing is opt-in. `BILLING_ENABLED=false` preserves self-hosted behavior. This implementation uses Stripe Checkout and the customer portal. No card details are stored in the application. + +## Offers + +Configure four EUR prices in Stripe. The amounts below are suggested launch prices, not hard-coded charges: + +- Author monthly: €5/month, recurring monthly. +- Author annual: €49/year, recurring yearly. +- Author review pass: €15 once, one existing repository for 180 days from checkout creation. +- Conference review package: €199 once, up to 100 repositories during a fixed review window of at most 180 days. + +Checkout uses the configured Stripe Price ID. The settings page displays its actual amount and cadence. Do not configure trials, metered prices, tiered prices, adjustable quantities, or subscription changes to unrelated products in the customer portal. New subscriptions need a paid invoice before access is granted. A failed renewal does not extend paid time. Cancellation at period end keeps access until that period ends; immediate cancellation ends new paid usage immediately. + +Only repository hosting is covered. Gists and pull requests retain their existing behavior. Lab plans are not included. + +Default capacities, all measured in source-file bytes: + +- Free account: 5 repositories, 2 GiB total. +- Author subscription: 20 repositories, 10 GiB total, and never-expiring repositories while subscribed. +- Repository pass: 1 repository, 2 GiB. +- Conference: 100 repositories, 50 GiB total. + +These are admission limits, not a measure or guarantee of physical disk consumption. Original files, anonymized caches, metadata and backups need additional capacity. Review production usage before enabling billing. File-size safeguards and request throttles still apply to paid users. Reviewers do not need accounts or payment. + +## Configuration + +Set these environment variables in your deployment's secret/configuration manager: + +```text +BILLING_ENABLED=true +BILLING_ORIGIN=https://anonymous.example.org +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +STRIPE_AUTHOR_MONTHLY_PRICE= +STRIPE_AUTHOR_ANNUAL_PRICE= +STRIPE_AUTHOR_PASS_PRICE= +STRIPE_CONFERENCE_PRICE= +BILLING_AUTOMATIC_TAX=false +``` + +Price IDs can be omitted to hide offers. Use test-mode keys and prices for staging. `BILLING_ORIGIN` must be the application's canonical HTTPS origin; HTTP is accepted only for localhost development. Redirects never use request-supplied hosts or return URLs. Enable and configure Stripe Tax before setting `BILLING_AUTOMATIC_TAX=true`; otherwise configure the appropriate tax treatment in Stripe before launch. Receipts, seller details and portal settings also need configuration in Stripe. + +Capacity overrides are positive integers: + +```text +FREE_AUTHOR_REPOSITORIES=5 +FREE_AUTHOR_STORAGE=2147483648 +PAID_AUTHOR_REPOSITORIES=20 +PAID_AUTHOR_STORAGE=10737418240 +PAID_PASS_STORAGE=2147483648 +PAID_CONFERENCE_REPOSITORIES=100 +PAID_CONFERENCE_STORAGE=53687091200 +``` + +Changing these values changes the allowance of existing plans too. Keep existing Price IDs configured for as long as subscriptions using them remain active. Plan and conference review-window changes are intentionally locked in this first version. + +Configure the Stripe customer portal for payment-method updates, invoice downloads and cancellation. Disable arbitrary subscription product switches. Configure a snapshot webhook at `/api/billing/webhook` using API version `2026-10-28.endive`, matching the installed Stripe SDK, for: + +- `checkout.session.completed` +- `checkout.session.async_payment_succeeded` +- `customer.subscription.created` +- `customer.subscription.updated` +- `customer.subscription.deleted` +- `customer.subscription.paused` +- `customer.subscription.resumed` +- `invoice.paid` +- `invoice.payment_failed` +- `charge.refunded` +- `charge.dispute.created` + +The webhook is mounted before JSON parsing so Stripe verifies the original request bytes. Failed durable receipt returns 500 for Stripe retries. Processing runs asynchronously from the stored event inbox. Duplicate deliveries do not extend passes or allocate duplicate grants. Subscription events retrieve current Stripe state and use a database revision check to prevent stale concurrent writes. Full refunds and disputes suspend the affected paid grant; partial refunds preserve access; reinstatement after a resolved dispute requires operator review. + +## Lifecycle and migration + +New paid conferences remain `pending_payment` until a verified checkout activates them. Collect billing addresses in Stripe. Historic conference plans remain editable and keep their existing status, but are no longer offered to new conferences. There is no automatic charge for historical estimated usage. + +Existing repositories are not deleted or hidden on rollout or subscription expiry. New repositories are marked `billingManaged`; their file trees must fit an allowance before publication. A repository that fails this check remains available in its owner's dashboard for upgrading and retrying. Buy a pass by entering that repository's anonymized ID in Settings. Automatic refresh pauses for billing-managed repositories on the free allowance, including after paid access expires. Owners can still request a refresh within that allowance. Existing review links retain their configured expiration dates. This is not a guarantee of perpetual hosting. + +Allowances are selected in order: paid conference, repository pass, author subscription, free account. MongoDB quota ledgers atomically reserve count and source-file bytes. They initially count existing repositories using stored size metadata. Reservations never shrink during refresh, so a concurrent or failed build cannot release capacity still in use. Confirmed removed repositories release reservations on the next admission. Expired repositories and orphan reservations remain reserved until operator reconciliation. This deliberately favors conservative capacity accounting over overselling. The account usage display uses reserved capacity once a ledger exists. A moved repository can retain conservative reservations in its previous scope until removal. + +For billing-managed repositories, on-demand recovery and ZIP downloads from truncated trees are rejected with `repository_tree_incomplete`. This prevents serving unaccounted files beyond the indexed tree. A complete source tree is required; refresh after reducing repository size or excluded content upstream. + +Billing records are in separate `billingaccounts`, `billinggrants`, `billingquotas` and `billinglocks` collections. They are not serialized on anonymous repository pages. Account deletion cancels recurring subscriptions and expires any known open checkout before deleting content. Minimal provider IDs and financial records remain for reconciliation. Do not disable billing configuration while active subscriptions still need webhook processing or cancellation. + +## Verification and operation + +Run focused policy and MongoDB tests against a disposable database: + +```sh +npx mocha test/billing.test.js +BILLING_TEST_MONGODB_URI=mongodb://127.0.0.1:27017/billing_test_paid npx mocha test/billing-integration.test.js +npm run test:ui +npm run build +``` + +The integration suite deletes data in its test collections and requires a database name beginning with `billing_test_`. It covers concurrent admission, duplicate fulfillment, customer binding, refunds, subscription cancellation, failed renewal, stale event writes and conference activation. It uses a fake Stripe client and real MongoDB; it does not charge cards. + +Before production rollout, run a Stripe test-mode purchase for each configured price, renewal with a test clock, failed renewal, cancellation, refund and webhook retry. Confirm currency, tax behavior, invoice details, capacities and backups. Inspect webhook delivery failures in Stripe. The in-app Refresh payment status action reconciles the latest checkout and known subscriptions; it is not a substitute for webhooks. Failed checkout requests preserve their idempotency key so retrying the same plan recovers the session. + +Back up billing collections with the rest of MongoDB. Watch source bytes, physical storage, bandwidth and request rates separately. Stripe fees, taxes and refunds reduce gross revenue. + + +### Coauthors and the free allowance + +Only repositories owned by an account consume its allowance. Joining another author's repository does not consume a slot, and coauthors do not need subscriptions. A coauthor's own subscription does not increase the owner's allowance. Edits and refreshes use the owner's allowance, or the repository's paid conference/pass allowance. If that allowance is full, a collaborator's attempt to refresh or enlarge the repository can also be blocked. Existing review links remain available. + +The free defaults are 5 repositories and 2 GiB, revised from the initial 10-repository proposal after the November 2, 2026 production audit. The audit counted ready, unexpired repositories; stored sizes were estimates rather than a fresh recount. Paid author subscriptions retain 20 repositories, 10 GiB and automatic refresh. + +### Never-expiring repositories + +Monthly and annual author subscribers can choose Never expire. This requires the repository owner's subscription even when a conference pays its storage allowance. A coauthor subscription or a one-time review pass does not unlock it. + +Newly selected Never expire repositories store a billing deadline 90 days after the paid-through date. The five-minute maintenance job extends that deadline on renewal and changes overdue repositories to ordinary removal expiration. Cancellation does not shorten the paid-through period. Revocation does not extend a previously recorded deadline. Renewal during the grace period preserves the repository; after expiration, the author must renew and explicitly reactivate it. + +Legacy never-expiring repositories have no billing deadline and remain unchanged. Disabling billing disables this reconciliation. Free users can choose a dated expiration within one year and extend it manually. Never expire does not exempt a repository from quotas, abuse controls, or service availability. + +Real sandbox validation and repeatable API test instructions are recorded in [Stripe sandbox validation](stripe-sandbox-validation.md). + +## Billing recovery and operation + +`BILLING_CHECKOUT_ENABLED=false` stops new purchases while webhooks, reconciliation, +the customer portal, and cancellation remain available. Keep `BILLING_ENABLED=true` +and the existing Stripe price IDs configured while subscriptions exist. + +Verified webhook routing references are durably inserted into `billingevents` before +HTTP acknowledgement. A ten-second worker claims events with a five-minute lease, +retries failures with exponential backoff, and marks an event failed after 12 failed +processing attempts. Repeated delivery cannot create another inbox record. A worker +that dies after fulfillment can replay the operation without allocating another grant. +Completed envelopes are removed after 90 days; failed events remain for investigation. +The inbox deliberately omits customer names, addresses, card data, and full payloads. +Do not register Connect events on this application's platform webhook. + +Every five minutes, a separate job selects up to ten due billing accounts. Each account +is normally reconciled daily; failures retry after 15 minutes. Reconciliation paginates +subscriptions, checkout sessions, and charges, including historical purchases absent +from MongoDB. It also reconstructs confirmed paid periods from invoice lines when the +latest invoice failed. This full customer-history scan favors recovery at the current +scale. Measure Stripe request volume before shortening the daily interval. + +Before the 90-day deadline triggers expiration, maintenance performs a fresh owner +reconciliation. Provider or database failures defer that expiration and log an error. +Legacy never-expiring repositories remain excluded. The recorded grace period does not +restart during retries. Ordinary repository requests continue using local entitlements. + +Checkout retries retain their key and exact creation parameters. Sessions have an +explicit expiry. After 23 hours, unresolved requests are searched by purchase-attempt +metadata before any retry. After 24 hours, an unmatched attempt can be replaced only if +its immutable request contains an expired session deadline. Older records without that +proof require operator review. Uncertain customer creation also stops after 23 hours; +customer discovery can recover the mapping without creating another customer. + +Partial refunds preserve access. Full refunds and disputes suspend the affected grant. +`billingrevocations` records the payment decision even if its grant does not exist yet. +A dispute after a reviewed refund requires review again. Reinstatement is an explicit operator +decision; it does not create paid time or restart a subscription. Do not grant a refund +merely to cancel renewal. + +The existing application error log receives billing failures and five-minute health +warnings for failed events, old pending events, unresolved checkout attempts, and failed +reconciliation. These warnings also appear in the existing admin error viewer. External +email or paging delivery still requires deployment monitoring configuration. + +### Operator commands + +Run with the application's normal server configuration and database access. Inspection +is read-only; mutation commands require `BILLING_OPERATOR` and an audit reason. They +write an intent record to `billingaudits` before performing work. Inspect resulting +state after a command failure; an audit intent alone does not prove completion. + +```sh +node -r ts-node/register src/scripts/billing-operations.ts health +node -r ts-node/register src/scripts/billing-operations.ts inspect OWNER_ID +BILLING_OPERATOR=operator-name node -r ts-node/register src/scripts/billing-operations.ts reconcile OWNER_ID 'Investigating missing paid access' +BILLING_OPERATOR=operator-name node -r ts-node/register src/scripts/billing-operations.ts replay test:platform:evt_ID 'Retry after correcting configuration' +BILLING_OPERATOR=operator-name node -r ts-node/register src/scripts/billing-operations.ts drain all 'Process queued recovery work' +BILLING_OPERATOR=operator-name node -r ts-node/register src/scripts/billing-operations.ts discover all 'Recover customer mappings after restore' +BILLING_OPERATOR=operator-name node -r ts-node/register src/scripts/billing-operations.ts reinstate pi_ID 'Dispute resolved; access reinstatement approved' +``` + +Discovery binds only customers tagged for this application to existing users. Conflicting +customer mappings require investigation. `reinstate` reviews a payment reversal and +keeps a subscription suspended if other unreviewed payment reversals remain. Reconcile +the owner afterward. Restrict shell access to authorized operators; these are not public +HTTP endpoints. + +### Restore and launch checks + +Back up `billingaccounts`, `billinggrants`, `billingquotas`, `billinglocks`, `billingevents`, +`billingrevocations`, `billingprices`, and `billingaudits` with repository data. Test a restore into an +isolated environment with sandbox credentials. Keep destructive repository maintenance +stopped during restore, discover customer mappings, reconcile restored owners, drain +queued events, and inspect health before re-enabling normal scheduling. Never connect a +restored test copy to live Stripe credentials. + +Before launch, configure seller details, live prices, tax treatment, invoice information, +portal restrictions, production email delivery, backup retention, and external alerts. +These are deployment/business settings, not changes made by this implementation. Grace +period email reminders are not currently sent by the app. + +The recovery suite uses a disposable MongoDB database and simulated Stripe failures: + +```sh +BILLING_TEST_MONGODB_URI=mongodb://127.0.0.1:27029/billing_test_recovery npx mocha test/billing.test.js test/billing-integration.test.js test/billing-reliability.test.js +``` + +## Payment within the repository workflow + +Repository hosting choices now appear in the anonymization form when capacity or a +new Never expire selection requires them. Free dated submissions and coauthor work +keep their existing flow. Conference admission and final capacity are still checked +server-side; an account usage display cannot authorize or permanently block admission. +Existing repository owners can also buy a repository-scoped pass from that form. + +Before leaving for hosted Stripe Checkout, the app flushes pending field edits and +saves a private task draft. `/api/billing/draft` requires authentication and the billing +CSRF token. Drafts retain only the form fields needed to resume, are limited to 64 KiB, +and expire after seven days. Retrieval checks the owner and expiry even before MongoDB's +TTL sweep runs. Account deletion also removes these drafts. + +Checkout returns to the draft's validated anonymization route, or the conference +being purchased, rather than always returning to Settings. No request-supplied external +return URL is accepted. An open or uncertain checkout attempt cannot be reused for a +different task. Returning restores the draft, refreshes billing and capacity, and leaves +submission to the author. The return token is consumed after restoration so later +navigation cannot overwrite newer edits. Confirmation does not create a repository. + +The payment form remains Stripe-hosted. Embedded Checkout is not required for this +workflow and is not enabled by this change. Settings remain available for invoices +and subscription management. + +Validation: 40 billing tests with disposable MongoDB and 160 frontend tests passed. +Additional HTTP checks cover draft CSRF, authentication, ownership and no-store headers. +The built UI was checked at desktop and mobile widths using simulated payment responses; +redaction edits survived the checkout return. These workflow changes were not charged +through a fresh real Stripe sandbox payment or deployed to production. + +### Current application layout + +The anonymization page keeps the main configuration sections visible beside a live +README preview. Source controls retain the existing GitHub connection selector, +manage-access action and branch refresh button. Conference code is a visible optional +field. Hosting choices appear within configuration, with saved drafts preserved through +Stripe Checkout and return. + +Settings separates anonymization defaults, hosting and billing, and account actions. +The header Upgrade action sits beside the user menu and opens the plan comparison. +Active subscribers see subscription management instead of another purchase offer. +The donation entry remains available. + +Local browser checks used simulated payment responses for the final layout. The real +Stripe sandbox checks and their limitations are documented in +[stripe-sandbox-validation.md](stripe-sandbox-validation.md). + +### Resolved source-file storage + +Git LFS pointer sizes do not represent cached object sizes. Source-cache fills reserve +bytes beyond the tree size before writing them, in blocks of at most 1 MiB. The ledger +keeps per-path high-water marks so retries and concurrent fills do not charge twice. +Reservations remain conservative across generations and failed fills and require +operator reconciliation to release unused capacity. This also covers public raw +downloads and expanded submodule content. Shared-cache hits and legacy content promoted +to the shared cache also reserve their resolved size before serving. Legacy cache hits +outside the shared cache are not retroactively measured by this write-time guard. + +Accepted one-time prices are stored in `billingprices` when checkout starts and when +existing purchases are fulfilled. Hiding an offer by removing its configured price +does not remove this mapping or invalidate paid grants. Include the mapping in backups +so reconciliation can reconstruct grants for retired offers and process their refunds. diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md index 652f20e..7e828d2 100644 --- a/docs/performance-monitoring.md +++ b/docs/performance-monitoring.md @@ -89,274 +89,70 @@ invalidate them. Raw Markdown served after a rendering failure has a separate validator, so later requests retry rendering when the transient failure clears. +## Production failures and restarts + +Run `node scripts/check-production-logs.js 15` from a machine with Node and the +Docker CLI to summarize recent application logs and container states. For a +remote Docker host, use +`DOCKER_HOST=ssh://ubuntu@host node scripts/check-production-logs.js 15`. +The optional +window is 1–1440 minutes. The report includes completed 5xx responses, interrupted +requests, token-refresh failures, missing-file warnings, native unhandled stream +errors, and Docker restart counts. It prints counts and container metadata; +request URLs, repository IDs, file paths, credentials, and Docker environment +variables are excluded. Restart counts cover the container's lifetime, while log +counts cover the selected window. Compare restart counts with the last deployment +baseline even when every container is currently healthy. + +The API and streamers log `process_fatal_error` through Node's +`uncaughtExceptionMonitor`. This records fatal errors before the default process +exit; it does not resume execution or swallow uncaught exceptions. Native stderr +may describe the same crash as well, so fatal log indicators are not a count of +unique process exits. Use Docker restart counts to confirm exits. + +Legacy OAuth reset requests are coalesced by owner, credential, and OAuth app. +A failed 401/404 reset backs off for 30 minutes; transient failures back off for +one minute. A replaced credential or changed OAuth configuration gets a new key. +GitHub App and personal access tokens do not use the OAuth reset endpoint. Refresh +failure leaves the credential unchanged and continues through normal token +validation and fallback. + +Confirmed GitHub content 404s back off for 30 seconds in a bounded process-local +cache. The key includes repository, path, commit, blob SHA, cache generation, +revision, and credential. Permission failures and transport errors are not cached. +This reduces repeated upstream misses without changing the file tree or treating +an unavailable file as a successful cache fill. Persistent 404s still require +checking the repository's source snapshot. Application rate limits remain in +force; 429 replies now include `rate_limited`, `resetAt`, and `Retry-After` so +clients can wait for the actual store reset. + +## Large repository metadata + +GitHub tree preparation consumes pages of plain metadata and persists at most +500 entries per insert. Preparation does not read the finished tree back into +memory. Traversal rejects more than 100,000 queued folders with +`repository_tree_too_large` and HTTP 413. Folder listings and folder-count responses reject results above 10,000 +entries with `file_listing_too_large` and HTTP 413. Narrower folder reads and +the bounded filename search remain available. This limit also applies to +recursive internal listing calls. + +A preparation reset switches tree generation and retires the old file rows and +active path-index key atomically. It does not wait for metadata deletion before +enqueueing the replacement. Maintenance discovers these retirement markers and +retries interrupted deletion. Activation performs at most ten batches per +retired metadata collection before deferring the remainder to maintenance. + +Each metadata delete selects at most 500 IDs, rechecks its filter during the +write, and yields between batches. Expiration and removal also check lifecycle +ownership between batches. `file-tree` logs report deleted rows and elapsed +time as `metadata cleanup complete` or `metadata cleanup deferred`. + +Run `npm run migrate:performance` when upgrading to add the sparse +`retiredPathIndexKeys` maintenance index without dropping existing indexes. +The constrained-heap regression fixture can be run without network or MongoDB: +`node --max-old-space-size=256 test/fixtures/large-repository-tree.js`. + ## Shared blob cache -The optional shared filesystem cache stores immutable source content across projects. -It uses Node's built-in SQLite module and requires Node 22.15 or newer. All API and -streamer processes on one host must mount the same local cache directory. SQLite -WAL is not a distributed lock service: do not put this directory on NFS or share -it across hosts. S3 storage continues to use the existing storage implementation. - -Enable the integrated implementation with `SHARED_CACHE_ENABLED=true`. The default -is false so a code deployment does not silently migrate production data. The other -settings are independent rollback controls: - -- `SHARED_CACHE_PATH`: defaults to `.shared-cache-v1` inside `FOLDER`. -- `SHARED_CACHE_BYTES`: source pool budget, default 10 GiB. -- `SHARED_CACHE_RESULT_BYTES`: transformed output budget, default 512 MiB. -- `SHARED_CACHE_MAX_FILE_BYTES`: maximum source admission size, default 32 MiB. -- `SHARED_CACHE_FREE_BYTES`: free-device-space reserve, default 1 GiB. -- `SHARED_CACHE_LOW_WATER`: eviction target fraction, default 0.9. -- `SHARED_CACHE_GZIP`: store gzip when beneficial, default true. -- `SHARED_CACHE_GZIP_MIN_SAVING`: minimum fractional saving, default 0.10. -- `SHARED_CACHE_DECISIONS`: enable unchanged decisions and changed-output reuse, - default true. -- `CACHE_MONITORING_ENABLED`: collect cache observations, default true. Monitoring - is independent of shared storage and also records legacy cache/inspection events. - -These are initial limits, not capacity recommendations from the production sample. -Restart processes together when changing pool limits. Reserve headroom for SQLite, -filesystem directories, existing caches and other application temporary files. -The budget accounts for blob allocation with a conservative 4 KiB minimum plus -in-flight reservations of three times the maximum object size and a small overhead. -Keep each pool budget larger than this reservation. Admission can bypass the cache -while other fills/readers occupy the budget. Oversized source objects are streamed -without admission, still subject to the application's file-size limit. - -Git blobs are verified against the requested Git SHA before publication. Resolved -LFS content is admitted only after verifying both the Git pointer and its SHA-256 -payload digest and size. Gzip is a representation of the same uncompressed content. -Files are written privately, synced and atomically renamed before index publication. -Fills and readers have renewable leases. Eviction skips readers, and stale fills -are recovered before their reservations are released. LRU access timestamps are -coarsened to 30 seconds. Orphan reconciliation runs at most hourly across processes. -Alias and reuse-history indexes are bounded; decisions expire after seven days and -are capped at 100,000 entries. - -Existing generation caches are read as a migration fallback and verified before -promotion. They are not deleted during promotion, so rollback remains possible. -The new pool budget does not include legacy copies: monitor total filesystem usage -and retire legacy data only after validating rollout. Uploaded ZIP originals and -other durable source material are untouched. Repository removal invalidates access -and project mappings, while shared blobs remain subject to global eviction. - -The HTTP fast path applies to individual file responses backed by the shared source -pool, including small text files. Decisions include content identity, path, project -context, all transformation options, generation/revision, mask, hostname and engine -version. Unchanged decisions serve the original representation. Changed output has -its own bounded pool. Gzip negotiation honors quality values, uses `Vary`, and -preserves MIME type. File validators are weak when shared delivery is enabled so -identity and gzip representations can revalidate the same logical content. HEAD -continues to use metadata only. Range requests receive a complete response; this -implementation does not add random access into gzip files. Website rendering keeps -its renderer cache; ZIP scans do not admit bulk entries into the source blob pool. - -The admin overview's Cache section reads `/api/admin/cache?minutes=60` behind the -existing admin authorization. Windows are 15 minutes, one hour, 24 hours and seven -days. SQLite telemetry batches at most 512 groups every five seconds and retains -minute aggregates for 48 hours and hourly aggregates for 30 days. No paths, terms, -user IDs or blob hashes appear in metric labels. The UI distinguishes unavailable -telemetry, dropped events, disabled storage and empty windows. Rows separate file, -website, ZIP, background and known-monitor traffic. Monitor classification is an -operational heuristic, not a complete bot detector. - -Hit counts and byte hit rates measure source lookups; misses classify first observed -keys, known misses and eviction refetches within bounded seven-day history. Reuse -intervals are consecutive blob accesses, not distinct people or browser visits. -Eviction time measures idle age; codec and inspection timings are elapsed work time, -not CPU samples. Cached decisions are separate from actual inspections. Minute and -hour boundaries make the displayed windows approximate. - -Run the reproducible acceptance workload with: - -```sh -node scripts/cache-workload.js docs/cache-workload-results.json -npx mocha test/shared-blob-cache.test.js test/shared-cache-http.test.js -``` - -The JSON report asserts cross-project source reuse, unchanged-decision reuse, -changed-output reuse, gzip delivery and budget enforcement. Its compression ratio -comes from synthetic text and must not be presented as a production projection. -Production rollout should compare request/byte hit rates, eviction refetches, p95 -latency, upstream errors and free disk over a representative week, with large files, -ZIP work and known monitors visible separately. - -### Benefits and costs - -| Choice | Benefit | Cost or limit | -| --- | --- | --- | -| Global content identity | Reuses identical files across paths and projects; renames do not duplicate content | Requires verified hashes and authorization on every request; uses digest-prefix directories to avoid one huge directory | -| Byte budget with approximate LRU | Predictable blob footprint and automatic reclamation | Evicted files incur upstream fetches; active readers can temporarily prevent admission | -| Selective gzip | Reduces payload storage and can serve compressed bytes directly | First-fill compression costs work; identity consumers and uncached anonymization require decoding | -| Rule-aware decisions and results | Repeated unchanged reads skip regex inspection; changed output is reusable | More metadata and a separate output budget; settings changes produce new decisions | -| SQLite coordination and telemetry | Cross-process coordination without another service; admin can measure actual reuse and rewrite work | Local-filesystem scope; short synchronous transactions; metadata/headroom are outside blob budgets | - -### Rollout sequence - -1. Deploy with shared storage disabled and collect legacy hits, inspections and codec - timings. Keep known-monitor traffic separate from file readers. -2. Enable the shared cache on the single-host filesystem deployment with both byte - budgets configured. Keep legacy copies for rollback. -3. Compare a representative week of byte/request hits, rewrite decisions, eviction - refetches, request latency, upstream errors and whole-device free space. Adjust - admission size and budgets from those observations. -4. Retire legacy cache copies only after the rollback window. Uploaded originals - remain durable and are never candidates for shared-pool eviction. - -The implementation and acceptance workload are local changes; this work does not -enable or migrate production storage. - -### Production observations used for this design - -The read-only audit on 2026-11-02 found 425,312 source files across 31,522 projects, -containing 144.32 GB of payload. Deduplication within projects could remove 1.87 GB; -global deduplication could remove 2.93 GB total (about 2.03%), including another -1.06 GB across projects. One file could not be read. These are decimal GB and -payload savings, not a compression estimate or the expected new working-set size. - -The preceding 24-hour access sample contained 29,980 successful file-body responses, -including 9,249 repeat responses (30.9%). URL repetitions are not the same as blob -cache hits. A separate approximately two-hour metric window recorded 2,302 source -hits out of 3,600 lookups (63.9%). This supports retaining a bounded source cache; -it does not establish the optimal byte budget. - -In the uniform anonymization replay, 937 resident files were measured: 764 text, -173 binary, and 21 with changed output. Thirteen resolved files exceeded the 4 MiB -sample limit, and missing files/mappings were excluded. The sample's mean gzip-6 -work was 1.46 ms per file and mean decompression was 0.24 ms; these replay timings -are not live-request latency or a forecast for large files. Continuous telemetry is -needed to replace these samples with representative workload measurements. - -### Local acceptance evidence - -The checked-in `cache-workload-results.json` is generated by assertions, not hand -written. Five file requests across two projects needed one upstream source fill; -unchanged and changed responses reused decisions, and changed bytes occupied one -result blob. The 64 KiB eviction workload ended at 53,248 allocated bytes with no -remaining fill reservations. Compression figures describe this synthetic fixture. - -Validation also covers separate operating-system producers, failed/invalid fills, -crash publication recovery, active-reader eviction protection, LFS pointer/payload -verification, gzip negotiation, rule changes, admin authorization and HEAD/304. -The disposable MongoDB/Redis performance integration run passed 130 tests. The -admin Cache section was checked at 1280 px and 390 px, including filter layout, -scrollable tables, empty filters and unavailable telemetry. - -The final complete default test run recorded 1,062 passing, 159 pending and one -failure: `test/error-codes.test.js` reports nine missing frontend translations for -the pre-existing maintenance API changes. Cache tests passed in that full run. -TypeScript compilation, the scoped lint checks (existing warnings only), the UI -build and `git diff --check` passed. Production rollout was not performed. - -## Initial cache performance evaluation, 2026-11-02 - -The initial implementation passed functional checks, but its metadata write path regressed small-file performance. The following measurements are preserved as the before-optimization baseline; the implementation and follow-up results below supersede the recommendations in this section. These are local measurements, not production capacity estimates. - -### Warm changed-text responses - -Each percentile pools two batches. Serial measurements contain 32 requests; concurrency-eight measurements contain 64. The small/medium baseline reads a local raw source and repeats anonymization/gzip. The 2 MiB baseline additionally uses the existing transformed-file cache, with 99 measured cache hits out of 100 requests. Authentication and Redis coordination are outside this comparison. - -| File | Baseline median, ms | Current median, ms | Experimental NORMAL median, ms | Baseline p95 at 8 clients, ms | Current p95 at 8 clients, ms | NORMAL p95 at 8 clients, ms | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | -| 16 KiB | 8.3 | 44.9 | 6.9 | 71.8 | 462.1 | 46.8 | -| 256 KiB | 36.5 | 45.9 | 11.7 | 250.8 | 444.1 | 89.6 | -| 2 MiB | 236.2 | 60.5 | 26.0 | 929.2 | 528.2 | 324.8 | - -### Findings - -- The cache hit path performs synchronous SQLite writes for reader leases, reuse history and release. The temporary-database A/B changes only pool connections to `PRAGMA synchronous=NORMAL`; the measured reduction supports commit latency as a major bottleneck. This is a diagnostic experiment, not an implemented fix. -- NORMAL in WAL mode can lose recent committed transactions following power loss or a system crash. Validate recovery/accounting before changing the durability of disposable cache indexes. See [SQLite synchronous documentation](https://www.sqlite.org/pragma.html#pragma_synchronous). -- For changed 2 MiB content at eight clients, current shared caching reduced combined client/server CPU from 253.2 to 55.6 ms per request. Peak sampled RSS was 308.0 versus 296.7 MiB. These are whole-process values, including runtime, HTTP client and worker threads. -- Synthetic text transferred about 62.2% fewer bytes with gzip. Incompressible 256 KiB binary data was stored and served as identity, avoiding ineffective gzip storage. These compression ratios are not production projections. -- Cold responses include first-use initialization. For 16 KiB changed text they took 145.9 ms in the baseline, 709.8 ms currently, and 212.0 ms in the NORMAL experiment. Do not use this single cold sample as steady-state miss latency. -- At 100,000 synthetic blob rows, a single pool stats query took 78.7 ms p95 synchronously; saving a decision took 45.5 ms p95. Cache/materialize admin aggregates and reduce retention scans on request paths. Dummy blob rows have no files, so this measures metadata scaling, not full directory reconciliation. -- A 4 MiB pool cycling sequentially over 100 distinct 64 KiB objects stayed below budget, but all 300 reads fetched again. Its working set was larger than the pool. Reuse, request order and admission policy must inform the budget; LRU alone cannot guarantee hits. - -### Reproduce and limitations - -Node v24.14.1, Linux, Intel Xeon E312xx (Sandy Bridge, IBRS update), 12 logical CPUs. This is a shared host with background load, warm OS page cache, loopback HTTP and a synthetic 20 ms upstream wait. It excludes real upstream bandwidth, TLS, reverse proxies, database authorization, multi-process throughput and production request distributions. Small samples and fixed scenario order mean p95 and speedup ratios are descriptive. An initial ts-node exploration was superseded by the compiled results below. The metadata probe does no worker execution, and excludes TypeScript loading from its timed samples. - -```sh -npx tsc --incremental false -CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js -CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js docs/cache-performance-normal-results.json --normal -CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js docs/cache-performance-legacy-results.json --legacy -node scripts/cache-index-performance.js -``` - -All 2,700 HTTP responses were checked against expected bytes or a validated wire digest. Eviction assertions passed. Raw latency samples, CPU, RSS, event-loop delay, storage and metric counters are in `cache-performance-results.json`, `cache-performance-normal-results.json`, `cache-performance-legacy-results.json` and `cache-index-performance-results.json`. `cache-performance-summary.json` contains pooled comparisons. - -Before rollout: reduce per-read durable commits without weakening active-reader eviction protection; consider NORMAL only for rebuildable indexes with crash/recovery tests; replace full admin stats scans with cached aggregates; then replay a representative mixed workload with several server processes. Application defaults and production were not changed by this evaluation. - - -## Optimized implementation, 2026-11-02 - -The pool now uses NORMAL WAL commits for reader leases, coarsened access history, -anonymization decisions and telemetry. Admission reservations, publication intent -before rename, and final blob/alias publication still use FULL commits. Payload -fsync remains in place. Reader leases still become visible immediately to other -processes and protect active streams from eviction. This saves hit-path disk -flushes without making payload accounting depend on disposable history. - -Recent disposable metadata and telemetry can be lost after system failure. -Reservation and publication recovery conservatively retain accounting until files -are indexed or removed; missing payload files are invalidated and fetched again. -The SIGKILL test stops an actual producer after rename and before index publication, -then verifies reservation retention, orphan cleanup and successful refill. This -tests process failure, not a physical power cut. - -Admin pool statistics now read transactional counters and an indexed oldest-access -lookup. Counters are initialized once for an existing index and maintained by -SQLite triggers in the same transactions as row changes. Decision/history/alias -retention deletes only excess rows through indexes. Expired decision/history -cleanup is limited to 256 rows each per admission or maintenance pass. Access -history persists at most once per key per 30 seconds, so reuse-window boundaries -have that resolution. Metadata remains capped at 100,000 decisions/history rows -and 200,000 aliases per pool. - -Stop all cache-using processes together before upgrading an existing index, then -restart them on the same version. Do not mix old and new writers during migration. -The admin API response shape is unchanged. Shared caching remains opt-in with -`SHARED_CACHE_ENABLED=true`; production configuration was not changed. - -The tradeoff is intentional: cache hits avoid durable flushes, while cold fills -still pay for durable publication and compression. Trigger updates add a small -amount of write work in exchange for constant-time aggregate reads. Approximate -LRU and fixed budgets can still thrash when the working set exceeds the pool. - - -### Follow-up measurements and validation - -All 2,200 responses in the complete optimized HTTP run and all 300 in the focused -repeat matched expected output. The repeat ran after this task's other tests and -benchmarks ended. The early scenarios in the complete run overlapped the metadata -probe, so both results are retained rather than treating the faster run as a -universal estimate. These remain small synthetic samples on a shared host. - -- 16 KiB changed text: warm serial median 44.9 → 7.3 ms; eight-client p95 462.1 → 114.9 ms. -- 256 KiB changed text: warm serial median 45.9 → 9.0 ms; eight-client p95 444.1 → 107.6 ms. -- 2048 KiB changed text: warm serial median 60.5 → 19.0 ms; eight-client p95 528.2 → 216.2 ms. - -At 100,000 synthetic blob rows, admin stats p95 fell from 78.70 to 0.50 ms, -decision-write p95 from 45.47 to 1.11 ms, and warm-source-hit p95 from 82.99 to -6.37 ms. The 4 MiB eviction probe peaked at 3,997,696 allocated bytes; all budget -assertions passed. Its oversized sequential working set still produced no hits. - -The final full suite recorded 1,065 passing, 159 pending and the existing single -error-code translation failure (nine maintenance API codes). New counter, -durability-mode and SIGKILL recovery tests passed, as did HTTP and admin UI -regressions. TypeScript, scoped lint and diff whitespace checks passed. -Production rollout and representative multi-process throughput testing remain -outside this local evaluation. - -Reproduce the follow-up without overwriting the original baselines: - -```sh -npx tsc -CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js docs/cache-performance-optimized-results.json -node scripts/cache-index-performance.js docs/cache-index-performance-optimized-results.json -CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js docs/cache-performance-optimized-repeat-results.json --shared -``` - -Raw results and the pooled comparison are in the corresponding -`cache-*-optimized-*.json` files. +See [Shared blob cache](shared-cache.md) for configuration, storage and quota +accounting, admin metrics, production observations, rollout and benchmark commands. diff --git a/docs/review-callback-handoff.md b/docs/review-callback-handoff.md new file mode 100644 index 0000000..bccba2e --- /dev/null +++ b/docs/review-callback-handoff.md @@ -0,0 +1,13 @@ +# Returning owner consent to review + +An optional callback registry maps the authenticated intent's client and callback IDs to one exact HTTPS URL. Configuration rejects credentials, queries, fragments, noncanonical paths, duplicate IDs and duplicate JSON fields. It permits at most 32 entries and a 4 KiB form-action policy. Callback selection never uses a browser-supplied address. + +After recording consent, the browser can request `POST /api/review-consent/handoff` with its signed ticket. The existing transport requires the current authenticated session, same origin and a session-bound CSRF token. The backend resolves the signed intent's callback before issuing a completion code and checks the current repository authority and saved consent. The transport reloads the session before returning the result. A missing registry leaves this endpoint unavailable. + +The page submits the completion envelope as the sole `completion` field in a native HTML POST form. Codes never enter URL parameters or browser storage. The page's form-action policy allows only the configured destinations and its own origin. It removes the temporary form when CSP blocks submission and shows a retry/status message. A late result after an account change cannot submit a form. The signed ticket stays in a controller closure for a retry; it is not submitted to the callback. + +The initial cross-site POST does not carry a review session cookie configured with SameSite=Strict. The review receiver must serve a non-caching landing page, then use a same-origin authenticated request and saved initiating-browser state before exchanging the code. Receiving the POST alone must not establish a binding. That receiver and startup activation are separate unfinished work. This change does not mount the upstream consent router or enable an integration. + +Validation covers callback configuration, current-session HTTP access, concurrent logout, signed callback scope, consent requirements, browser POST contents, account changes and malformed completions. The disposable MongoDB suite contains 23 passing tests. The local 100-replay binding benchmark measured median 2.20 ms and p95 2.69 ms on a one-CPU, 1 GiB container; this excludes provider traffic. Browser fixture results use synthetic HTTP backend responses and test TLS certificates, so they do not establish provider or certificate-verification behavior. + +The full local suite passed 762 tests, with 74 pending tests for optional fixtures. The targeted browser/page suite passed 17 tests, registry/HTTP tests passed 16 with one optional performance case pending, and TypeScript, targeted lint and the UI build passed. Chromium at 1280 and 320 pixels completed the native cross-site POST with no referrer or Strict cookie, no overflow, no external requests, no script errors and no WCAG A/AA violations. A third case blocked the destination with CSP and verified zero callback deliveries and removal of the code form. The first browser harness waited for a navigation that CSP intentionally blocked; changing the assertion to inspect the current document completed this case without changing application code. diff --git a/docs/review-completion-exchange.md b/docs/review-completion-exchange.md new file mode 100644 index 0000000..6337d5a --- /dev/null +++ b/docs/review-completion-exchange.md @@ -0,0 +1,15 @@ +# Completion codes and binding receipts + +The owner consent backend now saves the authenticated review intent with its immutable consent receipt. Existing consent records that lack this scope cannot issue completion codes. No caller may supply replacement submission, entitlement or policy fields. + +`completion(actor, ticket)` requires the same current browser account and session, a matching confirmed consent and current repository authority. It issues one code per intent, expiring at the earliest of five minutes, intent expiry or retention expiry. Repeating issuance returns the same code without extending its lifetime. The database stores a random nonce and a code hash. A domain-separated HMAC using the private consent signing key makes the code recoverable after response loss without storing it in plaintext. Retain that key for the code lifetime; changing it fails closed for outstanding issuance retries. + +`exchange(authenticatedClientId, command)` requires a separately verified service client. The argument is an internal authority boundary, not authentication by itself. First exchange checks the code, expiry, current account and repository authority, then atomically inserts a pending binding, a client-scoped request key and the original receipt. A later write failure rolls all three back. Competing transactions may report unavailable; callers explicitly retry the original command. The backend never automatically repeats a transaction. + +Exact authenticated exchanges recover the original receipt after response loss, code expiry or later owner changes. They cannot create another binding. Changed codes under the committed request ID conflict, a spent code cannot be used with another request ID, and an existing request ID cannot bind another intent. `receipt(authenticatedClientId, intentId)` provides post-expiry reconciliation without accepting an expired code for first use. Both operations remain client scoped and expose only protocol identifiers and policy. They never return source coordinates, account IDs or repository IDs. + +Bindings remain pending. No snapshot, artifact access, retained bytes or entitlement fulfillment is claimed. Browser completion POSTs, callback registration, service HTTP authentication, private configuration, startup wiring and activation remain separate tasks. The existing browser confirmation response intentionally does not expose the completion code yet. + +Receipt recovery uses majority reads outside transactions. The factory limits active authority transactions and service reads to four each. MongoDB queries have three-second limits, majority writes have a three-second acknowledgment timeout and commits have a five-second limit. The serving connection must also have bounded selection/socket/pool waits. Secrets and encoded completion payloads must never be logged. + +Tests use a disposable local MongoDB replica set and cover current ownership, absent consent, cross-session and cross-client attempts, altered codes, expiry, explicit replay, concurrent exchange, request-key reuse, rollback and persistence without raw tokens or codes. Set `TEST_REVIEW_CONSENT_MONGO` to the disposable replica-set URI and run `npx mocha test/review-owner-consent.test.js test/review-consent-http.test.js`. `TEST_REVIEW_COMPLETION_PERF_REPORT` records 100 immutable receipt replays; it excludes browser, TLS and provider latency. diff --git a/docs/review-completion-http.md b/docs/review-completion-http.md new file mode 100644 index 0000000..c6c108b --- /dev/null +++ b/docs/review-completion-http.md @@ -0,0 +1,15 @@ +# Completion service transport + +`createReviewCompletionService` serves the exact `POST /service/v1/completions/exchange` and `GET /service/v1/intents/{intentId}/receipt` routes when explicitly mounted before body parsing and browser authentication. Startup does not mount it. An absent scoped registry returns typed 404 responses without touching the backend. + +The private registry has exactly `version: 1` and `keys`. Each key contains `clientId`, `keyId`, `tokenSHA256`, `notBefore`, `notAfter` and `scopes`. Identifiers, ASCII-token hashing and whole-second validity windows follow the capabilities registry. Each key must explicitly authorize `completion.exchange`, `receipt.read`, or both. The existing capabilities-only configuration is rejected because it has no operation scopes. Limits are 64 KiB, 64 keys, 32 clients and 16 keys per client. Duplicate fields, key identities, digests and scope entries are rejected. + +Requests use `X-4open-Artifact-Client-Id`, `X-4open-Artifact-Service-Key-Id` and `Authorization: Bearer `. TLS is required, directly or through explicitly trusted proxy configuration. Browser cookies, origin/referrer/fetch metadata, duplicate headers, encoded bodies, query strings and route aliases are rejected. Authorization precedes body reads. Constant-time digest comparison and the key window are checked, with validity rechecked before backend execution and before returning a result. + +The transport accepts at most 64 KiB of strict JSON. The shared decoder rejects invalid UTF-8, duplicate decoded keys and excessive nesting. Commands have exactly the v1 exchange fields. A successful receipt must contain exactly the expected client/intent scope and v1 receipt fields. Errors contain a fixed protocol code and fresh random correlation ID, never provider or database diagnostics. Responses disable caching, referrers and CORS. Rejections close the connection. + +A client is limited to 60 requests per minute across its rotation keys. At most four operations run concurrently. Each request has a 15-second deadline. A disconnected or timed-out caller does not release an in-progress backend slot until the backend settles. A write may commit after response loss; callers must retain and explicitly retry their original request. No automatic retry occurs here. + +Tests use real loopback HTTP, synthetic trusted-proxy TLS metadata and a simulated backend. They do not measure incoming TLS, MongoDB or provider latency. Set `TEST_REVIEW_COMPLETION_HTTP_PERF` to capture 50 authenticated receipt reads. The opt-in review-repository interoperability test also passes against this actual TypeScript handler over certificate-verified HTTPS. It covers explicit replay, changed-request conflicts, post-expiry receipt reads, policy mismatch, scope separation and untrusted certificates. Its backend remains synthetic. Credentials, startup wiring, browser callback handoff and activation remain unfinished; capabilities still advertise no available integration features. + +The dedicated CI MongoDB fixture uses one CPU, a 1 GiB memory cap and a 512 MiB temporary data mount. The earlier 256 MiB mount failed during replica-set initialization with a no-space error; the increased fixture is still isolated and removed after testing. This does not change production storage or resource limits. diff --git a/docs/review-consent-http.md b/docs/review-consent-http.md new file mode 100644 index 0000000..3415727 --- /dev/null +++ b/docs/review-consent-http.md @@ -0,0 +1,21 @@ +# Browser transport for review consent + +`createReviewConsentRouter` exposes the consent backend through three routes when explicitly mounted at `/api/review-consent`: `GET /csrf`, `POST /preview` and `POST /confirm`. Application startup does not mount this router. Browser UI, private configuration, startup ordering and activation remain unfinished. + +The factory requires a canonical HTTPS origin, the consent backend and a dedicated private 32-byte CSRF key shared by serving processes. Mount it after Passport/session authentication and before any JSON parser. The current startup ordering must change before integration; mounting after a parser fails closed. Transport must be HTTPS directly or through the application's explicitly trusted proxy configuration. + +Identity comes from Passport's authenticated account and a fresh lookup in the session store. Neither request JSON nor a bearer credential can supply it. POSTs require the exact configured Origin and a constant-time-checked CSRF token bound to the account and session. CSRF tokens are derived with a protocol-specific HMAC; issuing one never saves a session. A late CSRF response therefore cannot recreate a session destroyed by a concurrent logout. Authority is reloaded after backend work before returning a preview or confirmation response. + +Requests use exact paths without queries, strict methods, closed JSON fields and literal access/retention acceptance. Cross-origin or cross-site context, bearer authentication, duplicate security headers, content encoding and unexpected content types are rejected. The CSRF GET accepts no body. JSON is limited to 12 KiB; tickets to 8 KiB. A 15-second total deadline closes incomplete requests, and browser disconnects cancel pending preview consumption. Authenticated requests are limited to 60 per account per minute with at most 1,024 live counters. + +Responses disable caching and referrers. Confirmations include only the request ID, accepted policy and confirmation time. Internal account/repository identifiers and provider/database errors are omitted. A confirmation already accepted by the backend can commit despite a lost response; the UI must preserve the same ticket and request ID for explicit recovery. This transport does not create artifact bindings or bypass final review-side authority checks. + +## Validation + +The tests use real Express and express-session middleware with a disposable in-memory session store, a synthetic backend and loopback HTTP representing the trusted TLS proxy boundary. They do not measure real MongoDB or provider latency. TLS verification of the service client and MongoDB authority transactions are tested separately. + +Coverage includes session-derived identity, CSRF replay across sessions, cross-origin/bearer rejection, bounded closed payloads, exact routes, session revocation during preview, no session resurrection during a CSRF/logout race, response redaction, fixed errors, rate limits, unauthorized incomplete uploads, browser cancellation and the authenticated 15-second slow-upload deadline. The optional benchmark performs 50 fresh loopback HTTP preview requests. The CI database job runs these HTTP tests alongside the consent transaction tests and retains both performance reports. + +The final local targeted run passes 12 cases in 16 seconds. Transport-only timings are mean 4.94 ms, median 4.51 ms and p95 7.26 ms. TypeScript and targeted lint pass. + +The final local broader suite passes 723 cases with 63 opt-in/environment-dependent cases pending. The separate CI replica-set job exercises the otherwise skipped consent database tests as well as this transport suite. diff --git a/docs/review-consent-ui.md b/docs/review-consent-ui.md new file mode 100644 index 0000000..7de25eb --- /dev/null +++ b/docs/review-consent-ui.md @@ -0,0 +1,13 @@ +# Artifact consent page + +`/review-link` serves a separate page without the ordinary site's third-party widget or profile header. Its content security policy allows only first-party scripts and connections. The page rejects URL queries and noncanonical aliases, disables caching and referrers, and accepts only filename-shaped manifest entries. + +The handoff fragment contains exactly `clientId`, `intentId` and `token`. The application removes it from the current history entry before starting application requests or the router. Intent credentials and signed preview tickets stay in a controller closure. They are not rendered or written to browser storage. Reloading loses the handoff; users must reopen a new link from their submission. Signed-out users sign in before reopening the link. + +Owners enter the anonymous repository ID and request a preview. The backend supplies the repository name, access policy, retention deadline and expiry. Confirmation requires two separate unchecked approvals. A failed confirmation keeps the same signed ticket and random request ID for an explicit retry. Repository changes are disabled while that confirmation is uncertain. Account changes and page disposal cancel requests and discard late results. Success says that consent is recorded and that artifact linking remains incomplete. + +The page does not mount the consent API or advertise available integration capabilities. Startup configuration, final binding exchange, provider activation and a review-side link launcher remain unfinished. The consent HTTP router still needs to be mounted after authentication and before body parsing. + +Validation uses the compiled frontend bundles. Targeted tests cover fragment removal, storage, approval flags, replay identity, departure during CSRF, account changes during preview, invalid handoffs and policies, endpoint unavailability, expiry, sign-in and mismatched receipts. Server tests check canonical paths, headers and unsafe manifests. Chromium checks at 1280 and 320 pixels exercise the full synthetic form flow with CSP enabled and check overflow, external requests, script errors and WCAG A/AA rules. No live database or provider request is involved. + +Run `npm run build:ui`, then `npx mocha test/review-consent-page.test.js test/review-consent-ui.test.js`. Set `TEST_REVIEW_PAGE_PERF_REPORT` to a private output path to measure 100 isolated HTML responses over loopback HTTP. This benchmark excludes browser rendering, database operations and provider latency. diff --git a/docs/review-intent-client.md b/docs/review-intent-client.md new file mode 100644 index 0000000..8c2fcd6 --- /dev/null +++ b/docs/review-intent-client.md @@ -0,0 +1,20 @@ +# Review intent consumption client + +`src/server/service/review-intent-client.ts` consumes a saved review-side link intent over authenticated HTTPS. The caller supplies a trusted, exact HTTPS origin, a client-bound service credential with a validity window, and an explicit request ID. The returned policy and opaque identifiers come from the authenticated review response. Browser-supplied access, retention and callback fields are rejected. + +The factory returns only `consume`; it copies credential values into a closure. A call makes one POST to `/service/v1/artifact-intents/consume`, validates the peer certificate with Node's trust store, and sends no browser cookies or origin. Redirects and automatic retries are disabled. An uncertain result requires the caller to retain the same request ID for an explicit retry. + +Limits are four active attempts, a ten-second total deadline, five-second inactivity timeout, 16 KiB response headers and 64 KiB body. An AbortSignal cancels the attempt. Invalid credentials or payloads fail before I/O. Responses must match the configured client and requested intent, use the v1 closed schema and valid future timestamps, and omit cookies and content encoding. Duplicate JSON keys, including escaped spellings, invalid UTF-8 and unknown fields fail. Errors expose only a fixed category; remote bodies and credentials are not included. + +This module is not wired into a route or configured in production. It does not authenticate an artifact owner, obtain consent, create a binding, exchange a completion code, or establish browser callback authority. Those are separate pending tasks. Credential distribution and upstream deployment also remain pending. + +## Validation + +Local tests use a disposable certificate and loopback HTTPS server. A child process loads that certificate through `NODE_EXTRA_CA_CERTS` at startup; an independent child with ordinary trust rejects it before sending credentials. No TLS bypass is used. Tests cover successful policy reads, copied configuration, request-ID replay, rejection before I/O, status classification without retry, malformed and oversized responses, duplicate keys, four-slot saturation and recovery, cancellation, credential expiry, inactivity and slow-stream total deadlines. + +- `npx mocha test/review-intent-client.test.js`: 11 behavioral cases; one performance case is opt-in. +- `npm test`: 712 pass, 50 pending opt-in or environment-dependent cases. +- Full TypeScript check and targeted ESLint pass. +- Opt-in 100-request performance run: fresh loopback TLS connections, mean 12.36 ms, median 12 ms, p95 15 ms. These are local synthetic timings, not an external-service latency promise. + +Run the measured test with `TEST_REVIEW_INTENT_PERF=1 TEST_REVIEW_INTENT_PERF_REPORT=/tmp/review-intent-perf.json npx mocha test/review-intent-client.test.js`. diff --git a/docs/review-owner-consent.md b/docs/review-owner-consent.md new file mode 100644 index 0000000..d323a8a --- /dev/null +++ b/docs/review-owner-consent.md @@ -0,0 +1,23 @@ +# Review owner consent backend + +`createReviewOwnerConsent` prepares a signed policy preview from the authenticated review intent consumer and records explicit access/retention acceptance in MongoDB. It is an unwired backend. Browser routes, CSRF protection, UI, completion-code exchange and activation remain unfinished. Capabilities continue to advertise no available integration features. + +The caller must provide an account ID and session ID derived from a current authenticated browser session. Never populate this principal from request JSON or treat a service credential as an owner's identity. The factory also needs the configured intent consumer, a dedicated random 32-byte signing key held outside Git, and a connected MongoDB replica set. Configure bounded server-selection and socket timeouts on that connection. Tests use MongoDB 7 with a three-second server-selection timeout and ten-second socket timeout. + +Preview checks the current account and repository before consuming the review intent, then checks them again afterward. The network request holds no MongoDB transaction or authority lock. The account must be active or have the legacy unset active status; the repository must be ready and unexpired. Ownership uses the current account ObjectId. Coauthors must match the account's current numeric GitHub ID. Legacy username-only coauthors and unrelated administrators cannot provide consent. + +The signed preview binds the saved intent and exact policy to the account, hashed session ID and repository ObjectId. Changing the account, session, repository or policy invalidates confirmation. The signing context is specific to this protocol; key rotation invalidates outstanding previews. Confirmation requires both access and retention acceptance and an explicit 32-hex request ID. Preview and replay expire with the intent or retention policy. + +Confirmation writes one immutable `review_owner_consents` record per client/intent. It records the account, repository, authority role, exact policy, request ID, confirmation time and a hash of the signed preview. It stores no raw session ID, intent token, service credential or preview ticket. Explicit retries return the same receipt only after current authority checks; another request ID or repository conflicts. No binding is created by accepting a policy. + +Transactions touch the current account and repository before writing or reading the receipt. Ownership transfer, coauthor removal and account disablement therefore serialize with confirmation. The private `reviewConsentRevision` fields are excluded from ordinary Mongoose reads. Four transactions may be active per factory. Database commands and commits have limits; transactions are not automatically retried. A conflict or uncertain commit returns a fixed `unavailable` error, and the caller must retain the same ticket and request ID for explicit recovery. + +The final binding flow must still revalidate the initiating review session and saved intent scope with the review service. This backend proves current upstream owner/coauthor consent, not continuing review-side author authority, immutable snapshots, restricted file access or preservation. Receipt retention/export and the HTTP adapter remain separate work; this change enables no provider or public route. + +## Validation + +The opt-in suite runs against a disposable MongoDB replica set. It covers literal acceptance, immutable replay, stable-ID coauthors, administrator/username rejection, ticket tampering, account/session binding, disabled accounts, ownership changes, removed coauthors, archived/expired repositories, permission changes during upstream I/O, concurrent confirmations, expiry, second-repository conflicts and authority-field privacy. A held ownership-change transaction forces a real write conflict; the explicit retry is denied and no receipt is created. + +Run `TEST_REVIEW_CONSENT_MONGO='mongodb://127.0.0.1:28743/?replicaSet=review-consent-test' npx mocha test/review-owner-consent.test.js`. Set `TEST_REVIEW_CONSENT_PERF_REPORT=/tmp/review-consent-perf.json` to include 100 durable receipt replays. The CI job starts and removes its own pinned MongoDB image and preserves the performance report. Local timings are synthetic and are not a production latency guarantee. + +Local validation passes 12 targeted cases with the benchmark enabled, 723 full-suite cases with 51 opt-in/environment-dependent cases pending, the full TypeScript check and targeted lint. The final 100-replay run measured mean 12.85 ms, median 12.82 ms and p95 14.89 ms against a one-CPU, 512 MiB disposable database. An earlier performance attempt returned `unavailable` while the privacy fixture was automatically building indexes. The log does not establish the underlying database error. The final fixture disables automatic schema/index creation and creates its collections explicitly. diff --git a/docs/review-service-capabilities.md b/docs/review-service-capabilities.md new file mode 100644 index 0000000..b2997c4 --- /dev/null +++ b/docs/review-service-capabilities.md @@ -0,0 +1,84 @@ +# Review service capabilities + +`GET /service/v1/capabilities` is an optional server-to-server endpoint for the +`4open.artifacts/1` contract. It returns the authenticated client ID, the supported +contract version, and an empty `available` list. Linking, snapshots, protected +files, retention, publication, entitlements and usage are not advertised. Clients +must decline those workflows until their required capabilities are implemented. + +The endpoint is mounted before JSON parsing, browser sessions and passport. The +entire `/service` namespace returns a typed 404 when configuration is absent. +Only the exact GET path is accepted. Query strings, browser context headers, +cookies, bodies, duplicate authentication headers and unknown paths are rejected. +Rejections close the connection immediately, including requests whose declared +body has not arrived. Responses contain no credentials, owner identity, repository data, cookies or CORS +permission; they have `no-store` and `no-referrer` headers. No database, queue or +provider call is involved. + +## Private configuration + +Leave `REVIEW_SERVICE_KEYS` unset until activation is approved. If configured, +its value is a JSON object with exactly `version: 1` and `keys`. Each key has +exactly these fields: + +- `clientId`: 32 lowercase hexadecimal characters. +- `keyId`: 1–64 ASCII letters, digits, underscores or hyphens. +- `tokenSHA256`: SHA-256 of the **ASCII 64-character lowercase hexadecimal bearer + token**, stored as 64 lowercase hexadecimal characters. Do not hash the decoded + token bytes and do not store a plaintext token in this registration. +- `notBefore` and `notAfter`: UTC timestamps, `YYYY-MM-DDTHH:mm:ssZ`, in 2000–2099. + The start is inclusive and the end exclusive. + +Use private runtime configuration outside Git. The caller sends +`Authorization: Bearer `, `X-4open-Artifact-Client-Id` and +`X-4open-Artifact-Service-Key-Id`. Keys grant only capabilities reads. Generate a +separate token for this operation and environment; never reuse GitHub credentials, +webhook secrets or future exchange credentials. The Go review client requires +HTTPS with a trusted server certificate; a deployment must provide that transport. + +The registry is bounded to 64 KiB, 64 keys, 32 clients and 16 keys per client. +Duplicate client/key pairs and reused digests are rejected. Unknown fields, +plaintext secret fields, invalid timestamps and empty configuration fail startup +with a generic diagnostic. Configuration uses ordinary JSON object parsing. +Credentials are copied into a private registry and compared by constant-time +SHA-256 digest equality. Overlapping validity windows permit rotation. Remove a +key and restart to revoke it; this is not hot reloading. + +Authenticated clients share 60 reads per minute across their keys. Counters are +bounded by registered clients, reset lazily, and have no timer. Limits are local +to each server process; they are not a distributed quota. A 429 response supplies +a typed failure and a bounded retry delay. This endpoint does not accept browser +session or repository-owner authority. + +## Verification + +`mocha --no-config --no-package test/review-capabilities.test.js` passes eight +HTTP/configuration scenarios, including 60 concurrent reads, cross-client and +wrong-key rejection, raw duplicate headers, exact path checks, browser context, +expiry, overlapping keys and restart revocation. Add +`RUN_REVIEW_SERVICE_BENCHMARK=1` for a 150-request synthetic loopback measurement. +The clean-install run averaged 2.035 ms, median 1.732 ms and p95 2.417 ms with fresh +HTTP connections. These are local tooling measurements, not production latency. + +A local HTTPS interoperability rehearsal uses the existing Go +`artifactclient.NewCapabilitiesReader` at review source `ee07d06`. It verifies +certificate trust, accepts the empty capability response, rejects both unsupported +access policies and rejects a wrong credential. No external provider is contacted. + +A clean `npm ci` install, the full TypeScript check and emitted build, UI build, +and targeted ESLint pass. The full test suite reports 700 passing in 36 seconds +and 49 pending opt-in cases; those skipped cases are not claimed as tested. +The capability suite separately passes eight cases including its benchmark. + +An initial check used dependencies copied from another checkout and produced +TypeScript diagnostics. Those installed versions differed from the lockfile; +reinstalling the locked dependencies resolved every diagnostic without source +changes. Use the lockfile when reproducing these results. + +The endpoint is implemented and tested locally. Upstream deployment, private key +distribution, ownership consent and the remaining artifact workflows are pending. + +A raw-socket regression first reproduced a rejected request waiting for its unread +10 MB body. Closing failed connections makes the same header-only request return +400 and close immediately; the complete eight-case endpoint suite and full +TypeScript checking pass. Full PR CI is repeated for this correction. diff --git a/docs/shared-cache.md b/docs/shared-cache.md new file mode 100644 index 0000000..c07e7bf --- /dev/null +++ b/docs/shared-cache.md @@ -0,0 +1,210 @@ +# Shared blob cache + +The optional shared filesystem cache stores immutable source content across projects. +It uses Node's built-in SQLite module and requires Node 22.15 or newer. All API and +streamer processes on one host must mount the same local cache directory. SQLite +WAL is not a distributed lock service: do not put this directory on NFS or share +it across hosts. S3 storage continues to use the existing storage implementation. + +Enable the integrated implementation with `SHARED_CACHE_ENABLED=true`. The default +is false so a code deployment does not silently migrate production data. The other +settings are independent rollback controls: + +- `SHARED_CACHE_PATH`: defaults to `.shared-cache-v1` inside `FOLDER`. +- `SHARED_CACHE_BYTES`: source pool budget, default 10 GiB. +- `SHARED_CACHE_RESULT_BYTES`: transformed output budget, default 512 MiB. +- `SHARED_CACHE_MAX_FILE_BYTES`: maximum source admission size, default 32 MiB. +- `SHARED_CACHE_FREE_BYTES`: free-device-space reserve, default 1 GiB. +- `SHARED_CACHE_LOW_WATER`: eviction target fraction, default 0.9. +- `SHARED_CACHE_GZIP`: store gzip when beneficial, default true. +- `SHARED_CACHE_GZIP_MIN_SAVING`: minimum fractional saving, default 0.10. +- `SHARED_CACHE_DECISIONS`: enable unchanged decisions and changed-output reuse, + default true. +- `CACHE_MONITORING_ENABLED`: collect cache observations, default true. Monitoring + is independent of shared storage and also records legacy cache/inspection events. + +Logical user quotas remain based on repository file sizes. Shared-pool deduplication, +compression and eviction do not change user quota usage. + +These are initial limits, not capacity recommendations from the production sample. +Restart processes together when changing pool limits. Reserve headroom for SQLite, +filesystem directories, existing caches and other application temporary files. +The budget accounts for blob allocation with a conservative 4 KiB minimum plus +in-flight reservations of three times the maximum object size and a small overhead. +A shared SQLite ledger reserves physical headroom across source and result pools +before producers write bytes. Its check uses a fresh free-space sample under a +cross-process lock and includes all outstanding reservations. Reservations remain +until cleanup or crash recovery completes. This is conservative while fills have +already written some bytes, and cannot reserve space against unrelated applications. +Keep each pool budget larger than this reservation. Admission can bypass the cache +while other fills/readers occupy the budget. Oversized source objects are streamed +without admission, still subject to the application's file-size limit. + +Git blobs are verified against the requested Git SHA before publication. Resolved +LFS content is admitted only after verifying both the Git pointer and its SHA-256 +payload digest and size. Gzip is a representation of the same uncompressed content. +Files are written privately, synced and atomically renamed before index publication. +Fills and readers have renewable leases. Eviction skips readers, and stale fills +are recovered before their reservations are released. LRU access timestamps are +coarsened to 30 seconds. Orphan reconciliation runs at most hourly across processes. +Alias and reuse-history indexes are bounded; decisions expire after seven days and +are capped at 100,000 entries. + +Existing generation caches are read as a migration fallback and verified before +promotion. They are not deleted during promotion, so rollback remains possible. +The new pool budget does not include legacy copies: monitor total filesystem usage +and retire legacy data only after validating rollout. Uploaded ZIP originals and +other durable source material are untouched. Repository removal invalidates access +and project mappings, while shared blobs remain subject to global eviction. + +Transient SQLite write contention bypasses caching instead of failing file requests. +Reader heartbeats retry contention within their last confirmed lease window and +terminate only if renewal cannot succeed before expiry or the lease is removed. +Admission-bypass streams retain the minimum expected-size check. +Publication installs a reader lease before releasing its transaction, including +duplicate blobs. Decisions retain sniffed text classification for consistent MIME +headers on repeat reads; binary responses omit the range-denial header. + +Shared result production and delivery use the same repository lifecycle checks as +the legacy transformed cache. Client disconnects cancel the active transformer and +its worker signal, releasing spool reservations. Completed evictions are committed +even when pinned readers still prevent a new admission. + +The HTTP fast path applies to individual file responses backed by the shared source +pool, including small text files. Decisions include content identity, path, project +context, all transformation options, generation/revision, mask, hostname and engine +version. Unchanged decisions serve the original representation. Changed output has +its own bounded pool. Gzip negotiation honors quality values, uses `Vary`, and +preserves MIME type. File validators are weak when shared delivery is enabled so +identity and gzip representations can revalidate the same logical content. HEAD +continues to use metadata only. Range requests receive a complete response; this +implementation does not add random access into gzip files. Website rendering keeps +its renderer cache; ZIP scans do not admit bulk entries into the source blob pool. + +The admin overview's Cache section reads `/api/admin/cache?minutes=60` behind the +existing admin authorization. Windows are 15 minutes, one hour, 24 hours and seven +days. SQLite telemetry batches at most 512 groups every five seconds and retains +minute aggregates for 48 hours and hourly aggregates for 30 days. No paths, terms, +user IDs or blob hashes appear in metric labels. The UI distinguishes unavailable +telemetry, dropped events, disabled storage and empty windows. Rows separate file, +website, ZIP, background and known-monitor traffic. Monitor classification is an +operational heuristic, not a complete bot detector. + +Hit counts and byte hit rates measure source lookups; misses classify first observed +keys, known misses and eviction refetches within bounded seven-day history. Reuse +intervals are consecutive blob accesses, not distinct people or browser visits. +Eviction time measures idle age; codec and inspection timings are elapsed work time, +not CPU samples. Cached decisions are separate from actual inspections. Minute and +hour boundaries make the displayed windows approximate. + +Run the reproducible acceptance workload with: + +```sh +mkdir -p benchmark-results/cache +node scripts/cache-workload.js benchmark-results/cache/workload.json +npx mocha test/shared-blob-cache.test.js test/shared-cache-http.test.js +``` + +The JSON report asserts cross-project source reuse, unchanged-decision reuse, +changed-output reuse, gzip delivery and budget enforcement. Its compression ratio +comes from synthetic text and must not be presented as a production projection. +Production rollout should compare request/byte hit rates, eviction refetches, p95 +latency, upstream errors and free disk over a representative week, with large files, +ZIP work and known monitors visible separately. + +### Benefits and costs + +| Choice | Benefit | Cost or limit | +| --- | --- | --- | +| Global content identity | Reuses identical files across paths and projects; renames do not duplicate content | Requires verified hashes and authorization on every request; uses digest-prefix directories to avoid one huge directory | +| Byte budget with approximate LRU | Predictable blob footprint and automatic reclamation | Evicted files incur upstream fetches; active readers can temporarily prevent admission | +| Selective gzip | Reduces payload storage and can serve compressed bytes directly | First-fill compression costs work; identity consumers and uncached anonymization require decoding | +| Rule-aware decisions and results | Repeated unchanged reads skip regex inspection; changed output is reusable | More metadata and a separate output budget; settings changes produce new decisions | +| SQLite coordination and telemetry | Cross-process coordination without another service; admin can measure actual reuse and rewrite work | Local-filesystem scope; short synchronous transactions; metadata/headroom are outside blob budgets | + +### Rollout sequence + +1. Deploy with shared storage disabled and collect legacy hits, inspections and codec + timings. Keep known-monitor traffic separate from file readers. +2. Enable the shared cache on the single-host filesystem deployment with both byte + budgets configured. Keep legacy copies for rollback. +3. Compare a representative week of byte/request hits, rewrite decisions, eviction + refetches, request latency, upstream errors and whole-device free space. Adjust + admission size and budgets from those observations. +4. Retire legacy cache copies only after the rollback window. Uploaded originals + remain durable and are never candidates for shared-pool eviction. + +The implementation and acceptance workload are local changes; this work does not +enable or migrate production storage. + +### Production observations used for this design + +The read-only audit on 2026-11-02 found 425,312 source files across 31,522 projects, +containing 144.32 GB of payload. Deduplication within projects could remove 1.87 GB; +global deduplication could remove 2.93 GB total (about 2.03%), including another +1.06 GB across projects. One file could not be read. These are decimal GB and +payload savings, not a compression estimate or the expected new working-set size. + +The preceding 24-hour access sample contained 29,980 successful file-body responses, +including 9,249 repeat responses (30.9%). URL repetitions are not the same as blob +cache hits. A separate approximately two-hour metric window recorded 2,302 source +hits out of 3,600 lookups (63.9%). This supports retaining a bounded source cache; +it does not establish the optimal byte budget. + +In the uniform anonymization replay, 937 resident files were measured: 764 text, +173 binary, and 21 with changed output. Thirteen resolved files exceeded the 4 MiB +sample limit, and missing files/mappings were excluded. The sample's mean gzip-6 +work was 1.46 ms per file and mean decompression was 0.24 ms; these replay timings +are not live-request latency or a forecast for large files. Continuous telemetry is +needed to replace these samples with representative workload measurements. + +### Metadata durability and accounting + +Reader leases, access history, decisions and telemetry use NORMAL WAL commits. +Reservations, publication intent and final blob publication use FULL commits; +payloads are synced before rename. Recent disposable metadata may be lost after +system failure. Recovery retains reservations until orphan files are removed or +accounted for. SIGKILL recovery is tested; physical power failure is not simulated. + +Admin statistics use transactional counters and an indexed oldest-access lookup. +History and decisions are capped at 100,000 rows each, aliases at 200,000. Expired +history/decision cleanup removes at most 256 rows per pass. Access history has +30-second resolution. Stop all cache processes before upgrading an existing index +and restart them on the same version; do not mix old and new writers. +`scripts/deploy.sh` coordinates restarts when existing containers have shared caching +enabled. For first activation or an explicit budget/configuration change, run +`COORDINATED_RESTART=1 bash scripts/deploy.sh`. It stops and verifies every old +application writer before starting replacements. This briefly interrupts service. + +### Performance and validation + +Local synthetic measurements on Node 24.14.1, dated 2026-11-02, compared the initial +FULL-commit implementation with optimized bookkeeping. Warm serial changed-text +medians fell from 44.9 to 7.3 ms at 16 KiB, 45.9 to 9.0 ms at 256 KiB, and 60.5 to +19.0 ms at 2 MiB. At 100,000 synthetic blob rows, admin stats p95 fell from 78.7 to +0.50 ms and decision-write p95 from 45.5 to 1.11 ms. + +The optimized runs verified 2,500 HTTP responses. The 4 MiB eviction probe peaked +at 3,997,696 allocated bytes. Its oversized sequential working set had no hits: +a bounded LRU cache does not guarantee useful reuse. Measurements used loopback +HTTP, warm OS caches and a shared host; they exclude production traffic, TLS, +authorization overhead and multi-process throughput. They are not capacity estimates. + +On the PR checkout based on main at d0b217a, the production build, scoped lint and +199 cache, HTTP, admin access, HEAD/revalidation and Vue UI tests passed. Coverage +includes process crash recovery, concurrent fills, active readers, LFS verification, +and public submodule origins without parent credentials. Admin layout was also +checked at 1280 px and 390 px during implementation. + +Generate fresh reports locally; generated output is ignored by Git: + +```sh +npx tsc +node scripts/cache-index-performance.js +CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js +CACHE_BENCH_COMPILED=1 node scripts/cache-performance.js benchmark-results/cache/repeat.json --shared +``` + +Default reports go to `benchmark-results/cache/`. Scripts also accept an explicit +output path. Keep raw measurements with the evaluation that produced them rather +than committing generated reports to the documentation directory. diff --git a/docs/stripe-sandbox-validation.md b/docs/stripe-sandbox-validation.md new file mode 100644 index 0000000..218c967 --- /dev/null +++ b/docs/stripe-sandbox-validation.md @@ -0,0 +1,104 @@ +# Stripe sandbox validation + +Validated November 2, 2026 using a real Stripe sandbox, disposable local MongoDB, Stripe-hosted Checkout and the customer portal. The Stripe CLI forwarded real signed events to the application's raw webhook route. + +## Results + +The repeatable API suite passed 16 scenarios: + +- Checkout creation, reuse, unpaid-access denial and session expiry for all four plans. +- Successful monthly and annual subscription payments and author entitlements. +- Duplicate subscription prevention for both recurring plans. +- Scheduled cancellation preserves the paid period. +- Immediate cancellation removes paid capacity; never-expiring repositories retain the recorded grace deadline and enter expiration after it. +- A declined initial payment grants no subscription access. +- Subscription refunds revoke access. Repeated signed events are idempotent; altered payloads fail signature verification; later subscription sync does not undo revocation. +- A Stripe test clock advances a monthly subscription through successful renewal, failed renewal and recovery. Only paid invoices advance paid-through time. +- Account deletion cancels its subscription. + +Interactive checks also passed: + +- A repository pass purchased through hosted Checkout activated through a real webhook. +- A conference purchase changed pending payment to ready through a real webhook. +- Monthly Checkout displayed an insufficient-funds decline without granting access, then activated after retry with a successful test card. +- Annual Checkout granted no access after failed 3-D Secure authentication and activated after successful authentication. +- Refunding the repository pass revoked its grant through a real webhook. +- The customer portal showed billing details and invoices and scheduled cancellation at the paid-period boundary. +- A disputed sandbox subscription payment triggered a real dispute webhook, revoked access and remained revoked after resync. + +The local billing regression suite passed 23 tests, including quota concurrency, coauthor accounting, authorization/CSRF, grace periods and signature checks. TypeScript and targeted lint checks passed. + +## Fixes found by real Stripe testing + +1. Open subscription Checkout sessions do not yet have a subscription ID. Fulfillment now ignores unfinished sessions instead of reporting an invalid purchase. +2. The customer portal can schedule cancellation using cancel_at while cancel_at_period_end remains false. The app now recognizes either representation when displaying cancelled renewal. + +## Repeating the API checks + +Create a private JSON file outside the repository with a secret field containing a sandbox secret key. The script rejects live keys and verifies Stripe reports sandbox mode before creating fixtures. + +Run against a disposable MongoDB database whose name begins with billing_test_: + +```sh +STRIPE_SANDBOX_CREDENTIALS=/absolute/path/to/private-credentials.json \ +BILLING_TEST_MONGODB_URI=mongodb://127.0.0.1:27029/billing_test_stripe \ +node scripts/test-stripe-sandbox.js +``` + +The script creates tagged test customers, one product and four test prices. It deletes its customers and test clocks and deactivates its prices/product afterward. Stripe retains transaction history. It does not change existing prices or production settings. Test price amounts are fixtures, not a published price commitment. + +## Limits of this validation + +- This covers the implemented card-payment flows, not every Stripe payment method or account configuration. +- Automatic tax was disabled. Tax registration, live tax calculation, live settlement, seller details and production email delivery were not tested. +- The test clock changes Stripe time, not the application's wall clock. Failed-renewal tests verify paid-through does not advance; local tests exercise access expiry and the 90-day boundary. +- Hosted Checkout, portal and live CLI-forwarded webhook checks were interactive. The repeatable API suite uses real Stripe responses with locally generated webhook signatures for its HTTP replay test. +- No production deployment or live payment occurred. Sandbox customers and active subscriptions from this run were cleaned up; products and prices were archived. + +## Reliability follow-up + +The repeatable 16-scenario sandbox suite passed again after introducing durable webhook +receipt, asynchronous processing, checkout expiry, and provider reconciliation. The +HTTP refund replay check now explicitly drains the inbox before asserting entitlements. +No live payments or production deployment were performed. + +The local billing suites now include crash recovery, concurrent event receipt, retry +exhaustion, privacy of stored envelopes, reconstruction after missing local grants, +missed renewal recovery, refund-before-fulfillment ordering, partial refunds, uncertain +checkout recovery, purchase shutdown, and expiration deferral during provider outages. +These are deterministic failure simulations against disposable MongoDB, not a claim +that production backup infrastructure or external alert delivery has been tested. + +Final follow-up checks passed: 38 local billing tests, 155 frontend regression/UI tests, +TypeScript compilation, the production build, and targeted ESLint checks. The 16-scenario +real Stripe sandbox suite also passed with historical invoice recovery enabled. + +## Full application UI check — November 2, 2026 + +Used the built application in the collaborative browser, production billing routes, +an isolated MongoDB database and real Stripe sandbox Checkout and customer portal. +GitHub login was replaced with a test-only session fixture outside the repository. + +| UI flow | Observed result | +| --- | --- | +| Free account | Five repositories and 2 GiB displayed. | +| Monthly subscription | Insufficient-funds decline granted no access; successful retry activated 20 repositories and 10 GiB. | +| Duplicate subscription | App rejected another subscription and displayed the explanation. | +| Customer portal | Billing details and invoices loaded; cancellation stopped renewal while preserving paid access. | +| Annual subscription | Failed 3-D Secure granted no access; successful authentication activated annual access. | +| Delayed webhook processing | With the worker paused, the checkout return recovered annual access from Stripe. Both billing and quota panels reflected the upgrade. Queued events completed after resuming the worker. | +| Abandoned repository pass | Returning without payment granted no access. Reopening reused the same checkout session. | +| Paid repository pass | Successful checkout created a repository-scoped grant. | +| Refunded pass | A sandbox API refund generated real webhooks; refreshing in the UI showed revoked access. Refund initiation was an operator action, not an app UI feature. | +| Conference package | Checkout from the pending conference activated the package; the conference page showed Ready. | + +Two UI issues were fixed during this run: checkout reconciliation now refreshes the +quota panel, and revoked purchases no longer display a future paid-access date. +The quota issue has a regression test. Four targeted UI tests and the UI build passed; +both fixes were also checked in the real browser. No failed or pending received +webhook events remained at the final pre-cleanup inspection. + +This run does not cover GitHub OAuth, repository ingestion, production deployment, +tax, email delivery, or the passage of the 90-day grace period. Those should not be +inferred from the payment UI results. Temporary customers/subscriptions were deleted, +prices and product archived, and the disposable local services removed after testing. diff --git a/docs/website-request-budgets.md b/docs/website-request-budgets.md new file mode 100644 index 0000000..d9cf7ad --- /dev/null +++ b/docs/website-request-budgets.md @@ -0,0 +1,31 @@ +# Website requests and folder counts + +Website assets under `/w/` have a separate Redis-backed request budget. The +default `WEBSITE_RATE_LIMIT=2000` allows 2,000 requests per visitor in 15 minutes, +including signed-out visitors. Requests across repositories share this budget. +They do not consume the `/api` or `/github` allowance. Website slowdown, access +checks, file-size limits and streaming protections still apply. Rejections +include `Retry-After` and the existing JSON `rate_limited` response. + +Visitor identity uses the existing trusted-proxy address resolver. Do not trust +arbitrary forwarding headers or change `TRUST_PROXY` without verifying the +actual proxy chain. API and maintenance-token rate policies are unchanged. + +The explorer requests `/api/repo/:repoId/files/counts?path=…` after loading each +directory, once per directory per viewer initialization. The path is the same +anonymized path used for browsing. A response contains recursive file counts for +that directory and its immediate children, rather than a repository-wide map. +Counts use the captured active tree generation and sum anonymized child-name +collisions. Entering a colliding directory resolves the same original directory +as browsing it. No original paths are returned. + +The aggregation has a two-second database deadline, allows disk spill, and +returns at most 10,000 child groups plus the parent total. It still scans files +in the requested subtree; root counts can scan the whole active tree. No +cross-request cache bypasses authorization or retains counts from old snapshots. +The legacy endpoint without `path` retains its existing bounded response. + +Counts are optional. If a count request fails, the explorer continues browsing +without unavailable badges and stops issuing further count requests until the +viewer is reinitialized. It does not label the number of loaded tree entries as +the repository's total file count. File listing guards remain in place. diff --git a/package-lock.json b/package-lock.json index 1a161b0..3218deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "crypto-js": "^4.2.0", "d3-geo": "^3.1.1", "decompress-stream-to-s3": "^2.1.1", + "dompurify": "^3.4.17", "dotenv": "^18.0.9", "express": "^5.3.0", "express-rate-limit": "^8.7.2", @@ -38,6 +39,7 @@ "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-katex-extension": "^5.1.13", + "mermaid": "^12.2.0", "mime-types": "^3.0.2", "mongoose": "^7.8.13", "node-schedule": "^2.1.2", @@ -48,6 +50,7 @@ "re2js": "^2.9.1", "redis": "^5.12.1", "sanitize-html": "^2.18.1", + "stripe": "^23.1.0", "topojson-client": "^3.1.0", "ts-custom-error": "^3.3.1", "unzip-stream": "^0.3.4" @@ -99,6 +102,19 @@ "url": "https://github.com/sponsors/tdurieux" } }, + "node_modules/@antfu/install-pkg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", + "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -538,6 +554,12 @@ "node": ">=18" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@bramus/specificity": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", @@ -575,6 +597,43 @@ "keyv": "^5.6.0" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", + "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "13.2.0", + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", + "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", + "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", + "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", + "license": "Apache-2.0" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1454,6 +1513,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", + "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^2.0.1", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@inquirer/external-editor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", @@ -1549,6 +1625,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@mermaid-js/parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", + "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~13.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.6.1.tgz", @@ -2927,16 +3015,259 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, "node_modules/@types/d3-geo": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", - "dev": true, "license": "MIT", "dependencies": { "@types/geojson": "*" } }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -2990,7 +3321,6 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, "license": "MIT" }, "node_modules/@types/http-cache-semantics": { @@ -3222,6 +3552,13 @@ "@types/geojson": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unzip-stream": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@types/unzip-stream/-/unzip-stream-0.3.4.tgz", @@ -3578,6 +3915,16 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.44", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", @@ -4630,6 +4977,22 @@ "node": "*" } }, + "node_modules/chevrotain": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", + "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "13.2.0", + "@chevrotain/gast": "13.2.0", + "@chevrotain/regexp-to-ast": "13.2.0", + "@chevrotain/types": "13.2.0", + "@chevrotain/utils": "13.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -5051,6 +5414,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -5139,6 +5511,95 @@ "dev": true, "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.34.4", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.4.tgz", + "integrity": "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -5151,6 +5612,188 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-geo": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", @@ -5163,6 +5806,231 @@ "node": ">=12" } }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -5422,6 +6290,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -5525,6 +6402,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", @@ -5626,6 +6512,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5817,6 +6709,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.53.1.tgz", + "integrity": "sha512-VhMc6IXmexu8iYFfL2aaAww6IvJTdHlqBLruLoEUs33YD4IcP5DdZcLOybMsMmmLsiy1c8PsiLsrM0DdIuuRUA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -7089,6 +7993,12 @@ "xtend": "~4.0.1" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -7203,9 +8113,9 @@ } }, "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" @@ -7394,6 +8304,16 @@ "node": ">= 4" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8237,6 +9157,11 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/knip": { "version": "6.44.1", "resolved": "https://registry.npmjs.org/knip/-/knip-6.44.1.tgz", @@ -8284,6 +9209,12 @@ "dayjs": "^1.11.23" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -8378,6 +9309,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -8580,6 +9517,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mermaid": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.2.0.tgz", + "integrity": "sha512-w7nr/SFrLO7BaVTs3dQTvYSV6nYSBj0Yy9DjBhsIJtAcHqRzXSq4Pg32ujHZ/AdWsAMNEvvhbm5dt9E2tOiHzw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^2.0.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "chevrotain": "~13.2.0", + "cytoscape": "^3.34.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.21", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", + "es-toolkit": "^1.45.1", + "katex": "^0.18.0", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9352,7 +10335,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", - "dev": true, "license": "MIT" }, "node_modules/pako": { @@ -9471,6 +10453,12 @@ "node": ">= 0.4.0" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9624,6 +10612,22 @@ "node": ">= 0.10" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -10089,6 +11093,24 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -10137,6 +11159,12 @@ "node": ">=0.12.0" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -10785,6 +11813,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-23.1.0.tgz", + "integrity": "sha512-fVUfMSfTPoukfoV//MUKAx2+YxyKmYaYtnAL9VkfnHfaPNQylFdWZMD+zgUFerI8rjbaiER8Vzj2XEUQ2AhAsQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/node": ">=20" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10880,6 +11931,15 @@ "node": ">= 6" } }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -11039,6 +12099,15 @@ "node": ">=14.0.0" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -11410,6 +12479,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz", + "integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index ee39c42..57b6f98 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "test:performance": "mocha test/performance-*.test.js", "benchmark:performance": "node scripts/performance-benchmark.js", "build:ui": "node gulpfile.js", - "test:ui": "npm run build:ui && mocha test/frontend-regressions.test.js test/dashboard-ui.test.js test/vue-ui.test.js" + "test:ui": "npm run build:ui && mocha test/frontend-regressions.test.js test/dashboard-ui.test.js test/vue-ui.test.js", + "vendor:markdown": "node scripts/sync-markdown-vendors.js" }, "repository": { "type": "git", @@ -55,6 +56,7 @@ "crypto-js": "^4.2.0", "d3-geo": "^3.1.1", "decompress-stream-to-s3": "^2.1.1", + "dompurify": "^3.4.17", "dotenv": "^18.0.9", "express": "^5.3.0", "express-rate-limit": "^8.7.2", @@ -70,6 +72,7 @@ "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-katex-extension": "^5.1.13", + "mermaid": "^12.2.0", "mime-types": "^3.0.2", "mongoose": "^7.8.13", "node-schedule": "^2.1.2", @@ -81,6 +84,7 @@ "redis": "^5.12.1", "sanitize-html": "^2.18.1", "topojson-client": "^3.1.0", + "stripe": "^23.1.0", "ts-custom-error": "^3.3.1", "unzip-stream": "^0.3.4" }, diff --git a/public/asset-manifest.json b/public/asset-manifest.json index b95838f..e353896 100644 --- a/public/asset-manifest.json +++ b/public/asset-manifest.json @@ -1,9 +1,9 @@ { - "core.min.js": "core.fefc61001a.min.js", - "vendor.min.js": "vendor.ddf6179ad0.min.js", - "mermaid.min.js": "mermaid.f848a72d16.min.js", - "all.min.css": "all.fb8a9e1005.min.css", - "markdown.min.js": "markdown.1d3b1ed22a.min.js", + "core.min.js": "core.d23af1c652.min.js", + "vendor.min.js": "vendor.25b155fc95.min.js", + "mermaid.min.js": "mermaid.a7356d5ed1.min.js", + "all.min.css": "all.c7fb588ad2.min.css", + "markdown.min.js": "markdown.6c8dfb7b1d.min.js", "pdf.min.js": "pdf.4d9cc6ff49.min.js", "notebook.min.js": "notebook.c3144c18ae.min.js", "org.min.js": "org.f61f262915.min.js", diff --git a/public/css/all.min.css b/public/css/all.min.css index 392d94b..241e86e 100644 --- a/public/css/all.min.css +++ b/public/css/all.min.css @@ -6,4 +6,4 @@ */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 0%;flex:1 1 0%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal .list-group-item.active{margin-top:0}.list-group-horizontal .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm .list-group-item.active{margin-top:0}.list-group-horizontal-sm .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md .list-group-item.active{margin-top:0}.list-group-horizontal-md .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg .list-group-item.active{margin-top:0}.list-group-horizontal-lg .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl .list-group-item.active{margin-top:0}.list-group-horizontal-xl .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush .list-group-item{border-right-width:0;border-left-width:0;border-radius:0}.list-group-flush .list-group-item:first-child{border-top-width:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}/*! * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\e05e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hive:before{content:"\e07f"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\e065"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\e013"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-innosoft:before{content:"\e080"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\e066"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\e01a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\e068"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-perbyte:before{content:"\e083"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\e07a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\e057"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\e06d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\e070"}.fa-store-slash:before{content:"\e071"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\e07b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-uncharted:before{content:"\e084"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\e074"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-watchman-monitoring:before{content:"\e087"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:'Font Awesome 5 Brands';font-style:normal;font-weight:400;font-display:block;src:url(../fonts/fa-brands-400.eot);src:url(../fonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-brands-400.woff2) format("woff2"),url(../fonts/fa-brands-400.woff) format("woff"),url(../fonts/fa-brands-400.ttf) format("truetype"),url(../fonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:'Font Awesome 5 Brands';font-weight:400}@font-face{font-family:'Font Awesome 5 Free';font-style:normal;font-weight:400;font-display:block;src:url(../fonts/fa-regular-400.eot);src:url(../fonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-regular-400.woff2) format("woff2"),url(../fonts/fa-regular-400.woff) format("woff"),url(../fonts/fa-regular-400.ttf) format("truetype"),url(../fonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:'Font Awesome 5 Free';font-weight:400}@font-face{font-family:'Font Awesome 5 Free';font-style:normal;font-weight:900;font-display:block;src:url(../fonts/fa-solid-900.eot);src:url(../fonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-solid-900.woff2) format("woff2"),url(../fonts/fa-solid-900.woff) format("woff"),url(../fonts/fa-solid-900.ttf) format("truetype"),url(../fonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:'Font Awesome 5 Free';font-weight:900}.nb-notebook{line-height:1.5}.nb-stderr,.nb-stdout{white-space:pre-wrap;margin:1em 0;padding:.1em .5em}.nb-stderr{background-color:#faa}.nb-cell+.nb-cell{margin-top:.5em}.nb-output table{border:1px solid #000;border-collapse:collapse}.nb-output th{font-weight:700}.nb-output td,.nb-output th{border:1px solid #000;padding:.25em;text-align:left;vertical-align:middle;border-collapse:collapse}.nb-notebook blockquote{border-left:5px solid #ccc;margin-left:0;padding-left:1em}.nb-cell{position:relative}.nb-raw-cell{white-space:pre-wrap;background-color:#f5f2f0;font-family:Consolas,Monaco,'Andale Mono',monospace;padding:1em;margin:.5em 0}.nb-output{min-height:1em;width:100%;overflow-x:scroll;border-right:1px dotted #ccc}.nb-output img{max-width:100%}.nb-input:before,.nb-output:before{position:absolute;font-family:monospace;color:#999;left:-7.5em;width:7em;text-align:right}.nb-input:before{content:"In [" attr(data-prompt-number) "]:"}.nb-output:before{content:"Out [" attr(data-prompt-number) "]:"}// Fix pandas dataframe formatting div[style="max-height:1000px;max-width:1500px;overflow:auto;"]{max-height:none!important}@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{font:normal 1.21em KaTeX_Main,math,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.18.10"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}.mermaid{text-align:center;margin:1em 0;overflow:auto}.mermaid svg{max-width:100%;height:auto}.dark-mode .mermaid{filter:invert(1) hue-rotate(180deg)}.dark-mode .mermaid svg{background:0 0}.dark-mode .mermaid .edgeLabel,.dark-mode .mermaid .nodeLabel{filter:invert(1) hue-rotate(180deg)}.mermaid .node circle,.mermaid .node ellipse,.mermaid .node polygon,.mermaid .node rect{stroke-width:2px}.mermaid .flowchart-link{stroke-width:2px}.mermaid .actor{fill:#f9f9f9;stroke:#333;stroke-width:2px}.mermaid .messageLine0,.mermaid .messageLine1{stroke:#333;stroke-width:1.5px}.mermaid .section0,.mermaid .section1,.mermaid .section2,.mermaid .section3{opacity:.8}.mermaid .pieChart .slice{opacity:.9}.markdown-body .mermaid{background:0 0;border:none;padding:10px}.mermaid .error{color:#d32f2f;font-family:monospace;font-size:12px;padding:10px;border:1px solid #d32f2f;border-radius:4px;background-color:#ffebee}@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff')}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.5;color:#24292e;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#6a737d}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#005cc5}.markdown-body .pl-e,.markdown-body .pl-en{color:#6f42c1}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#24292e}.markdown-body .pl-ent{color:#22863a}.markdown-body .pl-k{color:#d73a49}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#032f62}.markdown-body .pl-smw,.markdown-body .pl-v{color:#e36209}.markdown-body .pl-bu{color:#b31d28}.markdown-body .pl-ii{color:#fafbfc;background-color:#b31d28}.markdown-body .pl-c2{color:#fafbfc;background-color:#d73a49}.markdown-body .pl-c2::before{content:"^M"}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#22863a}.markdown-body .pl-ml{color:#735c0f}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#005cc5}.markdown-body .pl-mi{font-style:italic;color:#24292e}.markdown-body .pl-mb{font-weight:700;color:#24292e}.markdown-body .pl-md{color:#b31d28;background-color:#ffeef0}.markdown-body .pl-mi1{color:#22863a;background-color:#f0fff4}.markdown-body .pl-mc{color:#e36209;background-color:#ffebda}.markdown-body .pl-mi2{color:#f6f8fa;background-color:#005cc5}.markdown-body .pl-mdr{font-weight:700;color:#6f42c1}.markdown-body .pl-ba{color:#586069}.markdown-body .pl-sg{color:#959da5}.markdown-body .pl-corl{text-decoration:underline;color:#032f62}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit}.markdown-body strong{font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body hr{box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0}.markdown-body input{overflow:visible}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#0366d6;text-decoration:none}.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:0 0;border:0;border-bottom:1px solid #dfe2e5}.markdown-body hr::before{display:table;content:""}.markdown-body hr::after{display:table;clear:both;content:""}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:4px!important}.markdown-body .pl-2{padding-left:8px!important}.markdown-body .pl-3{padding-left:16px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:32px!important}.markdown-body .pl-6{padding-left:40px!important}.markdown-body::before{display:table;content:""}.markdown-body::after{display:table;clear:both;content:""}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:0}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0}.markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #c6cbd1;border-bottom-color:#959da5;border-radius:3px;box-shadow:inset 0 -1px 0 #959da5}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#1b1f23;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #eaecef}.markdown-body h2{padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #eaecef}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#6a737d}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:600}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #dfe2e5}.markdown-body table tr{background-color:#fff;border-top:1px solid #c6cbd1}.markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#005cc5;border-color:#005cc5}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #d1d5da;border-bottom-color:#c6cbd1;border-radius:3px;box-shadow:inset 0 -1px 0 #c6cbd1}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#0366d6}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.dark-mode{--canvas-bg-color:#161412;--sidebar-bg-color:#1C1A17;--main-bg-color:#161412;--header-bg-color:#161412;--hover-bg-color:rgba(255, 255, 255, 0.05);--header-color:#FAF9F6;--color:#D6D1C5;--link-color:#7CC7BE;--link-hover-color:#A8DDD6;--border-color:#423D37;--border-soft:#2A2825;--btn-color:#FAF9F6;--input-color:#E6E1D8;--input-bg:#25231F;--primary-bg:#FAF9F6;--primary-hover-bg:#FFFFFF;--primary-color:#1A1815;--primary-active-color:#1A1815;--accent:#7CC7BE;--accent-soft:rgba(124, 199, 190, 0.12);--ink-muted:#A39D92;--ink-faint:#8A857C;--ink-soft:#B5AFA2;--status-ready:#5FB27A;--status-progress:#E0A94A;--status-error:#F08A82;--status-inactive:#8A857C;--status-error-soft:rgba(240, 138, 130, 0.08);--status-error-soft-hover:rgba(240, 138, 130, 0.14);--admin-nav-bg:#1A1918;--admin-nav-active:rgba(124, 199, 190, 0.12);--admin-stat-bg:#25231F;--paper-card:#2C2925;--paper-bg-alt:#211E1B;--highlight:#3A341E;background-color:var(--canvas-bg-color);color:var(--color)}body{--canvas-bg-color:#FAF9F6;--sidebar-bg-color:#FFFFFF;--header-bg-color:#FAF9F6;--header-color:#1A1815;--main-bg-color:#FAF9F6;--hover-bg-color:rgba(26, 24, 21, 0.05);--input-color:#1A1815;--color:#1A1815;--link-color:#1F6A64;--link-hover-color:#17544F;--border-color:#E6E1D8;--border-soft:#EDE9E1;--btn-color:#1A1815;--input-bg:#FFFFFF;--primary-bg:#1A1815;--primary-hover-bg:#000000;--primary-color:#FAF9F6;--primary-active-color:#FAF9F6;--accent:#1F6A64;--accent-soft:#E4F0EE;--ink-muted:#6F6A61;--ink-faint:#8A857C;--ink-soft:#4A4641;--paper-bg-alt:#F3F1EC;--paper-card:#FFFFFF;--highlight:#FFF3C4;--status-ready:#2F7A44;--status-progress:#946512;--status-error:#B42318;--status-inactive:#8A857C;--status-error-soft:rgba(180, 35, 24, 0.04);--status-error-soft-hover:rgba(180, 35, 24, 0.08);--radius-control:6px;--radius-button:8px;--button-height:36px;--button-font-size:14px;--button-line-height:20px;--button-padding-x:13px;--button-compact-height:32px;--radius-surface:10px;--font-serif:'Instrument Serif','Iowan Old Style',Georgia,serif;--font-sans:'Inter',ui-sans-serif,system-ui,-apple-system,sans-serif;--font-mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;background-color:var(--canvas-bg-color);color:var(--color);font-family:var(--font-sans);font-size:14px;letter-spacing:-.005em;line-height:1.5}.fadeIn{opacity:1;animation-name:fadeInOpacity;animation-iteration-count:1;animation-timing-function:ease-in;animation-duration:.6s}@keyframes fadeInOpacity{0%{opacity:0}100%{opacity:1}}.btn:active,.btn:focus{box-shadow:none}:focus:not(:focus-visible){outline:0}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.btn:focus-visible,.dropdown-item:focus-visible,.form-control:focus-visible{outline:2px solid var(--accent);outline-offset:2px;box-shadow:none}input:active,input:hover{box-shadow:none!important}input:-webkit-autofill,input:-webkit-autofill:focus,input:-webkit-autofill:hover,select:-webkit-autofill,select:-webkit-autofill:focus,select:-webkit-autofill:hover,textarea:-webkit-autofill,textarea:-webkit-autofill:focus,textarea:-webkit-autofill:hover{border-color:var(--border-color);-webkit-text-fill-color:var(--color);-webkit-box-shadow:none;transition:background-color 1000s ease-in-out 0s}.custom-select,.form-control,input:-internal-autofill-selected{color:var(--input-color);background-color:var(--input-bg)!important;border-color:var(--border-color)}.form-control:focus{border-color:var(--border-color)}body,html{position:relative;height:100%;overflow-x:hidden}body{overflow-y:hidden}#app{flex:1 1 auto;min-height:0}.app-header{flex:0 0 auto}@media (min-width:992px){.app-header.is-contained .navbar{padding-left:max(40px,calc((100% - 1140px)/ 2 + 32px))!important;padding-right:max(40px,calc((100% - 1140px)/ 2 + 32px))!important}}.app-view{display:block;flex:1 1 auto;min-height:0;overflow-x:hidden!important;overflow-y:auto!important;max-width:100%}.markdown-body a,a{color:var(--link-color)}.markdown-body a:hover,a:hover{text-decoration:none;color:var(--link-hover-color)}.dropdown-menu a.dropdown-item{color:var(--color)}.dropdown-menu a.dropdown-item:hover{color:var(--color);text-decoration:none}.markdown-body .emoji{height:1.3em;margin:0;vertical-align:-.1em}.navbar.navbar-expand-lg{background:var(--header-bg-color)!important;border-bottom:1px solid var(--border-color);transition:background-color .5s ease;padding:10px max(20px,calc((100vw - 1200px)/ 2));font-family:var(--font-sans)}.navbar-toggler{border:1px solid var(--border-color)!important;color:var(--color)!important;width:44px;height:40px;padding:0!important;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-control)!important}.navbar-toggler .navbar-toggler-icon{width:22px;height:22px}.navbar-toggler .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='%231A1815' stroke-width='2' stroke-linecap='round' d='M4 8h22M4 15h22M4 22h22'/%3E%3C/svg%3E")!important}.navbar .navbar-nav{align-items:center}.navbar .nav-link{display:inline-flex;align-items:center;font-size:13px;color:var(--ink-muted)!important;padding:6px 0!important;margin:0 12px;border-bottom:1px solid transparent;letter-spacing:-.005em;line-height:1}.navbar .nav-link:hover{color:var(--color)!important}.navbar .nav-link.active{color:var(--color)!important;border-bottom-color:var(--color)}.navbar .nav-link.nav-icon{padding:0!important;margin:0 8px;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px}.navbar .nav-link .fa,.navbar .nav-link .fab{font-size:15px;line-height:1}.navbar #navbarSupportedContent{align-items:center}.navbar-brand{font-family:var(--font-serif)!important;font-size:20px!important;letter-spacing:-.01em;color:var(--color)!important;padding-right:18px;margin-right:12px;border-right:1px solid var(--border-color)}.navbar-brand em,.navbar-brand i{font-style:italic;font-weight:400}.navbar-dark .navbar-toggler{border-color:var(--border-color);color:var(--color)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='%231A1815' stroke-width='2' stroke-linecap='round' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar .btn-signin{display:inline-flex;align-items:center;height:32px;padding:0 14px!important;margin-left:6px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--color)!important;border-radius:var(--radius-control);font-size:13px;font-weight:500;line-height:1}.dark-mode .navbar .btn-signin{border-color:#5a554d;background:0 0}.navbar .btn-signin:hover{background:var(--hover-bg-color);border-color:var(--ink-faint);color:var(--color)!important}.navbar .nav-support{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;order:3;gap:6px;min-height:32px;margin-left:4px;padding:0 10px;border:1px solid transparent;border-radius:var(--radius-control);color:var(--ink-muted);font-size:13px;font-weight:500;text-decoration:none}.navbar .nav-support:hover{background:var(--hover-bg-color);color:var(--color)}.navbar button.nav-link{background:0 0;border:0;cursor:pointer}.navbar .nav-link:focus-visible,.navbar .nav-support:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-control)}.navbar .nav-signin-m{display:none}@media (min-width:992px){.navbar>.nav-support{display:none!important}}.navbar .nav-support-inline{display:inline-flex!important;align-items:center;gap:6px}.navbar .nav-support-inline .ui-icon{width:15px;height:15px}@media (max-width:991px){.navbar .nav-signin-m{display:inline-flex;align-items:center;order:0;height:40px;padding:0 14px;margin-left:0;margin-right:8px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);color:var(--color);font-size:13px;font-weight:500;text-decoration:none}.navbar .nav-support{order:0;width:44px;min-height:44px;margin:0 8px 0 auto;padding:0}.navbar .nav-support-label{display:none}[class*=floatingchat-container-wrap],iframe.floatingchat-container{display:none!important}}.navbar .user-chip{display:inline-flex!important;align-items:center;gap:8px;padding:4px 12px 4px 6px!important;margin-left:6px;border:1px solid var(--border-color);border-radius:999px;background:var(--sidebar-bg-color);font-size:12.5px;color:var(--color)!important}.navbar .user-chip:hover{background:var(--hover-bg-color)}.navbar .user-chip img{width:22px;height:22px}.navbar .user-chip .user-chip-name{color:var(--color);font-weight:500}.navbar .user-chip::after{color:var(--ink-muted)}#navbarSupportedContent{padding-right:0}.generalMessage{display:block;text-align:center;color:var(--header-color)}.rgba-gradient{background:var(--canvas-bg-color);color:var(--color)}.paper-landing{overflow-x:clip}.paper-wrap{width:min(1076px,calc(100% - 64px));margin-left:auto;margin-right:auto;padding-left:0;padding-right:0}.paper-eyebrow{font-family:var(--font-mono);font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-soft);font-weight:500}.paper-hero{max-width:1140px;margin:0 auto;padding:40px 32px 0;display:grid;grid-template-columns:.95fr 1.05fr;gap:48px;align-items:center}.paper-hero h1.hero-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.6rem,5.4vw,4.4rem);line-height:1.02;letter-spacing:-.025em;margin:14px 0 0;color:var(--color);text-wrap:balance}.paper-hero h1 .accent{color:var(--accent);font-style:italic}.hero-subtitle{margin-top:18px;font-size:17px;line-height:1.55;color:var(--ink-soft);max-width:30em;text-wrap:pretty}.paper-cta-row{display:flex;flex-wrap:wrap;gap:18px;align-items:center;margin-top:26px}.btn-hero{display:inline-flex;align-items:center;font-family:var(--font-sans);border:1px solid var(--color);background:var(--color);color:var(--canvas-bg-color);padding:12px 20px;font-size:14px;font-weight:500;border-radius:var(--radius-control);letter-spacing:-.005em;transition:background .15s,border-color .15s,transform .1s}.btn-hero:hover{background:var(--primary-hover-bg);border-color:var(--primary-hover-bg);color:var(--canvas-bg-color);text-decoration:none;transform:translateY(-1px)}.btn-hero-ghost{background:0 0;border:1px solid var(--border-color);color:var(--color);padding:12px 20px;font-size:14px;font-weight:500;border-radius:var(--radius-control)}.btn-hero-ghost:hover{background:var(--hover-bg-color);border-color:var(--color);color:var(--color)}.paper-link-arrow{font-size:14px;color:var(--ink-soft);text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--border-color)}.paper-link-arrow:hover{color:var(--color);text-decoration-color:var(--color)}.paper-reassure{list-style:none;margin:16px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:6px 18px;font-size:13px;color:var(--ink-muted)}.paper-reassure li{white-space:nowrap}@media (min-width:1100px){.paper-reassure{flex-wrap:nowrap}}.paper-reassure li::before{content:"\2713";color:var(--status-ready);margin-right:7px;font-size:14px;font-weight:600}.paper-frame{border-radius:var(--radius-surface);background:var(--paper-card);border:1px solid var(--border-color);box-shadow:0 24px 60px rgba(26,24,21,.1),0 2px 6px rgba(26,24,21,.04);overflow:hidden}.dark-mode .paper-frame{box-shadow:0 24px 60px rgba(0,0,0,.45)}.paper-frame-bar{display:flex;align-items:center;gap:8px;height:34px;padding:0 12px;border-bottom:1px solid var(--border-soft);background:var(--paper-bg-alt);font-family:var(--font-mono);font-size:11px;color:var(--ink-muted)}.paper-frame-bar .dots{display:flex;gap:5px}.paper-frame-bar .dots i{width:9px;height:9px;border-radius:50%;background:color-mix(in srgb,var(--ink-faint) 45%,transparent)}.paper-frame-bar .url{flex:1;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-frame img{display:block;width:100%;height:auto}.paper-hero-shot .paper-frame{width:100%}.hero-mock{display:grid;grid-template-columns:168px 1fr;min-height:360px;font-family:var(--font-sans);text-align:left}.hero-mock-tree{background:var(--paper-bg-alt);border-right:1px solid var(--border-soft);padding:12px 8px}.hero-mock-tree-title{font-size:11px;font-weight:600;color:var(--ink-muted);padding:0 8px 8px}.hero-mock-row{display:flex;align-items:center;gap:7px;height:26px;padding:0 8px;border-radius:var(--radius-control);font-size:12.5px;color:var(--ink-soft);white-space:nowrap}.hero-mock-row svg{width:14px;height:14px;flex-shrink:0;fill:none;stroke:var(--ink-muted);stroke-width:1.2;stroke-linejoin:round}.hero-mock-row.is-active{background:rgba(128,128,128,.15);color:var(--color);font-weight:500}.hero-mock-doc{min-width:0;background:var(--paper-card)}.hero-mock-doc-bar{display:flex;align-items:center;justify-content:space-between;height:40px;padding:0 18px;border-bottom:1px solid var(--border-soft);font-size:13px}.hero-mock-doc-bar span{font-size:12px;color:var(--ink-muted)}.hero-mock-doc-body{padding:18px 22px 22px;font-size:13.5px;line-height:1.6;color:var(--color)}.hero-mock-doc-body p{margin:0 0 10px}.hero-mock-h1{font-size:22px;font-weight:600;letter-spacing:-.01em;padding-bottom:8px;margin-bottom:12px;border-bottom:1px solid var(--border-soft)}.hero-mock-h2{font-size:15px;font-weight:600;margin:16px 0 8px}.hero-mock-doc-body pre{margin:0 0 12px;padding:10px 12px;background:var(--paper-bg-alt);border:1px solid var(--border-soft);border-radius:var(--radius-control);font-family:var(--font-mono);font-size:12px;line-height:1.6;color:var(--color)}.hero-mock .redact{background:var(--accent-soft);color:var(--accent);border-radius:4px;padding:1px 5px;font-family:var(--font-mono);font-size:.88em}@media (max-width:575px){.hero-mock{grid-template-columns:1fr;min-height:0}.hero-mock-tree{display:none}}.paper-proof{margin-top:48px;padding-top:18px;padding-bottom:18px;border-top:1px solid var(--border-color);display:flex;flex-wrap:wrap;gap:12px 30px;align-items:center}.paper-proof-venues{display:flex;gap:22px;align-items:center;font-family:var(--font-serif);font-size:22px;letter-spacing:-.01em;color:var(--ink-soft)}.paper-proof-venues i{font-style:normal;font-family:var(--font-sans);font-size:14px;color:var(--ink-faint)}.paper-stats{margin-top:40px;padding-top:16px;border-top:1px solid var(--border-color)}.paper-stats-meta-left{display:inline-flex;align-items:center;gap:8px}.paper-stats-meta{display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:14px}.paper-stats-meta-right{display:inline-flex;align-items:center;gap:6px}.paper-stats-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--status-ready)}.paper-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color)}.paper-stat-card{display:flex;flex-direction:column;padding:22px 26px 20px 0;margin-right:26px;border-right:1px solid var(--border-color);min-width:0}.paper-stat-card:last-child{border-right:0;margin-right:0;padding-right:0}.paper-stat-value{font-size:44px;font-weight:500;letter-spacing:-.04em;line-height:1;color:var(--color);font-variant-numeric:tabular-nums}.paper-stat-label{margin-top:6px;font-size:13px;color:var(--ink-soft)}.paper-stat-value.is-pending{color:var(--ink-faint)}.paper-stat-bars{display:block;width:100%;height:34px;margin-top:18px;overflow:visible}.paper-stat-bars rect{fill:color-mix(in srgb,var(--ink-muted) 55%,transparent)}.paper-stat-bars rect.is-latest{fill:var(--accent)}.paper-stat-delta{margin-top:10px;font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}.paper-section-head{margin-top:88px;display:grid;grid-template-columns:1fr 1.4fr;gap:40px;align-items:start}.paper-section-head>p{padding-top:34px}@media (max-width:991px){.paper-section-head>p{padding-top:0}}.paper-qa h2,.paper-section-head h2{font-family:var(--font-serif);font-weight:400;font-size:clamp(2rem,4.2vw,3.25rem);line-height:1.05;letter-spacing:-.02em;margin:10px 0 0;color:var(--color);text-wrap:balance}.paper-feature-title em,.paper-qa h2 em,.paper-section-head h2 em{font-style:italic;color:var(--accent)}.paper-section-head p{font-size:15px;color:var(--ink-soft);max-width:32em;margin:0;line-height:1.6}.paper-preview{margin-top:28px;display:grid;grid-template-columns:1fr 1fr;gap:14px}.paper-preview .pane{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);overflow:hidden;min-width:0}.paper-preview .pane-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid var(--border-soft)}.paper-preview .pane-label{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);white-space:nowrap}.paper-preview .pane-url{font-family:var(--font-mono);font-size:11px;color:var(--ink-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.paper-preview .pane-body{padding:16px 18px;font-family:var(--font-mono);font-size:12.5px;line-height:1.9;color:var(--ink-soft)}.paper-preview .pane-body strong{color:var(--color);font-weight:600}.paper-preview .tok{color:var(--ink-faint)}.paper-preview .term{background:color-mix(in srgb,var(--status-error) 9%,transparent);color:inherit;border-radius:4px;padding:0 4px;text-decoration:line-through;text-decoration-color:color-mix(in srgb,var(--status-error) 60%,transparent)}.dark-mode .paper-preview .term{background:color-mix(in srgb,var(--status-error) 16%,transparent)}.paper-preview .redact{background:var(--accent-soft);color:var(--accent);border-radius:4px;padding:0 5px}.paper-features{margin-top:28px;padding-top:28px;border-top:1px solid var(--border-color);display:grid;grid-template-columns:5fr 7fr;gap:48px;align-items:start}.paper-features-list{display:flex;flex-direction:column}.paper-feature{border-left:2px solid var(--border-soft);padding:16px 0 16px 20px;transition:border-color .2s}.paper-feature.is-on{border-left-color:var(--accent)}.paper-feature-tab{all:unset;display:block;position:relative;width:100%;cursor:pointer;color:inherit;border-radius:4px}.paper-feature:not(.is-on) .paper-feature-tab::after{content:"";position:absolute;right:4px;bottom:12px;width:8px;height:8px;border-right:1.5px solid var(--ink-muted);border-bottom:1.5px solid var(--ink-muted);transform:rotate(45deg)}.paper-feature:not(.is-on) .paper-feature-tab:hover::after{border-color:var(--color)}.paper-feature.is-on .paper-feature-tab::after{content:"";position:absolute;right:4px;bottom:10px;width:8px;height:8px;border-right:1.5px solid var(--color);border-bottom:1.5px solid var(--color);transform:rotate(-135deg)}.paper-feature-tab:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.paper-feature-tab .paper-eyebrow{display:block;margin-bottom:6px}.paper-feature-title{display:block;font-family:var(--font-serif);font-size:24px;line-height:1.15;letter-spacing:-.015em;color:var(--color);transition:color .2s}.paper-feature-title em{color:inherit}.paper-feature-tab:hover .paper-feature-title,.paper-feature.is-on .paper-feature-title{color:var(--color)}.paper-feature.is-on .paper-feature-title em{color:var(--accent)}.paper-feature-more{display:none}.paper-feature.is-on .paper-feature-more{display:block}.paper-feature-more .paper-feature-title-m{display:none}.paper-feature-more p{font-size:14.5px;color:var(--ink-soft);line-height:1.55;margin:10px 0 12px;max-width:30em}.paper-features-stage{min-width:0}.paper-feature-panel{animation:paper-feature-in .25s ease-out}@keyframes paper-feature-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion:reduce){.paper-feature-panel{animation:none}}.paper-crop{position:relative;height:380px;overflow:hidden;background:var(--paper-card)}.paper-crop img{position:absolute;left:0;top:0;max-width:none;height:auto;display:block}.paper-crop-anonymize img{width:213%;transform:translate(-1.5%,-18%)}.paper-crop-review img{width:213%;transform:translate(0,-6.5%)}.paper-crop-manage img{width:213%;transform:translate(-12.3%,-6.5%)}.paper-qa{margin-top:88px;display:grid;grid-template-columns:1fr 1.4fr;gap:40px;align-items:start}.paper-qa h2{margin-bottom:12px}.paper-qa .q{border-top:1px solid var(--border-color);padding:18px 0}.paper-qa .q:last-child{border-bottom:1px solid var(--border-color)}.paper-qa .q a::after{content:none;display:inline-block;width:7px;height:7px;margin-left:10px;border-right:1.5px solid var(--ink-muted);border-bottom:1.5px solid var(--ink-muted);transform:translateY(-2px) rotate(-45deg);transition:transform .15s,border-color .15s}.paper-qa .q a:hover::after{border-color:var(--accent);transform:translate(3px,-2px) rotate(-45deg)}.paper-qa .q a:hover{text-decoration:underline;text-underline-offset:3px}.paper-qa .q a{display:block;font-weight:500;font-size:15px;color:var(--color);margin-bottom:6px}.paper-qa .q a:hover{color:var(--accent);text-decoration:none}.paper-qa .q p{margin:0;font-size:14px;color:var(--ink-soft);line-height:1.6}@media (max-width:1100px){.paper-hero-shot .paper-frame{width:100%}}@media (max-width:991px){.paper-hero{grid-template-columns:1fr;gap:36px;padding-top:40px}.paper-qa,.paper-section-head{grid-template-columns:1fr;gap:16px}.paper-stats-grid{grid-template-columns:1fr 1fr}.paper-stat-card:nth-child(2n){border-right:0;margin-right:0;padding-right:0}.paper-stat-card:nth-child(-n+2){border-bottom:1px solid var(--border-color)}}@media (max-width:767px){.paper-wrap{width:calc(100% - 40px)}.paper-hero{padding-left:20px;padding-right:20px}.paper-reassure{flex-direction:column;gap:6px}.paper-hero h1.hero-title{font-size:clamp(2.4rem,11vw,3.25rem)}.hero-subtitle{font-size:16px}.paper-cta-row .btn-hero{width:100%;justify-content:center}.paper-proof{margin-top:48px}.paper-stat-card{padding:18px 16px 16px 0;margin-right:16px}.paper-stat-value{font-size:36px}.paper-qa,.paper-section-head{margin-top:72px}.paper-preview{grid-template-columns:1fr}.paper-preview .pane-body{font-size:11.5px;line-height:1.8}.paper-features{display:flex;flex-wrap:wrap;gap:0;padding-top:0;border-top:0;margin-top:24px}.paper-feature,.paper-features-list,.paper-features-stage{display:contents}.paper-feature-tab{order:1;flex:1 1 0;width:auto;padding:12px 0 10px;border-bottom:2px solid var(--border-color);border-radius:0;text-align:left}.paper-feature.is-on .paper-feature-tab{border-bottom-color:var(--color)}.paper-feature-tab .paper-eyebrow{margin:0;font-size:10.5px;letter-spacing:.08em}.paper-feature-tab .paper-feature-title{display:none}.paper-feature-panel{order:2;width:100%;margin-top:20px;animation:none}.paper-crop{height:240px}.paper-crop-anonymize img{width:230%;transform:translate(-1.5%,-8%)}.paper-crop-review img{width:240%;transform:translate(-1%,-8%)}.paper-crop-manage img{width:220%;transform:translate(-3%,-9%)}.paper-feature-more{order:3;width:100%}.paper-feature.is-on .paper-feature-more{margin-top:18px}.paper-feature-more .paper-feature-title-m{display:block;font-size:26px;line-height:1.1}.paper-feature-more p{max-width:none}.paper-footer-inner>.paper-footer-brand{grid-column:1/-1}.paper-footer-foot{flex-direction:column;gap:6px}}.dropdown-menu{background-color:var(--paper-card,var(--main-bg-color))!important;color:var(--color)!important;border:1px solid var(--border-color)!important;border-radius:8px!important;box-shadow:0 12px 28px rgba(26,24,21,.08),0 2px 6px rgba(26,24,21,.05)!important;padding:6px!important;font-family:var(--font-sans);font-size:13px;min-width:11rem}.dropdown-menu .dropdown-item{color:var(--color)!important;padding:7px 10px!important;border-radius:5px;font-size:13px}.dropdown-menu .dropdown-item:focus,.dropdown-menu .dropdown-item:hover{background:var(--hover-bg-color)!important;color:var(--color)!important}.dropdown-menu .dropdown-divider{border-top-color:var(--border-color)!important;margin:4px 2px}.dropdown-menu .dropdown-header{color:var(--ink-muted);font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase}.body,.file-content,.markdown-body{background-color:var(--main-bg-color)!important;color:var(--color)!important}.markdown-body table tr{background-color:var(--main-bg-color);border-top:1px solid var(--border-color)}.markdown-body table tr:nth-child(2n){background-color:var(--paper-bg-alt)}.markdown-body table td,.markdown-body table th{border:1px solid var(--border-color)}.file-content{padding:4px 7px;text-align:left;min-height:100%}.file-card{margin:16px 20px 28px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);overflow:clip}.file-card:not(.is-code) .file-card-head{position:sticky;top:0;z-index:2;border-radius:var(--radius-surface) var(--radius-surface) 0 0}.file-card.is-code{display:flex;flex-direction:column;height:calc(100% - 32px);margin-bottom:16px}.file-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:46px;padding:6px 8px 6px 16px;border-bottom:1px solid var(--border-color);background:var(--paper-card)}.dark-mode .file-card-head{background:var(--paper-card)}.file-card-meta{font-size:13px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}.file-card-meta .sep{margin:0 8px;color:var(--ink-faint)}.file-card-head .btn{height:32px;min-height:32px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:500;padding:0 10px}.file-card-head .btn-seg:not(.view-switch){gap:2px;padding-left:8px;margin-left:4px;border-left:1px solid var(--border-color)}.file-card-head .btn-seg:not(.view-switch)>.btn{border:0!important;border-radius:var(--radius-control)!important;margin:0!important;background:0 0!important;color:var(--ink-soft)}.file-card-head .btn-seg:not(.view-switch)>.btn:hover{background:var(--hover-bg-color)!important;color:var(--color)}.file-card-actions>.btn-seg:first-child,.file-card-head .btn-seg:not(.view-switch):has(>.btn:only-child){border-left:0;padding-left:0;margin-left:0}.file-card-head .btn.btn-icon-only{width:32px;padding:0!important;color:var(--ink-soft)}.file-card-head .btn.btn-icon-only:hover{color:var(--color);background:var(--hover-bg-color)!important}.file-card .file-card-body{flex:1 1 auto;min-height:0}.file-card-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.file-card-path{display:none;align-items:center;gap:2px;min-width:0}.file-card-path .paths{overflow:hidden}.file-card-path+.file-card-meta,.file-card-path+.view-switch{margin-left:8px}.status-bar.is-redundant{display:none}.file-card-path{display:inline-flex}@media (max-width:767px){.file-card-head{flex-wrap:wrap;row-gap:0}.file-card-head .file-card-meta{order:3;flex-basis:100%;margin:0 0 6px!important;padding:0!important;border:0!important;font-size:12px;color:var(--ink-muted)}.file-card-head .file-card-meta .meta-extra{display:inline!important}.file-card-head .btn-ghost{color:var(--ink-soft)}.file-card-head .breadcrumb-root,.file-card-head .breadcrumb-root+.breadcrumb-item::before{display:none!important}.file-card-head .breadcrumb-item{padding-left:0}.file-card-head{gap:6px;padding:6px 8px 6px 16px}.file-card-actions{gap:2px}.file-card-actions .btn-seg{margin-left:4px}}@media (min-width:992px){.file-card-path+.file-card-meta{padding-left:14px;border-left:1px solid var(--border-color)}.file-card{margin-top:20px}.file-card.is-code{height:calc(100% - 36px)}}.card-files-btn{margin-right:4px}.leftCol-zip{display:inline-flex;align-items:center;gap:8px;margin-bottom:6px;font-size:13px;font-weight:500;color:var(--ink-soft);text-decoration:none}.leftCol-zip:hover{color:var(--color);text-decoration:none}.leftCol-zip .ui-icon{width:15px;height:15px}.file-card-actions .btn-seg{margin-left:6px}.file-card-head .btn.btn-find,.file-card-head .btn.btn-goto{padding:0 10px;gap:6px;font-weight:400}@media (max-width:1180px){.file-card-head .btn-find .btn-find-label,.file-card-head .btn-goto .btn-find-label{display:none}.file-card-head .btn-find,.file-card-head .btn-goto{width:32px;padding:0!important}}@media (max-width:991px){.file-card-head .btn-find{width:32px;padding:0!important}.file-card-head .btn-find-label{display:none}}@media (max-width:767px){.file-card-head .btn-find{width:40px}}.ace_search{top:8px!important;right:12px!important;left:auto!important;background:var(--paper-card)!important;color:var(--color)!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-surface)!important;box-shadow:0 8px 24px rgba(26,24,21,.14);padding:6px!important;font-family:var(--font-sans)!important;font-size:13px!important}.ace_search_field{background:var(--input-bg)!important;color:var(--input-color)!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important;margin-right:4px!important;height:28px!important;padding:0 8px!important;font-family:var(--font-sans)!important}.ace_search_field:focus{border-color:var(--ink-muted)!important;outline:0;box-shadow:0 0 0 3px var(--hover-bg-color)}.ace_button,.ace_searchbtn{background:0 0!important;color:var(--ink-soft)!important;border:0!important;border-radius:var(--radius-control)!important;width:28px;height:28px!important;line-height:28px!important}.ace_button:hover,.ace_searchbtn:hover{background:var(--hover-bg-color)!important;color:var(--color)!important}.ace_button.checked{border-color:var(--accent)!important;color:var(--accent)!important}.ace_search_counter{color:var(--ink-soft)!important;font-size:13px;margin:0 8px 0 4px!important;font-variant-numeric:tabular-nums}.ace_search{display:flex;flex-wrap:nowrap;align-items:center;gap:6px;max-width:none!important;width:auto!important}.dark-mode .ace_search{background:var(--paper-card)!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:var(--accent)!important}.ace_search .ace_button[action=searchInSelection],.ace_search .ace_button[action=toggleReplace],.ace_search .ace_replace_form,.ace_search .ace_searchbtn[action=findAll]{display:none!important}.ace_search_field{width:200px!important}.ace_search_form{display:flex;align-items:center;margin:0!important;float:none!important}.ace_search_options{display:flex!important;align-items:center;gap:4px;margin:0!important;float:none!important;text-align:left!important}.ace_search_options .ace_button{width:28px;height:28px!important;line-height:26px!important;padding:0!important;border-radius:var(--radius-control)!important;font-size:12px!important;text-align:center}.ace_searchbtn_close{position:static!important;margin-left:2px}.ace_editor .ace_marker-layer .ace_selected-word{background:color-mix(in srgb,#e0a94a 30%,transparent)!important;border:0!important;border-radius:2px}.ace_editor.ace-chrome .ace_marker-layer .ace_selection,.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:#f2b544!important;box-shadow:0 0 0 1px #b7791f;border-radius:2px}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:#b7791f!important;box-shadow:0 0 0 1px #f2b544}.ace_editor .ace_indent-guide-active{background:0 0!important;box-shadow:inset 1px 0 0 var(--border-color)!important}.ace_searchbtn_close{filter:none!important;opacity:.6}.ace_prompt_container{background:var(--paper-card)!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-surface)!important;box-shadow:0 12px 32px rgba(26,24,21,.16)!important}.btn.btn-ghost{background:0 0!important;border-color:transparent!important;color:var(--ink-muted)}.btn.btn-ghost:hover{background:var(--hover-bg-color)!important;color:var(--color)}.btn.btn-ghost.is-on{color:var(--color);background:var(--hover-bg-color)!important;box-shadow:none}.btn.btn-ghost:focus:not(:focus-visible):not(.is-on):not(:hover){background:0 0!important;box-shadow:none}.dark-mode .file-card-head .btn-seg .btn{background:0 0!important;border-color:var(--border-color)!important;color:var(--color);font-weight:500}.file-card-head .btn.btn-find,.file-card-head .btn.btn-goto{color:var(--ink-soft)}.file-card-head>.view-switch{gap:2px;padding:2px;border-radius:calc(var(--radius-control) + 2px);background:var(--hover-bg-color)}.file-card-head>.view-switch .btn{height:28px;min-height:28px;margin:0!important;border:0!important;border-radius:var(--radius-control)!important;background:0 0!important;color:var(--ink-soft);font-weight:500}.file-card-head>.view-switch .btn:hover{color:var(--color)}.file-card-head>.view-switch .btn.is-on{background:var(--paper-card)!important;color:var(--color);box-shadow:0 1px 2px rgba(26,24,21,.1),0 0 0 1px var(--border-color);cursor:default}.dark-mode .file-card-head>.view-switch .btn.is-on{background:#3d3934!important;box-shadow:none}.status-bar .btn.btn-ghost.btn-zip{color:var(--ink-soft)}.copy-path-btn{margin-left:-4px}@media (max-width:767px){.file-card-head .btn,.file-card-head .btn.btn-icon-only{height:40px;min-height:40px}.file-card-head .btn.btn-icon-only{width:40px}.copy-path-btn{display:none!important}}.file-card .ace_editor{height:100%!important}@media (max-width:767px){.file-card{margin:0;border:0;border-radius:0}.file-card.is-code{height:100%}.file-card-head{position:sticky;top:0;z-index:2;padding-left:16px}}.dir-card{margin:20px 20px 0;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);overflow:hidden}.dir-card+.file-card{margin-top:16px}.dir-card-head{display:flex;align-items:baseline;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt)}.dark-mode .dir-card-head{background:#35312c}.dir-card-title{font-size:14px;font-weight:600;color:var(--color)}.dir-card-sub{font-size:13px;color:var(--ink-muted)}.dir-list{list-style:none;margin:0;padding:4px 0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:0}.dir-row{display:flex;align-items:center;gap:10px;width:100%;height:34px;padding:0 16px;border:0;background:0 0;font:inherit;font-size:14px;color:var(--color);text-align:left;text-decoration:none;cursor:pointer}.dir-row:hover{background:var(--hover-bg-color);color:var(--color);text-decoration:none}.dir-row:hover .dir-name{text-decoration:underline;text-underline-offset:3px}.dir-row.is-current .dir-name{font-weight:600}.dir-icon{width:16px;height:16px;flex-shrink:0;fill:none;stroke:var(--ink-muted);stroke-width:1.2;stroke-linejoin:round}.dir-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dir-meta{flex-shrink:0;font-size:12px;color:var(--ink-muted);font-variant-numeric:tabular-nums}.dir-list:not(.is-expanded)>li:nth-child(n+9){display:none}.dir-more{display:block;width:100%;height:40px;border:0;border-top:1px solid var(--border-soft);background:0 0;font:inherit;font-size:13px;font-weight:500;color:var(--link-color);cursor:pointer}.dir-more:hover{background:var(--hover-bg-color)}@media (max-width:767px){.dir-card{margin:0;border:0;border-bottom:1px solid var(--border-color);border-radius:0}.dir-list{grid-template-columns:1fr}.dir-card+.file-card{margin-top:0}}.explorer-content .file-card .file-content.markdown-body{max-width:920px;margin:0;min-height:0;padding:28px 40px 48px;background:var(--paper-card)!important;font-family:var(--font-sans);font-size:15px;line-height:1.65}.explorer-content .markdown-body h1,.explorer-content .markdown-body h2{font-family:var(--font-sans);font-weight:600;letter-spacing:-.01em;border-bottom:1px solid var(--border-color);padding-bottom:.3em}.explorer-content .markdown-body h1{font-size:28px;margin-top:0}.explorer-content .markdown-body h2{font-size:21px;margin-top:32px}.explorer-content .markdown-body h3{font-size:17px;font-weight:600;margin-top:24px}.explorer-content .markdown-body h4{font-size:15px;font-weight:600}.explorer-content .markdown-body img{border-radius:var(--radius-control)}.explorer-content .markdown-body p>a:only-child>img,.explorer-content .markdown-body p>img:only-child{border:1px solid var(--border-color)}.explorer-content .markdown-body code,.explorer-content .markdown-body pre{font-family:var(--font-mono);font-size:13px}.explorer-content .markdown-body pre{background:var(--paper-bg-alt)!important;border:1px solid var(--border-soft);border-radius:var(--radius-control);padding:14px 16px;line-height:1.55}.explorer-content .markdown-body :not(pre)>code{background:var(--hover-bg-color);border-radius:4px;padding:.15em .4em;font-size:.86em}@media (max-width:767px){.explorer-content .file-card .file-content.markdown-body{padding:20px 16px 72px;font-size:15px}.explorer-content .markdown-body h1{font-size:24px}}.ace_editor.ace-chrome,.ace_editor.ace-nord-dark{background:var(--paper-card)!important;color:var(--color)!important}.ace_editor.ace-chrome .ace_gutter,.ace_editor.ace-nord-dark .ace_gutter{background:color-mix(in srgb,var(--paper-bg-alt) 55%,var(--paper-card))!important;color:var(--ink-muted)!important;border-right:1px solid var(--border-soft)}.ace_editor .ace_gutter-cell{padding-right:14px;padding-left:12px;font-size:12px}@media (max-width:767px){.ace_editor .ace_gutter-cell{padding-right:8px;padding-left:6px}}.ace_editor .ace_gutter-active-line{background:0 0!important}.ace_editor .ace_indent-guide{background:0 0!important;box-shadow:inset 1px 0 0 var(--border-soft)}.ace_editor .ace_fold-widget{opacity:0;transition:opacity .15s}.ace_editor .ace_fold-widget.ace_closed,.ace_editor .ace_gutter:hover .ace_fold-widget{opacity:.7}.ace_editor .ace_marker-layer .ace_selection{background:var(--accent-soft)!important}.ace-chrome .ace_keyword,.ace-chrome .ace_storage{color:#b4362a}.ace-chrome .ace_string{color:#2b5a3e}.ace-chrome .ace_constant.ace_language,.ace-chrome .ace_constant.ace_numeric,.ace-chrome .ace_support.ace_constant{color:#1f6a64}.ace-chrome .ace_comment{color:#7a746a;font-style:italic}.ace-chrome .ace_entity.ace_name.ace_function,.ace-chrome .ace_support.ace_function{color:#6b45b8}.ace-chrome .ace_variable.ace_parameter{color:#8a5a12;font-style:normal}.ace-chrome .ace_entity.ace_name.ace_tag,.ace-chrome .ace_meta.ace_tag{color:#116329}.ace-chrome .ace_entity.ace_other.ace_attribute-name{color:#0550ae}.ace-chrome .ace_identifier,.ace-chrome .ace_paren,.ace-chrome .ace_punctuation,.ace-chrome .ace_variable{color:var(--color)}.ace-nord-dark .ace_keyword,.ace-nord-dark .ace_storage{color:#f2897d}.ace-nord-dark .ace_string{color:#9dd3a8}.ace-nord-dark .ace_constant.ace_language,.ace-nord-dark .ace_constant.ace_numeric,.ace-nord-dark .ace_support.ace_constant{color:#7cc7be}.ace-nord-dark .ace_comment{color:#9a948a;font-style:italic}.ace-nord-dark .ace_entity.ace_name.ace_function,.ace-nord-dark .ace_support.ace_function{color:#c9a8f5}.ace-nord-dark .ace_variable.ace_parameter{color:#e0a94a;font-style:normal}.ace-nord-dark .ace_entity.ace_name.ace_tag,.ace-nord-dark .ace_meta.ace_tag{color:#7ee787}.ace-nord-dark .ace_entity.ace_other.ace_attribute-name{color:#79c0ff}.ace-nord-dark .ace_identifier,.ace-nord-dark .ace_paren,.ace-nord-dark .ace_punctuation,.ace-nord-dark .ace_variable{color:var(--color)}.badge{font-size:75%;font-weight:500;padding:3px 8px;border-radius:10px;vertical-align:middle}.warning-feedback{width:100%;margin-top:.25rem;font-size:80%;color:#dc8e35}.dropdown-item:focus,.dropdown-item:hover{background-color:var(--hover-bg-color)}.card-header .btn,.card-header .btn:focus,.card-header .btn:hover{border:none!important;background:0 0!important}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:var(--button-height);padding:7px var(--button-padding-x);font:500 var(--button-font-size)/var(--button-line-height) var(--font-sans);vertical-align:middle;max-width:100%;transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease;color:var(--btn-color);border:1px solid var(--border-color);background:var(--paper-card);border-radius:var(--radius-button);white-space:nowrap}.btn:active,.btn:hover{color:var(--btn-color);border:1px solid var(--border-color);background-color:var(--hover-bg-color);box-shadow:none!important}.btn.btn-primary{color:var(--primary-color);background-color:var(--primary-bg)}.btn.btn-primary:active,.btn.btn-primary:hover{color:var(--primary-active-color);background-color:var(--primary-hover-bg)!important}.btn::placeholder{color:var(--color);opacity:1}.white_border{border:1px solid #fff;color:#fff}.leftCol{display:flex;position:relative;flex:0 0 auto;width:100%;z-index:9;flex-direction:column}.card,.leftCol,.sidePanel{background-color:var(--sidebar-bg-color);border:1px solid var(--border-color)}@media (min-width:768px){.leftCol{flex:0 0 var(--sidebar-w,280px);width:var(--sidebar-w,280px);height:100%}}.leftCol-search{padding:10px 12px 0}.tree-search-box{display:flex;align-items:center;gap:8px;background:var(--canvas-bg-color);border:1px solid var(--border-color);border-radius:8px;padding:7px 10px;transition:border-color .15s}.tree-search-box:focus-within{border-color:var(--ink-muted)}.tree-search-icon{font-size:12px;color:var(--ink-muted);flex-shrink:0}.tree-search-input{flex:1;border:none;outline:0;background:0 0;font-family:var(--font-sans);font-size:13px;color:var(--color);min-width:0}.tree-search-input::placeholder{color:var(--ink-muted)}.tree-search-kbd{font-family:var(--font-sans);font-size:11px;color:var(--ink-muted);background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:4px;padding:1px 6px;line-height:1.4;flex-shrink:0}.tree-search-clear{background:0 0;border:none;color:var(--ink-muted);font-size:16px;cursor:pointer;padding:0 2px;line-height:1}.tree-search-clear:hover{color:var(--color)}.leftCol-project-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 4px;gap:8px}.project-name{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--color);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.project-file-count{font-family:var(--font-sans);font-size:12px;color:var(--ink-muted);white-space:nowrap;flex-shrink:0;margin-left:auto}.files{position:relative;display:block;overflow:auto;height:100%;padding:4px 6px}.files ul{list-style:none;margin:0;padding:0}.files li ul{padding-left:8px;margin-left:7px;border-left:1px solid var(--border-color)}.files .folder>ul{display:none}.files .folder.open>ul{display:block}.files .file,.files .folder{position:relative;color:var(--ink-soft);cursor:pointer;word-break:break-all}.files a{display:flex;align-items:center;gap:5px;min-height:26px;padding:2px 8px 2px 4px;font-family:var(--font-sans);font-size:13px;color:var(--ink-soft);border-radius:var(--radius-control);text-decoration:none;line-height:1.5;position:relative}.files a:hover{background:var(--hover-bg-color);color:var(--color);text-decoration:none}.files:focus{outline:0}.files a.tree-focused{background:var(--hover-bg-color);color:var(--color)}.files .file.active>a::before{content:"";position:absolute;left:0;top:4px;bottom:4px;width:2px;border-radius:0 2px 2px 0;background:var(--accent)}.files .file.active>a{border-radius:0 var(--radius-control) var(--radius-control) 0;background:rgba(128,128,128,.12);color:var(--color);font-weight:500}.files .file.active>a .tree-icon-file{color:var(--color)}.files .file.active>a .tree-count{color:var(--ink-muted)}.tree-icon-file,.tree-icon-folder{display:inline-block;flex-shrink:0}.tree-icon-folder{width:14px;height:12px;color:var(--ink-muted);opacity:.75}.tree-icon-folder svg{display:block;width:100%;height:100%;fill:currentColor;fill-opacity:0;stroke:currentColor;stroke-width:1.2;stroke-linejoin:round}.files .folder.open>a .tree-icon-folder{color:var(--ink-muted);opacity:1}.files .folder.open>a .tree-icon-folder svg{fill-opacity:.12}.tree-icon-file{width:16px;font-size:14px;text-align:center;color:var(--ink-muted)}.tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:12px;height:14px;flex-shrink:0;opacity:.8}.tree-toggle::after{content:"";display:block;width:6px;height:6px;border-right:1.5px solid var(--ink-muted);border-bottom:1.5px solid var(--ink-muted);transform:translateX(-1px) rotate(-45deg);transition:transform .12s ease}.files .folder.open>a .tree-toggle::after{transform:translateY(-2px) rotate(45deg)}.tree-spacer{display:inline-block;width:12px;flex-shrink:0}.tree-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.files a:not(:hover):not(.tree-focused) .tree-count{opacity:0}.tree-count{transition:opacity .12s;font-family:var(--font-sans);font-size:12px;font-variant-numeric:tabular-nums;color:var(--ink-muted);flex-shrink:0;margin-left:auto;padding-left:8px;opacity:.7}.files .folder>a{color:var(--color)}.files .file::before,.files .folder::before{display:none}.files .folder>a::after{display:none}.tree-search-path{color:var(--ink-muted);font-size:11px}.tree-search-empty{padding:16px 12px;color:var(--ink-muted);font-size:12.5px;font-family:var(--font-sans);text-align:center}.leftCol{background:var(--paper-bg-alt,#f3f1ec);border:0;border-right:1px solid var(--border-color)}.dark-mode .leftCol{background:var(--paper-bg-alt)}.dark-mode .explorer-content .markdown-body img{filter:brightness(.8)}.dark-mode .hero-mock-tree{background:#1f1d1a}.dark-mode .hero-mock-row.is-active{background:rgba(255,255,255,.1)}.dark-mode .files .file.active>a{background:rgba(255,255,255,.1)}.status-bar{background:var(--canvas-bg-color);border-bottom:1px solid var(--border-color);padding:8px 20px;min-height:48px}.paths .breadcrumb-item,.paths .breadcrumb-item a{font-family:var(--font-sans);font-size:14px;color:var(--ink-muted);letter-spacing:0}.paths .breadcrumb-root a{color:var(--link-color)}.paths .breadcrumb-root a:hover{text-decoration:underline;text-underline-offset:3px}.paths .breadcrumb-item.active,.paths .breadcrumb-item:last-child{color:var(--color);font-family:var(--font-sans);font-size:14px;font-weight:600;letter-spacing:0}.status-files-btn{display:none!important}.leftCol-resizer{display:none}@media (min-width:992px){.leftCol-resizer{display:block;position:absolute;top:0;bottom:0;left:calc(var(--sidebar-w,280px) - 3px);width:6px;cursor:col-resize;z-index:10}.leftCol-resizer:hover,body.is-resizing .leftCol-resizer{background:color-mix(in srgb,var(--accent) 35%,transparent)}body.is-resizing{cursor:col-resize;user-select:none}}.leftCol-hide{display:none;align-items:center;justify-content:center;width:26px;height:26px;margin-right:-6px;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-muted);cursor:pointer}.leftCol-hide:hover{background:var(--hover-bg-color);color:var(--color)}.leftCol-hide .ui-icon{width:16px;height:16px}@media (min-width:992px){.leftCol-hide{display:inline-flex}.explorer-page.files-hidden .leftCol{display:none!important}.explorer-page.files-hidden .status-files-btn{display:inline-flex!important}}@media (max-width:991px){.status-files-btn{display:inline-flex!important;flex-shrink:0}.explorer-page .sidebar-toggle{display:none!important}}@media (max-width:767px){.status-bar .btn,.status-bar .btn.btn-icon-only{min-height:40px}.status-bar .btn.btn-icon-only{width:40px}}.dark-mode .status-bar .btn{background:var(--paper-card)}.btn-seg{display:inline-flex}.btn-seg>.btn{border-radius:0;margin:0}.btn-seg>.btn+.btn{margin-left:-1px}.btn-seg>.btn:first-child{border-radius:var(--radius-control) 0 0 var(--radius-control)}.btn-seg>.btn:last-child{border-radius:0 var(--radius-control) var(--radius-control) 0}.btn-seg>.btn:focus-visible,.btn-seg>.btn:hover{position:relative;z-index:1}.status-bar .btn{height:32px;min-height:32px;display:inline-flex;align-items:center;gap:6px}.status-bar .btn.btn-icon-only{padding-left:0;padding-right:0;width:32px;justify-content:center}.status-files-btn{gap:6px}@media (max-width:991px){.status-files-btn{height:40px!important;min-height:40px!important;padding:0 12px;border-radius:var(--radius-control)}}@media (max-width:767px){.file-card.is-code.no-wrap{position:relative}.file-card.is-code.no-wrap::after{content:"";position:absolute;top:46px;right:0;bottom:0;width:44px;pointer-events:none;background:linear-gradient(to right,transparent,var(--paper-card) 85%);z-index:20}.file-card-head .btn-goto{display:none!important}}@media (max-width:575px){.status-bar .btn-zip{display:none!important}.navbar .nav-support{display:none!important}.navbar .nav-signin-m{margin-left:auto}}.ace_editor .ace_gutter-cell{cursor:pointer}.ace_editor .ace_gutter-cell:hover{color:var(--color)}.status-bar .file-meta{flex:0 0 auto;margin-right:auto;font-size:12px;color:var(--ink-muted);white-space:nowrap;font-variant-numeric:tabular-nums}@media (max-width:575px){.status-bar .file-meta{display:none}}.paths .breadcrumb-item+.breadcrumb-item::before{content:'/';color:var(--ink-faint);padding:0 6px 0 3px}.last-update{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-muted);padding:8px 10px;border-top:1px solid var(--border-color);letter-spacing:.02em}.ace_editor{height:100%}.ace_scroller{cursor:default}.status-bar{display:flex;align-items:center;gap:12px;background-color:var(--canvas-bg-color);padding:10px 14px;margin:0;border-bottom:1px solid var(--border-color);border-radius:0;flex-shrink:0}.status-bar-actions{display:flex;align-items:center;margin-left:auto;gap:10px;flex-shrink:0}.status-bar-actions .btn{display:inline-flex;align-items:center;gap:6px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);border-radius:6px;padding:4px 10px;font-size:12.5px;min-height:30px}.status-bar-actions .btn:hover{background:var(--hover-bg-color);color:var(--color)}.status-bar-actions .btn.btn-active{border-color:currentColor;font-weight:600}.paths{flex:0 1 auto;min-width:0;padding:0;margin:0;background-color:initial;border:none;border-radius:0;align-items:center;overflow-x:auto;white-space:nowrap;flex-wrap:nowrap}.paths::-webkit-scrollbar{display:none}.paths a{color:var(--color)}.paths .breadcrumb-item{padding-left:3px}.paths .breadcrumb-item::before{padding-right:3px}.content{height:100%;overflow:scroll;padding-top:42px;background-color:var(--main-bg-color);text-align:center}.image-content{max-width:100%;max-height:100%;text-align:center;padding:4px}.pdf-viewer{display:flex;flex-direction:column;height:100%;min-height:0;background-color:var(--main-bg-color)}.pdf-toolbar{display:flex;align-items:center;gap:4px;flex:0 0 auto;padding:4px 8px;border-bottom:1px solid var(--border-color,rgba(0,0,0,.12))}.pdf-toolbar-group{display:inline-flex;align-items:center;gap:4px}.pdf-toolbar-right{margin-left:auto}.pdf-toolbar-btn{border:0;background:0 0;color:inherit;padding:4px 8px;border-radius:4px;font-size:13px;line-height:1.2;cursor:pointer}.pdf-toolbar-btn:hover:not(:disabled){background-color:rgba(128,128,128,.18)}.pdf-toolbar-btn:disabled{opacity:.4;cursor:default}.pdf-toolbar-zoom{min-width:52px;font-variant-numeric:tabular-nums}.pdf-toolbar-page{width:3.5em;text-align:right;padding:2px 4px;font-size:13px;color:inherit;background:0 0;border:1px solid var(--border-color,rgba(0,0,0,.2));border-radius:4px}.pdf-toolbar-count{font-size:13px;opacity:.75;font-variant-numeric:tabular-nums}.pdf-pages{flex:1 1 auto;min-height:0;overflow:auto;padding:8px 0}.pdf-viewer-page{margin:0 auto 12px}.pdf-viewer-page canvas{display:block;box-shadow:0 1px 4px rgba(0,0,0,.25)}.pdf-viewer-error{padding:24px;text-align:center;font-weight:700}.html-doc{display:block;height:100%}.html-doc-frame{display:block;width:100%;height:100%;border:0;background-color:#fff}.file-error{font-weight:700;font-size:50px;text-align:center}pdfviewer{display:block;padding:15px}pdfpageviewer{display:block;width:100%;margin-bottom:10px}notebook{display:block;text-align:left;padding:15px}@media (min-width:768px){notebook{padding-left:100px}}.nb-output td,.nb-output th{border:1px solid var(--border-color)!important}.paper-faq{max-width:1140px;margin:0 auto;padding:48px 32px 80px;font-family:var(--font-sans)}.paper-faq-hero{padding-bottom:40px;border-bottom:1px solid var(--border-color);margin-bottom:36px}.paper-faq-eyebrow{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);letter-spacing:.14em;text-transform:uppercase;margin-bottom:32px}.paper-faq-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.6rem,5.5vw,4.6rem);line-height:1.03;letter-spacing:-.02em;color:var(--color);margin:0 0 22px;max-width:14em}.paper-faq-title em{font-style:italic;color:var(--color)}.paper-faq-lede{color:var(--ink-soft);font-size:17px;line-height:1.55;max-width:38em;margin:0}.paper-faq-lede a{color:var(--link-color);border-bottom:1px solid currentColor}.paper-faq-lede a:hover{color:var(--accent);border-bottom-color:var(--accent)}.paper-faq-body{display:grid;grid-template-columns:220px 1fr;gap:60px;align-items:start}.paper-faq-toc{position:sticky;top:32px;font-size:13.5px}.paper-faq-toc-head{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-muted);letter-spacing:.14em;text-transform:uppercase;padding-bottom:10px;margin-bottom:6px;border-bottom:1px solid var(--border-color)}.paper-faq-toc nav{display:flex;flex-direction:column}.paper-faq-toc nav a{color:var(--ink-soft);padding:8px 0 8px 14px;border-left:2px solid transparent;text-decoration:none;margin-left:0}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover{color:var(--color);border-left-color:var(--color)}.faq-section{min-width:0}.faq-section>div{width:100%}.faq-section h4.faq-cat{font-family:var(--font-serif);font-weight:400;font-size:clamp(1.7rem,2.6vw,2.2rem);letter-spacing:-.015em;color:var(--color);margin:0 0 6px;padding-bottom:14px;border-bottom:1px solid var(--border-color)}.faq-section h4.faq-cat i{display:none}.faq-section .panel-group{margin-bottom:56px}.faq-section .panel-default .panel-title a{display:block;position:relative;padding:22px 0 22px 42px;font-family:var(--font-serif);font-weight:400;font-size:20px;line-height:1.35;letter-spacing:-.005em;color:var(--color);text-decoration:none;cursor:pointer}.faq-section .panel-default .panel-title a:hover{color:var(--accent)}.faq-section .panel-heading{background:0 0;padding:0!important;border:none}.faq-section .panel-default{background:0 0;border:none;border-bottom:1px solid var(--border-soft);border-radius:0;margin-bottom:0!important;box-shadow:none}.faq-section .panel-default .panel-title{margin-bottom:0}.faq-section .panel-default .panel-title a:after{content:"−";position:absolute;left:0;right:auto;top:22px;transform:none;font-family:var(--font-mono);font-size:18px;line-height:1.35;color:var(--ink-muted);float:none;margin:0;font-weight:400}.faq-section .panel-default .panel-title a.collapsed:after{content:"+"}.faq-section .panel-default .panel-body{padding:0 0 22px 42px!important;color:var(--ink-soft);font-size:15px;line-height:1.65;border-top:none;max-width:68ch}.table{color:var(--color)}.dropdown-divider,.table td,.table th,hr{border-color:var(--border-color)}.table.repositories td{word-break:break-word}loc{display:block;position:relative;width:100%;height:5px}loc .lang{float:left;height:100%}.rounded-circle.fa{display:inline-block;font-size:75px;height:140px;width:140px;border:4px solid;line-height:140px;text-align:center}.featurette-divider{margin:4.5rem 0;border-color:var(--border-color)}.featurette-heading{font-family:var(--font-serif);font-weight:400;font-size:2rem;line-height:1.15;letter-spacing:-.02em;margin-bottom:14px;color:var(--color)}.dark-mode .shadow{box-shadow:none}.dark-mode .overflow-auto::-webkit-scrollbar{width:10px}.dark-mode .overflow-auto::-webkit-scrollbar-track{-webkit-box-shadow:none;background:inherit;border-radius:0}.dark-mode .overflow-auto::-webkit-scrollbar-thumb{border-radius:10px;border:1px solid var(--border-color);background-color:var(--hover-bg-color)}.dark-mode .rgba-gradient{background:linear-gradient(135deg,#2a2d4a,#151728)}.section-number{padding-right:10px;font-weight:700;font-style:italic}.org-subscript-child{font-size:85%}.highlighted-line{position:absolute;background:rgba(100,200,100,.5);z-index:20}code,pre{font-family:"Fira Code","Courier New",Courier,monospace;line-height:1.1;color:var(--color)}.diff-file,.diff-index,.diff-lines{background:rgba(172,172,172,.5)}.diff-add{background:rgba(100,200,100,.5)}.diff-remove{background:rgba(200,100,100,.5)}.dashboard-title{font-size:1.4rem;font-weight:600;letter-spacing:-.01em}.quota-row{display:flex;gap:16px;margin-bottom:16px;padding:0}.quota-item{flex:1;min-width:0}.quota-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px}.quota-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px;opacity:.7}.quota-value{font-size:.8rem;font-weight:500;font-variant-numeric:tabular-nums}.quota-progress{height:6px;border-radius:3px;background:var(--hover-bg-color)}.quota-progress .progress-bar{border-radius:3px}.filter-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;font-size:.8rem;font-weight:500;background:var(--primary-bg);color:var(--primary-color);border-radius:12px;line-height:1.4}.filter-chip-close{display:inline-flex;align-items:center;justify-content:center;background:0 0;border:none;color:inherit;opacity:.7;cursor:pointer;padding:0;min-width:24px;min-height:24px;margin:-6px -6px -6px 0;font-size:1rem;line-height:1;border-radius:999px}.filter-chip-close:hover{opacity:1}.repo-list{list-style:none;padding:0;margin:0}.repo-list-item{display:flex;align-items:flex-start;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--border-color);gap:12px}.repo-list-item:first-child{border-top:1px solid var(--border-color)}.repo-list-item.repo-inactive{opacity:.6}.repo-list-item-content{flex:1;min-width:0}.repo-list-item-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px}.repo-name{font-family:var(--font-mono);font-size:14px;font-weight:500;color:var(--color);word-break:break-word}.repo-name:hover{color:var(--accent)}.repo-source{font-size:.85rem;opacity:.8;display:flex;flex-wrap:wrap;align-items:center;gap:6px}.repo-date{opacity:.7}.repo-meta{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;font-size:.8rem;opacity:.7}.repo-meta span{display:inline-flex;align-items:center;gap:4px;white-space:nowrap}.repo-list-item-actions{flex-shrink:0;padding-top:2px;margin-left:14px}.repo-list-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:48px 20px;opacity:.5;font-size:.95rem;list-style:none;border-bottom:1px solid var(--border-color);border-top:1px solid var(--border-color)}.repo-list-empty i{font-size:1.8rem}.type-badge{display:inline-flex;align-items:center;gap:3px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px;padding:2px 8px;border-radius:4px;white-space:nowrap}.type-badge{font-family:var(--font-mono);letter-spacing:.06em;padding:2px 8px;border-radius:999px;border:1px solid var(--border-color);background:var(--paper-bg-alt,transparent)}.type-badge.type-pr,.type-badge.type-repo{background:var(--paper-bg-alt);color:var(--ink-muted);border-color:var(--border-color)}.dark-mode .type-badge.type-pr,.dark-mode .type-badge.type-repo{background:rgba(255,255,255,.06);color:var(--ink-muted);border-color:var(--border-color)}.type-badge.type-coauthor{background:rgba(99,102,241,.12);color:#4f46e5;border-color:rgba(99,102,241,.35);margin-left:6px}.dark-mode .type-badge.type-coauthor{background:rgba(129,140,248,.18);color:#7cc7be;border-color:rgba(165,180,252,.45)}.btn-group .btn:not(.btn-primary){background:var(--hover-bg-color)}.anonymize-page{display:flex;flex-direction:column;min-height:0}.anonymize-landing{flex:1 1 0;min-height:min-content;display:flex;align-items:center;justify-content:center;padding:20px}.anonymize-landing-inner{width:100%;max-width:600px}.anonymize-landing-inner h2{font-weight:700}.anonymize-workspace{display:flex;flex-direction:column;flex:1 1 0;min-height:0;background:var(--canvas-bg-color)}.anonymize-topbar{background:var(--paper-card);border-bottom:1px solid var(--border-color);padding:18px 28px;flex:0 0 auto}.anonymize-topbar-inner{max-width:1400px;margin:0 auto}.anonymize-topbar-head{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.anonymize-topbar-title{margin:4px 0 0;font-size:clamp(1.8rem,3vw,2.4rem)}.anonymize-split{flex:1 1 auto;display:flex;min-height:0}.anonymize-form-col{flex:0 0 auto;width:100%;min-width:0;background:var(--canvas-bg-color);border-right:1px solid var(--border-color);overflow-y:auto}.anonymize-form-col .paper-settings-main{max-width:720px;padding:28px 32px 40px;margin:0}.anonymize-form-col .paper-settings-section:first-child{padding-top:0}.anonymize-form-col .anonymize-submit-bar{position:sticky;bottom:0;background:var(--canvas-bg-color);padding:14px 0 0;border-top:1px solid var(--border-color);margin-top:20px}.anonymize-form-col .anonymize-submit-bar .btn{width:100%;padding:12px 16px;font-size:15px}.anonymize-preview-col{flex:1 1 auto;min-width:0;overflow:auto;background:var(--paper-bg-alt);padding:28px 32px}.anonymize-preview-head{display:flex;align-items:baseline;gap:12px;padding-bottom:12px;border-bottom:1px solid var(--border-color);margin-bottom:20px}.anonymize-preview-head .paper-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.anonymize-preview-sub{color:var(--ink-soft);font-size:13px}.anonymize-preview-body{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:28px 32px}.form-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}.form-grid-3{display:grid;grid-template-columns:2fr 1fr 1fr;gap:16px}@media (max-width:700px){.form-grid-2,.form-grid-3{grid-template-columns:1fr}}.paper-optional{font-family:var(--font-sans);font-weight:400;color:var(--ink-muted);text-transform:none;letter-spacing:0;font-size:11px;margin-left:6px}.paper-plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.paper-plan-card{position:relative;display:block;padding:18px 20px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease;margin:0}.paper-plan-card input[type=radio]{position:absolute;opacity:0;pointer-events:none}.paper-plan-card.selected{border-color:var(--color);box-shadow:0 0 0 2px var(--color) inset}.paper-plan-head{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.paper-plan-price{font-family:var(--font-serif);font-size:2rem;color:var(--color);margin:6px 0 4px}.paper-plan-price .paper-plan-per{font-family:var(--font-sans);font-size:12px;color:var(--ink-soft)}.paper-plan-desc{font-size:13px;color:var(--ink-soft)}.paper-plan-desc ul{padding-left:18px;margin:0}.paper-settings-copy{color:var(--ink-soft);font-size:13px;margin:0 0 14px}.anonymize-section-title{font-size:.85rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px;margin:18px 0 8px 0;padding-bottom:6px;border-bottom:1px solid var(--border-color);opacity:.7}.anonymize-section-title i{margin-right:4px}.anonymize-submit-bar{padding:12px 0 4px 0}.anonymize-submit-bar .btn{font-size:1rem;padding:10px 20px;font-weight:600}.anonymize-page .form-check{padding-top:4px;padding-bottom:4px}.anonymize-page .form-check-label{font-weight:500}.anonymize-preview-col{min-height:0;min-width:0}.pr-diff pre{overflow-x:auto;max-width:100%}@media (min-width:768px){.anonymize-form-col{height:100%;width:520px;flex:0 0 520px}.anonymize-preview-col{height:100%;flex:1}}@media (max-width:767px){.anonymize-workspace{flex:0 0 auto;min-height:0}.anonymize-topbar{padding:14px 16px}.anonymize-topbar-title{font-size:1.6rem;margin-top:2px}.anonymize-split{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}.anonymize-form-col{width:100%!important;flex:0 0 auto!important;height:auto!important;border-right:none;border-bottom:1px solid var(--border-color);overflow:visible}.anonymize-form-col .paper-settings-main{padding:18px 16px 24px;max-width:none}.anonymize-form-col .anonymize-submit-bar{position:sticky;bottom:0;margin:20px -16px 0;padding:12px 16px;background:var(--canvas-bg-color);border-top:1px solid var(--border-color);z-index:10}.anonymize-form-col .anonymize-submit-bar .btn{width:100%;padding:12px 16px;font-size:15px}.anonymize-preview-col{width:100%;flex:0 0 auto;height:auto!important;max-height:none;padding:18px 16px 24px;overflow:visible}.anonymize-preview-body{padding:16px;border-radius:8px}.anonymize-preview-body .pr-diff pre,.anonymize-preview-body pre{overflow-x:auto;max-width:100%}.anonymize-landing-inner{padding:16px}}@media (min-width:1280px){.anonymize-form-col{width:620px;flex:0 0 620px}}@media (max-width:767px){.btn{min-height:38px;padding:6px 14px;font-size:14px}.btn-sm{min-height:34px;padding:5px 10px}.navbar .nav-link{padding:8px 12px}.navbar .nav-link .fa,.navbar .nav-link .fab{font-size:24px;line-height:32px}.container.page{padding-left:15px;padding-right:15px;max-width:100%}h1{font-size:1.6rem}h2{font-size:1.35rem}h3{font-size:1.15rem}.repo-list-item{flex-direction:column}.repo-list-item-actions{align-self:flex-start}.quota-row{flex-direction:column;gap:10px;padding:10px 12px}.dropdown-menu{position:absolute;right:0;left:auto}.form-group{margin-bottom:12px}.form-control{font-size:16px}.status-bar{gap:6px;padding:8px 10px}.status-bar .btn{font-size:12px;padding:4px 10px;min-height:32px}.status-bar .paths{font-size:11.5px;overflow-x:auto;flex-wrap:nowrap;white-space:nowrap;-webkit-overflow-scrolling:touch}.status-bar .paths::-webkit-scrollbar{display:none}.status-bar-actions{flex-shrink:0;gap:4px}#home .display-4{font-size:1.8rem}#home h4.subtext-header{font-size:1rem}.progress:not(.quota-progress){min-width:120px!important}.badge{font-size:70%}.col-2.input-group{width:100%!important;max-width:100%;flex:0 0 100%}.color-text-secondary span{display:inline-block;margin-bottom:2px}.floatingchat-container-wrap{display:none!important}.anonymize-landing{align-items:flex-start;padding-top:60px}.anonymize-page .container-fluid>.row{height:auto!important;min-height:100%}.anonymize-form-col{width:100%!important;flex:0 0 100%!important;border-right:none;border-top:1px solid var(--border-color)}.anonymize-preview-col{height:auto!important;max-height:none}.anonymize-section-title{font-size:.8rem;margin:12px 0 8px 0}.anonymize-submit-bar{position:sticky;bottom:0;background:var(--sidebar-bg-color);padding:10px 0;margin:0 -12px;padding-left:12px;padding-right:12px;border-top:1px solid var(--border-color);z-index:10}.anonymize-submit-bar .btn{font-size:1rem;padding:12px 20px}.anonymize-page .form-check{padding-top:6px;padding-bottom:6px;min-height:36px}.anonymize-page .form-check-label{font-size:.95rem}.anonymize-page .card-body{padding:12px}.pr-title{font-size:1.15rem}.pr-diff pre{font-size:.75rem}.pr-comment .mb-1 h5{font-size:1rem}.nav-tabs .nav-link{padding:8px 12px;font-size:.9rem}}@media (min-width:768px) and (max-width:991px){.container.page{max-width:100%;padding-left:15px;padding-right:15px}.leftCol{flex:0 0 250px;width:250px}}.explorer-page{display:flex;height:100%;width:100%;background:var(--canvas-bg-color);position:relative}.explorer-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden;background:var(--canvas-bg-color)}.explorer-content{flex:1 1 auto;min-height:0;width:100%;overflow:auto}.leftCol-close,.leftCol-head{display:none}.leftCol-body{flex:1 1 auto;min-height:0;overflow:auto}.leftCol-foot{padding:8px 12px;border-top:1px solid var(--border-color);background:var(--paper-bg-alt)}.leftCol-foot .last-update{display:block;border-top:none;padding:0;font-family:var(--font-sans);font-size:13px;font-weight:400;letter-spacing:0;line-height:1.5;color:var(--ink-muted)}.leftCol-foot{padding:10px 14px}.leftCol-backdrop{display:none}.paper-inline-warning{display:flex;align-items:flex-start;gap:8px;margin:8px;padding:10px 12px;background:rgba(138,107,30,.08);border:1px solid rgba(138,107,30,.35);border-radius:8px;color:var(--color);font-size:12.5px;line-height:1.4}.paper-inline-warning i{color:var(--status-progress);margin-top:2px;flex-shrink:0}.dark-mode .paper-inline-warning i{color:var(--status-progress)}@media (min-width:768px){.leftCol-body{padding:8px 4px}}.sidebar-toggle{display:none;align-items:center;gap:8px;position:fixed;bottom:18px;right:18px;z-index:1100;padding:10px 14px;background:var(--color);color:var(--canvas-bg-color);border:1px solid var(--color);border-radius:999px;font-family:var(--font-sans);font-size:13px;font-weight:500;box-shadow:0 6px 20px rgba(0,0,0,.15);cursor:pointer;outline:0}.sidebar-toggle:hover{background:var(--primary-hover-bg)}.sidebar-toggle:focus{outline:0}.sidebar-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media (max-width:991px){.sidebar-toggle{display:inline-flex}.leftCol{position:fixed;top:0;left:0;bottom:0;width:min(86vw,340px);max-width:340px;z-index:1200;border:none;border-right:1px solid var(--border-color);transform:translateX(0);transition:transform .22s ease;box-shadow:4px 0 24px rgba(0,0,0,.18);background:var(--sidebar-bg-color)}.leftCol.collapsed{transform:translateX(-105%);box-shadow:none;pointer-events:none}.leftCol-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt)}.leftCol-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.leftCol-close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;background:0 0;border:1px solid var(--border-color);border-radius:8px;color:var(--ink-muted);cursor:pointer}.leftCol-close:hover{color:var(--color);border-color:var(--color)}.leftCol-foot{display:block}.leftCol-body{padding:4px 0}.leftCol-backdrop{display:block;position:fixed;inset:0;background:rgba(0,0,0,.32);z-index:1150;backdrop-filter:blur(2px)}.dark-mode .leftCol-backdrop{background:rgba(0,0,0,.5)}}.admin-nav{display:flex;gap:4px;padding:4px;margin:8px 0 20px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap}.admin-nav a{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:7px;color:var(--ink-muted);text-decoration:none;font-family:var(--font-mono);font-size:11.5px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;transition:background-color .15s,color .15s;white-space:nowrap}.admin-nav a:hover{background:var(--paper-bg-alt);color:var(--color)}.admin-nav a.active{background:var(--color);color:var(--paper-card)}.admin-nav a i{font-size:12px;width:14px;text-align:center}@media (max-width:767px){.admin-nav{flex-direction:column}.admin-nav a{justify-content:flex-start}}.admin-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:16px}.admin-stat-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px}.admin-stat-card .stat-value{font-family:var(--font-serif);font-size:1.4rem;font-weight:400;line-height:1.1;color:var(--color)}.admin-stat-card .stat-label{font-family:var(--font-mono);font-size:9.5px;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;margin-top:4px}.admin-toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:16px 0 12px}.admin-toolbar .form-control{max-width:320px}@media (max-width:767px){.admin-toolbar .form-control{max-width:100%;width:100%}}.pagination-compact{display:inline-flex;align-items:center;gap:6px;flex-wrap:nowrap;font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}.pagination-compact input{width:60px;text-align:center;flex-shrink:0}.pagination-compact .btn{padding:4px 10px;min-height:34px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);flex-shrink:0}.pagination-compact>span{white-space:nowrap}.admin-list-item{display:flex;align-items:flex-start;gap:12px;padding:12px 0;border-bottom:1px solid var(--border-color)}.admin-list-item:last-child{border-bottom:none}.admin-list-item .item-main{flex:1;min-width:0}.admin-list-item .item-title{font-size:1rem;font-weight:600;margin-bottom:4px;word-break:break-word}.admin-list-item .item-meta{font-size:.85rem;opacity:.8}.admin-list-item .item-meta span{margin-right:12px;display:inline-block;margin-bottom:2px}.admin-list-item .item-actions{flex-shrink:0}@media (max-width:767px){.admin-list-item{flex-direction:column;gap:8px}.admin-list-item .item-actions{align-self:flex-start}}.status-badge{display:inline-block;font-size:11px;font-weight:600;padding:2px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.3px;vertical-align:middle}.status-badge{font-family:var(--font-sans);font-weight:500;letter-spacing:.02em;border:1px solid transparent;text-transform:none;font-size:11.5px;padding:1px 9px 2px;border-radius:999px}.status-badge.status-ready{background:#e8f3ec;color:#2f7a44;border-color:#cfe6d6}.status-badge.status-error{background:#fbe7e7;color:#b42318;border-color:#f3c7c7}.status-badge.status-download,.status-badge.status-preparing{background:#e4f0ee;color:#1f6a64;border-color:#d4d8fa}.status-badge.status-expired,.status-badge.status-expiring,.status-badge.status-removed,.status-badge.status-removing{background:var(--paper-bg-alt);color:var(--ink-muted);border-color:var(--border-color)}.status-badge.status-active{background:#e8f3ec;color:#2f7a44;border-color:#cfe6d6}.queue-job-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 18px;margin-bottom:10px}.queue-job-card .job-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;flex-wrap:wrap;gap:8px}.queue-job-card .job-id{font-family:var(--font-mono);font-size:.95rem;color:var(--color);word-break:break-all;display:flex;align-items:center;gap:10px}.queue-job-card .job-timestamps{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-muted);display:flex;flex-wrap:wrap;gap:14px}.queue-job-card .job-actions{display:flex;gap:8px;margin-top:10px}.queue-job-card .job-actions .btn{font-size:12px;padding:4px 12px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color)}.admin-section-header{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:24px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border-color)}.admin-section-header h2{margin:0;font-family:var(--font-mono);font-size:11px;font-weight:500;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em}.admin-section-header h2 i{margin-right:6px;color:var(--ink-muted)}.admin-section-header .section-count{background:var(--paper-bg-alt);color:var(--ink-muted);border:1px solid var(--border-color);font-family:var(--font-mono);font-size:11px;padding:1px 10px;border-radius:999px}.user-detail-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:22px;margin-bottom:24px}.user-detail-card .user-header{display:flex;align-items:center;gap:16px;margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--border-color);flex-wrap:wrap}.user-detail-card .user-header img{border-radius:50%}.user-detail-card .user-header h2{margin:0;font-family:var(--font-serif);font-weight:400;font-size:1.6rem;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.user-detail-grid{display:grid;grid-template-columns:160px 1fr;gap:10px 20px;font-size:.92rem}.user-detail-grid .detail-label{font-family:var(--font-mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);white-space:nowrap;padding-top:2px}.user-detail-grid .detail-value{word-break:break-all;color:var(--color)}@media (max-width:767px){.user-detail-grid{grid-template-columns:1fr}.user-detail-grid .detail-label{margin-top:8px;border-top:1px solid var(--border-color);padding-top:8px}.user-detail-grid .detail-label:first-child{border-top:none;margin-top:0;padding-top:0}}.paper-empty{min-height:calc(100vh - 60px);display:flex;align-items:center;justify-content:center;padding:40px 24px}.paper-empty-inner{max-width:560px;text-align:left}.paper-empty-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.4rem,6vw,4.4rem);line-height:1.02;letter-spacing:-.025em;margin:18px 0 20px;color:var(--color)}.paper-empty-title em{color:var(--accent);font-style:italic}.paper-empty-lede{font-size:17px;line-height:1.55;color:var(--ink-soft);text-wrap:pretty}.paper-empty-cta{display:flex;gap:10px;margin-top:28px;flex-wrap:wrap}.paper-page{max-width:1100px;margin:0 auto;padding:32px 32px 80px}@media (max-width:991px){.paper-page,.paper-page.container,.paper-page.container-fluid{padding:20px 18px 60px!important}.paper-page .row{margin-left:0;margin-right:0}.paper-page .row>.w-100,.paper-page .row>[class*=col]{padding-left:0;padding-right:0}.navbar.navbar-expand-lg{padding:10px 18px!important}.navbar .navbar-collapse{margin:10px -18px 0;padding:12px 18px 16px;border-top:1px solid var(--border-color);background:var(--header-bg-color)}.navbar .navbar-nav{align-items:stretch!important;gap:2px}.navbar .navbar-nav .nav-item,.navbar .navbar-nav .nav-link{width:100%}.navbar .nav-link{display:flex!important;align-items:center;justify-content:flex-start;margin:0!important;padding:12px 4px!important;border-bottom:1px solid var(--border-color)!important;border-radius:0;font-size:14px;line-height:1.2;gap:0;min-height:44px;box-sizing:border-box}.navbar .nav-link:not(.btn-signin):not(.user-chip){color:var(--color)!important}.navbar .nav-link .fa,.navbar .nav-link .fab,.navbar .nav-link .fas,.navbar .nav-link i{display:inline-flex!important;align-items:center;justify-content:center;width:28px;flex-shrink:0;font-size:16px;line-height:1;margin:0 10px 0 0!important}.navbar .navbar-nav .nav-item:last-child .nav-link,.navbar .navbar-nav>.nav-link:last-child{border-bottom-color:transparent!important}.navbar .nav-link.active{color:var(--color)!important;border-bottom-color:var(--border-color)!important}.navbar .nav-item>.nav-link:not(.btn-signin):not(.user-chip)::before{content:'';display:inline-block;width:28px;margin-right:10px;flex-shrink:0}.navbar .nav-link.nav-icon::before,.navbar .nav-link:has(>i)::before{display:none!important}.navbar .nav-link.nav-icon{width:100%!important;height:auto!important;justify-content:flex-start!important;padding:12px 4px!important}.navbar .nav-link.nav-icon::after{content:attr(title);font-family:var(--font-sans);font-size:14px;color:var(--color)}.navbar .btn-signin{display:inline-flex!important;width:auto!important;align-self:flex-start;justify-content:center;height:40px;padding:0 18px!important;margin:14px 0 0!important;border-radius:8px;border-bottom:none!important}.navbar .btn-signin i{margin:0 8px 0 0!important;width:auto!important}.navbar .user-chip-wrap{width:100%}.navbar .user-chip{display:flex!important;width:100%;margin:0!important;padding:10px 4px!important;background:0 0!important;border:none!important;border-bottom:1px solid var(--border-color)!important;border-radius:0!important;justify-content:flex-start;gap:0;min-height:44px}.navbar .user-chip::before{display:none!important}.navbar .user-chip img{width:28px!important;height:28px!important;margin-right:10px!important;flex-shrink:0}.navbar .user-chip .user-chip-name{font-size:14px;font-weight:500;color:var(--color)!important}.navbar .user-chip.dropdown-toggle::after{margin-left:auto;color:var(--ink-muted)}.navbar .user-chip-wrap .dropdown-menu{position:static!important;float:none;width:100%;margin:0!important;padding:0!important;background:0 0!important;border:none!important;box-shadow:none!important;transform:none!important}.navbar .user-chip-wrap .dropdown-menu .dropdown-item{padding:10px 4px 10px 42px!important;font-size:13.5px;color:var(--ink-muted)!important;background:0 0!important;border-bottom:1px solid var(--border-color);text-align:left!important}.navbar .user-chip-wrap .dropdown-menu .dropdown-item:last-child{border-bottom:none}.navbar .user-chip-wrap .dropdown-divider{display:none}.navbar-brand{border-right:none!important;padding-right:0!important;margin-right:0!important}.dark-mode .navbar .navbar-collapse{background:var(--header-bg-color);border-top-color:var(--border-color)}.dark-mode .navbar .nav-link{border-bottom-color:var(--border-color)!important}.dark-mode .navbar-toggler .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' d='M4 8h22M4 15h22M4 22h22'/%3E%3C/svg%3E")!important}}.status-pill{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:999px;background:var(--paper-bg-alt);border:1px solid var(--border-color);color:var(--ink-soft);font-family:var(--font-mono);font-size:11.5px;letter-spacing:.08em;text-transform:uppercase}.paper-progress{position:relative;height:auto;background:0 0;margin:18px 0 8px}.paper-progress::before{content:"";display:block;height:8px;background:var(--paper-bg-alt);border-radius:999px}.paper-progress .paper-progress-bar{position:absolute;top:0;left:0;height:8px;background:var(--color);border-radius:999px;transition:width .4s ease;min-width:4px}.paper-progress .paper-progress-label{display:flex;justify-content:space-between;align-items:center;margin-top:10px;font-family:var(--font-mono);font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted)}.paper-progress .paper-progress-pct{color:var(--color)}.paper-progress.paper-progress-ready .paper-progress-bar{background:var(--color)}.paper-error-card{margin-top:18px;padding:20px 22px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-left:3px solid var(--status-error);border-radius:10px;color:var(--color)}.dark-mode .paper-error-card{border-left-color:var(--status-error)}.paper-ratelimit-card{margin-top:18px;padding:20px 22px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-left:3px solid var(--status-progress);border-radius:10px;color:var(--color)}.dark-mode .paper-ratelimit-card{border-left-color:var(--status-progress)}.paper-ratelimit-head{display:flex;align-items:flex-start;gap:14px}.paper-ratelimit-head>i{font-size:18px;color:var(--status-progress);margin-top:4px;flex-shrink:0}.dark-mode .paper-ratelimit-head>i{color:var(--status-progress)}.paper-ratelimit-card strong{color:var(--color)}.paper-ratelimit-card{text-align:left;border-left-color:var(--status-progress)}.dark-mode .paper-ratelimit-card{border-left-color:var(--status-progress)}.paper-ratelimit-head>.ui-icon{width:18px;height:18px;margin-top:3px;flex-shrink:0;color:var(--status-progress)}.paper-ratelimit-card .paper-error-title{font-family:var(--font-sans);font-size:17px;font-weight:600}.paper-ratelimit-card .paper-error-msg{font-family:var(--font-sans);white-space:normal;margin-left:32px}.paper-ratelimit-card .paper-error-eyebrow{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.status-pill-ratelimit{border-color:var(--status-progress);color:var(--status-progress)}.dark-mode .status-pill-ratelimit{border-color:var(--status-progress);color:var(--status-progress)}.status-ratelimit{background:var(--status-progress)}.dark-mode .status-ratelimit{background:var(--status-progress)}.paper-error-head{display:flex;align-items:flex-start;gap:14px}.paper-error-head>i{font-size:18px;color:var(--status-error);margin-top:4px;flex-shrink:0}.dark-mode .paper-error-head>i{color:var(--status-error)}.paper-error-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.paper-error-title{font-family:var(--font-serif);font-size:1.4rem;line-height:1.2;margin-top:2px;color:var(--color)}.paper-error-msg{margin:12px 0 0;font-size:14px;line-height:1.55;color:var(--ink-soft);font-family:var(--font-mono);word-break:break-word;white-space:pre-wrap}.paper-error-hints{margin:14px 0 0;padding-left:18px;color:var(--ink-soft);font-size:13.5px;line-height:1.6}.paper-error-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}.paper-detail-grid{display:grid;grid-template-columns:160px 1fr;gap:10px 20px;margin-top:18px;font-size:.92rem}.paper-detail-grid .detail-label{font-family:var(--font-mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);white-space:nowrap;padding-top:2px}.paper-detail-grid .detail-value{word-break:break-all;color:var(--color)}.paper-detail-grid .detail-value a{color:var(--color);border-bottom:1px solid var(--border-color)}.paper-detail-grid .detail-value a:hover{border-bottom-color:var(--color)}@media (max-width:700px){.paper-detail-grid{grid-template-columns:1fr;gap:4px 0}.paper-detail-grid .detail-label{padding-top:8px}}.paper-support-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin:18px 0 28px}.paper-support-card{display:flex;flex-direction:column;gap:10px;padding:20px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;color:var(--color);text-decoration:none;transition:border-color .15s ease,transform .15s ease}.paper-support-card:hover{border-color:var(--color);text-decoration:none;color:var(--color)}.paper-support-card .paper-support-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);display:flex;align-items:center;gap:8px}.paper-support-card p{margin:0;font-size:14px;line-height:1.55;color:var(--ink-soft)}.paper-support-card .paper-support-cta{margin-top:auto;font-size:13px;font-weight:500;color:var(--color);display:inline-flex;align-items:center;gap:6px}.paper-support-card:hover .paper-support-cta i{transform:translateX(2px)}.paper-support-card .paper-support-cta i{transition:transform .15s ease}.paper-kofi-wrap{border:1px solid var(--border-color);border-radius:10px;overflow:hidden;background:var(--paper-card)}.paper-kofi-wrap iframe{border:0;width:100%;height:650px;display:block}.dark-mode .paper-kofi-wrap iframe{filter:invert(.88) hue-rotate(180deg) sepia(.15) saturate(1.2) brightness(1.1) contrast(.85)}@media (max-width:700px){.paper-kofi-wrap iframe{height:720px}}.pr-page{min-height:100%;background:var(--canvas-bg-color)}.pr-page-inner{padding-top:24px;padding-bottom:60px}.pr-header{margin:6px 0 18px}.pr-title{margin:4px 0 10px;font-size:clamp(1.6rem,3vw,2.4rem);line-height:1.15;word-break:break-word}.pr-header-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}.pr-header-meta .pr-meta-item{display:inline-flex;align-items:center;gap:6px}.pr-header-meta .pr-meta-item i{color:var(--ink-muted)}.pr-body-card{margin:18px 0 24px;padding:20px 22px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px}.pr-body-card .paper-section-eyebrow{margin-bottom:12px}.paper-tabs{display:flex;flex-wrap:wrap;gap:0;border-bottom:1px solid var(--border-color);margin:18px 0 0}.paper-tabs .paper-tab{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;background:0 0;border:0;border-bottom:2px solid transparent;font-family:var(--font-sans);font-size:13.5px;font-weight:500;color:var(--ink-muted);cursor:pointer;transition:color .15s ease,border-color .15s ease;margin-bottom:-1px}.paper-tabs .paper-tab:hover{color:var(--color)}.paper-tabs .paper-tab.active{color:var(--color);border-bottom-color:var(--color)}.paper-tabs .paper-tab i{color:inherit;opacity:.85}.paper-tabs .paper-tab:active,.paper-tabs .paper-tab:focus,.paper-tabs .paper-tab:focus-visible{outline:0;box-shadow:none}.paper-tabs .paper-tab:focus-visible{color:var(--color);background:var(--hover-bg-color);border-radius:6px 6px 0 0}.pr-diff{margin:16px 0 28px;display:flex;flex-direction:column;gap:16px}.diff-file-block{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;overflow:hidden}.diff-file-header{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--paper-bg-alt);border-bottom:1px solid var(--border-color);font-family:var(--font-mono);font-size:12.5px;color:var(--color)}.diff-file-icon{color:var(--ink-muted);flex-shrink:0}.diff-file-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:break-all}.diff-file-status{flex-shrink:0;padding:2px 8px;border-radius:999px;font-size:10px;letter-spacing:.12em;text-transform:uppercase;border:1px solid var(--border-color);color:var(--ink-muted);background:var(--paper-card)}.diff-file-status-added{color:#2f6b3e;border-color:rgba(47,107,62,.35);background:rgba(47,107,62,.08)}.diff-file-status-deleted{color:var(--status-error);border-color:rgba(161,58,46,.35);background:rgba(161,58,46,.08)}.diff-file-status-renamed{color:var(--status-progress);border-color:rgba(138,107,30,.35);background:rgba(138,107,30,.08)}.dark-mode .diff-file-status-added{color:#a7e2a7;border-color:rgba(167,226,167,.35);background:rgba(167,226,167,.08)}.dark-mode .diff-file-status-deleted{color:var(--status-error);border-color:rgba(255,139,123,.35);background:rgba(255,139,123,.08)}.dark-mode .diff-file-status-renamed{color:var(--status-progress);border-color:rgba(255,211,122,.35);background:rgba(255,211,122,.08)}.diff-file-table{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:12.5px;line-height:1.55;table-layout:fixed}.diff-file-table tr.diff-row{vertical-align:top}.diff-file-table .diff-gutter{width:48px;padding:0 10px;text-align:right;color:var(--ink-muted);background:var(--paper-bg-alt);border-right:1px solid var(--border-color);user-select:none;font-variant-numeric:tabular-nums;vertical-align:top;white-space:nowrap}.diff-file-table .diff-gutter-new{border-right:1px solid var(--border-color)}.diff-file-table .diff-sign{width:18px;padding:0 6px;text-align:center;color:var(--ink-muted);background:var(--paper-bg-alt);user-select:none;vertical-align:top}.diff-file-table .diff-code{padding:1px 12px;white-space:pre-wrap;word-break:break-word;color:var(--color)}.diff-row-hunk td{background:var(--paper-bg-alt)!important;color:var(--ink-muted);border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);font-size:12px;letter-spacing:.02em;padding-top:6px;padding-bottom:6px}.diff-row-hunk .diff-code{color:var(--ink-muted)}.diff-row-add .diff-code{background:rgba(47,107,62,.1);color:#1f4a2a}.diff-row-add .diff-gutter,.diff-row-add .diff-sign{background:rgba(47,107,62,.06);color:#2f6b3e}.diff-row-remove .diff-code{background:rgba(161,58,46,.1);color:#6e1f1a}.diff-row-remove .diff-gutter,.diff-row-remove .diff-sign{background:rgba(161,58,46,.06);color:var(--status-error)}.dark-mode .diff-row-add .diff-code{background:rgba(167,226,167,.1);color:#c9f0c9}.dark-mode .diff-row-add .diff-gutter,.dark-mode .diff-row-add .diff-sign{background:rgba(167,226,167,.06);color:#a7e2a7}.dark-mode .diff-row-remove .diff-code{background:rgba(255,139,123,.1);color:#ffc9c0}.dark-mode .diff-row-remove .diff-gutter,.dark-mode .diff-row-remove .diff-sign{background:rgba(255,139,123,.06);color:var(--status-error)}@media (max-width:700px){.diff-file-table{font-size:11.5px}.diff-file-table .diff-gutter{width:36px;padding:0 6px}.diff-file-table .diff-sign{width:14px;padding:0 4px}.diff-file-table .diff-code{padding:1px 8px}}.pr-comments{list-style:none;margin:16px 0 28px;padding:0;display:flex;flex-direction:column;gap:12px}.pr-comment{padding:16px 18px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px}.pr-comment-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;padding-bottom:8px;margin-bottom:8px;border-bottom:1px solid var(--border-color)}.pr-comment-author{font-weight:600;color:var(--color);font-size:14px;display:inline-flex;align-items:center;gap:6px}.pr-comment-author i{color:var(--ink-muted);font-size:12px}.pr-comment-date{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-muted);letter-spacing:.02em}.pr-comment-body{color:var(--color);font-size:14px;line-height:1.6}.pr-comment-body :last-child{margin-bottom:0}@media (max-width:700px){.pr-page-inner{padding-top:14px;padding-bottom:40px}.pr-body-card{padding:14px 14px;border-radius:8px}.pr-comment{padding:12px 14px}.paper-tabs .paper-tab{padding:10px 12px;font-size:13px}.pr-diff pre{font-size:11.5px;padding:12px}}.toast{background-color:var(--paper-card)!important;color:var(--color)!important;border:1px solid var(--border-color)!important;border-radius:10px!important;box-shadow:0 8px 24px rgba(0,0,0,.08);backdrop-filter:none;min-width:280px;max-width:calc(100vw - 24px)}.toast .toast-header{background:0 0!important;color:var(--color)!important;border-bottom:1px solid var(--border-color)!important;font-family:var(--font-sans);padding:10px 12px}.toast .toast-header strong{font-weight:600;color:var(--color);letter-spacing:.01em}.toast .toast-header .close{color:var(--ink-muted)!important;text-shadow:none;opacity:.9;font-size:1.2rem}.toast .toast-header .close:hover{color:var(--color)!important;opacity:1}.toast .toast-body{color:var(--color)!important;padding:12px;font-size:14px;line-height:1.45}.dark-mode .toast{box-shadow:0 8px 24px rgba(0,0,0,.5)}.paper-crumbs{font-family:var(--font-sans);font-size:12px;color:var(--ink-muted);letter-spacing:0}.paper-crumbs .here,.paper-crumbs a{color:var(--color);text-decoration:none}.paper-page-title{font-family:var(--font-sans);font-weight:600;font-size:32px;line-height:1.2;letter-spacing:-.035em;margin:8px 0 6px;color:var(--color)}.paper-page-title em{color:inherit;font-style:normal}.paper-page-lede{color:var(--ink-soft);font-size:16px;max-width:640px;line-height:1.55}.paper-meta-rule{display:flex;flex-wrap:wrap;gap:16px 24px;align-items:center;margin:22px 0 18px;padding:12px 0;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);font-family:var(--font-mono);font-size:11.5px;color:var(--ink-muted);letter-spacing:.02em}.paper-meta-rule b{color:var(--color);font-weight:500}.paper-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:10px;padding:20px 22px}.dark-mode .paper-card{background:var(--admin-stat-bg)}.paper-section-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);font-weight:500;margin-bottom:10px;display:block}.paper-section-title{font-family:var(--font-serif);font-weight:400;font-size:22px;letter-spacing:-.01em;margin:0 0 8px}.paper-kbd{display:inline-block;font-family:var(--font-mono);font-size:10.5px;padding:1px 6px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:3px;background:var(--paper-card,#fff);color:var(--color)}.paper-pill{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:10.5px;padding:2px 8px;border:1px solid var(--border-color);border-radius:999px;color:var(--ink-soft);background:var(--paper-card,#fff);letter-spacing:.04em}.paper-pill.accent{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}.paper-pill.good{border-color:#2f6b3e;color:#2f6b3e}.paper-pill.warn{border-color:var(--status-progress);color:var(--status-progress)}.paper-pill.bad{border-color:var(--status-error);color:var(--status-error)}.paper-row{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start;padding:18px 0;border-bottom:1px solid var(--border-color)}.paper-row:first-child{border-top:1px solid var(--border-color)}.paper-row .row-title{font-family:var(--font-serif);font-size:20px;letter-spacing:-.01em;line-height:1.2}.paper-row .row-title a{color:var(--color)}.paper-row .row-meta{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);letter-spacing:.03em;margin-top:6px;display:flex;flex-wrap:wrap;gap:14px}.paper-row .row-actions{display:flex;gap:6px;flex-shrink:0}.paper-form-step{display:grid;grid-template-columns:48px 1fr;gap:20px;padding:22px 0;border-bottom:1px solid var(--border-color)}.paper-form-step:first-child{border-top:1px solid var(--border-color)}.paper-form-step .step-num{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-muted);padding-top:4px;letter-spacing:.02em}.paper-form-step .step-title{font-family:var(--font-serif);font-size:22px;letter-spacing:-.01em;line-height:1.2;margin-bottom:4px}.paper-form-step .step-sub{color:var(--ink-soft);font-size:13.5px;margin-bottom:14px;text-wrap:pretty}.paper-field-label{font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--color);margin-bottom:6px;display:block}.paper-switch-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;gap:16px}.paper-switch-row+.paper-switch-row{border-top:1px dashed var(--border-soft)}.paper-switch-row .sw-label{font-weight:500}.paper-switch-row .sw-sub{display:block;font-size:12.5px;color:var(--ink-muted);margin-top:2px}.paper-page .custom-select,.paper-page .form-control{border-radius:6px;border-color:var(--border-color);font-size:14px;padding:8px 12px;height:auto;background:var(--input-bg);color:var(--input-color)}.paper-page .custom-select:focus,.paper-page .form-control:focus{color:var(--input-color);background:var(--input-bg);border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.custom-select:focus,.form-control:focus,input:focus,select:focus,textarea:focus{color:var(--input-color)!important;background-color:var(--input-bg)!important}.form-control::selection,input::selection,textarea::selection{background:var(--accent-soft);color:var(--input-color)}.dark-mode .form-control::selection,.dark-mode input::selection,.dark-mode textarea::selection{background:rgba(124,199,190,.3);color:var(--input-color)}.btn.btn-ink{background:var(--color);color:var(--canvas-bg-color);border:1px solid var(--color);font-weight:500;border-radius:6px;padding:8px 14px}.btn.btn-ink:hover{background:var(--primary-hover-bg);color:var(--canvas-bg-color);border-color:var(--primary-hover-bg)}.btn.btn-accent{background:var(--accent);color:#fff;border:1px solid var(--accent);font-weight:500;border-radius:6px}.btn.btn-accent:hover{background:#17544f;border-color:#17544f;color:#fff}.paper-page .dashboard-filter-row{display:flex;align-items:center;gap:10px;padding:14px 0;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);margin:18px 0 0;flex-wrap:wrap}.paper-page .dashboard-filter-row .form-control{height:38px;background:var(--paper-card)}.paper-page .dashboard-filter-controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.paper-page .dashboard-filter-row .btn-group .btn,.paper-page .dashboard-filter-row .dropdown-toggle{background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);height:38px;padding:0 14px;font-size:14px;font-weight:500;border-radius:var(--radius-control,6px);display:inline-flex;align-items:center;gap:6px;line-height:1}.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-label{color:var(--ink-muted);font-weight:500}.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-label::after{content:"·";margin-left:6px;color:var(--ink-faint)}.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-value{font-weight:500}.paper-page .dashboard-filter-row .dropdown-toggle .fas{font-size:11px;color:var(--ink-muted)}.paper-page .dashboard-filter-row .btn-group .btn:hover,.paper-page .dashboard-filter-row .dropdown-toggle:hover{background:var(--hover-bg-color);border-color:var(--border-color);color:var(--color)}.paper-page .dashboard-filter-row .btn-group{gap:0}.paper-page .dashboard-filter-row .btn-group .btn{border-radius:0;border-right-width:0}.paper-page .dashboard-filter-row .btn-group .btn:first-child{border-top-left-radius:6px;border-bottom-left-radius:6px}.paper-page .dashboard-filter-row .btn-group .btn:last-child{border-top-right-radius:6px;border-bottom-right-radius:6px;border-right-width:1px}.paper-page .dashboard-filter-row .btn-group .btn.btn-primary{background:var(--color);color:var(--canvas-bg-color);border-color:var(--color)}.paper-page .dashboard-filter-row .search-wrap{flex:1 1 280px;min-width:220px}.paper-page .quota-row{display:grid;grid-template-columns:repeat(3,1fr);gap:40px;margin:24px 0 0;padding:0;background:0 0;border:none;border-radius:0}.paper-page .quota-label{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);opacity:1}.paper-page .quota-value{font-family:var(--font-mono);font-size:13px;font-variant-numeric:tabular-nums;color:var(--color)}.paper-page .quota-value .quota-unlimited-tag{font-family:var(--font-sans);font-size:12px;color:var(--ink-muted);margin-left:4px}.paper-page .quota-track{height:6px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:999px;overflow:hidden;width:100%;min-width:0}.paper-page .quota-track .quota-fill{height:100%;min-width:2px;border-radius:999px;background:var(--color);transition:width .3s ease}.paper-page .quota-track.quota-warn .quota-fill{background:var(--status-progress)}.paper-page .quota-track.quota-danger .quota-fill{background:var(--status-error)}.paper-page .quota-track.quota-unlimited{background:repeating-linear-gradient(-45deg,var(--paper-bg-alt) 0 4px,var(--border-soft,var(--border-color)) 4px 6px)}.paper-page .repo-list{margin-top:8px}.paper-page .repo-list-item{padding:20px 0;gap:16px;border-bottom:1px solid var(--border-soft,var(--border-color))}.paper-page .repo-list-item:first-child{border-top:none}.paper-page .repo-list-item-header{gap:10px;margin-bottom:6px}.paper-page .repo-name{font-family:var(--font-mono);font-size:15px;color:var(--color)}.paper-page .repo-source{font-size:13px;color:var(--ink-soft);opacity:1;gap:10px}.paper-page .repo-source a{color:var(--ink-soft);border-bottom:1px dotted var(--border-color)}.paper-page .repo-source a:hover{color:var(--color)}.paper-page .repo-meta{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);opacity:1;gap:16px;margin-top:10px}.paper-page .type-badge{font-family:var(--font-mono);font-size:11px;padding:3px 8px;letter-spacing:.1em}.paper-page .repo-list-item-actions .dropdown-toggle{background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);border-radius:6px;font-size:13px;padding:6px 12px;height:auto}.paper-settings{max-width:1180px;margin:0 auto;padding:48px 32px 80px}.paper-settings .paper-page-title{max-width:14em}.paper-settings-body{display:grid;grid-template-columns:220px 1fr;gap:60px;margin-top:32px;border-top:1px solid var(--border-color);padding-top:28px}.paper-settings-toc{position:sticky;top:32px;align-self:start}.paper-settings-toc-head{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:12px}.paper-settings-toc nav{display:flex;flex-direction:column;gap:2px}.paper-settings-toc nav a{padding:8px 0 8px 14px;border-left:2px solid transparent;color:var(--ink-soft);font-size:14px;text-decoration:none}.paper-settings-toc nav a.active,.paper-settings-toc nav a:hover{color:var(--color);border-left-color:var(--color)}.paper-settings-main{min-width:0;max-width:720px}.paper-settings-section{padding:28px 0;border-bottom:1px solid var(--border-soft,var(--border-color))}.paper-settings-section:first-child{padding-top:0}.paper-settings-section:last-of-type{border-bottom:none}.paper-settings-section .form-group{margin-bottom:18px}.paper-settings-section .form-check{padding:10px 0 10px 28px;border-bottom:1px solid var(--border-soft,var(--border-color))}.paper-settings-section .form-check:last-child{border-bottom:none}.paper-settings-section .form-check-label{font-family:var(--font-sans);font-size:14px;font-weight:500;color:var(--color);cursor:pointer}.paper-settings-section .form-check-input{cursor:pointer;width:16px;height:16px;margin-top:3px;margin-left:-28px;accent-color:var(--color)}.paper-settings-section .form-text{color:var(--ink-soft);font-size:13px;margin-top:4px;max-width:48em}.paper-settings-section .paper-section-copy{margin:-4px 0 14px}.paper-settings-footer{padding-top:24px;display:flex;flex-direction:column;gap:14px}.paper-settings-footer .btn-ink{align-self:flex-start}.paper-settings-actions{display:flex;align-items:center;flex-wrap:wrap;gap:14px}.paper-settings-hint{font-size:13px;color:var(--ink-muted)}.paper-inline-alert{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border-radius:var(--radius-control,6px);border:1px solid var(--border-color);background:var(--paper-bg-alt);font-size:13px;color:var(--color)}.paper-inline-alert i{margin-top:2px;color:var(--ink-muted)}.paper-inline-alert-error{border-color:var(--status-error);background:var(--status-error-soft)}.paper-inline-alert-error i{color:var(--status-error)}.paper-account-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border:1px solid var(--border-color);border-radius:var(--radius-surface,10px);background:var(--paper-card);margin-bottom:20px}.paper-account-card img{flex-shrink:0}.paper-account-identity{flex:1 1 auto;min-width:0}.paper-account-name{font-family:var(--font-mono);font-size:14px;font-weight:500;color:var(--color)}.paper-account-meta{font-size:13px;color:var(--ink-muted);margin-top:2px}.paper-danger-zone{padding:16px;border:1px solid var(--status-error);border-radius:var(--radius-surface,10px);background:var(--status-error-soft)}.paper-danger-head{font-weight:600;font-size:14px;color:var(--status-error);margin-bottom:6px}.paper-danger-copy{font-size:13px;color:var(--ink-soft);max-width:52em;margin-bottom:12px}.paper-danger-zone .paper-inline-alert{margin-bottom:12px}.btn.btn-outline-danger{background:0 0;color:var(--status-error);border:1px solid var(--status-error);font-weight:500;border-radius:var(--radius-control,6px);padding:8px 14px}.btn.btn-outline-danger:hover{background:var(--status-error);color:#fff;border-color:var(--status-error)}.navbar .user-menu{min-width:260px}.navbar .user-menu .dropdown-item{display:flex;align-items:flex-start;gap:10px;padding:8px 10px!important}.navbar .user-menu .dropdown-item i{width:16px;margin-top:3px;font-size:13px;color:var(--ink-muted);text-align:center}.navbar .user-menu .dropdown-item span{display:flex;flex-direction:column;line-height:1.3}.navbar .user-menu .dropdown-item small{font-size:12px;color:var(--ink-muted)}.navbar .user-menu .dropdown-header{font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);padding:8px 10px 6px}.paper-field-label{font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--color);margin-bottom:6px;display:block}@media (max-width:900px){.paper-settings-body{grid-template-columns:1fr;gap:24px}.paper-settings-toc{position:static}.paper-settings-toc nav{flex-direction:row;flex-wrap:wrap;gap:8px}.paper-settings-toc nav a{border-left:none;border-bottom:2px solid transparent;padding:4px 0}.paper-settings-toc nav a.active,.paper-settings-toc nav a:hover{border-bottom-color:var(--color);border-left:none}}.paper-table{display:block;margin-top:20px;background:0 0;border:none;border-top:1px solid var(--border-color);border-radius:0;overflow:visible}.paper-table .paper-table-head,.paper-table .paper-table-row{display:grid;grid-template-columns:minmax(280px,2.4fr) minmax(140px,1fr) 140px 90px 140px 52px;align-items:center;gap:18px;padding:12px 4px}.paper-table.paper-table-conferences .paper-table-head,.paper-table.paper-table-conferences .paper-table-row{grid-template-columns:minmax(280px,2.4fr) 140px 90px 220px 52px}.paper-table.paper-table-repos .paper-table-head,.paper-table.paper-table-repos .paper-table-row{grid-template-columns:minmax(280px,2.4fr) minmax(140px,1fr) 90px 140px 52px}.paper-table.paper-table-repos.has-bulk .paper-table-head,.paper-table.paper-table-repos.has-bulk .paper-table-row{grid-template-columns:28px minmax(280px,2.4fr) minmax(140px,1fr) 90px 140px 52px;gap:12px}.paper-table .admin-users-row{grid-template-columns:28px minmax(280px,2.4fr) 90px 120px 100px 52px!important;gap:12px!important}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(320px,3fr) 190px 90px 170px 52px;gap:20px;padding:8px 4px}.paper-table.paper-table-dashboard .cell-anon .anon-text{gap:2px}.paper-table.paper-table-dashboard .cell-anon .repo-name{padding:0}.paper-table .paper-table-head{background:0 0;border-bottom:1px solid var(--border-color);font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);padding-top:8px;padding-bottom:8px}.paper-table .paper-table-head .sortable{cursor:pointer;user-select:none;display:inline-flex;align-items:center;gap:6px;background:0 0;border:none;padding:2px 0;margin:0;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;border-radius:4px}.paper-table .paper-table-head .sortable:hover{color:var(--color)}.paper-table .paper-table-head .sortable.active{color:var(--color)}.paper-table .paper-table-head .sortable i{font-size:10px}.paper-table .paper-table-head .num,.paper-table .paper-table-row .num{text-align:right}.paper-table .paper-table-head .num .sortable{flex-direction:row-reverse}.paper-table .paper-table-row{border-top:1px solid var(--border-soft,var(--border-color));font-size:14px;transition:background-color .12s ease}.paper-table .paper-table-row:hover{background:var(--hover-bg-color)}.paper-table .paper-table-row.row-clickable{cursor:pointer}.paper-table .paper-table-row.repo-inactive>:not(.cell-actions){opacity:.55}.paper-table .paper-table-row.repo-error{background:var(--status-error-soft);border-left:2px solid var(--status-error);padding-left:6px}.paper-table .paper-table-row.repo-error:hover{background:var(--status-error-soft-hover)}.cell-status .status-sub.status-sub-error,.paper-table .cell-status .status-sub-error{color:var(--status-error)}.paper-table .cell-status .status-sub-warn{color:var(--status-progress)}.paper-table .cell-anon{display:flex;align-items:center;gap:12px;min-width:0}.paper-table .cell-anon .type-badge{flex-shrink:0;min-width:52px;justify-content:center}.paper-table .cell-anon .anon-text{min-width:0;display:flex;flex-direction:column;gap:3px}.paper-table .cell-anon .anon-title{display:flex;align-items:center;gap:8px;min-width:0}.paper-table .cell-anon .anon-title .type-badge{margin-left:0}.paper-table .cell-anon .repo-name{font-family:var(--font-mono);font-size:14px;color:var(--color);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;max-width:100%;padding:2px 0}.paper-table .cell-anon .repo-name-static{color:var(--ink-muted);font-style:italic}.paper-table .cell-anon .anon-sub{font-family:var(--font-sans);font-size:13px;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:10px;min-width:0}.paper-table .cell-anon .anon-sub .anon-source{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-anon .anon-sub .conf-tag{flex:0 1 auto;min-width:72px;max-width:50%}.paper-table .cell-anon .anon-sub a{color:var(--ink-muted);border-bottom:1px dotted var(--border-color);padding:4px 0;margin:-4px 0}.paper-table .cell-anon .anon-sub a:hover{color:var(--color)}.paper-table .cell-anon .anon-sub .commit-hash{font-family:var(--font-mono);font-size:12px}.paper-table .cell-conf{font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-conf span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-status{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;font-size:14px;color:var(--color);min-width:0}.paper-table .cell-status .status-line{display:inline-flex;align-items:center;gap:8px}.paper-table .cell-status .status-sub{flex-basis:100%;font-family:var(--font-sans);font-size:13px;line-height:1.3;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-views{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--color)}.paper-table .cell-expires{font-family:var(--font-sans);font-size:13px;color:var(--ink-soft)}.paper-table .cell-expires .expires-never{color:var(--ink-muted)}.paper-table .cell-expires .expires-past{color:var(--ink-muted)}.paper-table .empty-dash{color:var(--ink-faint)}.paper-table .cell-actions{display:flex;justify-content:flex-end}.status-dot{width:8px;height:8px;border-radius:999px;display:inline-block;flex-shrink:0;background:var(--status-inactive)}.status-dot.status-ready{background:var(--status-ready)}.status-dot.status-error{background:var(--status-error)}.status-dot.status-preparing,.status-dot.status-progress{background:var(--status-progress)}.status-dot.status-expired,.status-dot.status-removed{background:var(--status-inactive)}.dashboard-page{padding-bottom:96px}.dashboard-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;margin-top:12px;font-size:13px;color:var(--ink-muted)}.dashboard-meta .dashboard-count{font-variant-numeric:tabular-nums}.btn-link-inline{background:0 0;border:none;padding:2px 4px;margin:-2px 0;font:inherit;color:var(--link-color);cursor:pointer;border-radius:4px}.btn-link-inline:hover{color:var(--link-hover-color);text-decoration:underline}.btn.btn-outline-ink{background:0 0;color:var(--color);border:1px solid var(--border-color);font-weight:500;border-radius:var(--radius-control,6px);padding:8px 14px}.btn.btn-outline-ink:hover{background:var(--hover-bg-color);color:var(--color);border-color:var(--ink-faint)}.paper-page .dropdown-menu .dropdown-item-danger,.paper-page .dropdown-menu .dropdown-item-danger i{color:var(--status-error)}.paper-page .dropdown-menu .dropdown-item-danger:focus,.paper-page .dropdown-menu .dropdown-item-danger:hover{background:var(--status-error-soft-hover);color:var(--status-error)}.paper-page .dropdown-menu button.dropdown-item{width:100%;text-align:left;background:0 0;border:none}.paper-page .dropdown-menu .dropdown-divider{border-top-color:var(--border-soft,var(--border-color));margin:4px 0}.paper-table .paper-table-skeleton{pointer-events:none}.skeleton{display:block;background:var(--paper-bg-alt);border-radius:4px;position:relative;overflow:hidden}.skeleton::after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,var(--hover-bg-color),transparent);animation:skeleton-shimmer 1.4s infinite}.skeleton-line{height:12px}.skeleton-line+.skeleton-line{margin-top:8px}.skeleton-line-sm{height:10px}.skeleton-badge{width:52px;height:22px;border-radius:999px;flex-shrink:0}@keyframes skeleton-shimmer{to{transform:translateX(100%)}}@media (prefers-reduced-motion:reduce){.skeleton::after{animation:none}}.paper-table .paper-table-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:56px 20px;color:var(--ink-muted);text-align:center;border-top:1px solid var(--border-soft,var(--border-color))}.paper-table .paper-table-empty>i{font-size:1.6rem;color:var(--ink-faint)}.paper-table .paper-table-empty-actions{display:flex;gap:8px;margin-top:8px}.bulk-bar{display:flex;align-items:center;gap:8px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:8px;padding:8px 12px;margin:8px 0}.paper-page.admin-page .paper-page-title{font-size:1.6rem;margin:4px 0}.paper-page.admin-page .paper-page-lede{display:none}.paper-page.admin-page .paper-crumbs{margin-bottom:4px}.paper-page.admin-page .admin-nav{margin:6px 0 12px}.admin-filter-toolbar{display:flex;flex-direction:column;gap:6px;margin:8px 0 12px;position:sticky;top:0;z-index:4;background:var(--canvas-bg-color);padding:6px 0;border-bottom:1px solid var(--border-soft,var(--border-color));box-shadow:0 1px 0 var(--canvas-bg-color)}@media (max-width:900px){.admin-filter-toolbar{position:static;box-shadow:none}}.admin-filter-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.admin-filter-row .search-wrap{flex:1;min-width:220px}.admin-filter-row .search-wrap .form-control{height:34px}.admin-filter-inline{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--ink-muted)}.admin-filter-inline>label{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-muted);margin:0;white-space:nowrap}.admin-filter-inline .form-control-sm{height:28px;padding:2px 8px;font-size:12px}.admin-filter-inline input[type=search].form-control-sm,.admin-filter-inline input[type=text].form-control-sm{width:130px}.admin-filter-inline input[type=date].form-control-sm{width:130px}.admin-filter-inline select.form-control-sm{width:auto;min-width:88px}.admin-filter-inline .arrow{color:var(--ink-muted);font-size:11px}.admin-filter-spacer{flex:1}.admin-active-chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:12px;color:var(--ink-muted)}.admin-active-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 4px 2px 10px;border:1px solid var(--border-color);border-radius:999px;background:var(--hover-bg-color);font-size:12px;color:var(--color)}.admin-active-chip .key{color:var(--ink-muted);font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.06em}.admin-active-chip button{background:0 0;border:none;cursor:pointer;padding:0 4px;color:var(--ink-muted);font-size:12px;border-radius:50%}.admin-active-chip button:hover{color:#b42318}.admin-search-hint{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);pointer-events:none;border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;background:var(--paper-card)}.admin-filter-row .search-wrap{position:relative}.admin-filter-row .search-wrap .form-control{padding-right:40px}.paper-table-row.row-selected{background:rgba(47,122,68,.06)}.queue-state-pills{display:inline-flex;gap:6px;margin-left:8px;flex-wrap:wrap}.queue-state-pills .pill{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--ink-muted)}.queue-state-pills .pill-active{color:var(--status-progress);border-color:rgba(196,138,46,.4)}.queue-state-pills .pill-failed{color:#b42318;border-color:rgba(180,35,24,.4)}.queue-state-pills .pill-completed{color:#2f7a44;border-color:rgba(47,122,68,.3)}.queue-state-pills .pill-waiting{color:var(--ink-soft)}.queue-state-pills .pill-delayed{color:var(--ink-muted)}.admin-summary{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:8px 0 12px;font-size:13px}.admin-summary .summary-total{font-family:var(--font-serif);font-size:1.05rem;color:var(--color);margin-right:8px}.admin-summary .summary-meta{color:var(--ink-muted);font-size:12px;margin-right:8px}.admin-summary .summary-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--ink-muted);cursor:pointer;font-size:12px;user-select:none;transition:background-color .1s,border-color .1s}.admin-summary .summary-pill:hover{background:var(--hover-bg-color)}.admin-summary .summary-pill .count{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--color)}.admin-summary .summary-pill.active{background:var(--primary-bg);color:var(--primary-color);border-color:var(--primary-bg)}.admin-summary .summary-pill.active .count{color:inherit}.admin-summary .summary-pill.error .count{color:#b42318}.admin-summary .summary-pill.warn .count{color:var(--status-progress)}.admin-summary .summary-pill.ok .count{color:#2f7a44}.dark-mode .admin-summary .summary-pill.error .count{color:#f08a82}.dark-mode .admin-summary .summary-pill.warn .count{color:var(--status-progress)}.dark-mode .admin-summary .summary-pill.ok .count{color:#7dc894}.dark-mode .queue-state-pills .pill-active{color:var(--status-progress);border-color:rgba(232,185,112,.4)}.dark-mode .queue-state-pills .pill-failed{color:#f08a82;border-color:rgba(240,138,130,.4)}.dark-mode .queue-state-pills .pill-completed{color:#7dc894;border-color:rgba(125,200,148,.3)}.job-progress{flex:1;height:6px;background:var(--border-soft,var(--border-color));border-radius:999px;overflow:hidden;margin-left:12px;max-width:200px}.job-progress-bar{height:100%;background:#2f7a44;transition:width .3s}.paper-table-empty{padding:48px 4px;text-align:center;color:var(--ink-muted);display:flex;flex-direction:column;align-items:center;gap:8px;border-top:1px solid var(--border-soft,var(--border-color))}.paper-table-empty>i{font-size:1.6rem}.q-header{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:4px}.q-header-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.q-range-btns{display:inline-flex;border:1px solid var(--border-color);border-radius:8px;overflow:hidden}.q-range-btns .btn{border:none;border-radius:0;padding:5px 14px;font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);background:var(--paper-card)}.q-range-btns .btn+.btn{border-left:1px solid var(--border-color)}.q-range-btns .btn.active{background:var(--color);color:var(--paper-card)}.btn-dark{background:var(--color)!important;color:var(--paper-card)!important;border-color:var(--color)!important;border-radius:999px;padding:5px 18px;font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.04em}.q-cards{display:flex;gap:10px;margin:16px 0;flex-wrap:wrap}.q-card{flex:1;min-width:140px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:14px 16px;cursor:pointer;transition:border-color .15s,box-shadow .15s}.q-card:hover{border-color:var(--ink-muted)}.q-card.selected{border-color:var(--color);box-shadow:0 0 0 1px var(--color)}.q-card-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.q-dot{width:8px;height:8px;border-radius:50%;background:#2f7a44;flex-shrink:0}.q-dot-red{background:#b42318}.dark-mode .q-dot{background:#7dc894}.dark-mode .q-dot-red{background:#f08a82}.q-card-name{font-family:var(--font-mono);font-size:11.5px;font-weight:500;color:var(--ink-muted);letter-spacing:.04em}.q-card-count{font-family:var(--font-serif);font-size:2rem;font-weight:400;line-height:1.1;color:var(--color);margin-bottom:6px}.q-card-sub{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-muted);letter-spacing:.04em}.q-card-bar{height:4px;background:var(--border-color);border-radius:2px;overflow:hidden;margin-top:6px}.q-card-bar-fill{height:100%;background:#1f6a64;border-radius:2px;transition:width .3s}.dark-mode .q-card-bar-fill{background:#7cc7be}.q-detail{display:grid;grid-template-columns:1fr 320px;gap:16px;margin-bottom:20px}.q-throughput{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px}.q-chart-wrap{position:relative}.q-throughput canvas{width:100%;display:block}.q-chart-tooltip{position:absolute;pointer-events:none;background:var(--paper-card);border:1px solid var(--border-color);border-radius:6px;padding:6px 10px;font-family:var(--font-mono);font-size:11px;line-height:1.6;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.15);z-index:10}.q-chart-tooltip .q-tip-time{color:var(--ink-muted);margin-bottom:2px}.q-chart-tooltip .q-tip-completed{color:#1f6a64}.q-chart-tooltip .q-tip-failed{color:#b42318}.dark-mode .q-chart-tooltip .q-tip-completed{color:#7cc7be}.dark-mode .q-chart-tooltip .q-tip-failed{color:#f08a82}.q-chart-crosshair{position:absolute;top:0;width:1px;height:100%;background:var(--ink-muted);opacity:.3;pointer-events:none;z-index:9}.q-stats-panel{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px;display:flex;flex-direction:column}.q-section-label{font-family:var(--font-mono);font-size:11px;font-weight:500;color:var(--ink-muted);text-transform:lowercase;letter-spacing:.04em;margin-bottom:14px}.q-section-right{float:right;text-transform:uppercase;font-size:10px;letter-spacing:.08em}.q-legend-completed{color:#1f6a64}.q-legend-failed{color:#b42318;margin-left:8px}.q-legend-exec{color:var(--status-progress);margin-left:8px;letter-spacing:2px}.dark-mode .q-legend-completed{color:#7cc7be}.dark-mode .q-legend-failed{color:#f08a82}.dark-mode .q-legend-exec{color:var(--status-progress)}.q-chart-tooltip .q-tip-exec{color:var(--status-progress)}.dark-mode .q-chart-tooltip .q-tip-exec{color:var(--status-progress)}.q-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 20px;flex:1}.q-stat-label{font-family:var(--font-mono);font-size:9.5px;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:2px}.q-stat-value{font-family:var(--font-serif);font-size:1.3rem;font-weight:400;color:var(--color);line-height:1.2}.q-stats-actions{display:flex;gap:8px;margin-top:16px;padding-top:14px;border-top:1px solid var(--border-color)}.q-stats-actions .btn{font-family:var(--font-mono);font-size:11px;padding:6px 14px;border-radius:8px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color)}.q-jobs{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px}.q-jobs-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:12px}.q-state-filters{display:inline-flex;gap:6px;flex-wrap:wrap}.q-state-toggle{cursor:pointer;display:inline-flex;align-items:center}.q-state-toggle input{display:none}.q-state-chip{font-family:var(--font-mono);font-size:11px;padding:3px 10px;border-radius:10px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--ink-muted);transition:opacity .15s,background .15s;text-transform:capitalize}.q-state-toggle input:checked+.q-state-chip{opacity:1}.q-state-toggle input:not(:checked)+.q-state-chip{opacity:.4;text-decoration:line-through}.q-state-badge,.q-state-chip{font-family:var(--font-mono);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.3px}.q-state-badge{display:inline-block;padding:2px 8px;border-radius:8px}.q-state-active{background:#dbeafe;color:#1d4ed8}.q-state-waiting{background:#fef3c7;color:#92400e}.q-state-delayed{background:#e0e7ff;color:#4338ca}.q-state-failed{background:#fee2e2;color:#b91c1c}.q-state-completed{background:#dcfce7;color:#166534}.dark-mode .q-state-active{background:rgba(59,130,246,.2);color:#93bbfd}.dark-mode .q-state-waiting{background:rgba(251,191,36,.15);color:#fbbf24}.dark-mode .q-state-delayed{background:rgba(99,102,241,.2);color:#7cc7be}.dark-mode .q-state-failed{background:rgba(239,68,68,.2);color:#fca5a5}.dark-mode .q-state-completed{background:rgba(34,197,94,.15);color:#86efac}.q-cell-state{width:120px}.q-delay-hint{display:block;font-size:10px;color:var(--ink-muted);margin-top:2px;font-family:var(--font-mono)}.q-search-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}.q-search-row .form-control{max-width:320px;height:34px}.q-auto-refresh{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);display:inline-flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap}.q-table{width:100%;border-collapse:collapse}.q-table thead th{font-family:var(--font-mono);font-size:9.5px;font-weight:500;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;padding:8px 10px;border-bottom:1px solid var(--border-color);text-align:left;white-space:nowrap}.q-table tbody tr{border-bottom:1px solid var(--border-color);transition:background .1s}.q-table tbody tr:hover{background:var(--paper-bg-alt)}.q-table tbody tr:last-child{border-bottom:none}.q-table tbody td{padding:10px 10px;font-size:.88rem;vertical-align:middle}.q-cell-id{font-family:var(--font-mono);font-size:12px;white-space:nowrap}.q-cell-id{display:flex;align-items:center;gap:8px}.q-chevron{font-size:9px;color:var(--ink-muted);transition:transform .15s;flex-shrink:0}.q-chevron-open{transform:rotate(90deg)}.q-cell-id a{color:var(--color)}.q-cell-payload{font-family:var(--font-mono);font-size:12px;color:var(--color)}.q-payload-detail{color:var(--ink-muted)}.q-cell-num{font-family:var(--font-mono);font-size:12px;text-align:center;white-space:nowrap}.q-cell-progress{min-width:120px}.q-progress-wrap{display:flex;align-items:center;gap:8px}.q-progress-bar{flex:1;height:6px;border-radius:3px;overflow:hidden;background-image:linear-gradient(to right,#1f6a64 var(--pct,0),var(--border-color) var(--pct,0))}.dark-mode .q-progress-bar{background-image:linear-gradient(to right,#7cc7be var(--pct,0),var(--border-color) var(--pct,0))}.q-progress-label{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);white-space:nowrap;min-width:32px;text-align:right}.q-cell-actions{text-align:right;white-space:nowrap}.q-cell-actions .btn{padding:3px 8px;font-size:11px;background:0 0;border:1px solid var(--border-color);color:var(--ink-muted)}.q-cell-actions .btn:hover{color:var(--color)}.q-row-failed{background:rgba(180,35,24,.04)}.dark-mode .q-row-failed{background:rgba(240,138,130,.06)}.q-error-detail{padding:4px 10px 12px;font-size:12px}.q-error-reason{color:#b42318;font-size:.82rem;margin-bottom:4px}.dark-mode .q-error-reason{color:#f08a82}.q-toast-error{background:#fbe7e7;color:#b42318;border:1px solid #f3c7c7;border-radius:8px;padding:10px 16px;margin-bottom:12px;font-family:var(--font-mono);font-size:12px;cursor:pointer;display:flex;align-items:center;gap:8px}.dark-mode .q-toast-error{background:rgba(240,138,130,.1);color:#f08a82;border-color:rgba(240,138,130,.25)}.q-error-stack{font-size:.78rem;max-height:120px;overflow:auto;margin:4px 0 0;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:6px;padding:8px 10px}.q-row-expanded>td{border-bottom-color:transparent}.q-detail-row>td{padding:0 10px 14px!important;border-bottom:1px solid var(--border-color)}.q-job-detail{background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:8px;padding:14px 16px}.q-job-detail-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px 20px;margin-bottom:10px}.q-job-detail-item{display:flex;flex-direction:column;gap:2px}.q-job-detail-label{font-family:var(--font-mono);font-size:9px;font-weight:500;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em}.q-job-detail-value{font-family:var(--font-mono);font-size:12px;color:var(--color);word-break:break-all}.q-job-detail-value a{color:var(--color)}.q-job-detail-error{margin:8px 0}.q-job-detail-actions{display:flex;gap:8px;margin-top:12px;padding-top:10px;border-top:1px solid var(--border-color)}.q-job-detail-actions .btn{font-family:var(--font-mono);font-size:11px;padding:5px 12px;border-radius:6px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color)}@media (max-width:900px){.q-detail{grid-template-columns:1fr}.q-cards{flex-direction:column}.q-card{min-width:100%}.q-jobs-header{flex-direction:column;align-items:flex-start}.q-tabs{flex-wrap:wrap}.q-search-row{flex-wrap:wrap}.q-search-row .form-control{max-width:100%;flex:1}.q-table{font-size:12px}.q-header{flex-direction:column}.q-job-detail-grid{grid-template-columns:1fr 1fr}}@media (max-width:900px){.paper-table .paper-table-head{display:none}.paper-table .admin-users-row,.paper-table .paper-table-row,.paper-table.paper-table-conferences .paper-table-row,.paper-table.paper-table-repos .paper-table-row{display:flex!important;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:14px 0;grid-template-columns:none!important}.paper-table .cell-anon{flex:1 1 calc(100% - 52px);min-width:0;order:1}.paper-table .cell-actions{flex:0 0 auto;order:2;align-self:center}.paper-table .cell-conf,.paper-table .cell-expires,.paper-table .cell-status,.paper-table .cell-views{order:3;font-size:13px;color:var(--ink-muted);display:inline-flex;align-items:center;gap:6px;text-align:left}.paper-table .cell-status{flex-wrap:nowrap;gap:6px;min-width:0}.paper-table .cell-status .status-sub{flex-basis:auto;font-size:13px;line-height:inherit}.paper-table .cell-status .status-sub::before{content:"·";margin-right:6px;color:var(--ink-faint)}.paper-table .cell-conf{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-views.num{text-align:left}.paper-table .paper-table-skeleton .cell-expires,.paper-table .paper-table-skeleton .cell-status,.paper-table .paper-table-skeleton .cell-views{flex:1 1 30%}.paper-table .cell-views::before{content:'\f06e';font-family:'Font Awesome 5 Free';font-weight:900;font-size:10px;color:var(--ink-muted)}.paper-table .cell-expires::before{content:'\f017';font-family:'Font Awesome 5 Free';font-weight:400;font-size:10px;color:var(--ink-muted)}.paper-table .cell-conf::before{content:'\f51c';font-family:'Font Awesome 5 Free';font-weight:900;font-size:10px;color:var(--ink-muted)}.paper-table-dashboard .cell-conf::before{content:none}.paper-table .cell-conf:has(.empty-dash),.paper-table .cell-expires:has(.empty-dash){display:none}.paper-table .cell-expires::before,.paper-table .cell-views::before{color:var(--ink-faint)}}.paper-page .dropdown-menu{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface,10px);box-shadow:0 12px 28px rgba(26,24,21,.08);padding:6px;min-width:220px;font-family:var(--font-sans);font-size:14px;color:var(--color);margin-top:6px}.paper-page .dropdown-menu .dropdown-header{font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);padding:8px 10px 6px;font-weight:500}.paper-page .dropdown-menu .dropdown-item{border-radius:6px;padding:8px 10px;color:var(--color);font-size:14px;display:flex;align-items:center;gap:8px}.paper-page .dropdown-menu .dropdown-item:focus,.paper-page .dropdown-menu .dropdown-item:hover{background:var(--hover-bg-color);color:var(--color)}.paper-page .dropdown-menu .dropdown-item i{width:16px;font-size:13px;color:var(--ink-muted)}.paper-page .dropdown-menu .form-check.dropdown-item{padding:8px 10px 8px 36px!important;position:relative}.paper-page .dropdown-menu .form-check.dropdown-item .form-check-input{position:absolute;left:12px;top:50%;transform:translateY(-50%);margin:0;width:16px;height:16px;accent-color:var(--color);cursor:pointer}.paper-page .dropdown-menu .form-check.dropdown-item .form-check-label{font-family:var(--font-sans);font-size:14px;color:var(--color);font-weight:400;margin:0;cursor:pointer}.btn.btn-icon-dots{width:32px;height:32px;padding:0;border:1px solid transparent;background:0 0;color:var(--ink-muted);border-radius:6px;display:inline-flex;align-items:center;justify-content:center;line-height:1}.btn.btn-icon-dots:focus,.btn.btn-icon-dots:hover,.btn.btn-icon-dots[aria-expanded=true]{background:var(--hover-bg-color);border-color:var(--border-color);color:var(--color)}.btn.btn-icon-dots::after{display:none}.btn.btn-icon-dots i{font-size:15px}.paper-page .repo-list-empty{border-top:none;padding:56px 20px;color:var(--ink-muted)}.paper-page .filter-chip{background:var(--paper-bg-alt);color:var(--color);border:1px solid var(--border-color);font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:4px 10px;border-radius:999px}@media (max-width:768px){.paper-page .paper-page-title{font-size:clamp(1.4rem,6vw,1.9rem);margin:4px 0 2px}.paper-page .paper-page-lede{font-size:13px;line-height:1.4;margin-bottom:0}.paper-page .btn-ink{padding:6px 12px;font-size:13px}.paper-page .quota-row{grid-template-columns:1fr;gap:12px;padding:0;margin:16px 0 0}.paper-page .dashboard-filter-row{padding:10px 0;margin-top:12px;gap:8px}.paper-page .quota-header{flex-direction:row;align-items:baseline;gap:8px}.paper-page .quota-value{white-space:nowrap}.paper-page .dashboard-filter-controls{width:100%}.paper-page .dashboard-filter-controls .btn-group{flex:1 1 100%;display:flex}.paper-page .dashboard-filter-controls .btn-group .btn{flex:1;justify-content:center}.paper-page .dashboard-filter-controls .dropdown{flex:1 1 0;min-width:0}.paper-page .dashboard-filter-controls .dropdown .dropdown-toggle{width:100%;justify-content:space-between}.paper-page .dashboard-filter-controls .dropdown .filter-btn-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}@media (max-width:480px){.paper-page .paper-page-lede{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}}.empty-state{text-align:center;padding:40px 20px;opacity:.6}.empty-state i{font-size:2rem;margin-bottom:10px;display:block}.paper-page .progress{height:10px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:999px;overflow:hidden;margin-bottom:20px}.paper-page .progress .progress-bar{background:var(--color);color:var(--canvas-bg-color);font-family:var(--font-mono);font-size:10px;line-height:10px}.paper-page .progress .progress-bar.bg-success{background:var(--status-ready)!important}.paper-page .progress .progress-bar.bg-warning{background:var(--status-progress)!important;color:#fff}.paper-page .progress .progress-bar.bg-danger{background:var(--status-error)!important}.paper-page .progress .progress-bar.bg-dark{background:var(--ink-faint)!important}.paper-page .form>h5,.paper-page form>h5,.paper-page>h5{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin:24px 0 8px}.paper-page .accordion .card{background:var(--paper-card,var(--sidebar-bg-color));border:1px solid var(--border-color);border-radius:8px!important;margin-bottom:8px;overflow:hidden}.paper-page .accordion .card+.card{border-top:1px solid var(--border-color)}.paper-page .accordion .card-header{background:0 0;border:none;padding:0}.paper-page .accordion .card-header .btn{color:var(--color)!important;font-family:var(--font-sans);font-weight:500;font-size:14px;padding:14px 18px!important;background:0 0!important;border:none!important;position:relative}.paper-page .accordion .card-header .btn::after{content:"+";position:absolute;right:18px;font-family:var(--font-serif);font-size:20px;color:var(--ink-muted)}.paper-page .accordion .card-header .btn:not(.collapsed)::after{content:"–"}.paper-page .accordion .card-body{padding:6px 18px 18px!important;border-top:1px solid var(--border-soft)}.paper-page .accordion .form-check-label{color:var(--color);font-weight:500;font-size:13.5px}.paper-page .accordion .form-text{color:var(--ink-muted);font-size:12px;margin-top:2px}.faq-section .panel-body code{font-family:var(--font-mono);font-size:13px;padding:1px 6px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:3px;color:var(--color)}.faq-section .panel-body a{color:var(--accent)}@media (max-width:820px){.paper-faq-body{grid-template-columns:1fr;gap:24px}.paper-faq-toc{position:static}}.paper-footer{margin-top:80px;padding:56px max(20px,calc((100% - 1076px)/ 2)) 28px;border-top:1px solid var(--border-color);background:var(--canvas-bg-color);font-family:var(--font-sans)}.paper-footer-inner{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr;gap:40px}.paper-footer-brand .paper-footer-mark{font-family:var(--font-serif);font-size:22px;letter-spacing:-.01em;color:var(--color)}.paper-footer-brand .paper-footer-mark em{font-style:italic;font-weight:400}.paper-footer-tag{margin-top:10px;color:var(--ink-soft);font-size:13.5px;line-height:1.55;max-width:340px}.paper-footer-head{font-family:var(--font-mono);font-size:12px;color:var(--ink-soft);letter-spacing:.1em;text-transform:uppercase;margin-bottom:14px}.paper-footer-col a{display:block;color:var(--color);font-size:13.5px;padding:4px 0;text-decoration:none}.paper-footer-col a:hover{color:var(--accent)}.paper-footer-rule{height:1px;background:var(--border-color);margin:40px 0 16px}.paper-footer-foot{display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px;font-size:13px;color:var(--ink-muted);font-family:var(--font-sans);letter-spacing:0}.paper-footer-foot .paper-footer-meta{font-family:var(--font-serif);font-style:italic;font-size:13.5px;letter-spacing:0;color:var(--ink-soft)}@media (max-width:768px){.paper-footer-inner{grid-template-columns:1fr 1fr}}.file.folder.truncated .truncated-warning{color:var(--status-progress);margin-left:4px;font-size:.85em;flex-shrink:0}.file.folder.truncated>a{color:var(--status-progress)}body{--bucket-error-bg:#FCEDED;--bucket-error-fg:#8A1F1F;--bucket-error-bd:#F0C4C4;--bucket-error-dot:#B53737;--bucket-warn-bg:#FFF3DF;--bucket-warn-fg:#7A4D00;--bucket-warn-bd:#F0D8A0;--bucket-warn-dot:#B07A2F;--bucket-info-bg:#EAF2EC;--bucket-info-fg:#2C5D3A;--bucket-info-bd:#C5DCCD;--bucket-info-dot:#5B8D6B;--card-shadow:0 1px 2px rgba(26, 24, 21, 0.04),0 4px 14px rgba(26, 24, 21, 0.05);--card-shadow-hover:0 1px 2px rgba(26, 24, 21, 0.06),0 8px 24px rgba(26, 24, 21, 0.08)}.dark-mode{--bucket-error-bg:rgba(255, 139, 123, 0.10);--bucket-error-fg:var(--status-error);--bucket-error-bd:rgba(255, 139, 123, 0.28);--bucket-error-dot:var(--status-error);--bucket-warn-bg:rgba(255, 211, 122, 0.10);--bucket-warn-fg:var(--status-progress);--bucket-warn-bd:rgba(255, 211, 122, 0.28);--bucket-warn-dot:var(--status-progress);--bucket-info-bg:rgba(152, 200, 168, 0.10);--bucket-info-fg:#98C8A8;--bucket-info-bd:rgba(152, 200, 168, 0.28);--bucket-info-dot:#98C8A8;--card-shadow:0 1px 2px rgba(0, 0, 0, 0.35),0 6px 18px rgba(0, 0, 0, 0.30);--card-shadow-hover:0 2px 4px rgba(0, 0, 0, 0.45),0 12px 28px rgba(0, 0, 0, 0.40)}.errors-page .errors-header{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:4px 0 8px}.errors-page .errors-actions{display:flex;gap:6px}.errors-page .kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin:14px 0 18px}.errors-page .kpi-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--card-shadow);transition:box-shadow 160ms ease,transform 160ms ease}.errors-page .kpi-card:hover{box-shadow:var(--card-shadow-hover)}.errors-page .kpi-label{font-family:var(--font-mono);font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted)}.errors-page .kpi-value{font-family:var(--font-serif);font-size:2.6rem;font-weight:400;line-height:1;margin-top:2px;color:var(--color)}.errors-page .kpi-card.kpi-error .kpi-value{color:var(--bucket-error-fg)}.errors-page .kpi-card.kpi-warn .kpi-value{color:var(--bucket-warn-fg)}.errors-page .kpi-card.kpi-info .kpi-value{color:var(--bucket-info-fg)}.errors-page .kpi-sub{font-size:.78rem;color:var(--ink-muted);font-style:italic}.errors-page .kpi-sub.up{color:var(--bucket-error-fg);font-style:normal}.errors-page .kpi-sub.down{color:var(--bucket-info-fg);font-style:normal}.errors-page .dropped-warn{color:var(--bucket-error-fg);font-weight:600;font-style:normal}.errors-page .volume-chart{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;margin-bottom:18px;box-shadow:var(--card-shadow)}.errors-page .volume-head{display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:.7rem;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}.errors-page .volume-legend{display:flex;gap:14px;align-items:center;text-transform:none;letter-spacing:0;font-family:var(--font-sans)}.errors-page .volume-legend .dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}.errors-page .dot.dot-error{background:var(--bucket-error-dot)}.errors-page .dot.dot-warn{background:var(--bucket-warn-dot)}.errors-page .dot.dot-info{background:var(--bucket-info-dot)}.errors-page .volume-bars{display:flex;align-items:flex-end;gap:4px;height:80px}.errors-page .volume-bar{display:flex;flex-direction:column-reverse;flex:1 1 0;min-width:6px;height:100%}.errors-page .volume-bar .seg{display:block;width:100%}.errors-page .seg.seg-error{background:var(--bucket-error-dot)}.errors-page .seg.seg-warn{background:var(--bucket-warn-dot)}.errors-page .seg.seg-info{background:var(--bucket-info-dot);opacity:.7}.errors-page .errors-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}.errors-page .seg-tabs{display:inline-flex;border:1px solid var(--border-color);border-radius:8px;overflow:hidden;background:var(--paper-card)}.errors-page .seg-tabs button{background:0 0;border:none;padding:6px 14px;font-size:.85rem;cursor:pointer;color:var(--color);font-family:var(--font-sans)}.errors-page .seg-tabs button+button{border-left:1px solid var(--border-color)}.errors-page .seg-tabs button.active{background:var(--primary-bg);color:var(--primary-color)}.errors-page .errors-toolbar .search-wrap{position:relative;flex:1 1 320px;min-width:220px}.errors-page .errors-toolbar .search-wrap input{width:100%;padding:6px 10px 6px 32px;border:1px solid var(--border-color);border-radius:8px;font-family:var(--font-mono);font-size:.82rem;background:var(--input-bg);color:var(--input-color)}.errors-page .errors-toolbar .search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--ink-muted);font-size:.85rem}.errors-page .errors-toolbar .filter-count{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:.72rem;color:var(--ink-muted)}.errors-page .errors-toolbar .select-wrap{display:inline-flex;align-items:center;gap:6px}.errors-page .errors-toolbar .select-wrap label{font-family:var(--font-mono);font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);margin:0}.errors-page .errors-toolbar .select-wrap select{background:var(--input-bg);color:var(--input-color);border:1px solid var(--border-color);border-radius:8px}.errors-page .errors-toolbar .autoref{display:inline-flex;gap:6px;align-items:center;font-size:.82rem;color:var(--color);cursor:pointer;margin:0}.errors-page .btn-icon{width:32px;padding:4px 0}.errors-page .errors-list{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;box-shadow:var(--card-shadow)}.errors-page .errors-list-head,.errors-page .errors-row-main{display:grid;grid-template-columns:110px 100px 100px 1fr 60px 70px;gap:16px;padding:18px 20px;align-items:start}.errors-page .errors-list-head{padding:12px 20px;align-items:center}.errors-page .errors-list-head{font-family:var(--font-mono);font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt)}.errors-page .errors-row{border-bottom:1px solid var(--border-soft)}.errors-page .errors-row:last-child{border-bottom:none}.errors-page .errors-row.open{background:var(--paper-bg-alt)}.errors-page .errors-row-main{cursor:pointer}.errors-page .errors-row-main:hover{background:var(--hover-bg-color)}.errors-page .col-when .when-rel{font-size:.95rem;color:var(--color);font-variant-numeric:tabular-nums}.errors-page .col-when .when-abs{font-family:var(--font-mono);font-size:.78rem;color:var(--ink-muted);font-variant-numeric:tabular-nums;margin-top:2px}.errors-page .col-sev{display:flex;align-items:center;gap:8px}.errors-page .sev-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.errors-page .sev-dot.sev-error{background:var(--bucket-error-dot)}.errors-page .sev-dot.sev-warn{background:var(--bucket-warn-dot)}.errors-page .sev-dot.sev-info{background:var(--bucket-info-dot)}.errors-page .sev-label{font-family:var(--font-mono);font-size:.74rem;letter-spacing:.08em;color:var(--color)}.errors-page .pill-module{display:inline-block;font-family:var(--font-mono);font-size:.78rem;background:var(--paper-bg-alt);color:var(--ink-soft);border:1px solid var(--border-color);padding:3px 10px;border-radius:6px}.errors-page .col-msg .msg-code{font-family:var(--font-mono);font-size:1rem;color:var(--color);font-weight:600}.errors-page .col-msg .msg-context{color:var(--ink-muted);font-style:italic;font-size:.88rem;margin-left:10px}.errors-page .col-msg .msg-detail{color:var(--ink-soft);font-family:var(--font-mono);font-size:.82rem;margin-left:10px}.errors-page .col-msg .msg-url{color:var(--ink-muted);font-family:var(--font-mono);font-size:.8rem;margin-top:6px;word-break:break-all}.errors-page .count-pill{display:inline-block;padding:2px 8px;border-radius:4px;background:var(--primary-bg);color:var(--primary-color);font-family:var(--font-mono);font-size:.78rem;font-variant-numeric:tabular-nums}.errors-page .count-pill.count-pill-muted{background:0 0;color:var(--ink-muted);border:1px solid var(--border-color)}.errors-page .status-pill{display:inline-block;padding:2px 10px;border-radius:4px;font-family:var(--font-mono);font-size:.8rem;font-variant-numeric:tabular-nums;border:1px solid transparent}.errors-page .status-pill.status-error{background:var(--bucket-error-bg);color:var(--bucket-error-fg);border-color:var(--bucket-error-bd)}.errors-page .status-pill.status-warn{background:var(--bucket-warn-bg);color:var(--bucket-warn-fg);border-color:var(--bucket-warn-bd)}.errors-page .status-pill.status-info{background:var(--bucket-info-bg);color:var(--bucket-info-fg);border-color:var(--bucket-info-bd)}.errors-page .errors-row-detail{padding:0 16px 16px}.errors-page .detail-tabs{display:flex;gap:4px;border-bottom:1px solid var(--border-color);margin-bottom:12px}.errors-page .detail-tabs button{background:0 0;border:0;border-bottom:2px solid transparent;padding:8px 12px;font-size:.85rem;cursor:pointer;color:var(--ink-muted);font-family:var(--font-sans)}.errors-page .detail-tabs button.active{color:var(--color);border-bottom-color:var(--color)}.errors-page .detail-body{display:grid;grid-template-columns:1fr 260px;gap:20px}.errors-page .detail-main pre{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;font-family:var(--font-mono);font-size:.85rem;line-height:1.7;color:var(--color);max-height:26em;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;margin:0;box-shadow:var(--card-shadow)}.errors-page .related-list{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;max-height:22em;overflow:auto;box-shadow:var(--card-shadow)}.errors-page .stack-pre{line-height:1.45;white-space:pre;overflow-x:auto}.errors-page .related-row{display:flex;gap:10px;padding:6px 12px;align-items:center;border-bottom:1px solid var(--border-soft);font-size:.8rem}.errors-page .related-row:last-child{border-bottom:none}.errors-page .detail-actions{display:flex;gap:6px;align-items:center;margin-top:10px}.errors-page .copy-hint{font-size:.78rem;color:var(--bucket-info-fg)}.errors-page .detail-aside{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px;box-shadow:var(--card-shadow)}.errors-page .aside-block+.aside-block{margin-top:16px}.errors-page .aside-label{font-family:var(--font-mono);font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);margin-bottom:4px}.errors-page .aside-value{font-family:var(--font-serif);font-size:1.05rem;color:var(--color);word-break:break-word;line-height:1.3}.errors-page .aside-value.mono{font-family:var(--font-mono);font-size:.82rem}.errors-page .aside-sub{color:var(--ink-muted);font-style:italic;font-size:.8rem;font-family:var(--font-sans)}.errors-page .errors-pager{display:flex;justify-content:space-between;align-items:center;padding:10px 0;color:var(--ink-muted);font-size:.85rem}@media (max-width:720px){.errors-page .errors-list-head,.errors-page .errors-row-main{grid-template-columns:80px 80px 1fr 60px}.errors-page .errors-list-head .col-mod,.errors-page .errors-list-head .col-sev,.errors-page .errors-row-main .col-mod,.errors-page .errors-row-main .col-sev{display:none}.errors-page .detail-body{grid-template-columns:1fr}}.overview-page .ov-header{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:2px}.overview-page .ov-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:6px;vertical-align:middle}.overview-page .ov-dot-ok{background:#2f7a44}.overview-page .ov-dot-warn{background:var(--status-progress)}.overview-page .ov-dot-idle{background:var(--ink-muted)}.dark-mode .overview-page .ov-dot-ok{background:#7dc894}.dark-mode .overview-page .ov-dot-warn{background:var(--status-progress)}.overview-page .ov-kpi-row{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin:16px 0 22px}.overview-page .ov-kpi-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--card-shadow);transition:box-shadow 160ms ease}.overview-page .ov-kpi-card:hover{box-shadow:var(--card-shadow-hover)}.overview-page .ov-kpi-label{font-family:var(--font-mono);font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted)}.overview-page .ov-kpi-value{font-family:var(--font-serif);font-size:2.6rem;font-weight:400;line-height:1;margin-top:2px;color:var(--color)}.overview-page .ov-kpi-value.ov-val-warn{color:#b42318}.dark-mode .overview-page .ov-kpi-value.ov-val-warn{color:#f08a82}.overview-page .ov-kpi-sub{font-size:.76rem;color:var(--ink-muted);margin-top:2px}.overview-page .ov-daily-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:-4px 0 22px}.overview-page .ov-daily-card{background:var(--primary-bg);border:1px solid var(--border-color);border-radius:12px;padding:16px 20px;box-shadow:var(--card-shadow)}.overview-page .ov-daily-label{font-family:var(--font-mono);font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted)}.overview-page .ov-daily-value{font-family:var(--font-serif);font-size:2.35rem;font-weight:400;line-height:1;color:var(--accent,#1f6a64);margin-top:6px}.overview-page .ov-daily-sub{font-size:.76rem;color:var(--ink-muted);margin-top:5px}.dark-mode .overview-page .ov-daily-card{background:var(--paper-card)}.dark-mode .overview-page .ov-daily-value{color:#7cc7be}.overview-page .ov-chart-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-bottom:18px}.overview-page .ov-chart-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px 14px;box-shadow:var(--card-shadow)}.overview-page .ov-chart-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.overview-page .ov-chart-title{font-family:var(--font-serif);font-size:1.05rem}.overview-page .ov-chart-legend{font-family:var(--font-mono);font-size:.7rem;color:var(--ink-muted);display:flex;align-items:center;gap:5px}.overview-page .ov-dot-legend{display:inline-block;width:8px;height:8px;border-radius:50%}.overview-page .ov-dot-accent{background:var(--accent,#1f6a64)}.overview-page .ov-dot-ok-fill{background:#2f7a44}.overview-page .ov-dot-user-fill{background:#8b5e2e}.overview-page .ov-dot-new-user-fill{background:var(--status-progress)}.dark-mode .overview-page .ov-dot-ok-fill{background:#7dc894}.dark-mode .overview-page .ov-dot-user-fill{background:var(--status-progress)}.dark-mode .overview-page .ov-dot-new-user-fill{background:var(--status-progress)}.overview-page .ov-spark-bars{display:flex;align-items:flex-end;gap:3px;height:140px}.overview-page .ov-spark-col{flex:1;display:flex;align-items:flex-end;height:100%;position:relative}.overview-page .ov-spark-fill{width:100%;background:var(--accent,#1f6a64);border-radius:2px 2px 0 0;transition:height .3s ease;opacity:.55}.overview-page .ov-spark-fill-alt{background:#2f7a44}.overview-page .ov-spark-fill-user{background:#8b5e2e}.overview-page .ov-spark-fill-new-user{background:var(--status-progress)}.dark-mode .overview-page .ov-spark-fill-alt{background:#7dc894}.dark-mode .overview-page .ov-spark-fill-user{background:var(--status-progress)}.dark-mode .overview-page .ov-spark-fill-new-user{background:var(--status-progress)}.overview-page .ov-spark-col:hover .ov-spark-fill{opacity:1}.overview-page .ov-spark-col.has-tip::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 10px);transform:translateX(-50%) translateY(4px);z-index:10;padding:6px 8px;border-radius:6px;background:var(--color);color:var(--canvas-bg-color);font-family:var(--font-mono);font-size:.68rem;line-height:1;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity 120ms ease,transform 120ms ease}.overview-page .ov-spark-col.has-tip::before{content:"";position:absolute;left:50%;bottom:calc(100% + 4px);transform:translateX(-50%) translateY(4px);z-index:10;border:5px solid transparent;border-top-color:var(--color);opacity:0;pointer-events:none;transition:opacity 120ms ease,transform 120ms ease}.overview-page .ov-spark-col.has-tip:hover::after,.overview-page .ov-spark-col.has-tip:hover::before{opacity:1;transform:translateX(-50%) translateY(0)}.overview-page .ov-spark-x{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:.66rem;color:var(--ink-muted);margin-top:6px}.overview-page .ov-triple-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:18px}.overview-page .ov-panel-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 20px;box-shadow:var(--card-shadow)}.overview-page .ov-panel-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px}.overview-page .ov-panel-title{font-family:var(--font-serif);font-size:1.05rem}.overview-page .ov-panel-meta{font-family:var(--font-mono);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-muted)}.overview-page .ov-panel-foot{margin-top:12px;padding-top:10px;border-top:1px solid var(--border-color);font-size:.82rem}.overview-page .ov-panel-foot a{color:var(--link-color);text-decoration:none}.overview-page .ov-panel-foot a:hover{text-decoration:underline}.overview-page .ov-performance{min-width:0;margin:22px 0}.overview-page .ov-performance-head{align-items:center;flex-wrap:wrap;gap:12px 20px}.overview-page .ov-performance-head h2{margin:0 0 4px}.overview-page .ov-performance-note{margin:0 0 12px;font-size:12px;line-height:1.5;color:var(--ink-muted)}.overview-page .ov-performance-head .ov-performance-note{margin:0}.overview-page .ov-performance-window{display:flex;align-items:center;gap:8px;flex-shrink:0}.overview-page .ov-performance-window label{margin:0;font-size:12px;color:var(--ink-muted)}.overview-page .ov-performance-window select{width:auto}.overview-page .ov-performance-tabs{margin:0 0 16px;gap:4px}.overview-page .ov-performance-tabs .paper-tab{white-space:nowrap}.overview-page .ov-metrics-scroll{max-width:100%;overflow-x:auto;border:1px solid var(--border-color);border-radius:var(--radius-control,8px)}.overview-page .ov-metrics-table{width:100%;border-collapse:collapse;color:var(--color);font-size:13px;line-height:1.5;font-variant-numeric:tabular-nums}.overview-page .ov-metrics-table td,.overview-page .ov-metrics-table th{padding:10px 12px;border-bottom:1px solid var(--border-color);vertical-align:middle;white-space:nowrap}.overview-page .ov-metrics-table thead th{background:var(--paper-bg-alt);color:var(--ink-muted);font-size:12px;font-weight:500}.overview-page .ov-metrics-table .num{text-align:right}.overview-page .ov-metrics-table tr:last-child>*{border-bottom:0}.overview-page .ov-metrics-table thead tr:last-child>*{border-bottom:1px solid var(--border-color)}.overview-page .ov-metrics-table tbody tr:hover{background:var(--hover-bg-color)}.overview-page .ov-metrics-table .ov-metric-name{min-width:190px;max-width:280px;text-align:left;white-space:normal;overflow-wrap:anywhere;font-weight:500}.overview-page .ov-metrics-table small{display:block;margin-top:3px;font-size:11px;font-weight:400;color:var(--ink-muted)}.overview-page .ov-metrics-table small .ui-icon{width:12px;height:12px}.overview-page .ov-metric-warning-row{background:rgba(196,138,46,.07)}.overview-page .ov-metric-warn,.overview-page .ov-metrics-table small.ov-metric-warn{color:#946519}.dark-mode .overview-page .ov-metric-warn,.dark-mode .overview-page .ov-metrics-table small.ov-metric-warn{color:var(--status-progress)}.overview-page .ov-performance-message{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:10px 12px;margin:0 0 16px;border:1px solid var(--border-color);border-radius:var(--radius-control,8px);font-size:13px;line-height:1.5}.overview-page .ov-performance-message span{flex:1;min-width:160px}.overview-page .ov-performance-warning{background:rgba(196,138,46,.07)}.overview-page .ov-performance-empty{margin:0;padding:24px 12px;text-align:center;font-size:13px;color:var(--ink-muted)}.overview-page .ov-performance-help{margin-top:16px;font-size:12px;line-height:1.6;color:var(--ink-muted)}.overview-page .ov-performance-help summary{width:fit-content;cursor:pointer}.overview-page .ov-performance-help p{margin:8px 0 0;max-width:80ch}@media (max-width:575px){.overview-page .ov-performance{padding:16px 12px}.overview-page .ov-performance-window{width:100%}.overview-page .ov-performance-window select{flex:1;min-width:0}.overview-page .ov-performance-tabs .paper-tab{flex:1;padding:10px 8px}}.overview-page .ov-stacked-bar{display:flex;height:18px;border-radius:6px;overflow:hidden;margin-bottom:14px}.overview-page .ov-bar-seg{min-width:2px;transition:width .4s ease}.overview-page .ov-bar-ready{background:#2f7a44}.overview-page .ov-bar-preparing{background:var(--status-progress)}.overview-page .ov-bar-error{background:#b42318}.overview-page .ov-bar-expired{background:#8a857c}.overview-page .ov-bar-removed{background:#b5afa2}.dark-mode .overview-page .ov-bar-ready{background:#7dc894}.dark-mode .overview-page .ov-bar-preparing{background:var(--status-progress)}.dark-mode .overview-page .ov-bar-error{background:#f08a82}.dark-mode .overview-page .ov-bar-expired{background:#6b665e}.dark-mode .overview-page .ov-bar-removed{background:#4a453e}.overview-page .ov-bar-legend{display:flex;flex-direction:column;gap:6px}.overview-page .ov-legend-item{display:flex;align-items:center;gap:8px;font-size:.82rem;text-decoration:none;color:inherit}.overview-page .ov-legend-item:hover{text-decoration:none;color:inherit}.overview-page .ov-swatch{display:inline-block;width:12px;height:12px;border-radius:3px;flex-shrink:0}.overview-page .ov-legend-n{margin-left:auto;font-family:var(--font-mono);font-size:.8rem;font-weight:600}.overview-page .ov-error-bars{display:flex;flex-direction:column;gap:12px}.overview-page .ov-ebar-row{display:flex;align-items:center;gap:10px}.overview-page .ov-ebar-label{width:30px;flex-shrink:0;font-family:var(--font-mono);font-size:.72rem;text-transform:uppercase;color:var(--ink-muted)}.overview-page .ov-ebar-track{flex:1;height:10px;background:var(--hover-bg-color);border-radius:5px;overflow:hidden}.overview-page .ov-ebar-fill{height:100%;border-radius:5px;transition:width .4s ease}.overview-page .ov-ebar-error{background:#b42318}.overview-page .ov-ebar-warn{background:var(--status-progress)}.overview-page .ov-ebar-info{background:var(--accent,#1f6a64)}.dark-mode .overview-page .ov-ebar-error{background:#f08a82}.dark-mode .overview-page .ov-ebar-warn{background:var(--status-progress)}.overview-page .ov-ebar-n{width:36px;text-align:right;font-family:var(--font-mono);font-size:.82rem;font-weight:600;flex-shrink:0}.overview-page .ov-routes-table{display:flex;flex-direction:column}.overview-page .ov-route-row{display:grid;grid-template-columns:1fr 60px 60px 60px;padding:8px 0;border-bottom:1px solid var(--border-soft,var(--border-color));text-decoration:none;color:inherit;font-size:.85rem;align-items:center}.overview-page .ov-route-row:last-child{border-bottom:none}.overview-page .ov-route-row:hover{background:var(--hover-bg-color);text-decoration:none;color:inherit}.overview-page .ov-route-head{font-family:var(--font-mono);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-muted);border-bottom:1px solid var(--border-color)}.overview-page .ov-route-head:hover{background:0 0}.overview-page .ov-route-name{font-family:var(--font-mono);font-size:.84rem}.overview-page .ov-route-n{text-align:right;font-family:var(--font-mono);font-size:.84rem}.overview-page .ov-route-lat{color:var(--ink-muted)}.overview-page .ov-n-bad{color:#b42318;font-weight:600}.dark-mode .overview-page .ov-n-bad{color:#f08a82}.overview-page .ov-services-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;box-shadow:var(--card-shadow);margin-bottom:18px}.overview-page .ov-services-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px}.overview-page .ov-services-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px}.overview-page .ov-svc{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border:1px solid var(--border-soft,var(--border-color));border-radius:10px}.overview-page .ov-svc-dot{width:10px;height:10px;border-radius:50%;margin-top:3px;flex-shrink:0}.overview-page .ov-svc-info{display:flex;flex-direction:column;gap:1px;flex:1}.overview-page .ov-svc-name{font-family:var(--font-mono);font-size:.88rem;font-weight:600}.overview-page .ov-svc-meta{font-size:.76rem;color:var(--ink-muted)}.overview-page .ov-svc-detail{font-size:.74rem;color:var(--ink-muted);white-space:nowrap}@media (max-width:900px){.overview-page .ov-kpi-row{grid-template-columns:repeat(3,1fr)}.overview-page .ov-daily-row{grid-template-columns:repeat(3,1fr)}.overview-page .ov-triple-row{grid-template-columns:1fr}}@media (max-width:720px){.overview-page .ov-kpi-row{grid-template-columns:repeat(2,1fr)}.overview-page .ov-daily-row{grid-template-columns:1fr}.overview-page .ov-chart-row{grid-template-columns:1fr}.overview-page .ov-header{flex-direction:column;gap:8px}}@media (max-width:480px){.overview-page .ov-kpi-row{grid-template-columns:1fr 1fr}.overview-page .ov-services-grid{grid-template-columns:1fr 1fr}}.dashboard-heading-actions{display:flex;flex-direction:column;align-items:flex-end;gap:8px;padding-bottom:12px}.dashboard-heading-actions .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px}.dashboard-connections-link{display:inline-flex;align-items:center;gap:7px;min-height:32px;color:var(--ink-muted);font-size:12px;line-height:1.5}.dashboard-connections-link:hover{color:var(--accent);text-decoration:none}.dashboard-connections-link:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:2px}@media (max-width:767px){.dashboard-heading-actions{align-items:flex-start;padding-bottom:0;gap:4px}.dashboard-connections-link{min-height:44px}}.paper-settings-connection-link{display:flex;align-items:center;gap:12px;padding:18px 0;color:var(--color);font-size:14px}.paper-settings-connection-link>i{color:var(--ink-muted);font-size:18px}.paper-settings-connection-link>span:first-of-type{flex:1}.paper-settings-connection-link small{display:block;margin-top:4px;color:var(--ink-muted);font-size:12px}.paper-settings-connection-link:hover{color:var(--accent);text-decoration:none}.paper-settings-connection-link:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.connections-heading{margin-bottom:36px}.connections-heading .paper-page-title{font-size:clamp(38px,4.5vw,56px);margin-bottom:12px}.connections-heading .paper-page-lede{font-size:15px}.connections-grid{display:grid;grid-template-columns:1.15fr 1fr;gap:56px;align-items:start;border-top:1px solid var(--border-color);padding-top:28px}.connections-grid>section{min-width:0}.connection-title,.connection-top{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.connection-top{margin-bottom:24px;min-height:28px}.connection-top>i{color:var(--ink-muted);font-size:24px}.connection-top>.fa-key{font-size:18px}.connection-title{margin-bottom:10px}.connections-page h2{font-family:inherit;font-size:21px;font-weight:600;letter-spacing:-.5px;margin:0}.connections-page p{font-size:14px;line-height:1.65;color:var(--ink-muted)}.connection-badge{background:var(--accent-soft);color:var(--accent);font-size:11px;font-weight:600;border-radius:5px;padding:4px 8px;white-space:nowrap}.connection-badge-muted{background:var(--paper-bg-alt);color:var(--ink-muted)}.connection-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-muted)}.connection-status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}.connection-status.is-connected{color:var(--status-ready)}.connection-actions{display:flex;flex-wrap:wrap;align-items:center;gap:16px;margin:22px 0 16px}.connections-page .btn{font-size:13px;padding:9px 13px;white-space:normal}.connection-revoke p,.connections-page .connection-help{font-size:12px}.connection-link{display:inline-flex;align-items:center;gap:8px;border:0;padding:0;background:0 0;color:var(--accent);font-size:13px;line-height:1.6;font-weight:500;text-align:left;cursor:pointer}.connection-link:hover{color:var(--link-hover-color);text-decoration:underline}.connection-link:disabled{color:var(--ink-muted);opacity:.65;cursor:not-allowed;text-decoration:none}.connection-link:focus-visible{outline:2px solid var(--accent);outline-offset:5px}.connection-installations{list-style:none;padding:0;margin:18px 0;border-top:1px solid var(--border-soft)}.connection-installations li{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:14px 0;font-size:12px;border-bottom:1px solid var(--border-soft)}.connection-revoke{border-top:1px solid var(--border-soft);padding-top:18px;margin-top:24px}.connection-danger{color:var(--status-error)}.connection-resources{margin-top:36px;padding-top:28px;border-top:1px solid var(--border-color)}.connection-count{color:var(--ink-muted);font-size:12px}.connection-empty{display:flex;align-items:center;gap:18px;padding:20px 0;margin:22px 0}.connection-empty>i{font-size:22px;color:var(--ink-muted)}.connection-empty h3{font-size:14px;font-weight:600;margin:0 0 5px}.connection-empty p{font-size:12px;margin:0}.connection-empty>a{margin-left:auto;flex-shrink:0}.connection-resource{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:20px 0;border-bottom:1px solid var(--border-color)}.connection-resource-name{flex:1;min-width:180px;overflow-wrap:anywhere;font-size:14px}.connection-resource-name small{display:block;color:var(--ink-muted);font-size:11px;margin-top:5px}.connection-resource-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.connection-resource-actions .connection-help{flex-basis:100%}.connections-page .connection-footnote{display:flex;align-items:baseline;gap:10px;margin-top:22px;font-size:12px}@media (max-width:767px){.connections-grid{grid-template-columns:1fr;gap:28px}.connections-grid>section+section{border-top:1px solid var(--border-color);padding-top:28px}.connection-empty{flex-wrap:wrap}.connection-empty>div{flex:1;min-width:180px}.connection-empty>a{margin-left:40px}.connection-resource-actions{flex-basis:100%}}.paper-table-empty-actions .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px}.paper-table-empty-actions .btn>i{font-size:1em;line-height:1;color:inherit;margin-right:0!important}.auth-back{display:inline-flex;align-items:center;gap:10px;font-size:13px;color:var(--ink-muted);margin-bottom:48px}.auth-layout{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:start}.auth-intro,.auth-options{padding-top:28px}.auth-title-mobile{display:none}.auth-intro .paper-page-title{font-size:clamp(48px,5vw,76px);line-height:1.06;margin:18px 0 24px}.auth-intro .paper-page-lede{max-width:330px}.auth-options{display:grid;gap:28px}.auth-options h2{font-family:inherit;font-size:21px;font-weight:600;margin:0 0 10px}.auth-options p{font-size:14px;line-height:1.65;color:var(--ink-muted)}.auth-options .auth-existing{font-size:12px;border-left:2px solid var(--accent);padding-left:14px;margin:0}.auth-permissions{padding:6px 0 12px;list-style:none;font-size:13px;line-height:1.6}.auth-permissions li{display:flex;gap:10px;margin:12px 0}.auth-permissions i{color:var(--status-ready);margin-top:4px}.auth-continue{display:flex!important;align-items:center;justify-content:space-between;min-height:48px;padding:12px 16px!important}.auth-options small{display:block;font-size:12px;text-align:center;color:var(--ink-muted);margin-top:12px}.auth-legacy{border-top:1px solid var(--border-color);padding-top:24px}.auth-legacy h2{font-size:16px;margin:0}.auth-legacy p{font-size:13px}.source-url-label-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 16px;margin-bottom:8px}.source-url-label-row>.paper-field-label{font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--color);margin-bottom:6px;display:block}.repo-access{margin:0}.repo-access-details{margin:12px 0 24px}.source-url-label-row .repo-access-switch{padding:2px}.source-url-label-row .repo-access-switch button{padding:6px 10px;font-size:12px;line-height:20px}.repo-access-main{display:flex;align-items:center;gap:10px;padding:0 0 8px;flex-wrap:wrap}.repo-access-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.repo-access-switch{display:inline-flex;gap:3px;padding:3px;border:1px solid var(--border-color);border-radius:7px;flex-wrap:wrap}.repo-access-switch button{border:0;border-radius:4px;background:0 0;color:var(--ink-muted);padding:7px 10px;font:inherit;font-size:12px;cursor:pointer}.repo-access-switch button[aria-pressed=true]{background:var(--accent-soft);color:var(--accent);opacity:1;cursor:default;font-weight:600}.repo-access-switch button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.repo-access-details .btn{font-size:12px;padding:8px 12px}.repo-access-note{display:flex;gap:6px 16px;flex-wrap:wrap;padding:0;color:var(--ink-muted);font-size:11px;line-height:1.6}@media (max-width:767px){.auth-page{max-width:480px}.auth-back,.auth-intro .paper-eyebrow,.auth-title-desktop{display:none}.auth-title-mobile{display:inline}.auth-layout{grid-template-columns:1fr;gap:24px}.auth-intro,.auth-options{padding-top:0}.auth-intro .paper-page-title{font-size:40px;line-height:1.15;margin:6px 0 12px}.auth-intro .paper-page-lede{max-width:none;font-size:14px;margin:0}.auth-options{gap:24px}.auth-options .connection-top{margin-bottom:16px}.auth-options h2{font-size:18px}.auth-options p{font-size:13px;margin-bottom:12px}.auth-permissions{padding:0;margin:14px 0 20px;font-size:12px}.auth-permissions li{margin:8px 0}.auth-continue{font-size:14px}.auth-legacy{padding-top:20px}.auth-legacy h2{font-size:15px}.auth-legacy .connection-link{min-height:44px}.repo-access-controls{width:100%;gap:12px}}.text-muted{color:var(--ink-muted)!important}.anonymize-review-note,.draft-status{font-size:13px;color:var(--ink-muted)}.anonymize-review-note{padding:12px 0;margin:0}.anonymize-preview-status{padding:20px 32px;color:var(--ink-muted)}.anonymize-preview-status p{margin:0 0 12px}@media (max-width:575px){.dashboard-page .paper-page-lede{margin-bottom:8px}.dashboard-page .dashboard-heading-actions{gap:4px}.dashboard-page .dashboard-connections-link{min-height:32px}.dashboard-page .dashboard-filter-row{margin-top:12px}.anonymize-preview-status{padding:16px}}.paper-page .paper-page-title{font-size:32px}.paper-page.admin-page .paper-page-title{font-size:28px}.paper-page .paper-page-lede{font-size:14px;line-height:1.5}.paper-settings{padding-top:32px}.paper-settings-body{grid-template-columns:180px minmax(0,1fr);gap:40px;margin-top:24px;padding-top:24px;border-top:0}.paper-settings-main .paper-settings-section{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);padding:20px;margin-bottom:16px}.anonymize-page .paper-section-eyebrow,.paper-settings .paper-section-eyebrow{font-family:var(--font-sans);font-size:16px;font-weight:600;color:var(--color);text-transform:none;letter-spacing:-.01em;margin-bottom:12px}.paper-settings .paper-settings-toc-head{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.paper-settings-section .paper-section-copy{margin-top:0}.paper-settings .quota-row{gap:24px}.paper-settings .quota-header{flex-direction:column;align-items:flex-start;gap:4px}.paper-settings .quota-value{white-space:nowrap}.paper-settings .paper-settings-footer{padding:0 0 20px}.paper-settings .form-control{font-size:14px;border-color:var(--border-color);border-radius:var(--radius-control)}.paper-settings .form-check{padding-top:8px;padding-bottom:8px}.anonymize-page .btn-ink,.paper-page .btn-ink{background:var(--accent);color:#fff;border-color:var(--accent)}.dark-mode .anonymize-page .btn-ink,.dark-mode .paper-page .btn-ink{color:#1a1815}.anonymize-page .btn-ink:hover,.paper-page .btn-ink:hover{background:var(--accent);border-color:var(--accent);color:#fff;filter:brightness(.94)}.dark-mode .anonymize-page .btn-ink:hover,.dark-mode .paper-page .btn-ink:hover{color:#1a1815}.anonymize-page .btn-ink:active,.paper-page .btn-ink:active{filter:brightness(.9)}input[type=date],select{color-scheme:light}.dark-mode input[type=date],.dark-mode select{color-scheme:dark}.draft-status{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;padding:10px 0}.draft-discard{padding:0;border:0;color:var(--accent);font:inherit;font-size:13px;text-decoration:underline;background:0 0;cursor:pointer}.conference-feedback{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px;margin-top:16px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);width:100%}.paper-table{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);margin-top:12px}.paper-table .paper-table-head{font-family:var(--font-sans);text-transform:none;letter-spacing:0;font-size:12px;background:var(--paper-bg-alt);border-radius:var(--radius-surface) var(--radius-surface) 0 0}.paper-table .paper-table-head,.paper-table .paper-table-row{padding-left:16px;padding-right:16px}.dashboard-page{--dashboard-space-xs:4px;--dashboard-space-sm:8px;--dashboard-space-md:12px;--dashboard-space-lg:16px;--dashboard-space-xl:24px;padding-top:var(--dashboard-space-xl)}.project-icon-definitions{position:absolute;width:0;height:0;overflow:hidden}.project-type-svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.project-empty-icon{width:28px;height:28px;color:var(--ink-faint)}.project-type-icon{display:inline-flex;color:var(--ink-muted);flex-shrink:0}.dashboard-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--dashboard-space-lg)}.dashboard-heading .paper-page-title{font-size:28px;margin-bottom:var(--dashboard-space-xs)}.dashboard-heading .paper-page-lede{display:none}.dashboard-heading .btn{min-height:40px;display:inline-flex;gap:var(--dashboard-space-sm);align-items:center}.dashboard-new-mobile{display:none}.dashboard-quota{position:relative;margin:0;padding:0;border:0;background:0 0}.dashboard-quota>summary{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:5px 10px;border:1px solid var(--border-color);border-radius:6px;background:var(--paper-card);color:var(--color);font-family:var(--font-sans);font-size:12px;cursor:pointer;list-style:none;white-space:nowrap}.dashboard-quota>summary::-webkit-details-marker{display:none}.dashboard-quota>summary:hover,.dashboard-quota[open]>summary{background:var(--paper-bg-alt);border-color:var(--ink-faint)}.quota-summary-label{font-weight:600}.quota-summary-value{color:var(--ink-muted);font-variant-numeric:tabular-nums}.quota-summary-ring{width:18px;height:18px;flex-shrink:0;fill:none;stroke-width:2.5}.quota-ring-track{stroke:var(--border-color)}.quota-ring-fill{stroke:var(--accent);transform:rotate(-90deg);transform-origin:50% 50%}.quota-summary-ring.quota-warn .quota-ring-fill{stroke:var(--status-progress)}.quota-summary-ring.quota-danger .quota-ring-fill{stroke:var(--status-error)}.quota-ring-unlimited{stroke:var(--accent);stroke-width:1.7}.quota-summary-chevron{width:14px;height:14px;margin-left:2px;color:var(--ink-muted);transition:transform .15s ease}.dashboard-quota[open] .quota-summary-chevron{transform:rotate(180deg)}.dashboard-quota .quota-panel{position:absolute;z-index:25;top:calc(100% + 8px);left:0;width:344px;max-width:calc(100vw - 36px);padding:16px;border:1px solid var(--border-color);border-radius:12px;background:var(--paper-card);box-shadow:0 12px 36px rgba(26,24,21,.16)}.quota-panel-heading{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.quota-panel-heading h2{font:600 15px var(--font-sans);margin:0}.quota-panel-heading .project-close{margin:-8px -8px -8px 0}.paper-page .quota-usage{display:grid;grid-template-columns:1fr;gap:16px;margin:0;padding:0}.quota-usage .quota-item+.quota-item{padding-top:16px;border-top:1px solid var(--border-soft,var(--border-color))}.quota-usage .quota-header{display:flex;align-items:center;gap:8px;margin-bottom:6px}.quota-usage .quota-label{display:inline-flex;align-items:center;gap:8px;font:500 12px var(--font-sans);letter-spacing:0;text-transform:none;color:var(--ink-muted)}.quota-usage .quota-label .project-type-svg{width:16px;height:16px}.quota-usage .quota-value{display:flex;align-items:baseline;gap:4px;font:12px var(--font-sans);font-variant-numeric:tabular-nums;color:var(--ink-muted);margin-bottom:8px}.quota-usage .quota-value strong{font-size:20px;line-height:1.2;font-weight:600;color:var(--color)}.quota-percent,.quota-usage .quota-unlimited-tag{margin-left:auto;padding:2px 6px;border-radius:4px;background:var(--paper-bg-alt);font:11px var(--font-sans);color:var(--ink-muted);white-space:nowrap}.quota-warn>.quota-header .quota-percent{color:var(--status-progress)}.quota-danger>.quota-header .quota-percent{color:var(--status-error)}.quota-usage .quota-track{height:5px;border:0;background:var(--paper-bg-alt)}.quota-usage .quota-track .quota-fill{min-width:0;background:var(--accent)}.quota-remaining{margin:6px 0 0;color:var(--ink-muted);font-size:11px}.quota-remaining.quota-limit{color:var(--status-error)}.quota-loading{font-size:12px;color:var(--ink-muted);margin:12px 0 0}.dashboard-page .dashboard-filter-row{display:flex;align-items:center;gap:var(--dashboard-space-md);padding:0;margin:var(--dashboard-space-lg) 0 0;border:0;background:0 0}.dashboard-page .dashboard-filter-row .search-wrap{flex:1 1 240px;min-width:0}.dashboard-page .dashboard-filter-row .form-control{height:40px;padding:8px 12px;font-size:13px;background:var(--paper-card)}.dashboard-page .dashboard-filter-controls{display:flex;flex-wrap:nowrap;align-items:center;gap:var(--dashboard-space-sm)}.dashboard-extra-filters{display:flex;gap:var(--dashboard-space-sm)}.dashboard-filters-toggle{display:none}.dashboard-page .dashboard-filter-row .btn-group .btn{border:0;background:0 0;border-radius:6px;height:36px;padding:6px 10px}.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary{background:var(--accent-soft);color:var(--accent)}.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary:active,.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary:hover{background:var(--accent-soft)!important;color:var(--accent)}.dashboard-page .dashboard-filter-row .dropdown-toggle{height:36px;padding:6px 10px}.dashboard-type-filter .project-type-svg{display:none}.sort-direction{width:14px;height:14px}.sort-direction.ascending{transform:rotate(180deg)}.dashboard-page .dashboard-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--dashboard-space-sm) var(--dashboard-space-md);margin:var(--dashboard-space-md) 0 0;padding:0}.dashboard-view-tabs{display:flex;align-items:center;gap:var(--dashboard-space-lg)}.dashboard-view-short{display:none}.dashboard-view-tabs button{min-height:36px;border:0;border-bottom:2px solid transparent;border-radius:0;background:0 0;font:inherit;font-size:13px;color:var(--ink-muted);padding:4px 0}.dashboard-view-tabs button[aria-pressed=true]{color:var(--color);border-bottom-color:var(--color);font-weight:600}.attention-count{padding:1px 5px;margin-left:4px;border-radius:4px;background:var(--paper-bg-alt);font-size:11px;font-variant-numeric:tabular-nums}.attention-count.has-attention{color:var(--status-error)}.dashboard-count{margin-left:auto;font-size:12px;color:var(--ink-muted)}.dashboard-active-filters{display:flex;align-items:center;flex-wrap:wrap;gap:var(--dashboard-space-sm);width:100%}.dashboard-page .filter-chip{display:inline-flex;font-family:var(--font-sans);font-size:11px;text-transform:none;letter-spacing:0;border-radius:6px}.dashboard-page .filter-chip .project-type-svg{width:12px;height:12px;margin-left:4px}.dashboard-page .filter-chip-close{min-width:24px;min-height:24px;display:inline-flex;align-items:center;justify-content:center;margin:-2px -4px -2px 2px}.dashboard-page .btn-link-inline{color:var(--ink-muted);font-size:12px}.dashboard-page .paper-table-dashboard{margin-top:var(--dashboard-space-md);border-left:0;border-right:0;border-radius:0}.dashboard-page .paper-table-dashboard .paper-table-head,.dashboard-page .paper-table-dashboard .paper-table-row{grid-template-columns:minmax(280px,3fr) 180px 72px 148px 64px;gap:var(--dashboard-space-lg);padding:8px 12px}.dashboard-page .paper-table-dashboard .paper-table-head{border-radius:0}.dashboard-page .paper-table .cell-anon{gap:var(--dashboard-space-sm)}.dashboard-page .paper-table .anon-text{flex:1}.dashboard-page .paper-table .repo-name{font-family:var(--font-sans);font-size:14px;font-weight:600}.dashboard-page .paper-table .anon-sub{display:flex;align-items:center;gap:var(--dashboard-space-sm);min-width:0}.anonymous-id{font-size:12px;font-family:var(--font-mono);color:var(--ink-muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashboard-page .paper-table .type-badge{font-family:var(--font-sans);letter-spacing:0;font-size:11px;text-transform:none;border-radius:6px}.dashboard-page .paper-table .status-sub{font-size:12px}.dashboard-page .paper-table .cell-conf{font-family:var(--font-sans);letter-spacing:0;text-transform:none}.dashboard-page .conf-tag .project-type-svg{width:14px;height:14px}.dashboard-row-meta{display:contents}.dashboard-page .cell-actions{gap:0;align-items:center}.copy-link-inline,.dashboard-page .btn-icon-dots,.project-close{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:6px;background:0 0;color:var(--ink-muted);padding:0;width:32px;height:32px;min-width:32px;cursor:pointer}.copy-link-inline:focus-visible,.copy-link-inline:hover,.dashboard-page .btn-icon-dots:hover,.project-close:hover{background:var(--accent-soft);color:var(--accent)}.copy-link-inline:disabled{opacity:.5;cursor:wait}.mobile-meta-icon{display:none}.project-attention-action{display:inline-block;color:var(--accent);border:0;background:0 0;padding:0;font:inherit;font-size:12px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}.project-attention-action{text-align:left}.dashboard-page .cell-status .project-attention-action{flex-basis:100%}.project-details-row{width:100%;border-bottom:1px solid var(--border-color)}.project-details{padding:var(--dashboard-space-xl);background:var(--paper-bg-alt)}.project-details-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--dashboard-space-sm)}.project-details-heading h2{font:600 16px var(--font-sans);margin:0}.project-details-list{display:grid;grid-template-columns:1fr 1fr;gap:var(--dashboard-space-md) var(--dashboard-space-xl);margin:12px 0}.project-details-list dt{font-size:12px;color:var(--ink-muted);font-weight:500;margin-bottom:4px}.project-details-list dd{margin:0;font-size:13px;overflow-wrap:anywhere}.project-details-list code{color:var(--color);font-size:12px}.project-name-form{margin-top:var(--dashboard-space-lg);padding-top:var(--dashboard-space-lg);border-top:1px solid var(--border-color)}.project-name-form label{display:block;font-size:13px;font-weight:500;margin-bottom:4px}.project-name-form p,.project-progress-help{font-size:12px;color:var(--ink-muted);margin-bottom:8px}.project-name-controls{display:flex;gap:var(--dashboard-space-sm);max-width:560px}.project-name-controls .form-control{min-width:0;font-size:14px}.project-name-controls .btn{flex-shrink:0;min-height:40px}.project-name-form .project-name-error{color:var(--status-error)}.project-details-button{min-height:44px;padding:0 8px;border:0;border-radius:6px;background:0 0;font:inherit;font-size:12px;color:var(--ink-muted);cursor:pointer}.anonymize-page .anonymize-topbar-title{font-family:var(--font-sans);font-size:28px;font-weight:600}.anonymize-topbar{padding:16px 24px}.anonymize-form-col{background:var(--paper-bg-alt)}.anonymize-form-col .paper-settings-main{padding:20px 24px}.anonymize-form-col .paper-settings-section{padding:18px}.anonymize-form-col .paper-settings-section:first-child{padding-top:18px}.anonymize-form-col .anonymize-submit-bar{background:var(--paper-bg-alt);padding:12px 0}.anonymize-preview-head .paper-eyebrow{font-family:var(--font-sans);font-size:14px;font-weight:600;color:var(--color);text-transform:none;letter-spacing:0}.anonymize-preview-col{background:var(--canvas-bg-color);padding:24px}.anonymize-preview-head{flex-wrap:wrap;gap:6px 12px;margin-bottom:16px}.anonymize-preview-body{padding:24px}.anonymize-mobile-tabs,.anonymize-preview-empty,.mobile-preview-jump{display:none}.anonymize-page .repo-access-note{font-size:12px}@media (max-width:900px){.paper-settings-body{grid-template-columns:1fr;gap:18px}.paper-settings-toc{padding:0}.paper-settings-toc-head{display:none}.paper-table .paper-table-row{padding-left:14px;padding-right:14px}}@media (max-width:767px){.anonymize-page .paper-page-title,.paper-page .paper-page-title{font-size:24px}.paper-settings{padding:20px 16px 40px}.paper-settings-main .paper-settings-section{padding:16px}.anonymize-topbar{padding:12px 16px}.anonymize-topbar .paper-crumbs{display:none}.anonymize-mobile-tabs{display:flex;gap:4px;padding:8px 16px;border-bottom:1px solid var(--border-color);background:var(--paper-card)}.anonymize-mobile-tabs button{flex:1;font:inherit;font-size:13px;padding:10px;border:0;border-radius:6px;background:0 0;color:var(--ink-muted)}.anonymize-mobile-tabs button[aria-selected=true]{background:var(--accent-soft);color:var(--accent);font-weight:600}.anonymize-page:not(.mobile-preview-active) .anonymize-preview-col{display:none}.mobile-preview-active .anonymize-form-col{display:none}.mobile-preview-active .anonymize-preview-empty{display:block;padding:24px 16px;color:var(--ink-muted)}.anonymize-form-col .paper-settings-main{padding:16px}.anonymize-form-col .anonymize-submit-bar{background:var(--paper-bg-alt)}.mobile-preview-jump{display:block;width:100%;min-height:44px}.anonymize-preview-col{padding:16px}.anonymize-preview-body{padding:16px}}@media (max-width:575px){.paper-settings .quota-header{flex-direction:row;flex-wrap:wrap;align-items:baseline;gap:4px 8px}}@media (max-width:1100px) and (min-width:901px){.dashboard-page .dashboard-filter-row{flex-wrap:wrap}.dashboard-page .dashboard-filter-row .search-wrap{flex-basis:100%}.dashboard-page .dashboard-filter-controls{width:100%;justify-content:space-between}}@media (max-width:900px){.dashboard-page .paper-table.paper-table-dashboard .paper-table-row{gap:4px 8px;padding:8px 12px}.dashboard-page .paper-table .cell-anon{flex:1 1 calc(100% - 96px);min-width:0}.dashboard-page .paper-table .cell-actions{flex:0 0 88px}.dashboard-row-meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;order:3;width:100%}.dashboard-page .dashboard-row-meta .cell-status{flex:1 1 80px;flex-wrap:wrap;gap:2px 8px}.dashboard-page .dashboard-row-meta .cell-expires,.dashboard-page .dashboard-row-meta .cell-views{flex-shrink:0;font-size:12px}.dashboard-page .cell-expires::before,.dashboard-page .cell-views::before{content:none}.dashboard-page .mobile-meta-icon{display:inline-block;width:14px;height:14px}.dashboard-page .dashboard-row-meta .status-sub-date{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dashboard-page .dashboard-row-meta .status-sub-error,.dashboard-page .dashboard-row-meta .status-sub-warn{flex-basis:100%;white-space:normal;overflow-wrap:anywhere}.dashboard-page .dashboard-row-meta .status-sub::before{content:none}.dashboard-page .project-attention-action{min-height:44px;display:inline-flex;align-items:center}.dashboard-page .paper-table .anon-sub .commit-hash,.dashboard-page .paper-table .anon-sub .conf-tag{display:none}}@media (max-width:767px){.container.dashboard-page{padding-top:12px}.dashboard-heading .paper-page-title{font-size:24px}.dashboard-heading-actions .btn-ink{min-height:44px}.dashboard-new-label{display:none}.dashboard-new-mobile{display:inline}.dashboard-quota>summary{min-height:44px;gap:6px;padding:10px;font-size:12px;line-height:20px}.dashboard-quota .quota-panel{max-height:calc(100dvh - 184px);overflow-y:auto;overscroll-behavior:contain}.dashboard-page .dashboard-filter-row{flex-wrap:wrap;gap:8px;margin-top:12px}.dashboard-page .dashboard-filter-row .search-wrap{flex:1 1 0}.dashboard-page .dashboard-filter-row .form-control{height:44px}.dashboard-page .dashboard-filters-toggle{display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:8px;min-height:44px;background:0 0;border:0}.dashboard-page .dashboard-filter-controls{display:none;width:100%}.dashboard-page .dashboard-filter-controls.expanded{display:flex;flex-wrap:wrap;gap:8px}.dashboard-page .dashboard-filter-controls .btn-group{width:100%;display:flex}.dashboard-page .dashboard-filter-controls .btn-group .btn{flex:1;min-height:44px}.dashboard-extra-filters{display:flex;width:100%}.dashboard-page .dashboard-extra-filters .dropdown{flex:1;min-width:0}.dashboard-page .dashboard-extra-filters .dropdown-toggle{width:100%;height:44px}.dashboard-type-filter .type-filter-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dashboard-type-filter .project-type-svg{display:inline-block}.dashboard-page .dashboard-meta{margin-top:8px;gap:4px 8px}.dashboard-view-tabs{gap:16px}.dashboard-view-tabs button{min-width:44px;min-height:44px;font-size:12px}.dashboard-count{font-size:11px}.dashboard-count{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.dashboard-view-long{display:none}.dashboard-view-short{display:inline}.dashboard-active-filters{display:contents}.dashboard-page .filter-chip{font-size:11px;padding:0 8px;min-height:44px;gap:4px}.dashboard-page .filter-chip-close{min-width:44px;min-height:44px;margin:0}.dashboard-page .btn-link-inline{min-height:44px;font-size:12px}.dashboard-page .paper-table-dashboard{width:calc(100% + 36px);margin:8px -18px 0;border-left:0;border-right:0;border-radius:0}.dashboard-page .paper-table .repo-name{font-size:13px}.project-details{padding:16px 12px}.project-details-list{grid-template-columns:1fr;gap:12px}.project-name-controls{flex-wrap:wrap}.project-name-controls .form-control{flex-basis:100%}.project-name-controls .btn{min-height:44px}}@media (max-width:360px){.dashboard-attention-prefix{display:none}.dashboard-attention-prefix+span{text-transform:capitalize}}.ui-icon-definitions{position:absolute;width:0;height:0;overflow:hidden;pointer-events:none}.ui-icon{display:inline-block;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0;vertical-align:-3px}.ui-icon.sort-direction{width:14px;height:14px}.pr-title-row{display:flex;align-items:flex-start;gap:16px}.pr-title-row .pr-title{flex:1;min-width:0}.pr-title-row>.btn{flex-shrink:0}.ui-icon.record-type-icon{color:var(--ink-muted)}.btn .ui-icon,.dropdown-item .ui-icon{width:16px;height:16px}.btn-icon-dots .ui-icon{width:18px;height:18px}.anonymize-page .paper-page-title,.paper-page .paper-page-title{font-size:28px}.paper-faq,.paper-page{padding-top:24px}.paper-page .paper-page-lede{margin-bottom:16px;overflow-wrap:anywhere}.paper-page-title{overflow-wrap:anywhere}.anonymize-page .form-control,.paper-page .form-control{min-height:40px;font-family:var(--font-sans);font-size:14px;border-radius:var(--radius-control)}.anonymize-page textarea.form-control,.paper-page textarea.form-control{height:auto}.anonymize-page .btn,.paper-page .btn,.status-bar-actions .btn{min-height:var(--button-height);padding:7px var(--button-padding-x);font:500 var(--button-font-size)/var(--button-line-height) var(--font-sans);border-radius:var(--radius-button);gap:6px}.anonymize-page .btn.btn-sm,.paper-page .btn.btn-sm,.status-bar-actions .btn{min-height:var(--button-compact-height);padding:5px 10px;font-size:13px;border-radius:6px}.anonymize-page .btn-ink,.paper-page .btn-ink{min-height:40px;padding:9px 15px}.btn-hero>i,.btn>.project-type-svg,.btn>.ui-icon,.btn>i{width:16px;height:16px;margin:0!important;flex-shrink:0}.btn.dropdown-toggle::after{margin-left:0}.btn.disabled,.btn:disabled{cursor:not-allowed}.btn.disabled:hover,.btn:disabled:hover{filter:none}.paper-page .btn.btn-link{background:0 0;border-color:transparent}.auth-continue.btn{min-height:44px;padding:11px 16px!important}.anonymize-page .btn.btn-icon,.paper-page .btn.btn-icon{width:32px;height:32px;min-width:32px;min-height:32px;padding:0}.paper-page .btn.btn-icon-dots{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;min-height:32px;padding:0;border:0;background:0 0;color:var(--ink-muted)}.paper-page .btn-icon-dots:hover{background:var(--accent-soft);color:var(--accent)}.paper-page .btn-group .btn-primary:active,.paper-page .btn-group .btn-primary:hover{background:var(--accent-soft)!important;color:var(--accent)}.paper-page .paper-section-eyebrow{font-family:var(--font-sans);font-size:16px;font-weight:600;color:var(--color);text-transform:none;letter-spacing:0;margin-bottom:12px}.paper-page .paper-field-label{font-family:var(--font-sans);font-size:13px;letter-spacing:0}.paper-tabs .paper-tab{min-height:44px}.paper-pill,.status-pill{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none;border-radius:6px}.paper-meta-rule{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none;gap:12px 24px}.paper-meta-rule b:not(.commit-hash){font-family:var(--font-sans);font-weight:500}.connections-grid{gap:16px}.connections-grid>section{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);padding:20px}.connections-heading{margin-bottom:24px}.claim-page .claim-form,.status-page>.paper-settings-section{padding:20px;margin-top:24px;border:1px solid var(--border-color);border-radius:var(--radius-surface);background:var(--paper-card)}.claim-page .claim-form{max-width:640px}.status-page .anonymize-submit-bar{background:0 0;gap:8px;flex-wrap:wrap}.anonymize-landing{padding-top:32px}.paper-settings .quota-usage{grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}.paper-settings .quota-usage .quota-item+.quota-item{padding-top:0;border-top:0;padding-left:16px;border-left:1px solid var(--border-color)}.paper-settings .quota-usage .quota-header{flex-direction:row;align-items:center;flex-wrap:wrap}.paper-settings .quota-usage .quota-value{white-space:normal;flex-wrap:wrap}.paper-faq-hero{padding-bottom:24px;margin-bottom:24px}.paper-faq-toc-head{font-family:var(--font-sans);font-size:12px;text-transform:none;letter-spacing:0;margin-bottom:8px}.paper-faq-eyebrow{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:14px}.paper-faq-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.4rem,4.6vw,3.6rem);line-height:1.05;letter-spacing:-.02em;max-width:13em;margin-bottom:16px;text-wrap:balance}.paper-faq-title em{font-style:italic;color:var(--accent)}.paper-faq-lede{font-size:14px}.paper-faq-body{grid-template-columns:180px minmax(0,1fr);gap:40px}.faq-section h4.faq-cat{font:400 30px/1.15 var(--font-serif);letter-spacing:-.015em;padding-bottom:12px}.faq-section h4.faq-cat .ui-icon{display:none}.faq-section .panel-group{margin-bottom:32px}.faq-section .panel-default .panel-title a{font:500 15px/1.5 var(--font-sans);padding:16px 0 16px 28px;min-height:44px}.faq-section .panel-default .panel-body{padding-left:28px!important}.faq-section .panel-default .panel-title a:after{content:"";top:22px;width:8px;height:8px;border-right:1.5px solid var(--ink-soft);border-bottom:1.5px solid var(--ink-soft);transform:translateX(4px) rotate(45deg);transition:transform .15s ease}.faq-section .panel-default .panel-title a.collapsed:after{content:"";transform:translateX(2px) rotate(-45deg)}.faq-section .panel-default .panel-title a:hover:after{border-color:var(--color)}.paper-faq-toc nav a.active{color:var(--color);font-weight:500}.paper-empty-title{font:600 32px/1.2 var(--font-sans);letter-spacing:-.025em}.paper-empty-title em{font-style:normal}.btn-hero{justify-content:center;gap:8px;min-height:48px;padding:13px 20px;font-size:15px;line-height:20px;background:var(--accent);border-color:var(--accent);color:#fff;border-radius:var(--radius-button)}.dark-mode .btn-hero{background:#1f6a64;border-color:#1f6a64;color:#fff}.btn-hero:hover{background:var(--accent);color:#fff;filter:brightness(.94)}.dark-mode .btn-hero:hover{background:#24786f;border-color:#24786f;color:#fff;filter:none}.paper-table:not(.paper-table-dashboard){border-left:0;border-right:0;border-radius:0}.paper-table:not(.paper-table-dashboard) .paper-table-head{border-radius:0}.paper-table .cell-anon{gap:8px}.paper-table .cell-anon .anon-text{flex:1}.paper-table-conferences .cell-anon .repo-name{font-family:var(--font-sans);font-weight:600}.paper-table .anon-sub>a,.paper-table .anon-sub>span{min-width:0;overflow:hidden;text-overflow:ellipsis}.paper-table .anon-sub .ui-icon{width:14px;height:14px}.paper-page .filter-chip{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none;border-radius:6px}.paper-page .filter-chip-close .ui-icon{width:12px;height:12px}.admin-nav{gap:4px;border:0;border-bottom:1px solid var(--border-color);border-radius:0;padding:0 0 8px;background:0 0}.admin-nav a{font-family:var(--font-sans);font-size:13px;letter-spacing:0;text-transform:none;border-radius:6px;min-height:40px;padding:8px 12px}.admin-nav a.active{background:var(--accent-soft);color:var(--accent)}.admin-nav .ui-icon{width:16px;height:16px}.admin-filter-toolbar{background:0 0;border:0;padding:0}.admin-filter-inline>label,.admin-filter-inline>span{font-family:var(--font-sans);text-transform:none;letter-spacing:0}.admin-summary .summary-total{font-family:var(--font-sans);font-size:18px;font-weight:600}.admin-summary .summary-pill{font:500 13px/20px var(--font-sans);min-height:32px;padding:5px 10px;border-radius:6px}.admin-summary .summary-pill.active{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}.admin-summary .summary-pill .count{font-family:var(--font-sans)}.admin-active-chip{font-family:var(--font-sans);font-size:12px}.admin-active-chip button .ui-icon{width:12px;height:12px}.user-detail-card .user-header h2{font:600 20px/1.4 var(--font-sans)}.errors-page .errors-toolbar .select-wrap label,.errors-page .volume-head,.overview-page .ov-panel-meta,.paper-detail-grid .detail-label,.paper-progress .paper-progress-label,.status-page .paper-error-eyebrow,.user-detail-grid .detail-label{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.overview-page .ov-chart-title,.overview-page .ov-panel-title,.paper-error-title{font:600 16px/1.4 var(--font-sans)}.paper-error-head>.ui-icon,.paper-ratelimit-head>.ui-icon{margin-top:3px;color:var(--status-error)}.paper-ratelimit-head>.ui-icon{color:var(--status-progress)}.errors-page .seg-tabs{border-radius:var(--radius-control)}.errors-page .seg-tabs button{min-height:36px;padding:7px 12px;font:500 14px/20px var(--font-sans)}.errors-page .seg-tabs button.active{background:var(--accent-soft);color:var(--accent)}.errors-page .errors-list,.errors-page .kpi-card:hover,.errors-page .volume-chart,.overview-page .ov-chart-card,.overview-page .ov-panel-card{box-shadow:none}.errors-page .kpi-value,.overview-page .ov-daily-value,.overview-page .ov-kpi-value,.q-card-count,.user-hero-name{font-family:var(--font-sans);font-size:28px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.errors-page .kpi-label,.overview-page .ov-daily-label,.overview-page .ov-kpi-label,.q-card-name,.q-section-label,.user-hero-details .detail-label{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.overview-page .ov-daily-card{background:var(--paper-card);box-shadow:none;border-radius:var(--radius-surface)}.dark-mode .overview-page .ov-daily-value,.overview-page .ov-daily-value{color:var(--accent)}.errors-page .kpi-card,.overview-page .ov-kpi-card{box-shadow:none;border-radius:var(--radius-surface)}.q-card{border-radius:var(--radius-surface)}.q-card.selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}.q-range-btns{border-radius:var(--radius-control)}.q-job-detail-actions .btn,.q-range-btns .btn{font-family:var(--font-sans);font-size:13px}.q-range-btns .btn.active{background:var(--accent-soft);color:var(--accent)}.btn-dark{border-radius:var(--radius-control);font-family:var(--font-sans);font-size:12px}@media (max-width:900px){.paper-settings .quota-usage{grid-template-columns:1fr;gap:16px}.paper-settings .quota-usage .quota-item+.quota-item{padding-left:0;border-left:0;border-top:1px solid var(--border-color);padding-top:16px}.paper-table:not(.paper-table-dashboard) .paper-table-row{gap:4px 8px!important;padding:8px 12px}.paper-table:not(.paper-table-dashboard) .cell-anon{flex:1 1 calc(100% - 52px);order:1}.paper-table:not(.paper-table-dashboard) .cell-actions{order:2;flex:0 0 44px}.paper-table .admin-users-row>[role=cell]:first-child,.paper-table.paper-table-repos.has-bulk .paper-table-row>[role=cell]:first-child{flex:0 0 24px;order:0}.paper-table .admin-users-row .cell-anon,.paper-table.paper-table-repos.has-bulk .cell-anon{flex-basis:calc(100% - 84px)}.paper-table:not(.paper-table-dashboard) .cell-anon .anon-sub{font-size:12px;gap:6px}.paper-table:not(.paper-table-dashboard) .cell-status .status-sub{font-size:12px}.paper-table .cell-expires::before,.paper-table .cell-views::before{content:none}.paper-table .cell-expires .mobile-meta-icon,.paper-table .cell-views .mobile-meta-icon{display:inline-block;width:14px;height:14px}.paper-faq-body{grid-template-columns:1fr;gap:24px}}@media (max-width:767px){.paper-faq,.paper-page{padding-top:20px}.anonymize-page .paper-page-title,.paper-page .paper-page-title,.paper-page.admin-page .paper-page-title{font-size:24px}.paper-faq-title{font-size:40px}.anonymize-page .form-control,.paper-page .form-control{min-height:44px}.anonymize-page .btn,.connection-link,.draft-discard,.paper-faq-toc a,.paper-page .btn,.paper-settings-toc nav a{min-height:44px}.anonymize-page .btn,.paper-page .btn,.status-bar-actions .btn{min-height:44px;padding:11px 14px;font-size:14px;line-height:20px;white-space:normal}.anonymize-page .btn.btn-sm,.paper-page .btn.btn-sm{min-height:44px;padding:11px 12px;font-size:14px}.anonymize-page .btn.btn-icon,.copy-link-inline,.paper-page .btn.btn-icon,.paper-page .btn.btn-icon-dots,.project-close{width:44px;height:44px;min-width:44px;min-height:44px;padding:0}.dashboard-page .dashboard-filter-row .btn-group .btn,.dashboard-page .dashboard-filter-row .dropdown-toggle{height:auto;min-height:44px;font-size:14px;padding:11px 10px}.dashboard-heading-actions .btn-ink{padding:11px 16px}.dashboard-page .dashboard-filters-toggle{padding:11px 10px}.errors-page .btn-icon,.pr-title-row .btn,.status-bar-actions .btn{min-width:44px;min-height:44px;padding:0}.status-bar-actions .btn{width:44px;height:44px}.errors-page .seg-tabs button,.paper-tab{min-height:44px}.errors-page .seg-tabs button{padding:11px 12px}.status-bar-actions{flex-wrap:wrap;max-width:100%}.leftCol-close,.tree-search-clear{min-width:44px;min-height:44px}.anonymize-landing{padding:20px 18px}.anonymize-landing .repo-access-switch button,.source-url-label-row .repo-access-switch button{min-height:44px;padding:11px 10px;font-size:13px}.paper-settings-body{margin-top:16px;padding-top:0;gap:16px}.paper-settings-toc nav{gap:0 12px}.paper-settings-toc nav a{display:inline-flex;align-items:center}.paper-faq{padding-left:18px;padding-right:18px}.paper-faq-toc nav{display:flex;flex-direction:row;flex-wrap:wrap;gap:0 16px}.paper-faq-toc a{display:inline-flex;align-items:center;padding:8px 0;border-left:0;border-bottom:2px solid transparent;margin-left:0}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover{border-bottom-color:var(--accent);color:var(--accent)}.claim-page .claim-form,.status-page>.paper-settings-section{padding:16px;margin-top:16px}.admin-nav{flex-direction:row;flex-wrap:nowrap;overflow-x:auto;gap:4px;max-width:100%;margin:8px 0 16px}.admin-nav a{flex-shrink:0;min-height:44px}.admin-summary .summary-pill{min-height:44px;padding:11px 12px;font-size:14px}.admin-active-chip button{min-width:44px;min-height:44px}.admin-filter-row{gap:8px}.admin-filter-row .admin-filter-inline{gap:8px;flex-wrap:wrap}.errors-page .kpi-grid{grid-template-columns:1fr 1fr;gap:12px}.q-cards{flex-direction:row}.q-card{min-width:130px}.q-header-actions{flex-wrap:wrap;gap:8px}}.file-error .viewer-error{width:100%;max-width:65ch;padding:2rem 1rem;overflow-wrap:anywhere;font-size:1rem;font-weight:400;text-align:left}.viewer-error .paper-empty-title{margin-bottom:1rem}.viewer-error p{line-height:1.6}.viewer-error .paper-error-actions{display:flex;flex-wrap:wrap;gap:.75rem}.markdown-body .markdown-alert{margin:1rem 0;padding:.5rem 1rem;border-left:4px solid var(--alert-color,#0969da)}.markdown-body .markdown-alert-note{--alert-color:#0969da}.markdown-body .markdown-alert-tip{--alert-color:#1a7f37}.markdown-body .markdown-alert-important{--alert-color:#8250df}.markdown-body .markdown-alert-warning{--alert-color:#9a6700}.markdown-body .markdown-alert-caution{--alert-color:#cf222e}.markdown-body .markdown-alert-title{color:var(--alert-color);font-weight:600;display:flex;align-items:center;gap:.5rem}.markdown-body .markdown-alert-title svg{fill:currentColor;flex-shrink:0}.markdown-body .markdown-alert>:last-child{margin-bottom:0}body.dark-mode .markdown-alert-note{--alert-color:#79c0ff}body.dark-mode .markdown-alert-tip{--alert-color:#7ee787}body.dark-mode .markdown-alert-important{--alert-color:#d2a8ff}body.dark-mode .markdown-alert-warning{--alert-color:#e3b341}body.dark-mode .markdown-alert-caution{--alert-color:#ff7b72}.markdown-body .emoji{display:inline;vertical-align:text-bottom}.markdown-body .footnotes{margin-top:2rem;border-top:1px solid var(--border-color,#d0d7de);padding-top:1rem;font-size:.875em}.markdown-body .katex-display{overflow-x:auto;overflow-y:hidden;padding:.25rem 0}.markdown-body .markdown-diagram{margin:1rem 0;border:1px solid var(--border-color,#d0d7de);padding:.75rem}.markdown-body .markdown-diagram svg{display:block;width:100%;max-height:360px}.markdown-body .markdown-diagram path{fill:#7cc7be;fill-opacity:.5;stroke:var(--link-color,#1f6a64);stroke-width:1;vector-effect:non-scaling-stroke}.markdown-body .diagram-controls{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem}.markdown-body .diagram-controls button{min-width:44px;min-height:44px;padding:.25rem .75rem;border:1px solid var(--border-color,#d0d7de);background:var(--main-bg-color,#fff);color:inherit;border-radius:4px}.markdown-body .diagram-controls button:hover{background:var(--hover-bg-color,#f6f8fa)}.markdown-body .diagram-controls button:focus-visible{outline:2px solid var(--link-color,#0969da);outline-offset:2px}.markdown-body .hljs-keyword,.markdown-body .hljs-selector-tag{color:#cf222e}.markdown-body .hljs-attr,.markdown-body .hljs-string{color:#0a3069}.markdown-body .hljs-literal,.markdown-body .hljs-number,.markdown-body .hljs-title{color:#8250df}.markdown-body .hljs-comment{color:#57606a}body.dark-mode .markdown-body .hljs-keyword{color:#ff7b72}body.dark-mode .markdown-body .hljs-attr,body.dark-mode .markdown-body .hljs-string{color:#a5d6ff}body.dark-mode .markdown-body .hljs-literal,body.dark-mode .markdown-body .hljs-number,body.dark-mode .markdown-body .hljs-title{color:#d2a8ff}body.dark-mode .markdown-body .hljs-comment{color:#b1bac4}body.dark-mode .markdown-body .gh-light-mode-only,body:not(.dark-mode) .markdown-body .gh-dark-mode-only{display:none}@media (max-width:600px){.ov-cache .ov-performance-window{display:grid;grid-template-columns:auto minmax(0,1fr);width:100%}.ov-cache .ov-performance-window select{width:100%;min-width:0}}.viewer-loading{padding:24px clamp(16px,4vw,40px);min-height:280px;color:var(--color)}.viewer-loading-status{min-height:84px}.viewer-loading-status strong{display:block;font-size:15px;font-weight:600}.viewer-loading-name{display:block;margin-top:6px;font-family:monospace;font-size:13px;overflow-wrap:anywhere;opacity:.75}.viewer-loading-status p{margin:12px 0;font-size:14px}.viewer-loading-lines{display:grid;gap:14px;max-width:720px;padding-top:16px}.viewer-loading-lines span{height:9px;width:70%;background:var(--border-color);border-radius:3px}.viewer-loading-lines span:nth-child(3n+1){width:48%}.viewer-loading-lines span:nth-child(3n+2){width:88%}.ace_scrollbar-h{height:8px!important}.ace_scrollbar-h::-webkit-scrollbar{height:8px}.ace_scrollbar::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--ink-faint) 55%,transparent);border-radius:4px}.ace_scrollbar::-webkit-scrollbar-track{background:0 0}.explorer-content,.explorer-main{background:var(--paper-card)}.explorer-content .dir-card,.explorer-content .file-card{margin:0!important;border:0;border-radius:0;background:0 0;overflow:visible}.explorer-content .file-card{min-height:100%}.explorer-content .file-card.is-code{height:100%!important;overflow:clip}.explorer-content .file-card-head{position:sticky;top:0;z-index:3;min-height:52px;padding:8px 12px 8px 32px;border-radius:0!important;border-bottom:1px solid var(--border-soft);background:color-mix(in srgb,var(--paper-card) 90%,transparent);-webkit-backdrop-filter:saturate(1.2) blur(8px);backdrop-filter:saturate(1.2) blur(8px)}.dark-mode .explorer-content .file-card-head{background:color-mix(in srgb,var(--paper-card) 90%,transparent)}.explorer-content .file-card .file-content.markdown-body{max-width:880px;padding:36px 32px 96px;background:0 0!important}.explorer-content .file-card .ace_editor{border-radius:0}.explorer-content .ace_editor.ace-chrome .ace_gutter,.explorer-content .ace_editor.ace-nord-dark .ace_gutter{background:var(--paper-card)!important;border-right:0}.explorer-content .ace_editor .ace_gutter-cell{padding-left:20px}.explorer-content .dir-card{border-bottom:1px solid var(--border-soft)}.explorer-content .dir-card-head{padding:14px 32px;background:0 0!important;border-bottom:0}.explorer-content .dir-row{padding:0 32px}.explorer-content .dir-card+.file-card{margin-top:0!important}@media (max-width:767px){.explorer-content .file-card-head{padding:6px 8px 6px 16px;min-height:0}.explorer-content .file-card .file-content.markdown-body{padding:20px 16px 72px}.explorer-content .ace_editor .ace_gutter-cell{padding-left:8px}.explorer-content .dir-card-head,.explorer-content .dir-row{padding-left:16px;padding-right:16px}}.leftCol-brand{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:52px;padding:0 14px;border-bottom:1px solid var(--border-soft)}.leftCol-logo{font-family:var(--font-serif);font-size:19px;letter-spacing:-.01em;color:var(--color)!important;text-decoration:none!important;white-space:nowrap}.leftCol-logo em{font-style:italic}.leftCol-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 8px 2px 6px;border-radius:999px;background:var(--hover-bg-color);font-size:11.5px;font-weight:500;color:var(--ink-soft);white-space:nowrap}.leftCol-badge .ui-icon{width:13px;height:13px;color:var(--status-ready)}.leftCol-search{padding-top:12px}.leftCol-links{display:flex;align-items:center;gap:14px;margin:0 0 10px;font-size:13px}.leftCol-links a{color:var(--ink-soft);text-decoration:none}.leftCol-links a:hover{color:var(--color);text-decoration:underline;text-underline-offset:3px}.leftCol-theme{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:auto;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-soft);cursor:pointer}.leftCol-theme:hover{background:var(--hover-bg-color);color:var(--color)}.leftCol-theme .ui-icon{width:16px;height:16px}.leftCol-foot .leftCol-zip{display:flex}.card-files-btn{display:none!important}@media (max-width:991px){.card-files-btn{display:inline-flex!important}}.card-files-btn.is-needed{display:inline-flex!important}@media (max-width:991px){.leftCol-brand{padding-right:60px}.leftCol-head{position:absolute;top:0;right:0;z-index:2;padding:4px 6px!important;border:0!important;background:0 0!important}.leftCol-head .leftCol-eyebrow{display:none}}@media (max-width:991px){.leftCol-head .leftCol-close{width:40px;height:40px;border:0!important;background:0 0!important;border-radius:var(--radius-control);color:var(--ink-soft)}.leftCol-head .leftCol-close:hover{background:var(--hover-bg-color)!important;color:var(--color)}.leftCol-brand{min-height:52px}}.leftCol-links .leftCol-donate{display:inline-flex;align-items:center;gap:5px}.leftCol-links .leftCol-donate .ui-icon{width:14px;height:14px}.leftCol-links .leftCol-donate:hover{color:var(--status-error);text-decoration:none}.leftCol-links .leftCol-donate:hover .ui-icon{fill:currentColor}.files .file.active>a{border-radius:var(--radius-control)!important}.files .file.active>a::before{left:4px!important;top:7px!important;bottom:7px!important;width:3px!important;border-radius:2px!important}.leftCol-backdrop{-webkit-backdrop-filter:none!important;backdrop-filter:none!important;background:rgba(20,18,16,.45)!important}@media (max-width:991px){.leftCol-head{right:6px!important}}.symbols{position:relative}.file-card-head .btn.btn-symbols{gap:6px;padding:0 10px;color:var(--ink-soft);font-weight:500}.symbols-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:30;width:300px;max-height:420px;overflow:auto;padding:6px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);box-shadow:0 12px 32px rgba(26,24,21,.16)}.symbols-head{padding:6px 8px 8px;font-size:12px;font-weight:600;color:var(--ink-muted)}.symbols-item{display:flex;align-items:center;gap:10px;width:100%;height:32px;padding:0 8px;border:0;border-radius:var(--radius-control);background:0 0;font:inherit;font-size:13px;color:var(--color);text-align:left;cursor:pointer}.symbols-item:focus-visible,.symbols-item:hover{background:var(--hover-bg-color);outline:0}.symbols-kind{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;font:600 11px/1 var(--font-mono);flex-shrink:0}.symbols-kind.is-function{background:color-mix(in srgb,#6b45b8 14%,transparent);color:#6b45b8}.symbols-kind.is-class{background:color-mix(in srgb,#b4362a 14%,transparent);color:#b4362a}.dark-mode .symbols-kind.is-function{color:#c9a8f5}.dark-mode .symbols-kind.is-class{color:#f2897d}.symbols-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:12.5px}.symbols-line{font-size:12px;color:var(--ink-muted);font-variant-numeric:tabular-nums}@media (max-width:1180px){.file-card-head .btn-symbols .btn-find-label{display:none}.file-card-head .btn-symbols{width:32px;padding:0!important}}.ace_search{top:0!important;left:0!important;right:0!important;border:0!important;border-bottom:1px solid var(--border-soft)!important;border-radius:0!important;box-shadow:none!important;padding:8px 12px 8px 8px!important;background:var(--paper-card)!important}.ace_search_field{border-color:var(--border-color)!important}.ace_search .ace_button{border:1px solid transparent!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:color-mix(in srgb,var(--accent) 40%,transparent)!important}.ace_search_counter{color:var(--ink-soft)!important;font-size:13px!important}.ace_editor .ace_marker-layer .ace_selected-word{background:var(--accent-soft)!important}.ace_editor.ace-chrome .ace_marker-layer .ace_selection{background:color-mix(in srgb,var(--accent) 28%,transparent)!important;box-shadow:0 0 0 1px var(--accent)!important}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:color-mix(in srgb,var(--accent) 34%,transparent)!important;box-shadow:0 0 0 1px var(--accent)!important}.leftCol-foot{padding:10px 14px 12px}.leftCol-links{margin:0 0 6px;gap:12px}.leftCol-repo-actions{display:inline-flex}.leftCol-project-header .leftCol-hide{display:inline-flex}@media (max-width:991px){.leftCol-project-header button.leftCol-hide{display:none}}.ace-chrome .ace_string{color:#11704f!important}.ace-chrome .ace_entity.ace_name.ace_function,.ace-chrome .ace_support.ace_function{color:#5b3dc2!important}.ace-chrome .ace_keyword,.ace-chrome .ace_storage{color:#b02f22!important}.ace-chrome .ace_comment{color:#6f6a61!important}.ace-chrome .ace_keyword.ace_operator,.ace-chrome .ace_paren,.ace-chrome .ace_punctuation{color:#5c574f!important}.dark-mode .ace_editor.ace-nord-dark,.dark-mode .explorer-content,.dark-mode .explorer-content .ace_editor.ace-nord-dark .ace_gutter,.dark-mode .explorer-main{background:var(--canvas-bg-color)!important}.dark-mode .ace_search,.dark-mode .explorer-content .file-card-head{background:color-mix(in srgb,var(--canvas-bg-color) 92%,transparent)!important}.dark-mode .leftCol{background:var(--sidebar-bg-color)}.dark-mode .symbols-menu{background:#22201c}.paper-stats-grid{grid-template-columns:repeat(4,minmax(0,1fr))!important}.paper-stats-grid .paper-stat-card{padding-left:24px!important;padding-right:24px!important}.paper-stats-grid .paper-stat-card:first-child{padding-left:0!important}.paper-stat-value{font-size:40px!important}.paper-stats-grid .paper-stat-card{margin-right:0!important}.paper-stats-grid .paper-stat-card:last-child{border-right:0;padding-right:0!important}@media (max-width:767px){.paper-stats-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}.paper-stats-grid .paper-stat-card:nth-child(odd){padding-left:0!important}}.dark-mode .leftCol-foot,.leftCol-foot{background:0 0!important}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover{border-left-color:var(--accent)!important}.card-files-label{display:none}@media (max-width:991px){.file-card-head .card-files-btn{width:auto!important;padding:0 10px!important;gap:6px;border:1px solid var(--border-color)!important;color:var(--color)!important}.card-files-label{display:inline;font-size:13px;font-weight:500}}@media (max-width:767px){.file-card-head .btn.btn-icon-only{color:var(--color)!important}.file-card-head .file-card-meta .meta-extra{display:none!important}.file-card.is-code.no-wrap::after{width:64px!important}}.btn.btn-ghost.is-on{color:var(--accent)!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 35%,transparent)!important}.explorer-content .ace_editor.ace-chrome .ace_gutter,.explorer-content .ace_editor.ace-nord-dark .ace_gutter{border-right:1px solid var(--border-soft)!important}.ace_search .ace_button{width:28px!important;height:28px!important;border:1px solid var(--border-color)!important;color:var(--ink-soft)!important;font-size:12px!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:var(--accent)!important}.ace_search_counter{margin-left:2px!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(242,204,96,.38)!important;box-shadow:none!important}.ace_editor.ace-chrome .ace_marker-layer .ace_selection,.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:#f2cc60!important;box-shadow:0 0 0 1px #c69026!important}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:rgba(242,204,96,.55)!important}.explorer-content .file-card .file-content.markdown-body{margin:0 auto!important}.leftCol-project-header .leftCol-zipbtn{width:auto!important;gap:4px;padding:0 6px;font-size:11.5px;font-weight:600;letter-spacing:.02em}.leftCol-project-header .leftCol-zipbtn .ui-icon{width:13px;height:13px}.leftCol-brand{justify-content:space-between}.repo-panel{display:grid;gap:8px;padding:14px 14px 12px;border-bottom:1px solid var(--border-soft)}.repo-panel-title{display:flex;align-items:center;gap:8px;min-width:0;font-size:14px;font-weight:600;color:var(--color)}.repo-panel-title span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.repo-panel-title .ui-icon{width:16px;height:16px;flex-shrink:0;color:var(--ink-muted)}.repo-panel-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;font-size:12.5px;color:var(--ink-soft)}.repo-panel-meta .last-update{font:inherit!important;color:inherit!important;padding:0;border:0}.repo-panel-meta>span+span::before{content:"·";margin-right:10px;color:var(--ink-faint)}.repo-panel-meta>.repo-panel-badge+span::before{content:none}.repo-panel-badge{display:inline-flex;align-items:center;gap:4px;padding:1px 8px 1px 6px;border-radius:999px;background:color-mix(in srgb,var(--status-ready) 12%,transparent);color:var(--status-ready);font-size:11.5px;font-weight:600}.repo-panel-badge .ui-icon{width:12px;height:12px}.repo-panel-zip{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important}.repo-panel-zip:hover{background:var(--hover-bg-color);border-color:var(--ink-faint)}.repo-panel-zip .ui-icon{width:15px;height:15px}.dark-mode .repo-panel-zip{background:0 0}.leftCol-project-header{display:none!important}.leftCol-search{padding-top:12px;padding-bottom:4px}.tb-file{display:inline-flex;align-items:center;gap:4px;margin-left:6px;padding-left:10px;border-left:1px solid var(--border-color)}.file-card-head .btn.tb-btn{height:32px;gap:6px;padding:0 12px!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important;background:var(--paper-card)!important;color:var(--color)!important;font-size:13px;font-weight:500}.file-card-head .btn.tb-btn:hover{background:var(--hover-bg-color)!important;border-color:var(--ink-faint)!important}.file-card-head .btn.tb-btn .ui-icon{width:15px;height:15px}.file-card-head .btn.tb-btn-icon{width:32px;padding:0!important;justify-content:center}.dark-mode .file-card-head .btn.tb-btn{background:0 0!important}.file-card-actions>.tb-file:first-child{border-left:0;padding-left:0;margin-left:0}.tb-more{position:relative;display:none}.tb-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:40;width:220px;padding:6px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);box-shadow:0 12px 32px rgba(26,24,21,.18)}.tb-menu>a,.tb-menu>button{display:flex;align-items:center;gap:10px;width:100%;height:40px;padding:0 10px;border:0;border-radius:var(--radius-control);background:0 0;font:inherit;font-size:14px;color:var(--color)!important;text-align:left;text-decoration:none!important}.tb-menu>a:hover,.tb-menu>button:hover{background:var(--hover-bg-color)}.tb-menu .ui-icon{width:16px;height:16px;color:var(--ink-soft)}.tb-menu-check{margin-left:auto;color:var(--accent);font-weight:600}.tb-menu-sep{height:1px;margin:6px 4px;background:var(--border-soft)}@media (max-width:767px){.tb-more{display:inline-flex}.file-card-head .btn-find,.file-card-head .btn-goto,.file-card-head .btn-toggle,.file-card-head .symbols,.file-card-head .tb-file,.file-card-head .view-switch+.file-card-actions .tb-file{display:none!important}.file-card-head .tb-more-btn{width:40px!important;height:40px!important}.file-card-head .card-files-btn{height:40px!important}.file-card-path .paths .breadcrumb-item:last-child{font-size:15px}}.feat-mock{height:380px;overflow:hidden;background:var(--paper-card);font-family:var(--font-sans);color:var(--color)}.fm-form{padding:22px 26px;display:grid;gap:8px}.fm-label{font-size:12px;font-weight:600;color:var(--ink-soft);margin-top:6px}.fm-input{height:36px;display:flex;align-items:center;padding:0 12px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--input-bg);font-size:13.5px}.fm-input-sm{width:150px}.fm-chips{display:flex;flex-wrap:wrap;gap:6px}.fm-chips span{padding:4px 10px;border-radius:999px;background:color-mix(in srgb,var(--status-error) 9%,transparent);color:var(--color);font-size:12.5px;text-decoration:line-through;text-decoration-color:color-mix(in srgb,var(--status-error) 55%,transparent)}.fm-chips .fm-chip-add{background:0 0;border:1px dashed var(--border-color);color:var(--ink-muted);text-decoration:none}.fm-row{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:end}.fm-toggles{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}.fm-toggles span{padding:5px 10px;border:1px solid var(--border-color);border-radius:var(--radius-control);font-size:12.5px;color:var(--ink-muted)}.fm-toggles .is-on{border-color:color-mix(in srgb,var(--accent) 45%,transparent);background:var(--accent-soft);color:var(--accent)}.fm-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:14px;padding-top:14px;border-top:1px solid var(--border-soft)}.fm-share{font-family:var(--font-mono);font-size:12px;color:var(--ink-soft)}.fm-btn{padding:9px 14px;border-radius:var(--radius-button);background:var(--accent);color:#fff;font-size:13px;font-weight:600;white-space:nowrap}.dark-mode .fm-btn{background:#1f6a64}.fm-viewer{display:grid;grid-template-columns:150px 1fr;height:100%}.fm-tree{padding:14px 8px;background:var(--paper-bg-alt);border-right:1px solid var(--border-soft);font-size:12.5px}.fm-tree div{padding:5px 10px;border-radius:var(--radius-control);color:var(--ink-soft)}.fm-tree .is-folder::before{content:"▸ ";color:var(--ink-faint)}.fm-tree .is-active{background:rgba(128,128,128,.14);color:var(--color);font-weight:500;box-shadow:inset 3px 0 0 var(--accent)}.fm-code{min-width:0}.fm-code-bar{display:flex;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border-soft);font-size:12.5px;color:var(--ink-muted)}.fm-code-bar b{color:var(--color);font-weight:600}.fm-code pre{margin:0;padding:12px 16px;font-family:var(--font-mono);font-size:12.5px;line-height:1.75;color:var(--color);background:0 0;white-space:pre}.fm-code .ln{display:inline-block;width:22px;margin-right:14px;text-align:right;color:var(--ink-faint)}.fm-code .k{color:#b02f22}.fm-code .f{color:#5b3dc2}.fm-code .s{color:#11704f}.fm-code .c{color:var(--ink-muted);font-style:italic}.dark-mode .fm-code .k{color:#f2897d}.dark-mode .fm-code .f{color:#c9a8f5}.dark-mode .fm-code .s{color:#9dd3a8}.fm-code .redact{padding:0 4px;border-radius:3px;background:var(--accent-soft);color:var(--accent);font-style:normal}.fm-dash{padding:12px 18px;font-size:13px}.fm-dash-head,.fm-dash-row{display:grid;grid-template-columns:1.7fr .9fr .5fr .9fr;gap:12px;align-items:center}.fm-dash-head{padding:8px 0;border-bottom:1px solid var(--border-color);font-size:12px;font-weight:600;color:var(--ink-muted)}.fm-dash-row{padding:12px 0;border-bottom:1px solid var(--border-soft)}.fm-dash-row b{display:block;font-weight:600}.fm-dash-row i{display:block;font-style:normal;font-size:12px;color:var(--ink-muted);font-family:var(--font-mono)}.fm-dash-row.is-muted{color:var(--ink-muted)}.fm-dash .st{display:inline-flex;align-items:center;gap:6px}.fm-dash .st::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.fm-dash .st.is-ready{color:var(--status-ready)}.fm-dash .st.is-progress{color:var(--status-progress)}.fm-dash .st.is-expired{color:var(--ink-muted)}@media (max-width:767px){.feat-mock{height:auto;min-height:260px}.fm-viewer{grid-template-columns:1fr}.fm-tree{display:none}.fm-dash-head span:nth-child(4),.fm-dash-row span:nth-child(4){display:none}.fm-dash-head,.fm-dash-row{grid-template-columns:1.6fr 1fr .5fr}.fm-row{grid-template-columns:1fr}}.paper-stat-delta{margin-top:6px!important;font-family:var(--font-sans)!important;font-size:13px!important;color:var(--ink-muted)!important}.paper-stats-grid .paper-stat-card{padding-top:22px!important;padding-bottom:22px!important}.paper-eyebrow,.paper-faq-eyebrow,.paper-stats-meta{font-family:var(--font-sans)!important;font-size:12.5px!important;font-weight:600!important;letter-spacing:.06em!important;color:var(--ink-soft)!important}.paper-feature-tab .paper-eyebrow{font-size:12px!important}.paper-reassure li::before{color:var(--ink-soft)!important}.feat-mock{height:auto!important;min-height:300px}@media (max-width:767px){.file-card.is-code.no-wrap::after{display:none!important}.file-card.is-code.no-wrap .ace_scroller{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 48px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 48px),transparent)}.explorer-content .file-card-head{row-gap:6px;padding-top:8px;padding-bottom:8px}.file-card-path{flex:1 1 auto;gap:10px}.file-card-path .paths{flex:1 1 auto;min-width:0}.file-card-path .paths .breadcrumb-item:last-child{overflow:hidden;text-overflow:ellipsis}.file-card-head .card-files-btn{height:32px!important;border-radius:var(--radius-control)!important;padding:0 10px!important}.file-card-head .tb-more-btn{width:36px!important;height:32px!important}.file-card-head>.view-switch{order:4;margin-left:auto!important}.file-card-head .file-card-meta{order:3;flex-basis:auto!important;flex:1 1 auto;margin:0!important;align-self:center}}@media (min-width:768px){.explorer-content .file-card-head{padding-left:20px}.explorer-content .file-card .file-content.markdown-body{margin:0!important;padding-left:20px!important;padding-right:40px!important;max-width:900px}}.dark-mode .files .file.active>a{background:rgba(255,255,255,.13)!important}.dark-mode .btn.btn-ghost.is-on{background:rgba(124,199,190,.18)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.45)!important}.dark-mode .leftCol{background:#1e1b18!important}.ace_searchbtn_close{width:24px!important;height:24px!important;background-size:12px!important;opacity:.7}.ace_search_field:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.navbar .nav-link:not(.active):not(.btn-signin){color:var(--ink-soft)!important}.navbar .nav-support-inline{color:var(--ink-soft)!important}.paper-stat-delta{color:var(--ink-soft)!important}.paper-faq-toc-head,.paper-footer-head{font-family:var(--font-sans)!important;font-size:12px!important;font-weight:600!important;letter-spacing:.06em!important;text-transform:uppercase;color:var(--ink-soft)!important}.paper-feature:not(.is-on) .paper-feature-tab::after{border-color:var(--ink-soft)!important;border-width:0 2px 2px 0!important}.faq-section .panel-default .panel-body{max-width:720px!important}.repo-panel-meta .last-update{flex-basis:100%}.repo-panel-meta .last-update::before{content:none!important}@media (max-width:767px){.file-card-head .file-card-path{flex:1 1 calc(100% - 56px)!important}.file-card-head .file-card-actions{order:1}.file-card-head .file-card-meta{order:3}.file-card-head>.view-switch{order:4}}.repo-panel{display:none!important}.repo-id{display:flex;align-items:center;gap:10px;padding:14px 14px 6px}.repo-id-tile{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex-shrink:0;border-radius:9px;background:linear-gradient(140deg,color-mix(in srgb,var(--accent) 16%,var(--paper-card)),color-mix(in srgb,var(--accent) 6%,var(--paper-card)));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 22%,transparent);color:var(--accent)}.repo-id-tile .ui-icon{width:17px;height:17px}.repo-id-text{min-width:0}.repo-id-title{font-size:14px;font-weight:600;color:var(--color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.repo-id-sub{margin-top:1px;font-size:12.5px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.repo-id-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 14px 12px 58px;border-bottom:1px solid var(--border-soft);font-size:12.5px}.repo-id-count{color:var(--ink-muted);font-variant-numeric:tabular-nums}.repo-id-zip{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:var(--radius-control);color:var(--link-color)!important;font-weight:500;text-decoration:none!important}.repo-id-zip:hover{background:var(--accent-soft)}.repo-id-zip .ui-icon{width:14px;height:14px}.leftCol-foot .leftCol-links{display:none!important}.acct-bar{display:flex;align-items:center;justify-content:space-between;gap:8px}.acct{position:relative;min-width:0}.acct-chip{display:inline-flex;align-items:center;gap:8px;max-width:150px;height:34px;padding:0 8px 0 4px;border:0;border-radius:var(--radius-control);background:0 0;font:inherit;font-size:13px;font-weight:500;color:var(--color);cursor:pointer}.acct-chip:hover,.acct-chip[aria-expanded=true]{background:var(--hover-bg-color)}.acct-chip img,.acct-initial{width:24px;height:24px;border-radius:50%;flex-shrink:0}.acct-initial{display:inline-flex;align-items:center;justify-content:center;background:var(--accent-soft);color:var(--accent);font-size:12px;font-weight:700}.acct-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acct-caret{width:12px;height:12px;color:var(--ink-muted);transform:rotate(-90deg)}.acct-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:40;width:190px;padding:6px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);box-shadow:0 12px 32px rgba(26,24,21,.18)}.acct-menu a{display:flex;align-items:center;gap:10px;height:36px;padding:0 10px;border-radius:var(--radius-control);font-size:13.5px;color:var(--color)!important;text-decoration:none!important}.acct-menu a:hover{background:var(--hover-bg-color)}.acct-menu .ui-icon{width:16px;height:16px;color:var(--ink-soft)}.acct-signin{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 12px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important}.acct-signin:hover{background:var(--hover-bg-color);border-color:var(--ink-faint)}.dark-mode .acct-signin{background:0 0}.acct-icons{display:inline-flex;align-items:center;gap:2px}.acct-icons a,.acct-icons button{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-soft)!important;cursor:pointer}.acct-icons a:hover,.acct-icons button:hover{background:var(--hover-bg-color);color:var(--color)!important}.acct-icons .acct-donate:hover{color:var(--status-error)!important}.acct-icons .ui-icon{width:17px;height:17px}.leftCol-foot{padding:8px 10px 10px 14px!important}.leftCol-foot .last-update{display:none}.file-card-head .card-files-btn,.file-card-head .card-files-btn.is-needed{width:34px!important;height:34px!important;padding:0!important;border:0!important;background:0 0!important;color:var(--ink-soft)!important}.file-card-head .card-files-btn:hover{background:var(--hover-bg-color)!important;color:var(--color)!important}.file-card-head .card-files-btn .ui-icon{width:19px;height:19px}.card-files-label{display:none!important}.tb-copy-m{display:none!important}@media (max-width:767px){.tb-copy-m{display:inline-flex!important;width:36px!important;height:34px!important}.file-card-head .file-card-path{flex:1 1 calc(100% - 96px)!important;gap:6px!important}}.ace_search{flex-direction:row}.ace_searchbtn_close{order:99;margin-left:auto!important;width:28px!important;height:28px!important}.ace_search .ace_button,.ace_searchbtn{color:var(--ink-soft)!important}.file-card-head .btn-find:focus:not(:focus-visible){background:0 0!important}.dark-mode .explorer-content .markdown-body img{filter:brightness(.92)!important}@media (max-width:767px){.file-card.is-code.no-wrap .ace_scroller{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 80px),rgba(0,0,0,.15) calc(100% - 8px),transparent)!important;mask-image:linear-gradient(to right,#000 calc(100% - 80px),rgba(0,0,0,.15) calc(100% - 8px),transparent)!important}}.acct-menu-head{padding:6px 10px 8px;margin-bottom:4px;border-bottom:1px solid var(--border-soft);font-size:12px;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acct-menu-head b{color:var(--color);font-weight:600}@media (max-width:767px){.file-card.is-code.no-wrap{position:relative}.file-card.is-code.no-wrap::before{content:"Scroll \2194";position:absolute;right:12px;bottom:14px;z-index:25;padding:5px 10px;border-radius:999px;background:color-mix(in srgb,var(--color) 82%,transparent);color:var(--canvas-bg-color);font:600 12px/1 var(--font-sans);pointer-events:none;box-shadow:0 4px 14px rgba(0,0,0,.18)}.file-card-head>.view-switch{order:5!important;flex:1 1 100%;margin-left:0!important;display:grid!important;grid-template-columns:1fr 1fr}.file-card-head>.view-switch .btn{height:36px!important;min-height:36px!important;justify-content:center;font-size:14px}.file-card-head .file-card-meta{font-size:13px!important}}.acct-menu{width:240px}.acct-menu-head{white-space:normal;overflow-wrap:anywhere}.paper-feature .paper-feature-tab::after{border-width:0 1.5px 1.5px 0!important;border-color:var(--ink-soft)!important}.hero-mock-doc-bar span,.hero-mock-tree-title{color:var(--ink-soft)!important}.paper-frame-bar{color:var(--ink-soft)!important}.paper-frame-bar .dots i{background:color-mix(in srgb,var(--ink-muted) 55%,transparent)!important}.dark-mode .hero-mock-doc,.dark-mode .paper-frame{background:#24211d!important}.dark-mode .hero-mock-tree{background:#1c1a17!important}.dark-mode .paper-stats-grid .paper-stat-card{border-color:#3a3631!important}.dark-mode .paper-stats,.dark-mode .paper-stats-grid{border-color:#3a3631!important}.faq-section .panel-default .panel-body{padding-bottom:18px!important}.btn.btn-ghost.is-on{background:var(--hover-bg-color)!important;color:var(--accent)!important;box-shadow:none!important}.dark-mode .btn.btn-ghost.is-on{background:rgba(255,255,255,.08)!important;color:#7cc7be!important;box-shadow:none!important}.acct-chip .acct-caret,.acct-chip .acct-name{display:none}.acct-chip{padding:0 5px!important}.acct-chip img,.acct-initial{width:26px!important;height:26px!important}@media (min-width:768px){.explorer-content .file-card .file-content.markdown-body{margin:0 auto!important;padding-left:40px!important;padding-right:40px!important;max-width:920px}}.files .file.active>a::before{top:5px!important;bottom:5px!important}@media (max-width:767px){.explorer-content .ace_editor .ace_gutter-cell{padding-left:6px!important;padding-right:6px!important}}.repo-id,.repo-id-actions{display:none!important}.leftCol-search{display:flex;align-items:center;gap:6px;padding:12px 12px 8px!important}.leftCol-search .tree-search-box{flex:1 1 auto;min-width:0}.search-zip{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:1px solid var(--border-color);border-radius:8px;background:var(--canvas-bg-color);color:var(--ink-soft)!important}.search-zip:hover{background:var(--hover-bg-color);color:var(--color)!important;border-color:var(--ink-faint)}.search-zip .ui-icon{width:17px;height:17px}.dark-mode .search-zip{background:0 0}.support-cta{display:flex;align-items:center;gap:10px;margin:0 0 10px;padding:9px 12px;border-radius:var(--radius-surface);background:color-mix(in srgb,#e5484d 9%,var(--paper-card));box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 22%,transparent);color:var(--color)!important;text-decoration:none!important;transition:background .15s,box-shadow .15s}.support-cta:hover{background:color-mix(in srgb,#e5484d 14%,var(--paper-card));box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 40%,transparent)}.support-cta .ui-icon{width:18px;height:18px;flex-shrink:0;color:#e5484d;fill:#e5484d}.support-cta span{display:grid;min-width:0}.support-cta b{font-size:13px;font-weight:600}.support-cta small{font-size:12px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dark-mode .support-cta{background:color-mix(in srgb,#e5484d 12%,var(--sidebar-bg-color))}.leftCol-foot{padding:10px 12px 10px!important}.tb-file{display:inline-flex!important;align-items:stretch;gap:0!important;margin-left:8px;padding-left:0!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control);overflow:hidden}.tb-seg-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 12px;border:0;background:var(--paper-card);font:inherit;font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important;cursor:pointer}.tb-seg-btn+.tb-seg-btn{border-left:1px solid var(--border-color)}.tb-seg-btn:hover{background:var(--hover-bg-color)}.tb-seg-icon{width:34px;padding:0;color:var(--ink-soft)!important}.tb-seg-icon:hover{color:var(--color)!important}.tb-seg-icon .ui-icon{width:15px;height:15px}.dark-mode .tb-seg-btn{background:0 0}.file-card-actions>.tb-file:first-child{margin-left:0}.file-card-head .btn-find,.file-card-head .btn-goto,.file-card-head .btn-toggle{width:32px!important;height:32px!important}.file-card.is-code.no-wrap::before{content:none!important;display:none!important}.acct-chip{width:34px;height:34px;padding:0!important;justify-content:center;border-radius:50%!important;background:0 0!important}.acct-chip:hover,.acct-chip[aria-expanded=true]{background:var(--hover-bg-color)!important}.ace_searchbtn_close{margin-left:4px!important}.dark-mode .explorer-content .markdown-body img{filter:brightness(.9)!important}.snapshot-line{display:flex;align-items:center;gap:6px;padding:0 14px 8px;font-size:12.5px;color:var(--ink-soft);white-space:nowrap;overflow:hidden}.snapshot-line span{overflow:hidden;text-overflow:ellipsis}.snapshot-line .ui-icon{width:14px;height:14px;flex-shrink:0;color:var(--ink-muted)}.leftCol-search{padding-bottom:6px!important}@media (max-width:767px){.explorer-content .file-card-head .file-card-meta{display:none!important}.explorer-content .file-card-head{flex-wrap:nowrap!important;min-height:52px!important}.file-card-head .card-files-btn,.file-card-head .tb-copy-m,.file-card-head .tb-more-btn{width:40px!important;height:40px!important}.file-card-head .card-files-btn{background:var(--hover-bg-color)!important;color:var(--color)!important;border-radius:var(--radius-control)!important}.file-card-head .file-card-path{flex:1 1 auto!important}}.acct-menu{box-shadow:0 16px 40px rgba(26,24,21,.22),0 0 0 1px var(--border-color)!important;border:0!important}.dark-mode .acct-menu{background:#26231f;box-shadow:0 16px 40px rgba(0,0,0,.55),0 0 0 1px #3a3631!important}.acct-chip{width:32px!important;height:32px!important}.acct-chip img,.acct-initial{width:30px!important;height:30px!important;font-size:13px!important}.ace_search{padding:6px 12px 6px 8px!important;min-height:40px}.ace_search_field{height:28px!important}.ace_search_options{gap:0!important;margin-left:4px!important}.ace_search_options .ace_button{border-radius:0!important;margin:0!important}.ace_search_options .ace_button+.ace_button{margin-left:-1px!important}.ace_search_options .ace_button:first-of-type{border-radius:var(--radius-control) 0 0 var(--radius-control)!important}.ace_search_options .ace_button:last-of-type{border-radius:0 var(--radius-control) var(--radius-control) 0!important}.ace_searchbtn_close{width:28px!important;height:28px!important;border-radius:var(--radius-control)!important;margin-left:8px!important;background-color:transparent!important;opacity:.65}.ace_searchbtn_close:hover{background-color:var(--hover-bg-color)!important;opacity:1}.ace_search_counter{color:var(--ink-soft)!important}.ace_editor .ace_marker-layer .ace_selected-word{background:color-mix(in srgb,var(--accent) 16%,transparent)!important}.ace_editor.ace-chrome .ace_marker-layer .ace_selection,.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:color-mix(in srgb,var(--accent) 34%,transparent)!important;box-shadow:0 0 0 1px var(--accent)!important}.dark-mode .btn.btn-ghost.is-on{background:rgba(124,199,190,.16)!important;color:#a8ddd6!important}.paper-features{grid-template-columns:1fr 1.4fr!important;gap:40px!important}.faq-section .panel-default .panel-title a:after{border-color:var(--ink-soft)!important}.paper-features .paper-feature.is-on .paper-feature-tab::after,.paper-features .paper-feature:not(.is-on) .paper-feature-tab::after{border-width:0 1.5px 1.5px 0!important;border-color:var(--ink-muted)!important}.markdown-body mark.redact-chip,.redact-chip{display:inline-block;padding:0 6px;margin:0 1px;border-radius:4px;background:var(--accent-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 25%,transparent);color:var(--accent);font-family:var(--font-mono);font-size:.82em;font-weight:500;line-height:1.5;vertical-align:.06em;cursor:help;transition:background .1s}.redact-chip:hover{background:color-mix(in srgb,var(--accent) 22%,transparent)}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip{font-size:.7em;vertical-align:.18em}.dark-mode .redact-chip{background:rgba(124,199,190,.14);color:#a8ddd6}.ace_editor .ace_marker-layer .ace_redacted{position:absolute;background:var(--accent-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent);border-radius:3px}.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.16)}.meta-redact{color:var(--accent);font-weight:500}.explorer-page{display:grid!important;grid-template-columns:var(--sidebar-w,280px) minmax(0,1fr);grid-template-rows:44px minmax(0,1fr)}.viewer-bar{grid-column:1/-1;grid-row:1;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px 0 14px;border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt);z-index:20}.dark-mode .viewer-bar{background:var(--sidebar-bg-color)}.explorer-page>.leftCol{grid-row:2;grid-column:1;width:auto!important;flex:none!important;height:auto!important;min-height:0;border-top:0}.explorer-page>.explorer-main{grid-row:2;grid-column:2;min-height:0}.explorer-page.files-hidden{grid-template-columns:minmax(0,1fr)}.explorer-page.files-hidden>.explorer-main{grid-column:1/-1}.leftCol-resizer{top:44px!important}.card-files-btn.is-needed{display:none!important}.vb-left,.vb-right{display:flex;align-items:center;gap:4px;min-width:0}.vb-logo{font-family:var(--font-serif);font-size:18px;letter-spacing:-.01em;color:var(--color)!important;text-decoration:none!important;white-space:nowrap;margin-right:10px}.vb-logo em{font-style:italic}.vb-icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-soft)!important;cursor:pointer;text-decoration:none!important}.vb-icon:hover{background:var(--hover-bg-color);color:var(--color)!important}.vb-icon .ui-icon{width:17px;height:17px}.vb-snapshot{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;margin-right:6px;border-radius:999px;background:var(--hover-bg-color);font-family:var(--font-mono);font-size:12px;color:var(--ink-soft);white-space:nowrap;cursor:help}.vb-snapshot .ui-icon{width:13px;height:13px;color:var(--ink-muted)}.vb-support{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 12px;margin-right:4px;border-radius:999px;background:color-mix(in srgb,#e5484d 10%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 28%,transparent);font-size:13px;font-weight:600;color:#c2353a!important;text-decoration:none!important}.vb-support:hover{background:color-mix(in srgb,#e5484d 16%,transparent)}.vb-support .ui-icon{width:15px;height:15px;fill:currentColor}.dark-mode .vb-support{color:#ff8a8e!important;background:rgba(229,72,77,.14)}.vb-signin{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 12px;margin-left:6px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important}.vb-signin:hover{background:var(--hover-bg-color)}.dark-mode .vb-signin{background:0 0}.viewer-bar .acct{margin-left:6px}.viewer-bar .acct-menu{top:calc(100% + 8px);bottom:auto;left:auto;right:0}.explorer-page>.leftCol .leftCol-search{padding-top:12px!important}@media (max-width:991px){.explorer-page{grid-template-columns:minmax(0,1fr)}.explorer-page>.explorer-main{grid-column:1/-1}.vb-sidebar{display:none}.explorer-page>.leftCol{grid-row:auto}}@media (max-width:575px){.vb-bug,.vb-snapshot{display:none}.vb-support span{display:none}.vb-support{width:32px;padding:0;justify-content:center}}@media (max-width:991px){.explorer-page>.leftCol .leftCol-head{position:static!important;display:flex!important;align-items:center;justify-content:space-between;min-height:52px;padding:6px 8px 6px 16px!important;border-bottom:1px solid var(--border-soft)!important;background:0 0!important}.explorer-page>.leftCol .leftCol-head .leftCol-eyebrow{display:inline!important;font:600 15px/1 var(--font-sans);letter-spacing:0;text-transform:none;color:var(--color)}.explorer-page>.leftCol .leftCol-close{width:44px!important;height:44px!important}.files a{min-height:38px!important;font-size:15px}}.vb-support{background:var(--paper-card)!important;box-shadow:inset 0 0 0 1px var(--border-color)!important;color:var(--color)!important;font-weight:500!important}.vb-support .ui-icon{color:#e5484d;fill:#e5484d}.vb-support:hover{background:var(--hover-bg-color)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 40%,var(--border-color))!important}.dark-mode .vb-support{background:0 0!important;color:var(--color)!important}.vb-snapshot{font-family:var(--font-sans)!important;font-variant-numeric:tabular-nums;font-size:12.5px!important}.file-card-path .breadcrumb-root a{color:var(--ink-soft)!important}.file-card-path .breadcrumb-root a:hover{color:var(--color)!important;text-decoration:underline}@media (max-width:767px){.viewer-bar{padding:0 6px 0 12px}.vb-icon,.vb-support{width:40px!important;height:40px!important}.vb-signin{height:36px}.file-card-head .card-files-btn{background:0 0!important;color:var(--ink-soft)!important}}.ace_search{background:var(--paper-bg-alt)!important;border-bottom:1px solid var(--border-color)!important;gap:6px!important}.dark-mode .ace_search{background:#1e1b18!important}.ace_search .ace_searchbtn{width:28px!important;height:28px!important;border-radius:var(--radius-control)!important;border:1px solid var(--border-color)!important;background:var(--paper-card)!important}.ace_searchbtn_close{order:99;margin-left:6px!important;border:1px solid transparent!important;background-position:center!important;opacity:.8!important}@media (max-width:767px){.file-card.is-code.no-wrap .ace_scroller{-webkit-mask-image:none!important;mask-image:none!important}.file-card.is-code.no-wrap .file-card-body{position:relative}.file-card.is-code.no-wrap .file-card-body::after{content:"";position:absolute;top:0;right:0;bottom:0;width:36px;z-index:8;pointer-events:none;background:linear-gradient(to right,color-mix(in srgb,var(--paper-card) 0,transparent),var(--paper-card) 85%)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 85%)}}.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.24)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.5)!important}.paper-reassure,.paper-stat-delta,.paper-stat-label{color:var(--ink-soft)!important}.paper-faq-title{font-size:clamp(2.2rem,4vw,3.1rem)!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--paper-card) 80%)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 80%)!important}}@media (max-width:575px){.vb-support{box-shadow:none!important;background:0 0!important;border-radius:var(--radius-control)!important}.vb-support:hover{background:var(--hover-bg-color)!important}}.meta-redact{display:inline-flex;align-items:center;height:20px;padding:0 7px;border-radius:999px;background:var(--accent-soft);color:var(--accent)!important;font-size:12px;font-weight:600;cursor:default}.dark-mode .meta-redact{background:rgba(124,199,190,.14);color:#a8ddd6!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.6em!important;padding:1px 6px!important;vertical-align:.25em!important}.ace_search .ace_button{color:var(--ink-soft)!important;font-size:12.5px!important;font-weight:600!important;background:var(--paper-card)!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:var(--accent)!important;position:relative;z-index:1}.ace_search{align-items:center!important}.ace_search_counter{align-self:center;line-height:28px}.hero-mock-row.is-active{background:rgba(128,128,128,.12)!important;box-shadow:inset 3px 0 0 var(--accent)}.mobile-sub{display:none}@media (max-width:767px){.file-card-head .file-card-path{display:grid!important;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;align-items:center}.file-card-head .file-card-path .card-files-btn{grid-row:1/span 2}.file-card-head .file-card-path .paths{grid-column:2}.mobile-sub{display:block;grid-column:2;margin-top:1px;font-size:12px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mobile-sub b{color:var(--accent);font-weight:600}.dark-mode .mobile-sub b{color:#a8ddd6}.vb-icon[href="/faq"]{display:none!important}}.ace_searchbtn_close{font-size:0!important;color:transparent!important;background-size:10px 10px!important;align-self:center}.acct-initial{background:var(--hover-bg-color)!important;color:var(--color)!important;box-shadow:inset 0 0 0 1px var(--border-color)}.acct-chip[aria-expanded=true] .acct-initial{background:var(--accent-soft)!important;color:var(--accent)!important}.viewer-bar .acct-menu{top:calc(100% + 12px)!important}.dark-mode .paper-frame{box-shadow:0 0 0 1px #3f3a34,0 24px 60px rgba(0,0,0,.5)!important}.faq-section .panel-default .panel-body{max-width:640px!important}.anonymize-landing{align-items:flex-start!important;padding-top:clamp(32px,9vh,88px)!important}.landing-url-row{display:flex;gap:8px;align-items:stretch}.landing-url-row .form-control{flex:1 1 auto;min-width:0}.landing-continue{flex-shrink:0;height:auto;min-height:46px;padding:0 20px;font-weight:600}@media (max-width:575px){.landing-url-row{flex-direction:column}.landing-continue{min-height:44px}.source-url-label-row{flex-direction:column;align-items:flex-start!important;gap:8px}}.paper-table.paper-table-dashboard{width:100%}.q-card-count,.q-stat-value{font-family:var(--font-sans)!important;font-variant-numeric:tabular-nums;font-weight:600}.paper-table .paper-table-head [role=columnheader],.paper-table-head>div,.q-stat-label{font-family:var(--font-sans)!important;font-size:12px!important;letter-spacing:.02em!important;color:var(--ink-soft)!important}.errors-page .kpi-card.kpi-info .kpi-value{color:var(--color)!important}.errors-page .col-msg,.errors-page .col-msg *{font-weight:400!important}.errors-page .col-msg .msg-code,.errors-page .msg-code{font-size:13px!important}.admin-page select,.anonymize-page select,.errors-page select,.paper-page select,.paper-table select{height:34px;padding:0 30px 0 10px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--input-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236F6A61' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center;-webkit-appearance:none;appearance:none;font:inherit;font-size:13px;color:var(--input-color)}input[type=checkbox]{accent-color:var(--accent);width:15px;height:15px}.anonymize-page .form-control,.paper-page .form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page textarea{border:1px solid color-mix(in srgb,var(--ink-faint) 55%,var(--border-color))!important;border-radius:var(--radius-control)!important}.anonymize-page code,.paper-page code,.paper-page kbd{font-family:var(--font-mono)!important}.filter-chip.is-active .count,.paper-chip.is-active .count,.status-chip.active .count{color:inherit!important}.user-header h2{display:flex;align-items:center;flex-wrap:wrap;gap:10px}.status-dot-wrap{display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:500;color:var(--ink-soft)}.user-actions{display:flex;flex-wrap:wrap;gap:8px}.user-detail-grid .detail-value.is-empty{color:var(--ink-muted);font-style:normal;font-family:var(--font-sans)!important}.admin-section-header h2{font-family:var(--font-sans)!important;font-size:15px!important;font-weight:600!important;letter-spacing:0!important;text-transform:none!important;color:var(--color)!important}.errors-page .kpi-sub.down,.errors-page .kpi-sub.up{color:var(--ink-soft)!important}.errors-page .kpi-card.kpi-error .kpi-value{color:var(--color)!important}.errors-page .kpi-card.kpi-error:has(.kpi-value:not(:empty)) .kpi-value{color:var(--color)!important}.errors-page .kpi-label{font-family:var(--font-sans)!important}.errors-page .col-msg{font-family:var(--font-mono)!important;font-size:13px!important}.admin-page .admin-head,.admin-page .paper-page-head,.admin-page>header{min-height:92px}.admin-page .paper-page-title{min-height:44px;display:flex;align-items:center;margin-bottom:8px!important}.admin-page .errors-header,.admin-page .ov-header,.admin-page .q-header{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;margin-bottom:8px!important}.admin-page .errors-header .paper-page-title,.admin-page .ov-header .paper-page-title,.admin-page .q-header .paper-page-title{margin-bottom:0!important}.admin-page .admin-nav{margin-top:0!important}.admin-page .paper-crumbs{min-height:20px;margin-bottom:4px!important}.dashboard-page,.paper-page{max-width:1112px}.landing-continue:disabled{background:var(--hover-bg-color)!important;border-color:var(--border-color)!important;color:var(--ink-muted)!important;opacity:1!important;box-shadow:none!important}.landing-continue:not(:disabled){background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.admin-summary .summary-pill{border-color:var(--border-color)!important;color:var(--color)!important;background:var(--paper-card)!important}.admin-summary .summary-pill::before{content:"";display:inline-block;width:7px;height:7px;margin-right:6px;border-radius:50%;background:var(--ink-faint);vertical-align:1px}.admin-summary .summary-pill.ok::before{background:var(--status-ready)}.admin-summary .summary-pill.progress::before,.admin-summary .summary-pill.warn::before{background:var(--status-progress)}.admin-summary .summary-pill.err::before,.admin-summary .summary-pill.error::before{background:var(--status-error)}.admin-summary .summary-pill .count{color:var(--ink-soft)!important;font-weight:600}.admin-summary .summary-pill.active{background:var(--accent-soft)!important;border-color:color-mix(in srgb,var(--accent) 45%,transparent)!important;color:var(--accent)!important}.admin-summary .summary-pill.active .count{color:var(--accent)!important}.admin-summary .summary-pill:has(.count:is(:empty)){opacity:.6}.admin-page .btn-dark,.admin-page .btn-primary{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.btn-outline-danger{border:1px solid color-mix(in srgb,var(--status-error) 45%,transparent)!important;color:var(--status-error)!important;background:0 0!important}.admin-page .paper-table-head>*,.admin-page [role=columnheader],.admin-page h3,.errors-page th,.q-card-title,.q-header-label,.q-panel-title{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12.5px!important;font-weight:600!important;color:var(--ink-soft)!important}.ov-flag{color:var(--status-error);font-weight:600}.overview-page .ov-daily-value{color:var(--color)!important}@media (max-width:767px){.paper-table.paper-table-dashboard{margin-left:0!important;margin-right:0!important;width:100%!important}.dashboard-meta{flex-wrap:wrap;row-gap:8px}.dashboard-active-filters{flex-basis:100%;flex-wrap:wrap}.anonymize-landing{padding-top:20px!important}.anonymize-landing .paper-page-lede{font-size:15px!important}}.anonymize-landing .source-privacy-note,.anonymize-landing .text-muted:has(.ui-icon){display:flex;gap:8px;align-items:flex-start;padding:10px 12px;margin-top:12px;border-radius:var(--radius-control);background:color-mix(in srgb,var(--status-progress) 9%,transparent);color:var(--color)!important;font-size:13px}.repo-access-note>span:has(.ui-icon){display:flex;gap:8px;align-items:flex-start;margin-top:10px;padding:9px 12px;border-radius:var(--radius-control);background:color-mix(in srgb,var(--status-progress) 10%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--status-progress) 25%,transparent);color:var(--color)!important;font-size:13px}.repo-access-note>span:has(.ui-icon) .ui-icon{color:var(--status-progress);flex-shrink:0;margin-top:2px}.admin-summary .summary-pill.active{background:var(--paper-card)!important;border-color:var(--border-color)!important;color:var(--color)!important}.admin-summary .summary-pill.active .count{color:var(--ink-soft)!important}.admin-summary .summary-pill:not(.active){background:0 0!important;border-style:dashed!important;color:var(--ink-muted)!important}.admin-summary .summary-pill:not(.active),.admin-summary .summary-pill:not(.active)>:not(.count){text-decoration:line-through;text-decoration-color:var(--ink-faint)}.admin-summary .summary-pill:not(.active) .count{text-decoration:none}.admin-summary .summary-pill:not(.active)::before{opacity:.45}.admin-page:has(.admin-users-row) .admin-summary .summary-pill:not(.active),.user-detail-card~* .admin-summary .summary-pill:not(.active){border-style:solid!important;text-decoration:none!important;color:var(--color)!important;background:var(--paper-card)!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill:not(.active)::before{opacity:1}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active{background:var(--accent-soft)!important;border-color:color-mix(in srgb,var(--accent) 45%,transparent)!important;color:var(--accent)!important}.copy-link-inline{display:inline-flex!important;align-items:center;gap:6px;height:30px;padding:0 10px!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control);background:var(--paper-card)!important;color:var(--color)!important;font-size:13px;font-weight:500}.copy-link-inline:hover{background:var(--hover-bg-color)!important}.copy-link-inline .project-type-svg{width:14px;height:14px}@media (max-width:767px){.copy-link-label{display:none}.copy-link-inline{width:36px;justify-content:center;padding:0!important}}.overview-page .ov-val-warn{color:var(--color)!important}.ov-flag{color:var(--status-progress)!important}@media (max-width:767px){.admin-page .admin-nav{flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none;-webkit-mask-image:linear-gradient(to right,#000 85%,transparent);mask-image:linear-gradient(to right,#000 85%,transparent)}.admin-page .admin-nav::-webkit-scrollbar{display:none}.admin-page .admin-nav a{flex-shrink:0}.admin-page .ov-header,.admin-page .paper-page-title{text-align:left!important;justify-content:flex-start!important}.overview-page .ov-kpi-row>:last-child:nth-child(odd){grid-column:1/-1}.admin-summary{flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none}.admin-summary>*{flex-shrink:0}}.user-detail-grid .detail-value:first-of-type{font-family:var(--font-mono);font-size:13px}.user-header .type-badge{font-family:var(--font-sans)!important;letter-spacing:0!important;text-transform:none!important;font-weight:600}.errors-page .count-badge,.errors-page .msg-count,.errors-page [class*=count]{background:var(--hover-bg-color)!important;color:var(--color)!important}.ov-flag.critical{color:var(--status-error)!important}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(280px,3fr) 180px 70px 150px 150px!important}.paper-table.paper-table-dashboard .paper-table-row>:last-child{display:flex;justify-content:flex-end;align-items:center;gap:4px}.copy-link-inline{white-space:nowrap}@media (max-width:767px){.admin-filter-row,.admin-page form[class*=filter]{display:grid!important;grid-template-columns:1fr;gap:8px}.admin-page form[class*=filter] input,.admin-page form[class*=filter] select{width:100%!important}.admin-page form[class*=filter]>*{width:100%}.admin-page form[class*=filter] label{font-size:12px!important}}@media (min-width:768px){.copy-link-inline{width:auto!important;min-width:0!important}}@media (max-width:767px){.copy-link-inline{width:44px!important;height:44px!important}}.errors-list-head,.errors-list-head>span,.q-section-label{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12.5px!important;font-weight:600!important;color:var(--ink-soft)!important}.errors-list-head>span{background:0 0!important}.q-state-chip{background:var(--paper-card)!important;color:var(--color)!important;border:1px solid var(--border-color)!important;text-transform:none!important;font-family:var(--font-sans)!important;letter-spacing:0!important}.q-state-chip::before{content:"";display:inline-block;width:7px;height:7px;margin-right:6px;border-radius:50%;background:var(--ink-faint)}.q-state-chip.q-state-active::before{background:var(--accent)}.q-state-chip.q-state-delayed::before,.q-state-chip.q-state-waiting::before{background:var(--status-progress)}.q-state-chip.q-state-failed::before{background:var(--status-error)}.q-state-chip.q-state-completed::before{background:var(--status-ready)}.role-pill{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;background:var(--hover-bg-color);font:600 12px/1 var(--font-sans);color:var(--color)}.user-header .type-badge{background:var(--hover-bg-color)!important;border:0!important}.admin-filter-inline label,.admin-page label{font-size:12px!important;color:var(--ink-soft)!important}.admin-page select,.admin-page select.form-control{padding-right:30px!important;background:var(--input-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236F6A61' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center!important;-webkit-appearance:none!important;appearance:none!important}.admin-page .paper-table-row .text-muted,.admin-page .paper-table-row [class*=meta]{color:var(--ink-soft)!important}.dashboard-page.container,.paper-page,.paper-page.container{max-width:1112px!important;padding-left:32px!important;padding-right:32px!important}@media (max-width:767px){.dashboard-page.container,.paper-page,.paper-page.container{padding-left:18px!important;padding-right:18px!important}}.page-head-row>a.btn{margin-top:6px}.paper-faq-toc nav,.paper-settings-toc nav{border-left:1px solid var(--border-color)}.paper-settings-toc nav a{margin-left:-1px;border-left:2px solid transparent}.paper-settings-toc nav a.active{border-left-color:var(--accent)!important;color:var(--color)}.paper-page textarea{resize:vertical}.paper-page input[type=date]{height:38px;padding:0 10px;color:var(--input-color);font-family:var(--font-sans)}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto!important;grid-template-areas:"name actions" "meta meta";row-gap:6px}.paper-table.paper-table-dashboard .paper-table-row>:first-child{grid-area:name}.paper-table.paper-table-dashboard .paper-table-row>:last-child{grid-area:actions;align-self:start}.paper-table.paper-table-dashboard .paper-table-row>:not(:first-child):not(:last-child){grid-column:1/-1}.copy-link-inline{width:36px!important;height:36px!important}.admin-filter-toolbar{display:grid!important;grid-template-columns:1fr 1fr;gap:8px}.admin-filter-toolbar>:first-child{grid-column:1/-1}.admin-filter-toolbar .admin-filter-inline{display:grid!important;gap:4px}.admin-filter-toolbar .admin-filter-inline label{margin:0}.admin-filter-toolbar>:last-child{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center}.overview-page .ov-header{align-items:flex-start!important;text-align:left!important}.overview-page .ov-header .paper-page-title{width:100%;justify-content:flex-start!important;text-align:left!important}}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{flex-wrap:wrap!important;align-items:flex-start!important;row-gap:6px}.paper-table.paper-table-dashboard .paper-table-row>.cell-anon{flex:1 1 0!important;min-width:0;order:1}.paper-table.paper-table-dashboard .paper-table-row>.cell-actions{flex:0 0 auto!important;order:2;width:auto!important;margin:0!important}.paper-table.paper-table-dashboard .paper-table-row>.dashboard-row-meta{flex:1 1 100%!important;order:3}.admin-filter-toolbar{display:block!important}.admin-filter-row{display:grid!important;grid-template-columns:1fr 1fr;gap:8px!important}.admin-filter-row>:first-child{grid-column:1/-1}.admin-filter-row .admin-filter-inline{display:grid!important;gap:4px;min-width:0}.admin-filter-row .admin-filter-inline input{width:100%!important}.admin-filter-row>:last-child{grid-column:1/-1;display:flex!important;justify-content:space-between;align-items:center;flex-direction:row!important}.admin-filter-row>:last-child .btn{width:auto!important}}.page-head-row .btn-primary,.paper-page .btn-primary{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.dark-mode .paper-page .btn-primary{background:#1f6a64!important;border-color:#1f6a64!important}.q-section-right,.q-section-right *,.q-stat-label{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12px!important;color:var(--ink-soft)!important}.q-card .q-card-sub,[class*=q-card] small{font-family:var(--font-sans)!important}.quota-ring-track{stroke:var(--border-color)!important;stroke-width:3!important;fill:none}.quota-ring-fill{stroke:var(--accent)!important;stroke-width:3!important;fill:none;stroke-linecap:round;transform:rotate(-90deg);transform-origin:12px 12px;min-width:0}.quota-summary-ring{width:18px!important;height:18px!important}.errors-list-head .col-count,.errors-list-head .col-msg{background:0 0!important;font-family:var(--font-sans)!important;font-weight:600!important}.errors-page .col-count>*{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;background:var(--hover-bg-color)!important;color:var(--color)!important;font:600 12px/1 var(--font-sans);box-shadow:none!important}.errors-page .col-count>*>*{background:0 0!important}.errors-page .col-when{display:flex;flex-direction:column;gap:0;line-height:1.3}.errors-page .errors-list>div:not(.errors-list-head){padding-top:10px!important;padding-bottom:10px!important;min-height:0!important}.errors-page .kpi-sub{font-style:normal!important;color:var(--ink-soft)!important}.q-legend-exec{display:inline-block;width:14px;height:0;border-top:2px dashed var(--ink-muted);vertical-align:middle;margin-right:4px}.admin-filter-row .admin-search,.admin-filter-row input[placeholder^=Search],.admin-filter-row input[type=search],.admin-filter-row>:first-child{flex:1 1 320px!important;min-width:280px}.repo-access-note>span:has(.ui-icon){background:var(--hover-bg-color)!important;box-shadow:none!important}.repo-access-note>span:has(.ui-icon) .ui-icon{color:var(--ink-soft)!important}.dark-mode .paper-table-row{background:0 0!important}.dark-mode .paper-table-row:hover{background:rgba(255,255,255,.04)!important}.dark-mode .btn-primary,.dark-mode .dashboard-page .btn-primary,.dark-mode .paper-page .btn-primary{background:#1f6a64!important;border-color:#1f6a64!important;color:#fff!important}.dark-mode .dashboard-type-filter.btn-primary{background:rgba(124,199,190,.16)!important;border-color:transparent!important;color:#a8ddd6!important}.paper-page .form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page textarea{border-color:var(--border-color)!important}.paper-page .form-control:focus,.paper-page textarea:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.errors-page .col-count{background:0 0!important;box-shadow:none!important;display:flex;align-items:center}.errors-page .col-count::after,.errors-page .col-count::before{content:none!important;display:none!important}.errors-page .col-count .count-pill{width:auto!important;min-width:0!important}.paper-page .dashboard-type-filter.btn-primary{background:var(--accent-soft)!important;border-color:transparent!important;color:var(--accent)!important}.paper-page>.row{margin-left:0!important;margin-right:0!important}.paper-page>.row>[class*=col],.paper-page>.row>[class*=w-100]{padding-left:0!important;padding-right:0!important}.q-table th{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12.5px!important;font-weight:600!important;color:var(--ink-soft)!important}.admin-page input:not([type=checkbox]),.claim-page input,.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=search],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea,.paper-page textarea.form-control{border:1px solid var(--border-color)!important}.dark-mode .admin-page input:not([type=checkbox]),.dark-mode .claim-page input,.dark-mode .paper-page input.form-control,.dark-mode .paper-page textarea{border-color:#5a554d!important}.landing-continue:disabled{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important;opacity:.45!important}.overview-page .ov-kpi-sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media (max-width:767px){.admin-page .admin-nav{flex-wrap:wrap!important;overflow:visible!important;-webkit-mask-image:none!important;mask-image:none!important;row-gap:4px}.admin-summary{flex-wrap:wrap!important;overflow:visible!important;row-gap:8px}.admin-page .paper-table-row .text-muted,.admin-page .paper-table-row [class*=meta]{white-space:normal!important}}.summary-total small{font-size:13px;font-weight:500;color:var(--ink-soft);margin-left:2px}@media (max-width:767px){.overview-page .ov-daily-row,.overview-page .ov-kpi-row,.overview-page [class*=ov-][class*="-row"]{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:10px!important}.overview-page .ov-kpi-card,.overview-page [class*=ov-daily-card]{min-width:0!important;width:auto!important}.overview-page .ov-kpi-row>:last-child:nth-child(odd){grid-column:1/-1}}.user-detail-grid .detail-value:nth-of-type(1){font-family:var(--font-mono)!important}.paper-table-empty .empty-title{display:block;font-size:15px;font-weight:600;color:var(--color);margin-bottom:4px}.paper-table-empty>span{max-width:440px;text-align:center}.paper-table-empty-actions{margin-top:12px;display:flex;gap:8px;justify-content:center}.q-action-sep{display:inline-block;width:1px;height:22px;margin:0 6px;background:var(--border-color);vertical-align:middle}.hidden-toggle{padding:0;border:0;background:0 0;color:var(--link-color);font:inherit;text-decoration:underline;text-underline-offset:3px;cursor:pointer}.ov-meter{height:4px;margin:6px 0 2px;border-radius:4px;background:var(--hover-bg-color);overflow:hidden}.ov-meter>span{display:block;height:100%;border-radius:4px;background:var(--ink-faint)}.ov-meter.warn>span{background:var(--status-progress)}.ov-meter.crit>span{background:var(--status-error)}.admin-filters-toggle{display:none}@media (max-width:767px){.admin-filters-toggle{display:inline-flex;margin:8px 0}.admin-filter-toolbar .admin-filter-row:not(.is-open)>:not(:first-child){display:none!important}.admin-filter-toolbar .admin-filter-row:not(.is-open){grid-template-columns:1fr!important}}.paper-settings-layout,.settings-layout{max-width:none!important}.paper-settings-layout>:last-child,.settings-layout>:last-child{max-width:none!important;flex:1 1 auto}.errors-list-head>span{font-weight:600!important}.errors-page .errors-list>div:not(.errors-list-head){min-height:56px!important}.reading-layout{display:grid;grid-template-columns:minmax(0,720px) 220px;justify-content:center;gap:48px;padding:0 32px}.reading-layout .file-content.markdown-body{max-width:none!important;margin:0!important;padding:40px 0 96px!important;font-size:16px!important;line-height:1.7!important}.reading-layout .markdown-body h1{font-size:34px!important;font-weight:600!important;letter-spacing:-.02em;border-bottom:0!important;margin-bottom:12px!important}.reading-layout .markdown-body h2{font-size:24px!important;font-weight:600!important;letter-spacing:-.01em;border-bottom:0!important;margin-top:44px!important;padding-bottom:0!important;scroll-margin-top:72px}.reading-layout .markdown-body h3{font-size:18px!important;margin-top:28px!important;scroll-margin-top:72px}.reading-layout .markdown-body>p:first-of-type{font-size:18px;color:var(--ink-soft)}.readme-toc{position:sticky;top:68px;align-self:start;margin-top:44px;max-height:calc(100% - 100px);overflow:auto;display:grid;gap:2px;font-size:13px}.readme-toc-head{font-size:12px;font-weight:600;color:var(--color);margin-bottom:8px}.readme-toc a{display:block;padding:4px 0 4px 12px;border-left:1px solid var(--border-color);color:var(--ink-soft)!important;text-decoration:none!important;line-height:1.35}.readme-toc a:hover{color:var(--color)!important;border-left-color:var(--ink-muted)}.readme-toc a.lvl-3{padding-left:24px;font-size:12.5px}@media (max-width:1199px){.reading-layout{grid-template-columns:minmax(0,760px)}.readme-toc{display:none}}@media (max-width:767px){.reading-layout{padding:0 16px}.reading-layout .file-content.markdown-body{padding:20px 0 72px!important}.reading-layout .markdown-body h1{font-size:26px!important}.reading-layout .markdown-body h2{font-size:20px!important}}.demo{margin-top:28px;display:grid;gap:14px}.demo-terms{display:grid;gap:6px}.demo-terms label{font-size:13px;font-weight:600;color:var(--color);margin:0}.demo-terms label span{font-weight:400;color:var(--ink-soft);margin-left:6px}.demo-terms input{height:42px;padding:0 14px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font:14px var(--font-mono);color:var(--color)}.demo-terms input:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.demo-panes{display:grid;grid-template-columns:1fr 1fr;gap:14px}.demo .pane{border:1px solid var(--border-color);border-radius:var(--radius-surface);background:var(--paper-card);overflow:hidden;display:flex;flex-direction:column}.demo .pane-header{display:flex;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid var(--border-soft);font-size:12px;color:var(--ink-soft)}.demo .pane-label{font-weight:600;color:var(--color)}.demo .pane-url{font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.demo .pane-after{box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,var(--border-color))}.demo .pane-body{min-height:150px;padding:14px 16px;font:13px/1.75 var(--font-mono);color:var(--color);white-space:pre-wrap}.demo-input{border:0;resize:vertical;background:0 0;outline:0;width:100%}.demo-input:focus{background:color-mix(in srgb,var(--accent) 3%,transparent)}.demo-output .redact-chip{font-size:.92em}@media (max-width:767px){.demo-panes{grid-template-columns:1fr}}.paper-frame-bar .dots{display:none!important}.paper-frame-bar{justify-content:flex-start!important;height:32px!important;background:0 0!important}.paper-frame-bar .url{text-align:left!important;flex:none!important}.reading-layout .markdown-body>p:first-of-type{font-size:inherit!important;color:inherit!important}@media (max-width:767px){.mobile-sub b{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:999px;background:var(--accent-soft);font-size:11.5px}.explorer-content .file-card-head{min-height:52px!important;padding-top:6px!important;padding-bottom:6px!important}.file-card-head .file-card-path{gap:4px!important;column-gap:4px!important}.file-card.is-code.no-wrap .file-card-body::after{width:48px!important;background:linear-gradient(to right,transparent,var(--paper-card) 60%)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 60%)!important}}.ace_search{justify-content:flex-start}.ace_searchbtn_close{margin-left:auto!important;order:99}.fm-chips span,.paper-preview .term{background:var(--hover-bg-color)!important;color:var(--ink-soft)!important;text-decoration-color:var(--ink-muted)!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.06)!important}.dark-mode .ace-nord-dark .ace_marker-layer .ace_redacted,.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.32)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.65)!important}.paper-link-arrow{font-size:15px!important}.paper-reassure{font-size:14px!important}.acct-menu-head,.dark-mode .acct-menu-head{color:var(--ink-soft)!important}.reading-layout{grid-template-columns:minmax(0,720px) 200px!important;gap:40px!important;padding:0 24px!important}.vb-snapshot{color:var(--ink-soft)!important;background:var(--paper-card)!important;box-shadow:inset 0 0 0 1px var(--border-color)}.paper-faq-lede a{color:var(--color)!important;border-bottom:1px solid var(--ink-faint)!important;text-decoration:none!important}.paper-faq-lede a:hover{border-bottom-color:var(--color)!important}.paper-qa,.paper-section-head{margin-top:96px!important}.ace-chrome .ace_constant.ace_language,.ace-chrome .ace_constant.ace_numeric,.ace-chrome .ace_support.ace_constant{color:#0550ae!important}.ace-nord-dark .ace_constant.ace_language,.ace-nord-dark .ace_constant.ace_numeric,.ace-nord-dark .ace_support.ace_constant{color:#79c0ff!important}.copy-url{display:inline-flex;align-items:center;gap:8px;max-width:230px;height:30px;padding:0 8px 0 10px;border:0;border-radius:var(--radius-control);background:var(--hover-bg-color);font:500 12.5px/1 var(--font-mono);color:var(--ink-soft);cursor:pointer;transition:background .12s,color .12s}.copy-url:hover{background:var(--accent-soft);color:var(--accent)}.copy-url:disabled{cursor:progress}.copy-url-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.copy-url .project-type-svg{width:14px;height:14px;flex-shrink:0;opacity:.75}.copy-url:hover .project-type-svg{opacity:1}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(260px,3fr) 170px 64px 140px 280px!important}@media (max-width:767px){.copy-url{max-width:44px;padding:0;justify-content:center;width:40px;height:40px}.copy-url-text{display:none}}.dashboard-page .btn.dashboard-type-filter.btn-primary,.paper-page .btn.dashboard-type-filter.btn-primary{background:var(--accent-soft)!important;border-color:transparent!important;color:var(--accent)!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(242,204,96,.42)!important;box-shadow:none!important;border-radius:2px}.ace_editor.ace-chrome .ace_marker-layer .ace_selection{background:#f2cc60!important;box-shadow:0 0 0 1px #c69026!important}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:rgba(242,204,96,.55)!important;box-shadow:0 0 0 1px #f2cc60!important}.ace_searchbtn_close{background-image:none!important;width:28px!important;height:28px!important;display:inline-flex!important;align-items:center;justify-content:center;border-radius:var(--radius-control)!important;opacity:1!important;color:var(--ink-soft)!important}.ace_searchbtn_close::before{content:"\00d7";font:400 20px/1 var(--font-sans);color:var(--ink-soft)}.ace_searchbtn_close:hover{background-color:var(--hover-bg-color)!important}.ace_search{padding:6px 12px!important}.readme-toc a.is-active{color:var(--color)!important;border-left:2px solid var(--accent)!important;margin-left:-.5px;font-weight:500}.demo .pane-after{box-shadow:none!important;border-color:var(--border-color)!important}.demo .pane-body{min-height:120px!important}.demo-input{resize:none!important}.vb-snapshot .vb-dot{margin:0 2px;color:var(--ink-faint)}.dark-mode .btn-hero,.dark-mode .btn-primary,.dark-mode .landing-continue:not(:disabled){background:#2b8a80!important;border-color:#2b8a80!important;color:#fff!important}.paper-reassure li::before{content:""!important;display:inline-block;width:14px;height:14px;margin-right:7px;vertical-align:-2px;background:var(--accent);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center/contain no-repeat}.nav-support-inline .nav-heart{width:14px;height:14px;color:#e5484d;fill:#e5484d}.fm-chips span{color:var(--color)!important}@media (max-width:767px){.vb-icon,.vb-support{width:36px!important;height:36px!important}.vb-signin{height:36px!important}.navbar .nav-signin-m,.navbar-toggler{height:36px!important;border-radius:var(--radius-control)!important}}.overview-page .ov-spark-fill,.overview-page .ov-spark-fill-alt,.overview-page .ov-spark-fill-new-user,.overview-page .ov-spark-fill-user{background:color-mix(in srgb,var(--accent) 70%,transparent)!important}.overview-page .ov-dot-legend{background:var(--accent)!important}.errors-page .bucket-info,.errors-page .col-sev .sev-info::before,.errors-page .errors-chart [class*=info]{background:var(--ink-faint)!important}.errors-header .btn-danger{background:0 0!important;color:var(--status-error)!important;border:1px solid color-mix(in srgb,var(--status-error) 45%,transparent)!important;margin-left:10px}.conferences-page .paper-table:has(.paper-table-empty) .paper-table-head{display:none!important}.copy-url{max-width:280px!important}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(240px,3fr) 150px 60px 130px 320px!important}.dark-mode .paper-table-dashboard .paper-table-row{background:0 0!important}.ace_editor .ace_marker-layer .ace_redacted{background:color-mix(in srgb,var(--accent) 18%,transparent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 60%,transparent)!important}.ace_searchbtn_close{margin-left:4px!important}.ace_search .ace_button{font-size:12px!important}.nav-support-inline .nav-heart,.support-cta .ui-icon,.vb-support .ui-icon{color:#c25b5e!important;fill:#c25b5e!important}.nav-support-inline:hover .nav-heart,.support-cta:hover .ui-icon,.vb-support:hover .ui-icon{color:#e5484d!important;fill:#e5484d!important}.vb-support{background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important}.dark-mode .nav-support-inline .nav-heart,.dark-mode .vb-support .ui-icon{color:#d9787b!important;fill:#d9787b!important}.readme-toc .toc-rail{border-left:1px solid var(--border-color)}.readme-toc a,.readme-toc a.is-active{border-left:0!important;margin-left:-1px!important;padding:5px 0 5px 13px!important;line-height:1.4}.readme-toc a.lvl-3{padding-left:25px!important}.readme-toc a:hover{box-shadow:inset 1px 0 0 var(--ink-muted)}.readme-toc a.is-active{box-shadow:inset 2px 0 0 var(--accent)!important}@media (min-width:992px){.demo-panes{grid-template-columns:1fr 1.4fr;gap:40px}}.fm-chips span{text-decoration:none!important}.dark-mode .btn-hero,.dark-mode .btn-primary,.dark-mode .landing-continue:not(:disabled){background:#237a71!important;border-color:#237a71!important}.dark-mode .paper-frame{box-shadow:0 0 0 1px #34302b,0 24px 60px rgba(0,0,0,.5)!important}.dark-mode .leftCol{background:#1d1a17!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.09)!important}.dark-mode .explorer-content .markdown-body img{filter:none!important}@media (max-width:767px){.viewer-bar{padding-left:12px!important;padding-right:12px!important}.vb-signin{height:30px!important;padding:0 10px!important}}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(240px,3fr) 190px 60px 120px 300px!important}.paper-table-dashboard .cell-status .status-sub-date{white-space:normal!important;overflow:visible!important;text-overflow:clip!important}.row-edit{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--radius-control);color:var(--ink-soft)!important}.row-edit:hover{background:var(--hover-bg-color);color:var(--color)!important}.row-edit .ui-icon{width:16px;height:16px}.cell-actions{display:flex;align-items:center;gap:4px;justify-content:flex-end}.dashboard-filter-controls .btn-group[aria-label=Type]{background:var(--hover-bg-color);padding:3px;border-radius:calc(var(--radius-control) + 3px);gap:2px}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn{border:0!important;background:0 0!important;color:var(--ink-soft)!important;border-radius:var(--radius-control)!important;height:32px;box-shadow:none!important}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn.btn-primary{background:var(--paper-card)!important;color:var(--color)!important;box-shadow:0 1px 2px rgba(0,0,0,.08),0 0 0 1px var(--border-color)!important}.admin-filter-row .form-control,.dashboard-page .dashboard-filter-row .form-control,.paper-page .search-wrap .form-control{border:1px solid #a39d92!important}.dark-mode .admin-filter-row .form-control,.dark-mode .dashboard-page .dashboard-filter-row .form-control,.dark-mode .paper-page .search-wrap .form-control{border-color:#4a443d!important}.ov-health{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;margin:0 0 20px;border-radius:var(--radius-control);background:color-mix(in srgb,var(--status-error,#b42318) 8%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--status-error,#b42318) 30%,transparent);font-size:14px;color:var(--color)}.ov-health .ui-icon{width:18px;height:18px;flex:none;margin-top:1px;color:var(--status-error,#b42318)}.dark-mode .ov-health{background:rgba(240,138,130,.1);box-shadow:inset 0 0 0 1px rgba(240,138,130,.35)}.dark-mode .ov-health .ui-icon{color:#f08a82}.cell-anon .record-type-icon{display:none!important}.cell-anon{padding-left:0!important}.errors-page .errors-row-main{padding-top:10px!important;padding-bottom:10px!important;min-height:46px;align-items:center!important}.errors-page .col-when{display:flex;gap:8px;align-items:baseline}.errors-page .when-abs{font-size:12px}.errors-page .msg-code{font-family:var(--font-sans)!important;font-weight:500!important;font-size:13.5px!important}@media (max-width:767px){.viewer-bar .vb-icon,.viewer-bar .vb-signin,.viewer-bar .vb-support{height:36px!important;border-radius:var(--radius-control)!important;background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important;border:0!important}.viewer-bar .vb-icon,.viewer-bar .vb-support{width:36px!important}.viewer-bar .vb-signin{padding:0 12px!important}.file-card-head .card-files-btn,.file-card-head .tb-copy-m,.file-card-head .tb-more-btn{width:36px!important;height:36px!important;border-radius:var(--radius-control)!important;background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important;color:var(--color)!important}.file-card.is-code.no-wrap .file-card-body::after{width:64px!important;background:linear-gradient(to right,transparent,var(--paper-card) 70%)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 75%)!important}}.btn-toggle.is-on,.btn-toggle[aria-pressed=true]{color:var(--accent)!important;background:var(--accent-soft)!important}.dark-mode .btn-toggle.is-on{background:rgba(124,199,190,.14)!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.78em!important;padding:0 6px!important;vertical-align:.08em!important;font-weight:600!important}@media (min-width:1200px){.readme-toc{padding-top:10px}}.fm-toggles span{color:var(--color)!important}.dark-mode .paper-frame{border:0!important;box-shadow:0 0 0 1px #34302b,0 24px 60px rgba(0,0,0,.45)!important}.q-section-right .q-legend-completed{color:#2f7d4f!important}.q-section-right .q-legend-failed{color:#b42318!important}.dark-mode .q-section-right .q-legend-completed{color:#6fbf8a!important}.dark-mode .q-section-right .q-legend-failed{color:#f08a82!important}.q-chart-wrap canvas{max-height:180px}.avatar-fallback{width:28px;height:28px;flex:none;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--hover-bg-color);color:var(--ink-soft);font-size:12px;font-weight:600}.mono-id{font-family:var(--font-mono)!important;font-size:13px!important}.landing-continue:disabled{background:var(--hover-bg-color)!important;border-color:var(--border-color)!important;color:var(--ink-soft)!important;opacity:1!important;cursor:not-allowed}.paper-table .anon-sub,.paper-table .anon-sub *{font-size:12px}.dashboard-heading-actions{display:flex;align-items:center;gap:10px}.dashboard-heading-actions .dashboard-quota{position:relative;margin:0!important}.dashboard-heading-actions .quota-panel{right:0;left:auto!important}@media (max-width:767px){.admin-nav{display:flex!important;flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none;gap:4px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px}.admin-nav::-webkit-scrollbar,.admin-summary::-webkit-scrollbar{display:none}.admin-nav a{flex:none;white-space:nowrap}.admin-summary{display:flex!important;flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none;gap:6px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px}.admin-summary>*{flex:none;white-space:nowrap}.paper-table .anon-sub{flex-wrap:wrap!important;white-space:normal!important;row-gap:2px}.paper-table .anon-sub>a,.paper-table .anon-sub>span{overflow:visible!important;text-overflow:clip!important;white-space:nowrap}.paper-table .cell-anon .repo-name{white-space:normal!important;overflow-wrap:anywhere;overflow:visible!important;text-overflow:clip!important}.overview-page .ov-kpi-sub{white-space:normal!important;overflow:visible!important;text-overflow:clip!important}.overview-page .ov-kpi-row{grid-template-columns:1fr 1fr!important}}.dashboard-page .dashboard-heading-actions{flex-direction:row!important;align-items:center!important;gap:10px!important}@media (max-width:767px){.admin-page .admin-nav{flex-wrap:nowrap!important;overflow-x:auto!important;margin-left:-16px;margin-right:-16px;padding:0 16px 4px}header.viewer-bar .vb-icon,header.viewer-bar .vb-support{box-shadow:inset 0 0 0 1px var(--border-color)!important;background:0 0!important}}@media (max-width:767px){.dashboard-page .dashboard-heading{flex-direction:column!important;align-items:stretch!important;gap:12px}.dashboard-page .dashboard-heading-actions{justify-content:space-between!important;width:100%}.row-edit{display:none!important}}@media (max-width:767px){.paper-table-dashboard .copy-url{background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important}}@media (max-width:767px){.mobile-sub{display:flex!important;align-items:center;gap:6px;margin-top:3px!important}.mobile-sub>*+::before{content:"·";margin-right:6px;color:var(--ink-muted);font-weight:400}.mobile-sub>b::before{display:none}header.viewer-bar .vb-icon,header.viewer-bar .vb-support{box-shadow:none!important;background:0 0!important}.file-card-head .card-files-btn,.file-card-head .tb-copy-m,.file-card-head .tb-more-btn{box-shadow:none!important;background:0 0!important}.file-card-head .card-files-btn:active,.file-card-head .tb-copy-m:active,.file-card-head .tb-more-btn:active{background:var(--hover-bg-color)!important}.file-card-head{padding-top:6px!important;padding-bottom:6px!important}}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{box-shadow:none!important;border:0!important;padding:0 5px!important;font-weight:500!important}@media (min-width:1200px){.reading-layout{gap:40px!important}}.ace_search .ace_search_field:focus,.ace_search_field:focus{box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 22%,transparent)!important;border-color:color-mix(in srgb,var(--accent) 70%,transparent)!important}.demo-terms label span,.paper-eyebrow,.paper-footer,.paper-footer a{color:var(--ink-soft)!important}.dark-mode .anonymize-page .btn-ink,.dark-mode .btn-hero,.dark-mode .btn-primary,.dark-mode .landing-continue,.dark-mode .page-head-row .btn-primary,.dark-mode .paper-page .btn-ink,.dark-mode .paper-page .btn-primary{background:#237a71!important;border-color:#237a71!important;color:#fff!important}.dark-mode .anonymize-page .btn-ink:hover,.dark-mode .btn-primary:hover,.dark-mode .paper-page .btn-ink:hover{background:#1e6b63!important;color:#fff!important;filter:none!important}.landing-continue,.landing-continue:disabled{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important;opacity:1!important;cursor:pointer}.errors-page .seg.seg-info{background:var(--ink-faint)!important;opacity:1!important}.errors-page .dot-info,.errors-page .sev-dot.sev-info{background:var(--ink-muted)!important}@media (max-width:767px){.admin-page .admin-nav,.admin-summary{-webkit-mask-image:linear-gradient(to right,#000 82%,transparent)!important;mask-image:linear-gradient(to right,#000 82%,transparent)!important;scroll-snap-type:x proximity}.admin-page .admin-nav a,.admin-summary>*{scroll-snap-align:start}.admin-page .admin-filters-toggle,.admin-page .dashboard-filters-toggle{width:auto!important}}.admin-filter-inline{display:inline-flex;align-items:center;gap:6px;margin-left:14px}.admin-filter-inline label{margin:0!important;font-size:12.5px;color:var(--ink-soft)}.admin-filter-row .form-control,.dashboard-page .dashboard-filter-row .form-control,.paper-page .search-wrap .form-control{border-color:var(--border-color)!important;height:36px!important}.admin-filter-row .form-control:focus,.dashboard-page .dashboard-filter-row .form-control:focus{border-color:var(--accent)!important}.dashboard-page .dashboard-filter-controls .btn,.dashboard-page .dashboard-filter-controls .dropdown-toggle{height:36px}.admin-page .cell-repos a,.paper-table [role=cell].num a{color:var(--color)!important;font-family:var(--font-sans)!important}.errors-page .col-count{font-weight:inherit!important;color:inherit!important}.errors-page .errors-row-main{min-height:52px!important;padding-top:8px!important;padding-bottom:8px!important}.connections-grid{display:grid!important;grid-template-columns:1fr 1fr!important;align-items:stretch}@media (max-width:767px){.connections-grid{grid-template-columns:1fr!important}}.admin-page .admin-filter-inline .form-control,.admin-page .admin-filter-row .form-control,.admin-page .search-wrap .form-control{height:36px!important;min-height:36px!important;border:1px solid var(--border-color)!important;font-size:13.5px!important;padding-top:0!important;padding-bottom:0!important}.admin-page .admin-filter-inline .form-control:focus,.admin-page .admin-filter-row .form-control:focus{border-color:var(--accent)!important}.admin-page select.form-control{height:36px!important;border:1px solid var(--border-color)!important}.admin-page .paper-table .anon-sub{gap:0!important;column-gap:0!important}.admin-page .paper-table .anon-sub>a{border-bottom:0!important;text-decoration:none}.tb-menu-note{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13px;color:var(--ink-soft)}.tb-menu-note .ui-icon{width:16px;height:16px}.readme-toc a{color:var(--ink-soft)!important;font-size:13.5px!important}.readme-toc a.lvl-3{font-size:13px!important}.readme-toc a.is-active{color:var(--accent)!important;font-weight:500!important}.dark-mode .readme-toc a.is-active{color:#a8ddd6!important}.ace_search_form .ace_searchbtn{border-radius:0!important;margin:0!important}.ace_search_form .ace_searchbtn.prev{border-radius:var(--radius-control) 0 0 var(--radius-control)!important;margin-left:8px!important}.ace_search_form .ace_searchbtn.next{border-radius:0 var(--radius-control) var(--radius-control) 0!important;margin-left:-1px!important}.ace_search .ace_searchbtn.next,.ace_search .ace_searchbtn.prev{box-shadow:none!important}.ace_editor .ace_marker-layer .ace_selected-word,.ace_editor .ace_marker-layer .ace_selection{box-shadow:none!important;border:0!important}@media (max-width:767px){.file-card-head{padding-left:12px!important;padding-right:12px!important}.file-card-head .file-card-path{column-gap:8px!important}}.dark-mode .files .file.active>a{background:rgba(255,255,255,.07)!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,700px) 190px!important;gap:40px!important;padding:0 24px!important}}.ace-chrome .ace_string{color:#0a3069!important}.ace-nord-dark .ace_string,.dark-mode .ace_editor .ace_string{color:#a5c8ff!important}.ace_editor .ace_marker-layer .ace_redacted{background:color-mix(in srgb,var(--accent) 20%,transparent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 55%,transparent)!important;border-radius:3px}.ace_search .ace_button{height:28px!important;min-width:30px!important;font-size:12px!important;color:var(--ink-soft)!important;display:inline-flex!important;align-items:center;justify-content:center}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;box-shadow:inset 0 0 0 1px var(--accent)!important}.ace_search_counter{font-size:12.5px!important;color:var(--ink-soft)!important}.ace_searchbtn_close::before{font-size:18px!important;font-weight:300!important}.btn-toggle.is-on{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 45%,transparent)!important}.vb-snapshot{background:0 0!important;box-shadow:none!important;color:var(--ink-soft)!important;padding-left:4px!important;padding-right:4px!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.1)!important;color:#f2eee8!important}.paper-faq-title,.paper-hero h1,.paper-section-head h2{text-wrap:balance}@media (min-width:992px){.demo-panes{grid-template-columns:1fr 1fr!important;gap:20px!important}}.paper-faq details a:not(.btn),.paper-faq-a a{text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;text-decoration-color:color-mix(in srgb,currentColor 40%,transparent)}@media (max-width:767px){.file-card.is-code .ace_gutter-cell{padding-left:8px!important;padding-right:8px!important}}.auto-refresh-label{font-family:var(--font-sans)!important}.ov-spark-fill{opacity:1!important;background:color-mix(in srgb,var(--accent) 70%,transparent)!important}.connections-grid .connection-link:not(.connection-danger){display:inline-flex;align-items:center;height:34px;padding:0 12px;border:1px solid var(--border-color);border-radius:var(--radius-control);color:var(--color)!important;text-decoration:none!important}.reading-layout .file-content.markdown-body,.reading-layout .markdown-body{padding-left:0!important;padding-right:0!important}.explorer-content .file-card .reading-layout .file-content.markdown-body{padding-left:0!important;padding-right:0!important}.admin-page h1.paper-page-title{margin:0!important;line-height:44px!important;min-height:44px}.admin-page .errors-header,.admin-page .ov-header,.admin-page .q-header{min-height:44px;margin:0 0 12px!important;display:flex;align-items:center;justify-content:space-between}.admin-page>h1.paper-page-title{margin-bottom:12px!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active .count{color:rgba(255,255,255,.8)!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active::before{display:none!important}.admin-page input[type=checkbox]{accent-color:var(--accent);width:16px;height:16px}.ov-health{background:var(--paper-card)!important;box-shadow:inset 3px 0 0 var(--status-error,#b42318),inset 0 0 0 1px var(--border-color)!important}.dark-mode .ov-health{background:var(--paper-card)!important;box-shadow:inset 3px 0 0 #f08a82,inset 0 0 0 1px var(--border-color)!important}.dashboard-quota .quota-summary-ring{display:none!important}.errors-page .errors-row-main{min-height:44px!important}.errors-page .when-abs{display:none!important}@media (max-width:767px){.admin-summary{flex-wrap:wrap!important;overflow:visible!important;-webkit-mask-image:none!important;mask-image:none!important;margin-left:0!important;margin-right:0!important;padding:0!important}.admin-summary .summary-pill{height:30px!important;padding:0 10px!important;font-size:13px!important}.admin-page .admin-nav{flex-wrap:wrap!important;overflow:visible!important;-webkit-mask-image:none!important;mask-image:none!important;margin-left:0!important;margin-right:0!important;padding:0!important;gap:2px!important}.admin-page .admin-nav a{min-height:34px!important;padding:0 10px!important;font-size:13.5px!important}.admin-page .admin-nav a .ui-icon{display:none!important}}.fm-toggles span{display:inline-flex;align-items:center;gap:6px}.fm-toggles span::before{content:"";width:12px;height:12px;flex:none}.fm-toggles .is-on::before{background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E") center/contain no-repeat}.faq-toc a,.paper-faq nav a,.paper-faq-toc a{text-decoration:none!important}@media (max-width:991px){.navbar .navbar-toggler{border-color:transparent!important;background:0 0!important;box-shadow:none!important}}.ace-chrome .ace_gutter-cell{color:#8a847a!important}.dark-mode .ace_editor .ace_gutter-cell{color:#7a746b!important}.ace-chrome .ace_keyword,.ace-chrome .ace_keyword.ace_operator.ace_new,.ace-chrome .ace_storage{color:#7a3e6e!important}.ace-chrome .ace_string{color:#6b5a1e!important}.ace-chrome .ace_entity.ace_name.ace_function,.ace-chrome .ace_support.ace_function{color:#2b2824!important;font-weight:500}.ace-chrome .ace_constant,.ace-chrome .ace_constant.ace_language,.ace-chrome .ace_constant.ace_numeric{color:#8a4b12!important}.ace-chrome .ace_comment{color:#8a847a!important;font-style:italic}.ace-chrome .ace_identifier,.ace-chrome .ace_variable{color:#2b2824!important}.ace-chrome .ace_keyword.ace_operator,.ace-chrome .ace_paren,.ace-chrome .ace_punctuation{color:#6e6860!important}.ace-chrome .ace_entity.ace_name.ace_tag,.ace-chrome .ace_support.ace_class,.ace-chrome .ace_support.ace_type{color:#7a3e6e!important}.ace-chrome .ace_entity.ace_other.ace_attribute-name{color:#8a4b12!important}.dark-mode .ace_editor .ace_entity.ace_name.ace_tag,.dark-mode .ace_editor .ace_keyword,.dark-mode .ace_editor .ace_storage,.dark-mode .ace_editor .ace_support.ace_type{color:#d7a6cb!important}.dark-mode .ace_editor .ace_string{color:#d9c08a!important}.dark-mode .ace_editor .ace_entity.ace_name.ace_function,.dark-mode .ace_editor .ace_identifier,.dark-mode .ace_editor .ace_support.ace_function,.dark-mode .ace_editor .ace_variable{color:#ece7df!important}.dark-mode .ace_editor .ace_constant,.dark-mode .ace_editor .ace_constant.ace_numeric,.dark-mode .ace_editor .ace_entity.ace_other.ace_attribute-name{color:#e0b070!important}.dark-mode .ace_editor .ace_comment{color:#8a847a!important;font-style:italic}.dark-mode .ace_editor .ace_keyword.ace_operator,.dark-mode .ace_editor .ace_paren,.dark-mode .ace_editor .ace_punctuation{color:#a39d92!important}.ace_editor .ace_marker-layer .ace_redacted{background:rgba(31,106,100,.22)!important;box-shadow:inset 0 0 0 1px rgba(31,106,100,.55)!important;border-radius:3px;mix-blend-mode:multiply}.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.26)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.6)!important;mix-blend-mode:screen}.ace-chrome .ace_gutter-cell{color:#77716a!important}.dark-mode .ace_editor .ace_gutter-cell{color:#8a847a!important}@media (min-width:1200px){.reading-layout{justify-content:center!important}}.fm-toggles span:not(.is-on)::before{display:none!important}.demo-panes .pane-header{display:flex;gap:10px;min-width:0}.demo-panes .pane-url{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (max-width:767px){.mobile-sub{font-size:12.5px!important;line-height:1.4}}.ov-live{font-size:12.5px;color:var(--ink-soft)}.admin-summary .summary-pill:not(.active) .count{text-decoration:none!important}.paper-table:not(.paper-table-dashboard) .paper-table-head,.paper-table:not(.paper-table-dashboard) .paper-table-row{column-gap:24px!important}.connection-label{font-size:12.5px;color:var(--ink-soft);margin-right:4px}.paper-table-dashboard .copy-url:hover{background:var(--hover-bg-color)!important;box-shadow:inset 0 0 0 1px var(--border-color)!important}.dashboard-page .dashboard-count button,.dashboard-page .show-hidden{font-size:13px!important}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{display:grid!important;grid-template-columns:minmax(0,1fr) auto!important;grid-template-areas:"name more" "meta meta" "link link"!important;row-gap:8px!important}.paper-table.paper-table-dashboard .paper-table-row>.cell-anon{grid-area:name!important;grid-column:auto!important;min-width:0}.paper-table.paper-table-dashboard .paper-table-row>.dashboard-row-meta{grid-area:meta!important;grid-column:auto!important}.paper-table.paper-table-dashboard .paper-table-row>.cell-actions{display:contents!important}.paper-table-dashboard .cell-actions>.copy-url{grid-area:link;width:100%!important;height:36px!important;padding:0 12px!important;justify-content:space-between!important;background:var(--hover-bg-color)!important;box-shadow:none!important}.paper-table-dashboard .cell-actions>.copy-url .copy-url-text{display:inline!important;min-width:0;overflow:hidden;text-overflow:ellipsis}.paper-table-dashboard .cell-actions>.dropdown{grid-area:more;align-self:start}.paper-table-dashboard .cell-actions>.project-details-button,.paper-table-dashboard .cell-actions>.row-edit{display:none!important}}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{padding-right:12px!important}}.vb-support{background:0 0!important;box-shadow:none!important;border:0!important}.vb-support:hover{background:var(--hover-bg-color)!important}.vb-snapshot{margin-right:8px!important}.paper-eyebrow,.paper-stat small,.paper-stat-sub,.paper-stats .sub{color:#5e5950!important}.dark-mode .paper-eyebrow,.dark-mode .paper-stat-sub{color:#b5aea3!important}.paper-qa,.paper-section-head{margin-top:80px!important}.demo{margin-bottom:0!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(242,204,96,.35)!important}.ace_editor .ace_marker-layer .ace_selection{background:rgba(232,184,58,.55)!important}.paper-faq details>:not(summary),.paper-faq-a{max-width:720px!important}.q-state-chip.q-state-active::before{background:var(--color)!important}.q-state-chip.q-state-delayed::before{background:var(--ink-muted)!important}.repo-access-note>span:last-child{display:flex;gap:8px;align-items:flex-start;padding:10px 12px;border-radius:var(--radius-control);background:color-mix(in srgb,#b7791f 8%,transparent);box-shadow:inset 3px 0 0 #b7791f;color:var(--color)!important}.repo-access-note>span:last-child .ui-icon{color:#9a6416;flex:none;margin-top:2px}.dark-mode .repo-access-note>span:last-child{background:rgba(224,176,112,.1);box-shadow:inset 3px 0 0 #e0b070}.repo-access-note>span:has(.ui-icon){background:color-mix(in srgb,#b7791f 8%,transparent)!important;box-shadow:inset 3px 0 0 #b7791f!important;color:var(--color)!important}.repo-access-note>span:has(.ui-icon) .ui-icon{color:#9a6416!important}.dark-mode .repo-access-note>span:has(.ui-icon){background:rgba(224,176,112,.1)!important;box-shadow:inset 3px 0 0 #e0b070!important}.dark-mode .repo-access-note>span:has(.ui-icon) .ui-icon{color:#e0b070!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,680px) 190px!important;padding:0 40px!important}}.ace-chrome .ace_entity.ace_name.ace_tag,.ace-chrome .ace_keyword,.ace-chrome .ace_storage,.ace-chrome .ace_support.ace_type{color:#93306a!important}.ace-chrome .ace_string{color:#7a5300!important}.ace-chrome .ace_constant,.ace-chrome .ace_constant.ace_numeric{color:#a14a0f!important}.dark-mode .ace_editor .ace_string{color:#cdb98e!important}.ace_editor .ace_marker-layer .ace_redacted{background:rgba(31,106,100,.28)!important;box-shadow:inset 0 0 0 1px rgba(31,106,100,.65)!important}.ace_search{background:var(--paper-card)!important;box-shadow:inset 0 1px 0 var(--border-color),inset 0 -1px 0 var(--border-color)!important}.ace_search .ace_search_field{height:32px!important;border-radius:var(--radius-control)!important}.ace_search .ace_searchbtn{height:32px!important}.paper-stats-dot{background:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.paper-stats-meta-right{color:var(--ink-soft)!important;font-weight:400!important}.paper-frame{box-shadow:0 0 0 1px var(--border-color),0 12px 32px rgba(40,30,20,.08)!important;border:0!important}.dark-mode .paper-frame{box-shadow:0 0 0 1px #34302b,0 12px 32px rgba(0,0,0,.4)!important}.paper-faq details>summary,.paper-faq summary{font-weight:600!important}@media (max-width:767px){.file-card-head .file-card-path{column-gap:12px!important}.file-card.is-code.no-wrap .file-card-body::after{width:72px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 65%)!important}}.dashboard-page .hidden-toggle{font-size:13px!important;color:var(--color)!important}.paper-table-dashboard .btn-icon-dots{width:32px!important;height:32px!important}.access-q{display:block;font-size:13px;color:var(--ink-soft);margin:4px 0 6px}.repo-access-note{max-width:none!important;width:100%}.repo-access-note>span:has(.ui-icon){width:100%}@media (max-width:767px){.anonymize-page .repo-access-switch,.anonymize-page [role=group][aria-label*=onnection]{width:100%!important;display:flex!important}.anonymize-page .repo-access-switch button{flex:1 1 0}}.admin-empty,.paper-table-empty{border:1px solid var(--border-color)!important;border-radius:var(--radius-surface,12px)!important;background:var(--paper-card)!important;padding:40px 24px!important;text-align:center}.dark-mode .paper-table-head{background:#26221e!important}.dark-mode .paper-table-row{background:#1e1b18}.paper-table:has(.paper-table-empty),:has(>.paper-table-empty:only-child){border-top:0!important;border-bottom:0!important;box-shadow:none!important}.ace_editor .ace_text-layer .ace_redacted{color:var(--accent)!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent);border-radius:3px;font-weight:500}.dark-mode .ace_editor .ace_text-layer .ace_redacted{color:#a8ddd6!important;background:rgba(124,199,190,.16)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.35)}.ace_editor .ace_marker-layer .ace_redacted{display:none!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.82em!important;padding:0 .2em!important;background:color-mix(in srgb,var(--accent) 9%,transparent)!important;box-shadow:none!important;border:0!important;line-height:1.1!important;vertical-align:baseline!important;font-weight:500!important}.vb-snapshot{color:var(--color)!important;opacity:.78}.ace_search{box-shadow:inset 0 -1px 0 var(--border-color)!important}.btn-find.is-on,.btn-toggle.is-on{color:var(--accent)!important;background:var(--accent-soft)!important}.readme-toc a{text-indent:0;padding-left:13px!important}.readme-toc a.lvl-3{padding-left:25px!important}@media (max-width:767px){.anonymize-page .repo-access,.repo-access{flex-wrap:wrap;width:100%}.repo-access-switch{display:flex!important;width:100%!important}.repo-access-switch button{flex:1 1 0!important;white-space:nowrap!important;justify-content:center}}.q-card.selected{box-shadow:none!important;border-color:color-mix(in srgb,var(--accent) 60%,transparent)!important}.dark-mode .paper-table-head{background:#2e2a25!important}.dark-mode .paper-table-row{background:#1c1a17!important}.ov-chart-legend{font-family:var(--font-sans)!important;font-size:12px!important;color:var(--ink-soft)!important}.readme-toc a.lvl-2{font-weight:500!important;color:var(--color)!important}.readme-toc a.lvl-3{font-size:12.5px!important;color:var(--ink-soft)!important;font-weight:400!important}.readme-toc a.is-active{color:var(--accent)!important}.readme-toc a.lvl-2+a.lvl-2,.readme-toc a.lvl-3+a.lvl-2{margin-top:4px}.dark-mode .ace_editor .ace_text-layer .ace_redacted,.dark-mode .redact-chip{color:#9fe0d7!important;background:rgba(124,199,190,.2)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.38)!important}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true]{color:var(--accent)!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 45%,transparent)!important}.dark-mode .file-card-head .btn.btn-toggle.is-on{color:#a8ddd6!important;background:rgba(124,199,190,.14)!important}@media (max-width:767px){.file-card-head .file-card-path .card-files-btn{align-self:start!important}}@media (max-width:767px){.paper-hero-shot{max-height:300px;overflow:hidden;-webkit-mask-image:linear-gradient(#000 70%,transparent);mask-image:linear-gradient(#000 70%,transparent)}}.dark-mode .hero-mock-doc,.dark-mode .paper-frame{background:#24211d!important}.paper-stats-meta-right{color:#5e5950!important}.dark-mode .paper-stats-meta-right{color:#b5aea3!important}.errors-page .status-pill.status-neutral{background:var(--hover-bg-color)!important;color:var(--ink-soft)!important;border-color:var(--border-color)!important}.connections-grid .btn,.connections-grid .connection-link:not(.connection-danger){height:36px!important;min-height:36px!important;padding-top:0!important;padding-bottom:0!important;display:inline-flex!important;align-items:center}.claim-page .form-control,.claim-page input{border:1px solid var(--border-color)!important}@media (max-width:767px){.overview-page .ov-kpi-sub{white-space:nowrap!important;font-size:12px!important;overflow:visible!important}.overview-page .ov-kpi-row>:last-child:nth-child(odd){grid-column:1/-1}.admin-summary .summary-pill{height:28px!important}.admin-summary .summary-total{flex-basis:100%}.admin-page .paper-table:not(.paper-table-dashboard) .paper-table-row{position:relative;padding-right:44px!important}.admin-page .paper-table:not(.paper-table-dashboard) .paper-table-row>:last-child{position:absolute!important;top:10px;right:4px}.admin-page .paper-table .anon-sub>span{white-space:nowrap}}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true]{background:0 0!important;box-shadow:none!important;color:var(--accent)!important}.file-card-head .btn.btn-toggle.is-on:hover{background:var(--hover-bg-color)!important}.dark-mode .file-card-head .btn.btn-toggle.is-on{background:0 0!important;color:#a8ddd6!important}.ace-chrome .ace_string{color:#3d5a73!important}.dark-mode .ace_editor .ace_string{color:#afc4d6!important}.ace-chrome .ace_gutter-cell{color:#6b655d!important}.readme-toc a.lvl-2{color:var(--ink-soft)!important;font-weight:500!important}.readme-toc a.lvl-3{color:var(--ink-soft)!important;opacity:.9}.readme-toc a.is-active{color:var(--accent)!important}.readme-toc-head{font-size:12.5px!important}.vb-snapshot{opacity:1!important;color:var(--ink-soft)!important}@media (max-width:767px){.viewer-bar .vb-signin{height:32px!important}}.paper-footer-meta{font-size:15px!important;color:var(--ink-soft)!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.07)!important}@media (min-width:992px){.anonymize-landing{justify-content:flex-start!important;align-items:flex-start!important}.anonymize-landing-inner{margin-left:max(4px,calc((100vw - 1048px)/ 2 - 20px))!important;margin-right:auto!important;max-width:640px!important}}.paper-settings-body>:not(aside){max-width:none!important}.paper-settings-body .paper-card,.paper-settings-body section{max-width:none!important}.dark-mode .paper-table-row{background:0 0!important}.dark-mode .paper-table-head{background:#26231f!important}@media (max-width:767px){.overview-page .ov-kpi-sub{white-space:normal!important;font-size:12px!important;padding-right:4px}.dashboard-count{position:static!important;width:auto!important;height:auto!important;margin:0 0 0 auto!important;overflow:visible!important;clip:auto!important;font-size:12.5px!important}}.errors-page input[type=search]{font-family:var(--font-sans)!important}.paper-settings-body .form-text,.paper-settings-body .paper-field-help,.paper-settings-body small{max-width:none!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip{font-size:.7em!important;vertical-align:.12em!important}.ace_search{border-bottom:1px solid var(--border-color)!important;box-shadow:none!important;border-top:0!important}.ace_search .ace_button,.ace_search .ace_searchbtn{height:32px!important}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true]{background:var(--accent-soft)!important;color:var(--accent)!important;box-shadow:none!important}.dark-mode .file-card-head .btn.btn-toggle.is-on{background:rgba(124,199,190,.14)!important}@media (max-width:767px){.file-card-head .card-files-btn{width:32px!important;height:32px!important}.file-card-head .file-card-path{column-gap:8px!important}}.dark-mode .leftCol{background:#1c1a17!important}.dark-mode .explorer-content,.dark-mode .file-card{background:#141311!important}.nav-support-inline{color:var(--color)!important;font-weight:500!important}@media (min-width:992px){.anonymize-landing{padding-top:24px!important}.anonymize-landing-inner{padding-top:0!important;margin-top:0!important}}.anonymize-page .access-q+.btn-outline-ink{height:30px!important;padding:0 10px!important;font-size:13px!important}.admin-summary .summary-pill:not(.active){color:var(--ink-soft)!important}.volume-legend>span{display:inline-flex;align-items:center;gap:5px;margin-left:12px}.dark-mode .leftCol{background:#221f1b!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{box-shadow:inset -1px 0 0 var(--border-color),inset -10px 0 10px -10px rgba(0,0,0,.18)!important}}.readme-toc a.is-active{font-weight:600!important}.paper-stats-meta-right{color:var(--ink-soft)!important;font-weight:500!important;letter-spacing:inherit}.paper-faq details a:not(.btn),.paper-faq-a a,.paper-section a{text-decoration-color:color-mix(in srgb,currentColor 35%,transparent)!important;text-decoration-thickness:1px!important}.volume-axis{display:flex;justify-content:space-between;font-size:11.5px;color:var(--ink-soft);margin-top:6px}.errors-page .seg.seg-info{background:color-mix(in srgb,var(--ink-muted) 55%,transparent)!important}.paper-crumbs a{color:var(--ink-soft)!important}.paper-crumbs .here{color:var(--color)!important}.paper-table .mobile-meta-icon,.paper-table [role=cell]>.ui-icon{opacity:.45}@media (min-width:768px){.paper-table-dashboard .cell-expires{padding-left:24px!important}}.dark-mode .paper-table-dashboard .paper-table-head,.dark-mode .paper-table-head{background:0 0!important;box-shadow:inset 0 -1px 0 var(--border-color)!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.copy-url{max-width:none!important;justify-self:stretch}.paper-table.paper-table-dashboard .paper-table-row{padding-right:16px!important}.dashboard-filters-toggle{border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important;height:44px;padding:0 12px!important}.overview-page .ov-kpi-card{padding-left:14px!important;padding-right:14px!important}.overview-page .ov-kpi-sub{white-space:nowrap!important;font-size:11.5px!important}.admin-filter-row kbd,.search-wrap .kbd,.search-wrap kbd{display:none!important}}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{background:0 0!important;padding:0!important;font-size:.78em!important;color:var(--accent)!important;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:6px;text-decoration-color:color-mix(in srgb,var(--accent) 30%,transparent)}.dark-mode .markdown-body h1 .redact-chip,.dark-mode .markdown-body h2 .redact-chip{color:#9fe0d7!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:56px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 55%)!important}}.readme-toc a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media (min-width:768px){.paper-table-dashboard .cell-expires,.paper-table-dashboard .paper-table-head>:nth-child(4){text-align:left!important;justify-content:flex-start!important;padding-left:24px!important}}.anonymize-page #sourceUrl-landing,.anonymize-page .form-control-lg{border:1px solid var(--border-color)!important}.anonymize-page .form-control-lg:focus{border-color:var(--accent)!important}.repo-access-note>span:has(.ui-icon){background:var(--accent-soft)!important;box-shadow:inset 3px 0 0 var(--accent)!important;color:var(--color)!important;font-size:13.5px}.repo-access-note>span:has(.ui-icon) .ui-icon{color:var(--accent)!important}.dark-mode .repo-access-note>span:has(.ui-icon){background:rgba(124,199,190,.1)!important;box-shadow:inset 3px 0 0 #7cc7be!important}.dark-mode .repo-access-note>span:has(.ui-icon) .ui-icon{color:#7cc7be!important}.readme-toc a{white-space:normal!important;overflow:visible!important;text-overflow:clip!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::before{content:"Scroll \2192";position:absolute;right:10px;bottom:14px;z-index:9;font:500 12px/1 var(--font-sans);color:var(--ink-soft);background:var(--paper-card);box-shadow:0 0 0 1px var(--border-color),0 2px 6px rgba(0,0,0,.06);border-radius:999px;padding:6px 10px;pointer-events:none}}.ace-chrome .ace_string{color:#2d5f86!important}.ace-chrome .ace_keyword,.ace-chrome .ace_storage{color:#9a2b6b!important}.dark-mode .ace_editor .ace_string{color:#9cc3e6!important}.dark-mode .ace_editor .ace_keyword,.dark-mode .ace_editor .ace_storage{color:#e3a6d2!important}.reading-layout .markdown-body h1{font-size:34px!important;letter-spacing:-.015em}.errors-page .btn-group>.btn,.errors-page .btn-icon,.errors-page .form-control,.errors-page select{height:36px!important;min-height:36px!important}.errors-page .col-when .when-rel{font-size:13.5px!important}.q-danger-group{display:inline-flex;gap:6px;margin-left:auto;padding-left:12px;border-left:1px solid var(--border-color)}.q-stats-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.repo-access-note>span:has(.ui-icon){background:color-mix(in srgb,#b7791f 8%,transparent)!important;box-shadow:inset 3px 0 0 #b7791f!important}.repo-access-note>span:has(.ui-icon) .ui-icon{color:#9a6416!important}.dark-mode .repo-access-note>span:has(.ui-icon){background:rgba(224,176,112,.1)!important;box-shadow:inset 3px 0 0 #e0b070!important}.dark-mode .repo-access-note>span:has(.ui-icon) .ui-icon{color:#e0b070!important}@media (max-width:767px){.admin-page .paper-table [role=cell]:has(>input[type=checkbox]),.admin-page .paper-table input[type=checkbox],.admin-page .paper-table-head [role=columnheader]:has(>input[type=checkbox]){display:none!important}.admin-page .admin-filters-toggle,.admin-page .dashboard-filters-toggle{width:auto!important}}.q-danger-group{border-left:0!important;padding-left:0!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::before{animation:scrollHintOut .4s ease 2.6s forwards}}@keyframes scrollHintOut{to{opacity:0;visibility:hidden}}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{text-decoration:none!important;background:color-mix(in srgb,var(--accent) 8%,transparent)!important;padding:0 .15em!important;border-radius:4px}.readme-toc a.lvl-3{padding-left:29px!important}.ace_search .ace_button{color:var(--color)!important;opacity:.8}.ace_search .ace_button.checked{opacity:1}.dark-mode .files .file>a,.dark-mode .files .folder>a{color:#d8d2c8!important}.ov-live{display:inline-flex;align-items:center;gap:6px}.ov-live-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.anon-sub .meta-sep{display:inline-flex;align-items:center;white-space:nowrap}@media (max-width:767px){.anon-sub{column-gap:0!important}}.connection-status-legacy{color:var(--ink-soft)!important}.connection-status-legacy::before{background:var(--ink-muted)!important}.q-detail{align-items:start!important}.q-chart-wrap canvas{max-height:200px}.admin-page .paper-table .cell-anon .anon-text:has(.meta-sep){overflow:hidden}.admin-page .paper-table .anon-sub:has(.meta-sep){display:flex!important;flex-wrap:wrap!important;margin-left:-14px!important;gap:0!important}.admin-page .paper-table .anon-sub:has(.meta-sep)>*{white-space:nowrap}.admin-page .paper-table .anon-sub:has(.meta-sep)>::before{content:"\00b7";display:inline-block;width:14px;text-align:center;color:var(--ink-muted)}.ace-chrome .ace_gutter-cell,.dark-mode .ace_editor .ace_gutter-cell{font-size:12px!important}.q-stats-actions{display:grid!important;grid-template-columns:auto auto 1fr;gap:8px;align-items:center}.q-danger-group{grid-column:1/-1;justify-self:start;margin:4px 0 0!important;padding:10px 0 0!important;border-top:1px solid var(--border-color)!important;width:100%;display:flex!important;gap:8px}@media (max-width:767px){.overview-page .ov-header{flex-wrap:nowrap!important;align-items:center!important}.ov-live{font-size:12px}.ov-live .ov-live-text-extra{display:none}.overview-page .ov-daily-card,.overview-page .ov-kpi-card{padding-left:14px!important;padding-right:14px!important}}.admin-page input[type=checkbox]{width:14px!important;height:14px!important}.demo-input-wrap{position:relative}.demo-input-wrap .demo-backdrop{position:absolute;inset:0;margin:0;overflow:hidden;color:var(--color);pointer-events:none;overflow-wrap:break-word}.demo-input-wrap .demo-input{position:relative;color:transparent!important;caret-color:var(--color);background:0 0!important;overflow-wrap:break-word}.demo-input-wrap .demo-input::selection{color:var(--color);background:color-mix(in srgb,var(--accent) 25%,transparent)}.demo-match{background:0 0;color:inherit;padding:0;border-radius:2px;box-shadow:inset 0 -2px 0 color-mix(in srgb,var(--accent) 55%,transparent);background-color:color-mix(in srgb,var(--accent) 10%,transparent)}.demo-output .redact-chip{font-size:12.5px!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::before{display:none!important}.file-card.is-code.no-wrap .file-card-body::after{width:36px!important;background:linear-gradient(to right,rgba(0,0,0,0),rgba(60,45,30,.1))!important;box-shadow:inset -1px 0 0 rgba(60,45,30,.18)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(0,0,0,0),rgba(0,0,0,.45))!important;box-shadow:inset -1px 0 0 rgba(255,255,255,.12)!important}}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:44px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 70%)!important;box-shadow:inset -8px 0 10px -6px rgba(40,30,20,.28),inset -1px 0 0 rgba(40,30,20,.16)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(0,0,0,0),var(--canvas-bg-color) 70%)!important;box-shadow:inset -8px 0 10px -6px rgba(0,0,0,.6),inset -1px 0 0 rgba(255,255,255,.1)!important}}.q-chart-wrap{position:relative}.q-chart-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;background:var(--paper-card);border-bottom:1px solid var(--border-color);text-align:center}.q-chart-empty strong{font-size:14px;font-weight:500;color:var(--color)}.q-chart-empty span{font-size:12.5px;color:var(--ink-soft)}.q-danger-group .btn{margin-left:0!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{background:0 0!important;padding:0!important;border-radius:0!important}.paper-settings-body .form-control,.paper-settings-body input[type=date],.paper-settings-body input[type=text],.paper-settings-body input[type=url],.paper-settings-body textarea{border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important}.admin-page .paper-table .cell-views .ui-icon{display:none!important}.admin-page:has(.admin-users-row) .paper-table-head,.admin-page:has(.admin-users-row) .paper-table-row{column-gap:40px!important}@media (max-width:767px){.overview-page .ov-header{justify-content:space-between!important}.ov-live{margin-left:0!important}}.dark-mode .paper-table-dashboard .paper-table-row{background:0 0!important}.mono-hash{font-family:var(--font-mono)!important;font-size:11.5px}.nav-heart-m{display:none}@media (max-width:991px){.nav-heart-m{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;margin-left:auto;color:#c25b5e}.nav-heart-m .ui-icon{width:18px;height:18px;fill:currentColor}.nav-heart-m+.nav-signin-m{margin-left:4px!important}}.file-card:has(.ace_search:not([style*=none])) .btn-find{color:var(--accent)!important;background:var(--accent-soft)!important}.paper-link-arrow{color:var(--color)!important;text-decoration-color:color-mix(in srgb,var(--accent) 55%,transparent)!important}.paper-faq details p,.paper-faq details>:not(summary),.paper-faq-a{max-width:none!important}.paper-stat-value{font-family:var(--font-display,var(--font-serif))!important;font-weight:400!important;letter-spacing:-.01em}@media (max-width:767px){.ov-live-extra{display:none}.paper-table-dashboard .cell-views.num::after{content:" views";color:var(--ink-soft)}.paper-table-dashboard .cell-expires>span:not(.expires-never):not(.expires-past)::before{content:"Expires ";color:var(--ink-soft)}.admin-page .admin-filters-toggle{width:auto!important;height:36px!important;padding:0 12px!important}}.dashboard-quota summary{border:0!important;box-shadow:none!important;background:0 0!important;padding-left:0!important}.q-card.selected{background:var(--accent-soft)!important}.dark-mode .q-card.selected{background:rgba(124,199,190,.08)!important}.dark-mode .ov-meter .fill.critical,.dark-mode .ov-meter-fill.critical{background:#f08a82!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-family:inherit!important;font-weight:inherit!important;font-size:.9em!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent)!important;padding:0 .25em!important;border-radius:6px!important;color:var(--accent)!important;text-decoration:none!important}.dark-mode .markdown-body h1 .redact-chip,.dark-mode .markdown-body h2 .redact-chip,.dark-mode .markdown-body h3 .redact-chip{background:rgba(124,199,190,.16)!important;color:#9fe0d7!important}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true],.file-card:has(.ace_search:not([style*=none])) .btn-find{background:0 0!important;color:var(--accent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 50%,transparent)!important}.dark-mode .file-card-head .btn.btn-toggle.is-on,.dark-mode .file-card:has(.ace_search:not([style*=none])) .btn-find{background:0 0!important;color:#a8ddd6!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.5)!important}@media (min-width:768px){.file-card-head{padding-left:46px!important}}.breadcrumb-root{display:none!important}.breadcrumb-root+.breadcrumb-item::before{display:none!important}.ace_editor .ace_marker-layer .ace_selection{background:rgba(232,170,40,.65)!important;box-shadow:0 0 0 1px rgba(160,100,10,.6)!important}.ace_search_counter{color:var(--color)!important;opacity:.75}.dark-mode .files .file.active>a{background:rgba(255,255,255,.12)!important}.dark-mode .leftCol{box-shadow:inset -1px 0 0 rgba(255,255,255,.08)!important}.hero-mock-doc-bar span,.paper-stat-delta,.paper-stat-sub,.paper-stats .sub{color:#5e5950!important}.dark-mode .paper-stat-delta,.dark-mode .paper-stat-sub{color:#b5aea3!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.72em!important;font-weight:600!important;vertical-align:.1em;padding:.05em .3em!important}.is-unsynced{color:var(--ink-muted)!important;font-size:12.5px;font-family:var(--font-sans)!important}.q-dot.q-dot-idle{background:var(--ink-muted)!important}.dark-mode .paper-table,.dark-mode .paper-table-dashboard{background:0 0!important}.dark-mode .paper-table .paper-table-head{background:#24211d!important}.dark-mode .paper-table .paper-table-row{background:#141311!important}@media (min-width:768px){.file-card.is-code .file-card-head{padding-left:16px!important}.readme-toc{margin-left:16px}.reading-layout{grid-template-columns:minmax(0,660px) 190px!important;gap:56px!important}}.dark-mode .leftCol{box-shadow:none!important;border-right:1px solid var(--border-color)!important}.dark-mode .leftCol-resizer,.dark-mode .sidebar-resizer{background:0 0!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{box-shadow:none!important}.files .file.active>a::before{left:4px!important;top:6px!important;bottom:6px!important;width:3px!important;border-radius:2px!important}.hero-mock-row.is-active{box-shadow:inset 3px 0 0 var(--accent)!important;border-radius:6px}.fm-label,.paper-eyebrow,.paper-stats-meta-left,.paper-stats-meta-right{color:#57524a!important}.dark-mode .paper-eyebrow,.dark-mode .paper-stats-meta-left,.dark-mode .paper-stats-meta-right{color:#bdb6ab!important}.demo-match{box-shadow:inset 0 -1px 0 color-mix(in srgb,var(--accent) 50%,transparent)!important}.redact-glue{white-space:nowrap}@media (min-width:768px){.file-card-head,.file-card.is-code .file-card-head{padding-left:20px!important}}.dark-mode .files .file.active>a{background:rgba(255,255,255,.08)!important}.files .file.active>a{position:relative}.files .file.active>a::before{left:0!important;top:4px!important;bottom:4px!important;width:3px!important;border-radius:0 2px 2px 0!important}.admin-page .paper-table [role=cell]>svg.ui-icon[aria-hidden],.paper-table .cell-expires .mobile-meta-icon{display:none!important}@media (max-width:767px){.admin-page .paper-table .cell-views.num::after{content:" views";color:var(--ink-soft)}}.dark-mode .paper-table .paper-table-row{background:#1e1b18!important}.dark-mode .paper-table .paper-table-head{background:#26231f!important}.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=email],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea,.paper-page textarea.form-control{border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important}.paper-page input:focus,.paper-page select:focus,.paper-page textarea:focus{border-color:var(--accent)!important}.q-detail{align-items:stretch!important}.q-throughput{display:flex;flex-direction:column}.q-throughput .q-chart-wrap{flex:1;min-height:180px}.admin-page .search-wrap{flex:0 1 420px!important;max-width:420px!important}@media (max-width:767px){.overview-page .ov-header{display:flex!important;flex-direction:row!important;flex-wrap:nowrap!important;justify-content:space-between!important;align-items:center!important}.overview-page .ov-header .ov-live{margin:0 0 0 auto!important;align-self:center}}.hero-mock-count{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent)!important;font-weight:600;font-size:11.5px}.dark-mode .hero-mock-count{background:rgba(124,199,190,.16);color:#a8ddd6!important}.vb-support{color:var(--ink-soft)!important;font-weight:400!important}.vb-support:hover{color:var(--color)!important}@media (max-width:767px){.file-card.is-code .ace_gutter-cell{padding-left:4px!important;padding-right:6px!important}.file-card.is-code .ace_scroller{left:30px!important}.file-card.is-code .ace_gutter{width:30px!important}}.md-find-bar{position:sticky;top:52px;z-index:5;display:flex;align-items:center;gap:10px;padding:8px 20px;background:var(--paper-card);border-bottom:1px solid var(--border-color)}.md-find-input{width:260px;height:32px;padding:0 10px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);color:var(--color);font:14px var(--font-sans)}.md-find-input:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.md-find-count{font-size:12.5px;color:var(--ink-soft);min-width:64px;font-variant-numeric:tabular-nums}.md-find-nav{display:inline-flex}.md-find-close,.md-find-nav button{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border-color);background:var(--paper-card);color:var(--color)}.md-find-nav button:first-child{border-radius:var(--radius-control) 0 0 var(--radius-control)}.md-find-nav button:last-child{border-radius:0 var(--radius-control) var(--radius-control) 0;margin-left:-1px}.md-find-nav button:disabled{opacity:.45}.md-find-close{border:0;background:0 0;font-size:20px;border-radius:var(--radius-control)}.md-find-close:hover{background:var(--hover-bg-color)}mark.md-find{background:rgba(242,204,96,.45);color:inherit;border-radius:2px;padding:0}mark.md-find.is-current{background:rgba(232,170,40,.8);box-shadow:0 0 0 1px rgba(160,100,10,.6)}@media (min-width:1280px){.viewer-bar a.vb-icon[href="/faq"]{width:auto!important;padding:0 10px!important;gap:6px}.viewer-bar a.vb-icon[href="/faq"]::after{content:"FAQ";font:500 13px var(--font-sans);color:var(--ink-soft)}}.file-card-head .btn-md-find.is-on{background:0 0!important;color:var(--accent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 50%,transparent)!important}.md-find-input::-webkit-search-cancel-button{-webkit-appearance:none;display:none}@media (min-width:768px){.md-find-bar{padding-left:63px!important}}.md-find-input{width:240px!important}.file-card:has(.md-find-bar) .readme-toc{top:120px!important}.claim-page input,.claim-page textarea{border-color:var(--border-color)!important}.connection-status{font-size:12.5px!important}@media (max-width:767px){.admin-page .admin-filters-toggle::before{content:"";width:14px;height:14px;margin-right:6px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 6h16M7 12h10M10 18h4'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 6h16M7 12h10M10 18h4'/%3E%3C/svg%3E") center/contain no-repeat}.admin-page .admin-filters-toggle{display:inline-flex!important;align-items:center}}.paper-faq-title{text-wrap:balance}.file-card-head,.md-find-bar{background:var(--paper-card)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important;opacity:1!important}.dark-mode .file-card-head,.dark-mode .md-find-bar{background:#141311!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,640px) 190px!important;gap:56px!important;justify-content:center!important;padding:0 24px!important}}.reading-layout .file-content.markdown-body{padding-top:32px!important}@media (min-width:1200px){.md-find-bar{padding-left:calc((100% - 886px)/ 2)!important}}@media (min-width:1280px){.viewer-bar a.vb-bug{width:auto!important;padding:0 10px!important;gap:6px}.viewer-bar a.vb-bug::after{content:"Report a bug";font:500 13px var(--font-sans);color:var(--ink-soft)}}.vb-snapshot{font-size:13px!important;color:var(--ink-soft)!important}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn,.dashboard-page .dashboard-filter-controls .dropdown-toggle,.dashboard-page .dashboard-filter-row .form-control{height:36px!important;min-height:36px!important}.dashboard-filter-controls .btn-group[aria-label=Type]{padding:0!important;height:36px;align-items:center}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn{height:30px!important;min-height:30px!important;margin:3px 0}.errors-page .errors-toolbar{flex-wrap:nowrap!important}.errors-page .errors-toolbar input[type=search]{min-width:0;flex:1 1 260px}.paper-table .paper-table-head{background:color-mix(in srgb,var(--hover-bg-color) 60%,transparent)!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,700px) 190px!important;gap:48px!important}}.md-find-bar{padding-top:4px!important;padding-bottom:4px!important}.md-find-input{height:30px!important}.md-find-input:focus{box-shadow:0 0 0 1px var(--accent)!important}.ace_editor .ace_marker-layer .ace_selection,mark.md-find.is-current{background:rgba(240,190,80,.6)!important;box-shadow:0 0 0 1px rgba(170,110,20,.5)!important}.viewer-bar .vb-right{gap:12px!important}.viewer-bar .vb-right>*{margin:0!important}.auto-refresh-label{white-space:nowrap!important}.errors-page .errors-toolbar{flex-wrap:wrap!important}.errors-page .errors-row-head,.errors-page .errors-thead,.paper-table .paper-table-head,.q-table thead th{background:#f3f1ec!important}.dark-mode .errors-page .errors-thead,.dark-mode .paper-table .paper-table-head,.dark-mode .q-table thead th{background:#24211d!important}.summary-show-label{font-size:12.5px;color:var(--ink-soft);margin-right:2px;align-self:center}.anon-sub a.mono-hash,.mono-hash{text-decoration:none!important;border-bottom:0!important}.errors-page .errors-list-head{background:#f3f1ec!important}.dark-mode .errors-page .errors-list-head{background:#24211d!important}.errors-page .errors-toolbar .search-wrap,.errors-page .errors-toolbar input[type=search]{flex:1 1 220px!important;max-width:340px!important;min-width:0!important}.errors-page .errors-toolbar{flex-wrap:nowrap!important;gap:8px!important}.faq-section .panel-default .panel-title a{padding:16px 32px 16px 0!important;position:relative;display:block}.faq-section .panel-default .panel-body{padding-left:0!important}.faq-section .panel-default .panel-title a:after{left:auto!important;right:6px!important}.md-find-bar{padding-left:20px!important}.vb-logo{font-size:19px!important}@media (max-width:767px){.vb-logo{font-size:18px!important}.admin-summary .summary-show-label{flex-basis:100%}}.btn-ghost-danger{background:0 0!important;border:1px solid transparent!important;color:#b42318!important;display:inline-flex;align-items:center;gap:6px}.btn-ghost-danger:hover{background:color-mix(in srgb,#b42318 8%,transparent)!important}.dark-mode .btn-ghost-danger{color:#f08a82!important}mark.md-find{background:rgba(242,204,96,.32)!important;box-shadow:none!important}mark.md-find.is-current{background:rgba(232,160,30,.85)!important;box-shadow:0 0 0 1px rgba(150,90,10,.7)!important;color:#2b2824!important}.md-find-opt{height:30px;min-width:32px;padding:0 8px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);color:var(--ink-soft);font:600 12px var(--font-sans)}.md-find-opt.is-on{color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.md-find-nav button{color:var(--ink-soft)!important}.md-find-close{font-size:18px!important;font-weight:300!important;color:var(--ink-soft)!important}@media (min-width:768px){.file-card .ace_search{left:0!important;right:0!important;padding-left:20px!important;margin:0!important;max-width:none!important}}@media (min-width:768px){.md-find-bar{padding-left:71px!important}}@media (min-width:768px){.md-find-bar{padding-left:71px!important}.file-card .ace_search{overflow:visible!important}}@media (min-width:1200px){.file-card:has(.md-find-bar) .readme-toc,.readme-toc{top:120px!important}}.dark-mode .dashboard-filter-controls .btn-group[aria-label=Type]>.btn.btn-primary{background:#3a3631!important;color:#f2eee8!important;box-shadow:0 0 0 1px rgba(255,255,255,.14)!important}.admin-page .paper-table .user-name,.admin-page .paper-table .username,.admin-page:has(.admin-users-row) .cell-anon .repo-name,.admin-users-row .repo-name{font-family:var(--font-sans)!important;font-weight:600}.paper-page input[type=checkbox]{-webkit-appearance:none;appearance:none;width:16px!important;height:16px!important;margin:0;border:1.5px solid #a39d92;border-radius:4px;background:var(--paper-card);display:inline-grid;place-content:center;cursor:pointer;vertical-align:middle}.paper-page input[type=checkbox]::before{content:"";width:9px;height:9px;transform:scale(0);background:#fff;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat}.paper-page input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}.paper-page input[type=checkbox]:checked::before{transform:scale(1)}.paper-page input[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.dark-mode .paper-page input[type=checkbox]{border-color:#6e6860;background:0 0}.admin-page .detail-value:is(:empty),.detail-value.is-empty{color:var(--ink-muted)!important;font-style:italic}.paper-table-dashboard .btn-icon-dots,.row-edit{width:32px!important;height:32px!important;border-radius:var(--radius-control)}.paper-table-dashboard .btn-icon-dots:hover,.row-edit:hover{background:var(--hover-bg-color)!important}.q-state-toggle input[type=checkbox]{position:absolute;opacity:0;width:1px!important;height:1px!important;pointer-events:none}.q-state-toggle{position:relative;cursor:pointer}.q-state-toggle input:not(:checked)+.q-state-chip{opacity:.45;text-decoration:line-through}.q-state-toggle input:focus-visible+.q-state-chip{outline:2px solid var(--accent);outline-offset:2px}@media (min-width:768px){.md-find-bar{padding-left:31px!important}}.ace_search .ace_search_field,.anonymize-page .form-control,.md-find-input,.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea{border-radius:8px!important}.page-head-row .btn-primary,.paper-page .btn-ink,.paper-page .btn-primary{min-height:40px!important}.errors-page .errors-list-head>*,.paper-table .paper-table-head [role=columnheader],.paper-table .paper-table-head>div{font-weight:500!important;font-size:12.5px!important;letter-spacing:0!important;text-transform:none!important;color:var(--ink-soft)!important}.admin-page .admin-section-header h2{font:600 18px/1.3 var(--font-sans)!important;display:inline-flex;align-items:center;gap:8px}.admin-page .admin-section-header h2 .ui-icon{width:16px;height:16px;color:var(--ink-soft)}.files .file.active>a{background:#e2ded7!important}.files .file.active>a::before{left:3px!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.09)!important}.admin-page:has(.admin-users-row) .paper-table-head,.admin-page:has(.admin-users-row) .paper-table-row{grid-template-columns:28px minmax(220px,320px) 120px 140px 1fr 40px!important}@media (max-width:767px){.mobile-sub{margin-top:3px!important}}.paper-faq-lede,.paper-page-lede{text-wrap:pretty;max-width:640px}.errors-page .status-pill.status-neutral:not([title=""])[title]{background:0 0!important;border-color:transparent!important;color:var(--ink-muted)!important}.md-find-bar{min-height:47px!important;padding-top:7px!important;padding-bottom:7px!important}.ace_search .ace_search_field:focus,.md-find-input:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}mark.md-find{background:rgba(246,224,138,.7)!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(246,224,138,.7)!important}.files .file.active>a{background:#e6e3dc!important}@media (min-width:1200px){.readme-toc{margin-top:32px!important;padding-top:0!important}}.errors-page .kpi-card.kpi-info .kpi-value{color:var(--ink-soft)!important;font-weight:500!important;font-size:26px!important}.errors-page .kpi-card.kpi-info{background:color-mix(in srgb,var(--paper-card) 70%,transparent)!important}.ov-meter span{background:var(--accent)}.ov-meter.warn span{background:#b7791f!important}.ov-meter.crit span{background:#b42318!important}@media (max-width:767px){.admin-summary .summary-show-label{flex-basis:auto!important}}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto auto!important;grid-template-areas:"name edit more" "meta meta meta" "link link link"!important;column-gap:2px!important}.paper-table-dashboard .cell-actions>.row-edit{display:inline-flex!important;grid-area:edit;align-self:start;align-items:center;justify-content:center}.dashboard-page .paper-table-dashboard .dashboard-row-meta{display:flex!important;flex-wrap:nowrap!important;align-items:center!important;gap:0!important;padding-left:26px;font-size:12.5px;color:var(--ink-soft)}.dashboard-page .dashboard-row-meta .cell-status{flex:0 0 auto!important}.dashboard-page .dashboard-row-meta .cell-status .status-line{gap:6px}.dashboard-page .dashboard-row-meta .cell-expires,.dashboard-page .dashboard-row-meta .cell-views{display:inline-flex!important;align-items:center;font-family:var(--font-sans,Inter,sans-serif)!important;font-size:12.5px!important;color:var(--ink-soft)!important;white-space:nowrap;margin:0!important;padding:0!important;text-align:left!important;justify-content:flex-start!important}.dashboard-page .dashboard-row-meta .cell-expires::before,.dashboard-page .dashboard-row-meta .cell-views::before{content:"·";margin:0 8px;color:var(--ink-muted)}.dashboard-page .dashboard-row-meta .cell-views .ui-icon,.dashboard-page .dashboard-row-meta .cell-views svg{display:none!important}.dashboard-page .dashboard-row-meta .cell-expires .cell-label,.dashboard-page .dashboard-row-meta .cell-expires small{font-size:inherit!important;color:inherit!important;margin-right:4px}}.dashboard-page .dashboard-quota>summary{border:1px solid var(--border-color)!important;border-radius:999px!important;padding:0 12px 0 10px!important;height:36px;min-height:36px!important;display:inline-flex!important;align-items:center;gap:8px;background:var(--paper-card)!important}.dashboard-page .dashboard-quota>summary:hover{background:var(--hover-bg-color)!important}.dashboard-page .dashboard-quota .quota-summary-ring{display:inline-block!important;width:16px;height:16px}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,660px) 176px!important;gap:56px!important;justify-content:center!important}}@media (max-width:767px){.admin-summary .summary-show-label{flex-basis:100%!important;margin:4px 0 -2px!important}.admin-summary .summary-meta{flex:1 1 auto}}.q-throughput:has(.q-chart-empty) .q-chart-wrap{flex:0 0 auto!important;min-height:120px!important;height:120px}.q-throughput:has(.q-chart-empty) .q-chart-wrap canvas{visibility:hidden}.q-chart-empty{border-bottom:0!important;border-radius:8px}@media (min-width:768px){.md-find-bar{padding-left:20px!important}.file-card .ace_search{padding-left:20px!important}.file-card .ace_search .ace_search_form{margin-left:0!important}}@media (min-width:768px){.file-card .ace_search{left:0!important;padding-left:0!important}}@media (min-width:1200px){.md-find-bar{padding-left:max(20px,calc((100% - 892px)/ 2))!important}}.q-throughput:has(.q-chart-empty) .q-chart-wrap{flex:1 1 auto!important;height:auto!important;min-height:160px!important}.q-danger-group{align-items:center;flex-wrap:wrap}.q-danger-group::before{content:"Clear jobs";flex-basis:100%;font-size:12px;color:var(--ink-soft);margin-bottom:2px}.q-legend .q-legend-item,.q-legend button,.q-legend span{font-size:12px!important}.q-state-filters .q-state-toggle,.q-state-filters .q-state-toggle *{font-size:12px!important}.admin-nav a.active,.admin-nav a.router-link-active{background:0 0!important;color:var(--accent)!important;box-shadow:inset 0 -2px 0 var(--accent)!important;border-radius:0!important}.admin-nav a:not(.active):hover{background:0 0!important;color:var(--color)!important;box-shadow:inset 0 -2px 0 var(--border-color)!important}.dark-mode .q-card.selected,.q-card.selected{background:var(--paper-card)!important;border-color:var(--accent)!important;box-shadow:inset 0 0 0 1px var(--accent)!important}.errors-page .kpi-card .kpi-value,.errors-page .kpi-card.kpi-info .kpi-value,.errors-page .kpi-card.kpi-warn .kpi-value{color:var(--color)!important;font-weight:600!important;font-size:28px!important}.errors-page .kpi-card.kpi-info{background:var(--paper-card)!important}.paper-table-dashboard .btn-icon-dots,.paper-table-dashboard .row-edit{color:var(--color)!important;opacity:.72}.paper-table-dashboard .btn-icon-dots:hover,.paper-table-dashboard .row-edit:hover{opacity:1}@media (max-width:767px){.paper-table-dashboard .cell-actions>.dropdown,.paper-table-dashboard .cell-actions>.row-edit{margin-top:-6px}}@media (min-width:1280px){.viewer-bar a.vb-bug,.viewer-bar a.vb-icon[href="/faq"]{width:32px!important;padding:0!important}.viewer-bar a.vb-bug::after,.viewer-bar a.vb-icon[href="/faq"]::after{content:none!important}}.markdown-body h2{margin-bottom:12px!important}.markdown-body h3{margin-top:32px!important;margin-bottom:8px!important}.markdown-body h4{margin-top:24px!important;margin-bottom:6px!important;color:var(--ink-soft)!important}@media (min-width:768px){.file-card .ace_search{padding-left:16px!important}}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:48px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 55%)!important;box-shadow:inset -1px 0 0 var(--border-color)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(20,19,17,0),#141311 55%)!important}}.connections-page .connection-actions{flex-wrap:nowrap!important;align-items:center;gap:12px!important}.connections-page .connection-actions .connection-link{white-space:nowrap}.paper-table-dashboard .paper-table-head .col-link{font-size:inherit;color:inherit}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:40px!important;background:linear-gradient(to right,rgba(40,32,24,0),rgba(40,32,24,.07)),linear-gradient(to right,rgba(255,255,255,0),#fff 70%)!important;border-right:2px solid rgba(40,32,24,.12)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(0,0,0,0),rgba(0,0,0,.35)),linear-gradient(to right,rgba(20,19,17,0),#141311 70%)!important;border-right-color:rgba(255,255,255,.08)}}.connections-page .connection-actions .btn{white-space:nowrap}.connections-page .connection-refresh{width:36px;height:36px;justify-content:center;padding:0!important;border-radius:var(--radius-control,8px);margin-left:auto}.faq-section .faq-cat,.faq-section .panel-group{max-width:720px}.ace-chrome .ace_gutter-cell{color:#5f5a52!important}.connections-grid{align-items:start!important}.connection-installations{border-bottom:0!important;margin-bottom:0!important;padding-bottom:0!important}.anonymize-page .form-control,.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea,.paper-page textarea.form-control{border-color:#c2baac!important}.dark-mode .anonymize-page .form-control,.dark-mode .paper-page input.form-control,.dark-mode .paper-page input[type=date],.dark-mode .paper-page input[type=text],.dark-mode .paper-page input[type=url],.dark-mode .paper-page select.form-control,.dark-mode .paper-page textarea{border-color:#4a453e!important}.paper-page input::placeholder,.paper-page textarea::placeholder{color:#a39b8e!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.dropdown,.paper-table-dashboard .cell-actions>.dropdown>.btn-icon-dots,.paper-table-dashboard .cell-actions>.row-edit{width:36px!important;height:36px!important;margin-top:-4px!important;align-self:start!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}}.anonymize-page .repo-access-switch{border:0!important;background:0 0!important;padding:0!important;box-shadow:none!important;gap:0!important;display:inline-flex!important;align-items:center;width:auto!important;font-size:12.5px;color:var(--ink-soft)}.anonymize-page .repo-access-switch::before{content:"Access:";margin-right:6px;color:var(--ink-soft)}.anonymize-page .repo-access-switch button{background:0 0!important;border:0!important;box-shadow:none!important;padding:2px 4px!important;height:auto!important;min-height:0!important;font-size:12.5px!important;font-weight:500!important;color:var(--ink-soft)!important;flex:0 0 auto!important;border-radius:4px!important}.anonymize-page .repo-access-switch button+button::before{content:"·";margin-right:8px;color:var(--ink-muted)}.anonymize-page .repo-access-switch button[aria-pressed=true]{color:var(--color)!important;font-weight:600!important;opacity:1!important}.anonymize-page .repo-access-switch button:not([aria-pressed=true]):hover{color:var(--accent)!important;text-decoration:underline}.anonymize-page .repo-access-switch button .ui-icon{display:none!important}.ace_editor .ace_marker-layer .ace_selection,mark.md-find.is-current{background:rgba(240,206,90,.95)!important;box-shadow:0 0 0 1.5px #5c4a12!important;color:inherit!important}.dark-mode .ace_editor .ace_marker-layer .ace_selection,.dark-mode mark.md-find.is-current{background:rgba(214,178,60,.55)!important;box-shadow:0 0 0 1.5px #f2d77a!important}.ace_search .ace_search_field,.md-find-input,.paper-page input:not([type=checkbox]):not([type=radio]):not([type=range]),.paper-page select,.paper-page textarea{border-color:#cfc7b9!important}.dark-mode .md-find-input,.dark-mode .paper-page input:not([type=checkbox]):not([type=radio]):not([type=range]),.dark-mode .paper-page select,.dark-mode .paper-page textarea{border-color:#48433c!important}.ace_search .ace_search_field:focus,.md-find-input:focus,.paper-page input:focus,.paper-page select:focus,.paper-page textarea:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.q-dot.q-dot-idle{background:#2f7d4f!important}.dark-mode .q-dot.q-dot-idle{background:#6fbf8a!important}@media (min-width:1200px){.readme-toc{margin-top:0!important;padding-top:0!important}}@media (min-width:1200px){.readme-toc{margin-top:-30px!important}}.anonymize-workspace .anonymize-topbar{background:var(--paper-bg,var(--background))!important;border-bottom:1px solid var(--border-color)!important}.anonymize-workspace .type-badge{font-family:var(--font-sans)!important;letter-spacing:0!important;text-transform:none!important;font-size:12px!important;font-weight:600!important;padding:2px 8px!important;border-radius:999px!important;background:var(--hover-bg-color)!important;color:var(--ink-soft)!important;border:0!important;vertical-align:middle}.anonymize-page input[type=checkbox]{-webkit-appearance:none;appearance:none;width:16px!important;height:16px!important;margin:2px 0 0;border:1.5px solid #a39d92;border-radius:4px;background:var(--paper-card);display:inline-grid;place-content:center;cursor:pointer;flex:0 0 16px}.anonymize-page input[type=checkbox]::before{content:"";width:9px;height:9px;transform:scale(0);background:#fff;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat}.anonymize-page input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}.anonymize-page input[type=checkbox]:checked::before{transform:scale(1)}.anonymize-page input[type=checkbox]:disabled{opacity:.45;cursor:not-allowed}.anonymize-page input[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.dark-mode .anonymize-page input[type=checkbox]{border-color:#6e6860;background:0 0}.anonymize-page #commit{font-family:var(--font-mono, "JetBrains Mono", monospace)!important;font-size:13px!important}.anonymize-page .form-control,.anonymize-page select,.anonymize-page textarea{border-color:#cfc7b9!important}.dark-mode .anonymize-page .form-control,.dark-mode .anonymize-page select,.dark-mode .anonymize-page textarea{border-color:#48433c!important}.anonymize-page .form-control:focus,.anonymize-page select:focus,.anonymize-page textarea:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.anonymize-review-note{font-size:12.5px!important;color:var(--ink-soft)!important;margin:0 0 8px!important}.anonymize-page .form-check-input[type=checkbox]{position:absolute!important;margin-left:-28px!important;margin-top:3px!important}.anonymize-page .repo-access-controls:not(:has(button,a)),.anonymize-page .repo-access-main:not(:has(button,a)){display:none!important}.anonymize-page .repo-access-note:not(:has(span+span)){margin-top:0!important}.anonymize-form-col .anonymize-submit-bar{position:sticky;bottom:0;z-index:3;background:var(--paper-bg-alt)!important;border-top:1px solid var(--border-color);padding:12px 0 16px!important;margin-top:8px}.anonymize-form-col .anonymize-submit-bar::before{content:"";position:absolute;left:0;right:0;top:-25px;height:24px;background:linear-gradient(to bottom,transparent,var(--paper-bg-alt));pointer-events:none}.anonymize-form-col .anonymize-review-note,.anonymize-form-col .draft-status{font-size:12.5px!important;color:var(--ink-soft)!important;margin:0 0 6px!important;padding:0 2px}.anonymize-form-col .anonymize-review-note::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--status-progress,#b7791f);margin-right:8px;vertical-align:1px}.anonymize-page .repo-access-switch button[aria-pressed=true]{color:var(--accent)!important;box-shadow:inset 0 -2px 0 var(--accent)!important;border-radius:0!important}.anonymize-page .input-group-append .btn-icon{height:40px!important;width:40px!important;min-height:40px!important;display:inline-flex!important;align-items:center;justify-content:center;border:1px solid #cfc7b9!important;border-radius:8px!important;margin-left:8px!important;background:var(--paper-card)!important}.dark-mode .anonymize-page .input-group-append .btn-icon{border-color:#48433c!important}.custom-select,input.form-control,select.form-control,textarea.form-control{border-color:#cfc7b9!important}.dark-mode .custom-select,.dark-mode input.form-control,.dark-mode select.form-control,.dark-mode textarea.form-control{border-color:#48433c!important}input.form-control:focus,select.form-control:focus,textarea.form-control:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}@media (max-width:767px){.paper-table-dashboard .cell-views.num::after{content:"views"!important;margin-left:4px}}@media (min-width:768px){.paper-table-dashboard .cell-views.num::after{content:none!important}}@media (max-width:767px){.admin-page .paper-table:not(.users-table) .cell-views.num::after{margin-left:4px}}.markdown-body li .redact-chip,.markdown-body p .redact-chip{line-height:1!important;padding-top:1px!important;padding-bottom:1px!important;vertical-align:baseline}.dark-mode .explorer-sidebar,.dark-mode .leftCol{background:#1e1c19!important}.errors-page .seg-tabs,.q-range-btns{display:inline-flex!important;gap:2px!important;padding:3px!important;background:var(--hover-bg-color)!important;border:0!important;border-radius:9px!important}.errors-page .seg-tabs button,.q-range-btns .btn{border:0!important;background:0 0!important;color:var(--ink-soft)!important;border-radius:7px!important;height:28px!important;min-height:28px!important;padding:0 10px!important;font-size:13px!important;font-weight:500!important;box-shadow:none!important}.errors-page .seg-tabs button.active,.q-range-btns .btn.active{background:var(--paper-card)!important;color:var(--color)!important;box-shadow:0 1px 2px rgba(40,32,24,.12),0 0 0 1px rgba(40,32,24,.06)!important}.dark-mode .errors-page .seg-tabs button.active,.dark-mode .q-range-btns .btn.active{background:#2c2925!important;box-shadow:0 0 0 1px rgba(255,255,255,.06)!important}.ace_search .ace_search_field,.md-find-input{border-width:1px!important}.ace_search .ace_search_field:focus,.md-find-input:focus{outline:0!important;border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.anonymize-form-col .anonymize-review-note{color:#8a5a12!important;font-weight:500}.dark-mode .anonymize-form-col .anonymize-review-note{color:#e3b062!important}@media (max-width:767px){.anonymize-form-col .anonymize-submit-bar{position:sticky!important;bottom:0!important;padding:12px 0 calc(12px + env(safe-area-inset-bottom))!important}}@media (max-width:767px){.anonymize-form-col{overflow:visible!important}.anonymize-page .form-group+.form-group,.anonymize-page .form-row>[class*=col]+[class*=col]{margin-top:0!important}.anonymize-page .input-group-append .btn-icon{height:44px!important;width:44px!important}}#sourceUrl,#sourceUrl-landing{border-color:#cfc7b9!important}.dark-mode #sourceUrl,.dark-mode #sourceUrl-landing{border-color:#48433c!important}#sourceUrl-landing:focus,#sourceUrl:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.anonymize-form-col .anonymize-review-note{color:#6e470c!important}.dark-mode .anonymize-preview-col .markdown-body h1,.dark-mode .anonymize-preview-col .markdown-body h2,.dark-mode .anonymize-preview-col .markdown-body hr{border-color:var(--border-color)!important}.dark-mode .q-dot.q-dot-idle,.q-dot.q-dot-idle{background:var(--ink-muted)!important}.ace_search .ace_search_counter,.md-find-count{color:var(--color)!important;font-weight:500!important;font-variant-numeric:tabular-nums;font-size:12.5px!important}.readme-toc a:not(.is-active){color:#57524a!important}.dark-mode .readme-toc a:not(.is-active){color:#b8b1a6!important}.viewer-bar .vb-support{margin-right:8px!important}:root{--redact-bar:#1F1C19;--redact-ink:#5A544C;--redact-reveal:#F4EFE6}.dark-mode{--redact-bar:#000000;--redact-ink:#3E3A35;--redact-reveal:#F4EFE6}.demo-output .redact-chip,.hero .redact,.hero-mock .redact,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip,.markdown-body h4 mark.redact-chip,.markdown-body mark.redact-chip,span.redact{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:none!important;border:0!important;border-radius:2px!important;padding:0 .28em!important;text-decoration:none!important;font-weight:inherit;letter-spacing:.02em;-webkit-box-decoration-break:clone;box-decoration-break:clone;transition:color .12s ease;cursor:help}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact{box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.demo-output .redact-chip:hover,.markdown-body mark.redact-chip:hover,span.redact:hover{color:var(--redact-reveal)!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip{font-size:inherit!important;vertical-align:baseline!important;padding:0 .2em!important}.ace_text-layer .ace_redacted{background:var(--redact-bar)!important;color:var(--redact-ink)!important;border-radius:2px;box-shadow:none!important;text-decoration:none!important}.dark-mode .ace_text-layer .ace_redacted{box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.meta-redact{background:0 0!important;color:var(--ink-soft)!important;box-shadow:none!important;padding:0!important;display:inline-flex!important;align-items:center;gap:6px;font-weight:500!important}.meta-redact::before{content:"";width:14px;height:8px;border-radius:1.5px;background:var(--redact-bar);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted{background:var(--redact-bar)!important;color:var(--redact-ink)!important;border-radius:2px!important}.ace_editor .ace_text-layer .ace_redacted{box-shadow:none!important}.dark-mode .ace_editor .ace_text-layer .ace_redacted{box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.dark-mode .demo-output .redact-chip:hover,.dark-mode .markdown-body mark.redact-chip:hover{color:var(--redact-reveal)!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{color:#3e3933!important}.dark-mode .meta-redact{color:var(--ink-soft)!important}.dark-mode .hero-mock-count,.hero-mock-count{background:0 0!important;color:var(--ink-soft)!important;padding:0!important;gap:6px;font-weight:500!important}.hero-mock-count::before{content:"";width:12px;height:7px;border-radius:1.5px;background:var(--redact-bar);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}@media (max-width:767px){.dark-mode .mobile-sub b,.mobile-sub b{background:0 0!important;color:var(--ink-soft)!important;padding:0!important;gap:5px;font-weight:500!important}.mobile-sub b::before{content:"";width:12px;height:7px;border-radius:1.5px;background:var(--redact-bar);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}}:root{--redact-bar:#E4DED3;--redact-ink:#8A8378;--redact-reveal:#2B2824}.dark-mode{--redact-bar:#34302A;--redact-ink:#8F887D;--redact-reveal:#F1ECE3}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted,.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact,.demo-output .redact-chip,.markdown-body mark.redact-chip,span.redact{box-shadow:none!important;border-radius:3px!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{color:var(--redact-ink)!important}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip{box-shadow:none!important}.hero-mock-count::before,.meta-redact::before,.mobile-sub b::before{box-shadow:none!important}:root{--redact-bar:#EEEAE2;--redact-ink:#8C8478;--redact-reveal:#2B2824;--redact-deboss:inset 0 1px 2px rgba(60,48,32,.18),inset 0 0 0 1px rgba(60,48,32,.06)}.dark-mode{--redact-bar:#0E0D0B;--redact-ink:#7A746A;--redact-reveal:#F1ECE3;--redact-deboss:inset 0 1px 2px rgba(0,0,0,.7),inset 0 0 0 1px rgba(255,255,255,.05)}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact,.demo-output .redact-chip,.markdown-body mark.redact-chip,span.redact{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:var(--redact-deboss)!important;border-radius:4px!important;padding:.05em .45em!important;font-family:var(--font-mono, "JetBrains Mono", monospace)!important;font-size:.78em!important;font-weight:500!important;letter-spacing:.02em!important;line-height:1.4!important;vertical-align:.08em!important;white-space:nowrap}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip{font-size:.55em!important;vertical-align:.3em!important;color:var(--redact-ink)!important;padding:.15em .5em!important}.demo-output .redact-chip:hover,.markdown-body mark.redact-chip:hover,span.redact:hover{color:var(--redact-reveal)!important}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:var(--redact-deboss)!important;border-radius:3px!important}.hero-mock-count::before,.meta-redact::before,.mobile-sub b::before{background:var(--redact-bar)!important;box-shadow:var(--redact-deboss)!important;border-radius:2px!important}:root{--slot-bg:#E3DCD0;--slot-bg-sel:#D3CBBD;--slot-num:#1F5F5A}.dark-mode{--slot-bg:#3A352F;--slot-bg-sel:#4A443C;--slot-num:#7CC7BE}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact,.demo-output .redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip,.markdown-body h4 mark.redact-chip,.markdown-body mark.redact-chip,.redact-slot,span.redact{display:inline-flex!important;align-items:center!important;justify-content:flex-end!important;min-width:2.5em!important;height:1.12em!important;padding:0 .32em!important;margin:0 .06em!important;background:var(--slot-bg)!important;color:var(--slot-num)!important;box-shadow:none!important;border:0!important;border-radius:3px!important;vertical-align:-.2em!important;line-height:1!important;font-size:inherit!important;letter-spacing:0!important;text-decoration:none!important;cursor:help;white-space:nowrap}.markdown-body mark.redact-chip i,.redact-slot i,span.redact i{font:700 .66em/1 var(--font-sans,Inter,sans-serif)!important;font-style:normal!important;color:var(--slot-num)!important}.demo-output .redact-chip{font:700 .66em/1 var(--font-sans,Inter,sans-serif)!important;min-width:3.8em!important;height:1.7em!important;vertical-align:-.35em!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{height:.95em!important;vertical-align:-.1em!important;border-radius:4px!important}.files .redact-slot{min-width:2.3em!important;height:1.08em!important;vertical-align:-.18em!important}.files .file.active>a .redact-slot,.files a[aria-current] .redact-slot{background:var(--slot-bg-sel)!important}.breadcrumb .redact-slot{vertical-align:-.22em!important}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted{position:relative;color:transparent!important;background:var(--slot-bg)!important;box-shadow:none!important;border-radius:3px!important}.ace_editor .ace_text-layer .ace_redacted::after{position:absolute;right:.35em;top:50%;transform:translateY(-50%);font:700 9.5px/1 var(--font-sans,Inter,sans-serif);color:var(--slot-num)}.hero-mock-count::before,.meta-redact::before,.mobile-sub b::before{background:var(--slot-bg)!important;box-shadow:none!important;border-radius:2px!important}.files .tree-name{display:inline-flex;align-items:center;gap:0;min-width:0}.files .tree-name .redact-slot{vertical-align:middle!important;margin:0 .08em!important}.markdown-body li mark.redact-chip,.markdown-body p mark.redact-chip{vertical-align:-.16em!important}.ace_editor .ace_marker-layer .ace_selection,mark.md-find.is-current{background:rgba(240,196,70,.95)!important;box-shadow:0 0 0 2px rgba(196,140,20,.55)!important}.dark-mode .ace_editor .ace_marker-layer .ace_selection,.dark-mode mark.md-find.is-current{background:rgba(214,170,50,.6)!important;box-shadow:0 0 0 2px rgba(240,200,90,.55)!important}.demo-output .redact-chip,span.redact i{font-size:10px!important}.dark-mode{--slot-num:#8FD6CC}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{min-width:1.9em!important;height:.78em!important;vertical-align:0!important;padding:0 .22em!important}.viewer-bar .vb-right{gap:4px!important}.viewer-bar .vb-right .vb-icon{width:34px!important;height:34px!important;margin:0!important}.viewer-bar .vb-support{margin-right:10px!important}.viewer-bar .vb-snapshot{margin-right:14px!important}@media (max-width:767px){.mobile-sub{font-size:13px!important;color:#5f5a52!important}.dark-mode .mobile-sub{color:#b3aca1!important}}.file-card-meta{overflow:visible!important}:root{--control-border:#CFC7B9}.dark-mode{--control-border:#48433C}.admin-page .btn:not(.btn-ink):not(.btn-primary):not(.btn-outline-danger):not(.summary-pill),.admin-page select,.anonymize-page #branch,.anonymize-page #sourceUrl,.anonymize-page #sourceUrl-landing,.anonymize-page .form-control-lg,.anonymize-page select.form-control,.dashboard-page .dashboard-quota>summary,.dashboard-page .dropdown-toggle,.dashboard-page .filter-btn,.errors-page select,.q-range-btns+.btn{border-color:var(--control-border)!important}.anonymize-page #sourceUrl-landing:focus,.anonymize-page #sourceUrl:focus{border-color:var(--accent)!important}.errors-page .seg-tabs{height:36px!important;align-items:center}.errors-page .seg-tabs button{height:30px!important}.admin-page .admin-toolbar .btn,.admin-page .btn-export,.admin-page a.btn[href*=export],.admin-page button[title*=xport]{height:36px!important;min-height:36px!important}@media (max-width:767px){.paper-table .cell-views{gap:0!important}}@media (min-width:1200px){.anonymize-workspace .anonymize-split,.anonymize-workspace .anonymize-topbar-inner{max-width:1048px;margin-left:auto!important;margin-right:auto!important}.anonymize-workspace .anonymize-topbar{padding-left:0!important;padding-right:0!important}.anonymize-workspace .anonymize-form-col{padding-left:0!important}}@media (max-width:767px){.anonymize-form-col .anonymize-submit-bar{margin:8px 0 0!important;padding:10px 0 calc(10px + env(safe-area-inset-bottom))!important}.anonymize-form-col .anonymize-submit-bar .btn{border-radius:10px!important}}.img-unavailable{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px dashed var(--control-border);border-radius:8px;font-size:12.5px;color:var(--ink-soft);background:var(--hover-bg-color)}.anonymize-form-col .draft-status{color:var(--ink-soft)!important}.errors-page .errors-toolbar .form-control,.errors-page .errors-toolbar select,.errors-page form select{border-color:var(--control-border)!important}@media (min-width:1200px){.anonymize-workspace .anonymize-form-col,.anonymize-workspace .anonymize-form-col .paper-section,.anonymize-workspace .anonymize-form-col form{padding-left:0!important}.anonymize-workspace .anonymize-form-col{background:0 0!important;padding-right:24px!important}.anonymize-workspace .anonymize-form-col .anonymize-submit-bar,.anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:var(--paper-bg,var(--background))!important}.anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:linear-gradient(to bottom,transparent,var(--paper-bg,#faf9f6))!important}}@media (max-width:767px){.anonymize-page .form-row>.form-group,.anonymize-page .form-row>[class*=col] .form-group{margin-bottom:14px!important}.anonymize-page .form-row>[class*=col]{margin-bottom:0!important;padding-bottom:0!important}}@media (min-width:1200px){.anonymize-workspace .anonymize-form-col .anonymize-submit-bar{background:#faf9f6!important}.anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:linear-gradient(to bottom,rgba(250,249,246,0),#faf9f6)!important}.dark-mode .anonymize-workspace .anonymize-form-col .anonymize-submit-bar{background:#161412!important}.dark-mode .anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:linear-gradient(to bottom,rgba(22,20,18,0),#161412)!important}}:root{--slot-num:#0F4F4A}.dark-mode{--slot-num:#A8E6DD;--slot-bg:#3F3A33}.markdown-body li mark.redact-chip,.markdown-body p mark.redact-chip,.markdown-body td mark.redact-chip,span.redact{display:inline-block!important;height:1.08em!important;line-height:1.08em!important;vertical-align:-.17em!important;min-width:2.5em!important;text-align:right!important;padding:0 .34em!important}.breadcrumb .redact-slot i,.files .redact-slot i,.markdown-body li mark.redact-chip i,.markdown-body p mark.redact-chip i,.markdown-body td mark.redact-chip i,span.redact i{font-size:max(10.5px,.72em)!important;line-height:inherit!important;vertical-align:top!important;font-weight:700!important}.markdown-body h1 mark.redact-chip i,.markdown-body h2 mark.redact-chip i,.markdown-body h3 mark.redact-chip i{font-size:.62em!important}.ace_editor .ace_text-layer .ace_redacted::after{font-size:10.5px!important}.demo-output .redact-chip{font-size:11px!important}.errors-page .bar-info,.errors-page .vol-info,.errors-page [class*=seg-info]{opacity:.45}.connections-grid{align-items:stretch!important}.connections-grid>section{display:flex;flex-direction:column}.connections-grid>section .connection-installations{margin-top:auto!important;border-top:1px solid var(--border-color);border-bottom:0!important}.navbar .nav-link.active,.site-nav .nav-link.active,header .nav-link.active{color:var(--color)!important;box-shadow:inset 0 -2px 0 var(--accent)!important;border-bottom-color:transparent!important}@media (max-width:767px){.anonymize-topbar .paper-crumbs{display:block!important;margin-bottom:4px;font-size:13px}}.anonymize-form-col .draft-discard{background:0 0;border:0;padding:0 0 0 6px;font-size:12.5px;font-weight:500;color:var(--color)!important;text-decoration:underline;text-underline-offset:3px}.anonymize-form-col .draft-discard:hover{color:#b42318!important}@media (max-width:767px){.mobile-sub b{display:inline-flex!important;align-items:center;gap:5px;font-weight:400!important;color:inherit!important}.mobile-sub b::before{content:""!important;display:inline-block!important;width:12px;height:7px;border-radius:2px;background:var(--slot-bg)!important}.mobile-sub span:has(+b)::after{content:"·";margin:0 6px;opacity:.7}}.demo-output .redact-chip,span.redact{display:inline-block!important;height:1.15em!important;line-height:1.15em!important;vertical-align:-.2em!important;text-align:right!important}.demo-output .redact-chip{font-size:11px!important;min-width:3.4em!important}.ace-chrome .ace_gutter-cell{color:#5a554d!important}.dark-mode .ace_editor .ace_gutter-cell{color:#a39c91!important}.files .file.active>a{background:#e2ded6!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,700px) 176px!important;gap:44px!important}}@media (hover:none){.anonymize-page .repo-access-switch button:not([aria-pressed=true]):hover{color:var(--ink-soft)!important;text-decoration:none!important}}.connections-grid>section .connection-installations{margin-top:16px!important}.dashboard-page .dashboard-quota .quota-summary-ring{display:none!important}.dashboard-page .dashboard-quota>summary::before{content:"";width:28px;height:4px;border-radius:2px;background:linear-gradient(to right,var(--accent) var(--quota-pct,5%),var(--border-color) 0);flex:0 0 28px}.paper-table-dashboard .row-open{margin-right:2px}@media (max-width:767px){.paper-table-dashboard .cell-actions>.row-open{display:none!important}}.dashboard-page .dashboard-quota>summary::before{width:32px;height:5px;flex-basis:32px;background:linear-gradient(to right,var(--accent) 0 max(3px,var(--quota-pct,5%)),#d9d3c8 0)!important}.dark-mode .dashboard-page .dashboard-quota>summary::before{background:linear-gradient(to right,#7cc7be 0 max(3px,var(--quota-pct,5%)),#3a352f 0)!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,680px) 176px!important;gap:44px!important}.md-find-bar{padding-left:max(20px,calc((100% - 900px)/ 2))!important}}.breadcrumb .redact-slot i,.files .redact-slot i,.markdown-body li mark.redact-chip i,.markdown-body p mark.redact-chip i,.markdown-body td mark.redact-chip i,span.redact i{font-size:max(11px,.8em)!important;letter-spacing:.01em}.files .file.active>a{background:#ebe7e0!important}.files .file.active>a .redact-slot{background:#d6cec0!important}.demo-output .redact-chip{font-size:12px!important;height:1.3em!important;line-height:1.3em!important;min-width:3em!important;vertical-align:-.28em!important;padding:0 .4em!important}.paper-table-dashboard .cell-actions{gap:4px!important}.paper-table-dashboard .cell-actions>.copy-url{margin-right:6px!important}.paper-table-dashboard .btn-icon-dots,.paper-table-dashboard .row-edit{width:32px!important;height:32px!important;display:inline-flex!important;align-items:center;justify-content:center;border-radius:8px}.paper-table-dashboard .btn-icon-dots:hover,.paper-table-dashboard .row-edit:hover{background:var(--hover-bg-color)!important}.anonymize-page #commit{text-overflow:ellipsis}@media (max-width:767px){.anonymize-page .form-grid-2{row-gap:0!important;gap:0!important}.anonymize-page .form-grid-2>.form-group{margin-bottom:16px!important}}.connections-grid>section .connection-installations{border-top:0!important;padding-top:0!important}.vb-help{position:relative;display:inline-flex}.vb-help-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:0;border-radius:8px;background:0 0;color:var(--ink-soft);font:500 13px var(--font-sans,Inter,sans-serif);cursor:pointer}.vb-help-btn:hover,.vb-help-btn[aria-expanded=true]{background:var(--hover-bg-color);color:var(--color)}.vb-help-menu{position:absolute;top:calc(100% + 8px)!important;right:0;left:auto;min-width:250px;z-index:50}.vb-help-menu a{display:flex;gap:10px;align-items:flex-start}.vb-help-menu small{display:block;font-size:12px;color:var(--ink-soft);font-weight:400;margin-top:1px}@media (max-width:767px){.viewer-bar .vb-help{display:none!important}}.paper-table-dashboard .paper-table-head .col-link{padding-left:0!important;margin-left:-16px}@media (min-width:768px){.anonymize-workspace .anonymize-topbar{padding-top:12px!important;padding-bottom:12px!important}.anonymize-workspace .anonymize-topbar .paper-crumbs{margin-bottom:2px!important}.anonymize-workspace .anonymize-topbar .paper-page-title,.anonymize-workspace .anonymize-topbar h1{font-size:26px!important;margin:0!important}}#sourceUrl-landing::placeholder,#terms::placeholder,.form-control::placeholder,input::placeholder,textarea::placeholder{color:#a39b8e!important;opacity:1!important}.dark-mode .form-control::placeholder,.dark-mode input::placeholder,.dark-mode textarea::placeholder{color:#6f695f!important}.q-dot-active,.q-state-filters .q-legend-active,.q-state-filters [class*=active]>.q-dot,.q-state-toggle .q-dot-active{background:var(--accent)!important}@media (max-width:767px){.mobile-sub b{font-size:inherit!important;gap:6px!important;margin-left:0!important;padding-left:0!important}.mobile-sub span:has(+b)::after{margin:0 6px 0 6px!important}.mobile-sub b::before{width:14px!important;height:8px!important;background:#d6cec0!important}.file-card-head{min-height:56px!important}}.markdown-body h1 mark.redact-chip i,.markdown-body h2 mark.redact-chip i{font-weight:600!important;font-size:.58em!important}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{min-width:1.6em!important;padding:0 .18em!important}.readme-toc a.lvl-3{font-size:12.5px!important;color:#6e685f!important}.dark-mode .readme-toc a.lvl-3{color:#9c958a!important}.file-card-head .file-card-tools,.file-card-head .view-tools{margin-right:10px!important;padding-right:10px!important;border-right:1px solid var(--border-color)!important}.files .file.active>a{background:#e5e0d8!important}.files .file.active>a .redact-slot{background:#cec5b5!important}.markdown-body li mark.redact-chip,.markdown-body p mark.redact-chip,.markdown-body td mark.redact-chip{vertical-align:-.1em!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:24px!important;transition:opacity .15s}.file-card.at-right-edge .file-card-body::after{opacity:0!important}}.hero-mock .is-active,.hero-mock [class*=active]{position:relative}.hero-mock .is-active::before{left:0!important}.anonymize-form-col form{padding-bottom:36px!important}.connections-grid>section p{font-size:13.5px!important;line-height:1.55!important}.connections-grid>section>.btn,.connections-grid>section>a.btn{width:auto!important;align-self:flex-start}@media (max-width:767px){.paper-table-dashboard .cell-actions>.dropdown,.paper-table-dashboard .cell-actions>.dropdown>.btn-icon-dots,.paper-table-dashboard .cell-actions>.row-edit{width:40px!important;height:40px!important}.dashboard-page .dashboard-row-meta .cell-expires,.dashboard-page .dashboard-row-meta .cell-status,.dashboard-page .dashboard-row-meta .cell-views{padding:0!important;margin:0!important}.dashboard-page .dashboard-row-meta .cell-expires::before,.dashboard-page .dashboard-row-meta .cell-views::before{margin:0 7px!important;width:auto!important}.dashboard-page .dashboard-row-meta .status-line{padding:0!important;margin:0!important}.dashboard-page .dashboard-row-meta .cell-expires .cell-label{margin-right:4px!important}}.connection-installations,.connection-installations li{border:0!important;padding-bottom:0!important;margin-bottom:0!important}.connection-installations li{padding-top:0!important}.connections-page .connection-refresh{margin-left:0!important}.anonymize-form-col .anonymize-submit-bar::before{display:none!important}.anonymize-form-col .anonymize-submit-bar{border-top:1px solid var(--border-color)!important;box-shadow:0 -6px 12px -10px rgba(40,32,24,.18)}.anonymize-form-col form{padding-bottom:24px!important}.img-unavailable{display:flex!important;align-items:center;justify-content:center;gap:8px;width:100%;min-height:72px;border:1px solid var(--border-color)!important;border-style:solid!important;background:var(--hover-bg-color);font:500 12.5px var(--font-sans,Inter,sans-serif)!important;color:var(--ink-soft)}.img-unavailable::before{content:"";width:16px;height:13px;border:1.5px solid currentColor;border-radius:2px;opacity:.7;background:linear-gradient(135deg,transparent 55%,currentColor 55% 62%,transparent 62%)}.admin-summary .summary-pill:not(.active) .count{text-decoration:none!important;opacity:1!important;color:var(--ink-soft)!important}.admin-summary .summary-pill:not(.active)::before{background:#8a8680!important}.dashboard-page .dashboard-quota>summary::before{width:44px!important;flex-basis:44px!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,664px) 176px!important;gap:40px!important}.md-find-bar{padding-left:max(20px,calc((100% - 880px)/ 2))!important}}.file-card-head .tb-file{margin-left:8px!important;position:relative}.file-card-head .tb-file::before{content:"";position:absolute;left:-9px;top:6px;bottom:6px;width:1px;background:var(--border-color)}.tree-search-input::placeholder{color:#8a8378!important}.tree-search-kbd{color:#6e685f!important;border:1px solid var(--border-color)!important;background:var(--paper-card)!important}@media (max-width:767px){.mobile-sub{gap:0!important;column-gap:0!important}.mobile-sub span:has(+b)::after,.mobile-sub>span+span::before{margin:0 6px!important}.mobile-sub b::before{margin:0!important}.file-card-head{min-height:52px!important;padding-top:6px!important;padding-bottom:6px!important}}.errors-page .dot-info,.errors-page .seg-info{background:#cfc9bf!important;opacity:1!important}.dark-mode .errors-page .dot-info,.dark-mode .errors-page .seg-info{background:#4a453e!important}.errors-page .dot-error,.errors-page .seg-error{background:#b42318!important}.errors-page .dot-warn,.errors-page .seg-warn{background:#b7791f!important}.dark-mode .leftCol,.dark-mode .viewer-bar{background:#221f1b!important}.paper-table-dashboard .copy-url{box-shadow:inset 0 0 0 1px var(--control-border,#cfc7b9)!important;background:var(--paper-card)!important}.paper-table-dashboard .copy-url .ui-icon,.paper-table-dashboard .copy-url svg{width:15px!important;height:15px!important;color:var(--color)!important;opacity:.75}.paper-table-dashboard .copy-url:hover{background:var(--hover-bg-color)!important}@media (min-width:1200px){.anonymize-workspace .anonymize-preview-col{padding-right:0!important}}.anonymize-submit-bar .draft-status{display:block;margin:0 0 8px!important;font-size:12.5px!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.row-open{display:inline-flex!important;grid-area:auto}.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto auto auto!important;grid-template-areas:"name open edit more" "meta meta meta meta" "link link link link"!important}.paper-table-dashboard .cell-actions>.row-open{grid-area:open;width:40px!important;height:40px!important;align-self:start;margin-top:-4px}}.overview-page :is(.ov-kpi,.ov-card,.kpi-card):has(.ov-meter.crit){border-color:rgba(180,35,24,.45)!important;box-shadow:inset 3px 0 0 #b42318!important}.overview-page :is(.ov-kpi,.ov-card,.kpi-card):has(.ov-meter.warn):not(:has(.ov-meter.crit)){border-color:rgba(183,121,31,.45)!important;box-shadow:inset 3px 0 0 #b7791f!important}.dark-mode{--slot-bg:#46403A}.faq-section .panel-default .panel-title a:after{transform:rotate(45deg)!important}.faq-section .panel-default .panel-title a[aria-expanded=true]:after{transform:rotate(-135deg)!important}.overview-page .ov-kpi-card:has(.ov-meter.crit){border-color:rgba(180,35,24,.45)!important;box-shadow:inset 3px 0 0 #b42318!important}.overview-page .ov-kpi-card:has(.ov-meter.warn):not(:has(.ov-meter.crit)){border-color:rgba(183,121,31,.45)!important;box-shadow:inset 3px 0 0 #b7791f!important}@media (max-width:767px){.paper-table-dashboard .cell-anon,.paper-table-dashboard .cell-anon a,.paper-table-dashboard .cell-anon strong,.paper-table-dashboard .cell-anon>*{min-width:0}.paper-table-dashboard .cell-anon .anon-name,.paper-table-dashboard .cell-anon .anon-title,.paper-table-dashboard .cell-anon .anon-title>a{white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important;word-break:normal!important;overflow-wrap:normal!important;display:block}}.admin-page .summary-pill.ok::before,.users-page .summary-pill.ok::before{background:var(--status-ready,#2f7d4f)!important}.admin-page .summary-pill.error::before{background:var(--status-error,#b42318)!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.row-open{display:none!important}.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto auto!important;grid-template-areas:"name edit more" "meta meta meta" "link link link"!important}.paper-table-dashboard .cell-anon .anon-title,.paper-table-dashboard .cell-anon .anon-title>a{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;overflow-wrap:anywhere!important}}.ace-chrome .ace_keyword,.ace-chrome .ace_storage,.ace-chrome .ace_storage.ace_type{color:#7a3e6b!important}.dark-mode .ace_editor .ace_keyword,.dark-mode .ace_editor .ace_storage{color:#d3a3c6!important}.dark-mode{--slot-bg:#4A443D;--slot-num:#9FE0D6}.faq-hero h1,.faq-page h1,.paper-page-title{text-wrap:balance}@media (min-width:1200px){.readme-toc{margin-top:-18px!important}}@media (min-width:768px){.leftCol-search{height:52px!important;min-height:52px!important;box-sizing:border-box;display:flex!important;align-items:center;padding-top:0!important;padding-bottom:0!important;border-bottom:1px solid var(--border-color)!important;margin-bottom:6px!important}}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:32px!important;background:linear-gradient(to right,rgba(255,255,255,0),#fff 75%)!important;border-right:2px solid rgba(40,32,24,.14)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(20,19,17,0),#141311 75%)!important;border-right-color:rgba(255,255,255,.1)}}.anonymize-form-col form{padding-bottom:0!important}.anonymize-form-col .anonymize-submit-bar{margin-bottom:0!important}.errors-page .kpi-filter{cursor:pointer;transition:border-color .12s,box-shadow .12s}.errors-page .kpi-filter:hover{border-color:var(--control-border,#cfc7b9)!important}.errors-page .kpi-filter.is-selected{border-color:var(--accent)!important;box-shadow:inset 0 0 0 1px var(--accent)!important}.errors-page .kpi-filter:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.readme-toc a:not(.is-active){color:#4f4a43!important}.readme-toc a.lvl-3:not(.is-active){color:#625c53!important}.ace_search .ace_search_field:focus,.md-find-input:focus{box-shadow:0 0 0 2px var(--accent-soft)!important;border-color:var(--accent)!important}@media (max-width:767px){.file-card-head{align-items:flex-start!important}.file-card-head .btn-sidebar-toggle,.file-card-head .mobile-files-toggle,.file-card-head>button:first-child{margin-top:2px!important}}@media (max-width:767px){.dashboard-page #search,.dashboard-page .btn-filters,.dashboard-page .dashboard-heading-actions .btn,.dashboard-page .dashboard-quota>summary,.dashboard-page .filters-toggle{height:40px!important;min-height:40px!important}}.profile-page .quota-tile:has(.quota-unlimited)::after,.quota-panel .quota-item:has(.quota-unlimited)::after{content:"No limit on file count";display:block;font-size:12.5px;color:var(--ink-soft);margin-top:6px}.admin-page .detail-value.is-empty,.admin-page .is-unsynced{color:#9a9389!important;font-style:normal!important}@media (min-width:768px){.anonymize-form-col .anonymize-submit-bar{display:flex!important;align-items:center;gap:16px}.anonymize-submit-bar .draft-status{margin:0!important;flex:0 1 auto;max-width:46%;line-height:1.35}.anonymize-submit-bar .btn-ink{flex:1 1 auto}}.anonymize-landing .repo-access-note>span:has(.ui-icon){display:none!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{box-shadow:inset -8px 0 8px -8px rgba(40,32,24,.35)!important;border-right:0!important}}.dark-mode .leftCol{background:#1c1a17!important}.dark-mode .viewer-bar{background:#181614!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:44px!important;background:linear-gradient(to right,rgba(250,249,246,0) 0,rgba(250,249,246,.92) 55%,rgba(226,220,210,.95) 100%)!important;box-shadow:inset -1px 0 0 rgba(40,32,24,.18)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(20,19,17,0) 0,rgba(20,19,17,.92) 55%,rgba(44,40,35,.95) 100%)!important;box-shadow:inset -1px 0 0 rgba(255,255,255,.14)!important}}@media (max-width:767px){.file-card-head{min-height:48px!important;align-items:center!important;padding-top:4px!important;padding-bottom:4px!important}.file-card-head .file-card-path{display:flex!important;flex-direction:row!important;align-items:center!important;flex-wrap:nowrap!important;gap:8px!important;min-width:0}.file-card-head .file-card-path .breadcrumb{min-width:0;flex:0 1 auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.mobile-sub{margin:0!important;flex:0 0 auto;white-space:nowrap}.mobile-sub>span{display:none!important}.mobile-sub span:has(+b)::after{content:none!important}.mobile-sub b{font-size:12.5px!important}}.paper-features,.paper-qa,.paper-section-head,.paper-stats{margin-top:88px!important}.paper-hero+.paper-stats{margin-top:40px!important}@media (min-width:1100px){.paper-faq-body{grid-template-columns:180px minmax(0,1fr) 220px!important;gap:48px!important}.paper-faq-help{display:block}}.paper-faq-help{display:none}.paper-faq-help-card{position:sticky;top:96px;border:1px solid var(--border-color);border-radius:12px;background:var(--paper-card);padding:18px}.paper-faq-help-card strong{display:block;font-size:14px;margin-bottom:6px}.paper-faq-help-card p{font-size:13px;line-height:1.5;color:var(--ink-soft);margin:0 0 12px}.anonymize-submit-bar .draft-status{white-space:nowrap;flex:0 0 auto!important;max-width:none!important}.q-throughput .q-range-btns{float:right;margin:-4px 0 0 12px}.admin-page .paper-table{border:1px solid var(--border-color)!important;border-radius:12px!important;overflow:hidden;background:var(--paper-card)}.dark-mode{--ink-soft:#ADA699;--ink-muted:#8C8579}.hero-mock-count::before{background:var(--slot-bg)!important}@media (min-width:1100px){.paper-faq-help{display:block!important}}.q-throughput{position:relative}.q-throughput .q-range-btns{position:absolute!important;top:12px;right:16px;float:none!important;margin:0!important;width:auto!important;display:inline-flex!important;z-index:2} \ No newline at end of file + */.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\e05e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hive:before{content:"\e07f"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\e065"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\e013"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-innosoft:before{content:"\e080"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\e066"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\e01a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\e068"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-perbyte:before{content:"\e083"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\e07a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\e057"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\e06d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\e070"}.fa-store-slash:before{content:"\e071"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\e07b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-uncharted:before{content:"\e084"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\e074"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-watchman-monitoring:before{content:"\e087"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:'Font Awesome 5 Brands';font-style:normal;font-weight:400;font-display:block;src:url(../fonts/fa-brands-400.eot);src:url(../fonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-brands-400.woff2) format("woff2"),url(../fonts/fa-brands-400.woff) format("woff"),url(../fonts/fa-brands-400.ttf) format("truetype"),url(../fonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:'Font Awesome 5 Brands';font-weight:400}@font-face{font-family:'Font Awesome 5 Free';font-style:normal;font-weight:400;font-display:block;src:url(../fonts/fa-regular-400.eot);src:url(../fonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-regular-400.woff2) format("woff2"),url(../fonts/fa-regular-400.woff) format("woff"),url(../fonts/fa-regular-400.ttf) format("truetype"),url(../fonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:'Font Awesome 5 Free';font-weight:400}@font-face{font-family:'Font Awesome 5 Free';font-style:normal;font-weight:900;font-display:block;src:url(../fonts/fa-solid-900.eot);src:url(../fonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-solid-900.woff2) format("woff2"),url(../fonts/fa-solid-900.woff) format("woff"),url(../fonts/fa-solid-900.ttf) format("truetype"),url(../fonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:'Font Awesome 5 Free';font-weight:900}.nb-notebook{line-height:1.5}.nb-stderr,.nb-stdout{white-space:pre-wrap;margin:1em 0;padding:.1em .5em}.nb-stderr{background-color:#faa}.nb-cell+.nb-cell{margin-top:.5em}.nb-output table{border:1px solid #000;border-collapse:collapse}.nb-output th{font-weight:700}.nb-output td,.nb-output th{border:1px solid #000;padding:.25em;text-align:left;vertical-align:middle;border-collapse:collapse}.nb-notebook blockquote{border-left:5px solid #ccc;margin-left:0;padding-left:1em}.nb-cell{position:relative}.nb-raw-cell{white-space:pre-wrap;background-color:#f5f2f0;font-family:Consolas,Monaco,'Andale Mono',monospace;padding:1em;margin:.5em 0}.nb-output{min-height:1em;width:100%;overflow-x:scroll;border-right:1px dotted #ccc}.nb-output img{max-width:100%}.nb-input:before,.nb-output:before{position:absolute;font-family:monospace;color:#999;left:-7.5em;width:7em;text-align:right}.nb-input:before{content:"In [" attr(data-prompt-number) "]:"}.nb-output:before{content:"Out [" attr(data-prompt-number) "]:"}// Fix pandas dataframe formatting div[style="max-height:1000px;max-width:1500px;overflow:auto;"]{max-height:none!important}@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{font:normal 1.21em KaTeX_Main,math,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.18.10"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}.mermaid{text-align:center;margin:1em 0;overflow:auto}.mermaid svg{max-width:100%;height:auto}.dark-mode .mermaid{filter:invert(1) hue-rotate(180deg)}.dark-mode .mermaid svg{background:0 0}.dark-mode .mermaid .edgeLabel,.dark-mode .mermaid .nodeLabel{filter:invert(1) hue-rotate(180deg)}.mermaid .node circle,.mermaid .node ellipse,.mermaid .node polygon,.mermaid .node rect{stroke-width:2px}.mermaid .flowchart-link{stroke-width:2px}.mermaid .actor{fill:#f9f9f9;stroke:#333;stroke-width:2px}.mermaid .messageLine0,.mermaid .messageLine1{stroke:#333;stroke-width:1.5px}.mermaid .section0,.mermaid .section1,.mermaid .section2,.mermaid .section3{opacity:.8}.mermaid .pieChart .slice{opacity:.9}.markdown-body .mermaid{background:0 0;border:none;padding:10px}.mermaid .error{color:#d32f2f;font-family:monospace;font-size:12px;padding:10px;border:1px solid #d32f2f;border-radius:4px;background-color:#ffebee}@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff')}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.5;color:#24292e;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#6a737d}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#005cc5}.markdown-body .pl-e,.markdown-body .pl-en{color:#6f42c1}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#24292e}.markdown-body .pl-ent{color:#22863a}.markdown-body .pl-k{color:#d73a49}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#032f62}.markdown-body .pl-smw,.markdown-body .pl-v{color:#e36209}.markdown-body .pl-bu{color:#b31d28}.markdown-body .pl-ii{color:#fafbfc;background-color:#b31d28}.markdown-body .pl-c2{color:#fafbfc;background-color:#d73a49}.markdown-body .pl-c2::before{content:"^M"}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#22863a}.markdown-body .pl-ml{color:#735c0f}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#005cc5}.markdown-body .pl-mi{font-style:italic;color:#24292e}.markdown-body .pl-mb{font-weight:700;color:#24292e}.markdown-body .pl-md{color:#b31d28;background-color:#ffeef0}.markdown-body .pl-mi1{color:#22863a;background-color:#f0fff4}.markdown-body .pl-mc{color:#e36209;background-color:#ffebda}.markdown-body .pl-mi2{color:#f6f8fa;background-color:#005cc5}.markdown-body .pl-mdr{font-weight:700;color:#6f42c1}.markdown-body .pl-ba{color:#586069}.markdown-body .pl-sg{color:#959da5}.markdown-body .pl-corl{text-decoration:underline;color:#032f62}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit}.markdown-body strong{font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body hr{box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0}.markdown-body input{overflow:visible}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#0366d6;text-decoration:none}.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:0 0;border:0;border-bottom:1px solid #dfe2e5}.markdown-body hr::before{display:table;content:""}.markdown-body hr::after{display:table;clear:both;content:""}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:4px!important}.markdown-body .pl-2{padding-left:8px!important}.markdown-body .pl-3{padding-left:16px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:32px!important}.markdown-body .pl-6{padding-left:40px!important}.markdown-body::before{display:table;content:""}.markdown-body::after{display:table;clear:both;content:""}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:0}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0}.markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #c6cbd1;border-bottom-color:#959da5;border-radius:3px;box-shadow:inset 0 -1px 0 #959da5}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#1b1f23;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #eaecef}.markdown-body h2{padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #eaecef}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#6a737d}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:600}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #dfe2e5}.markdown-body table tr{background-color:#fff;border-top:1px solid #c6cbd1}.markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#005cc5;border-color:#005cc5}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #d1d5da;border-bottom-color:#c6cbd1;border-radius:3px;box-shadow:inset 0 -1px 0 #c6cbd1}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#0366d6}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.dark-mode{--canvas-bg-color:#161412;--sidebar-bg-color:#1C1A17;--main-bg-color:#161412;--header-bg-color:#161412;--hover-bg-color:rgba(255, 255, 255, 0.05);--header-color:#FAF9F6;--color:#D6D1C5;--link-color:#7CC7BE;--link-hover-color:#A8DDD6;--border-color:#423D37;--border-soft:#2A2825;--btn-color:#FAF9F6;--input-color:#E6E1D8;--input-bg:#25231F;--primary-bg:#FAF9F6;--primary-hover-bg:#FFFFFF;--primary-color:#1A1815;--primary-active-color:#1A1815;--accent:#7CC7BE;--accent-soft:rgba(124, 199, 190, 0.12);--ink-muted:#A39D92;--ink-faint:#8A857C;--ink-soft:#B5AFA2;--status-ready:#5FB27A;--status-progress:#E0A94A;--status-error:#F08A82;--status-inactive:#8A857C;--status-error-soft:rgba(240, 138, 130, 0.08);--status-error-soft-hover:rgba(240, 138, 130, 0.14);--admin-nav-bg:#1A1918;--admin-nav-active:rgba(124, 199, 190, 0.12);--admin-stat-bg:#25231F;--paper-card:#2C2925;--paper-bg-alt:#211E1B;--highlight:#3A341E;background-color:var(--canvas-bg-color);color:var(--color)}body{--canvas-bg-color:#FAF9F6;--sidebar-bg-color:#FFFFFF;--header-bg-color:#FAF9F6;--header-color:#1A1815;--main-bg-color:#FAF9F6;--hover-bg-color:rgba(26, 24, 21, 0.05);--input-color:#1A1815;--color:#1A1815;--link-color:#1F6A64;--link-hover-color:#17544F;--border-color:#E6E1D8;--border-soft:#EDE9E1;--btn-color:#1A1815;--input-bg:#FFFFFF;--primary-bg:#1A1815;--primary-hover-bg:#000000;--primary-color:#FAF9F6;--primary-active-color:#FAF9F6;--accent:#1F6A64;--accent-soft:#E4F0EE;--ink-muted:#6F6A61;--ink-faint:#8A857C;--ink-soft:#4A4641;--paper-bg-alt:#F3F1EC;--paper-card:#FFFFFF;--highlight:#FFF3C4;--status-ready:#2F7A44;--status-progress:#946512;--status-error:#B42318;--status-inactive:#8A857C;--status-error-soft:rgba(180, 35, 24, 0.04);--status-error-soft-hover:rgba(180, 35, 24, 0.08);--radius-control:6px;--radius-button:8px;--button-height:36px;--button-font-size:14px;--button-line-height:20px;--button-padding-x:13px;--button-compact-height:32px;--radius-surface:10px;--font-serif:'Instrument Serif','Iowan Old Style',Georgia,serif;--font-sans:'Inter',ui-sans-serif,system-ui,-apple-system,sans-serif;--font-mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;background-color:var(--canvas-bg-color);color:var(--color);font-family:var(--font-sans);font-size:14px;letter-spacing:-.005em;line-height:1.5}.fadeIn{opacity:1;animation-name:fadeInOpacity;animation-iteration-count:1;animation-timing-function:ease-in;animation-duration:.6s}@keyframes fadeInOpacity{0%{opacity:0}100%{opacity:1}}.btn:active,.btn:focus{box-shadow:none}:focus:not(:focus-visible){outline:0}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.btn:focus-visible,.dropdown-item:focus-visible,.form-control:focus-visible{outline:2px solid var(--accent);outline-offset:2px;box-shadow:none}input:active,input:hover{box-shadow:none!important}input:-webkit-autofill,input:-webkit-autofill:focus,input:-webkit-autofill:hover,select:-webkit-autofill,select:-webkit-autofill:focus,select:-webkit-autofill:hover,textarea:-webkit-autofill,textarea:-webkit-autofill:focus,textarea:-webkit-autofill:hover{border-color:var(--border-color);-webkit-text-fill-color:var(--color);-webkit-box-shadow:none;transition:background-color 1000s ease-in-out 0s}.custom-select,.form-control,input:-internal-autofill-selected{color:var(--input-color);background-color:var(--input-bg)!important;border-color:var(--border-color)}.form-control:focus{border-color:var(--border-color)}body,html{position:relative;height:100%;overflow-x:hidden}body{overflow-y:hidden}#app{flex:1 1 auto;min-height:0}.app-header{flex:0 0 auto}@media (min-width:992px){.app-header.is-contained .navbar{padding-left:max(40px,calc((100% - 1140px)/ 2 + 32px))!important;padding-right:max(40px,calc((100% - 1140px)/ 2 + 32px))!important}}.app-view{display:block;flex:1 1 auto;min-height:0;overflow-x:hidden!important;overflow-y:auto!important;max-width:100%}.markdown-body a,a{color:var(--link-color)}.markdown-body a:hover,a:hover{text-decoration:none;color:var(--link-hover-color)}.dropdown-menu a.dropdown-item{color:var(--color)}.dropdown-menu a.dropdown-item:hover{color:var(--color);text-decoration:none}.markdown-body .emoji{height:1.3em;margin:0;vertical-align:-.1em}.navbar.navbar-expand-lg{background:var(--header-bg-color)!important;border-bottom:1px solid var(--border-color);transition:background-color .5s ease;padding:10px max(20px,calc((100vw - 1200px)/ 2));font-family:var(--font-sans)}.navbar-toggler{border:1px solid var(--border-color)!important;color:var(--color)!important;width:44px;height:40px;padding:0!important;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-control)!important}.navbar-toggler .navbar-toggler-icon{width:22px;height:22px}.navbar-toggler .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='%231A1815' stroke-width='2' stroke-linecap='round' d='M4 8h22M4 15h22M4 22h22'/%3E%3C/svg%3E")!important}.navbar .navbar-nav{align-items:center}.navbar .nav-link{display:inline-flex;align-items:center;font-size:13px;color:var(--ink-muted)!important;padding:6px 0!important;margin:0 12px;border-bottom:1px solid transparent;letter-spacing:-.005em;line-height:1}.navbar .nav-link:hover{color:var(--color)!important}.navbar .nav-link.active{color:var(--color)!important;border-bottom-color:var(--color)}.navbar .nav-link.nav-icon{padding:0!important;margin:0 8px;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px}.navbar .nav-link .fa,.navbar .nav-link .fab{font-size:15px;line-height:1}.navbar #navbarSupportedContent{align-items:center}.navbar-brand{font-family:var(--font-serif)!important;font-size:20px!important;letter-spacing:-.01em;color:var(--color)!important;padding-right:18px;margin-right:12px;border-right:1px solid var(--border-color)}.navbar-brand em,.navbar-brand i{font-style:italic;font-weight:400}.navbar-dark .navbar-toggler{border-color:var(--border-color);color:var(--color)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='%231A1815' stroke-width='2' stroke-linecap='round' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar .btn-signin{display:inline-flex;align-items:center;height:32px;padding:0 14px!important;margin-left:6px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--color)!important;border-radius:var(--radius-control);font-size:13px;font-weight:500;line-height:1}.dark-mode .navbar .btn-signin{border-color:#5a554d;background:0 0}.navbar .btn-signin:hover{background:var(--hover-bg-color);border-color:var(--ink-faint);color:var(--color)!important}.navbar .nav-support{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;order:3;gap:6px;min-height:32px;margin-left:4px;padding:0 10px;border:1px solid transparent;border-radius:var(--radius-control);color:var(--ink-muted);font-size:13px;font-weight:500;text-decoration:none}.navbar .nav-support:hover{background:var(--hover-bg-color);color:var(--color)}.navbar button.nav-link{background:0 0;border:0;cursor:pointer}.navbar .nav-link:focus-visible,.navbar .nav-support:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-control)}.navbar .nav-signin-m{display:none}@media (min-width:992px){.navbar>.nav-support{display:none!important}}.navbar .nav-support-inline{display:inline-flex!important;align-items:center;gap:6px}.navbar .nav-support-inline .ui-icon{width:15px;height:15px}@media (max-width:991px){.navbar .nav-signin-m{display:inline-flex;align-items:center;order:0;height:40px;padding:0 14px;margin-left:0;margin-right:8px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);color:var(--color);font-size:13px;font-weight:500;text-decoration:none}.navbar .nav-support{order:0;width:44px;min-height:44px;margin:0 8px 0 auto;padding:0}.navbar .nav-support-label{display:none}}.navbar .user-chip{display:inline-flex!important;align-items:center;gap:8px;padding:4px 12px 4px 6px!important;margin-left:6px;border:1px solid var(--border-color);border-radius:999px;background:var(--sidebar-bg-color);font-size:13px;color:var(--color)!important}.navbar .user-chip:hover{background:var(--hover-bg-color)}.navbar .user-chip img{width:22px;height:22px}.navbar .user-chip .user-chip-name{color:var(--color);font-weight:500}.navbar .user-chip::after{color:var(--ink-muted)}#navbarSupportedContent{padding-right:0}.generalMessage{display:block;text-align:center;color:var(--header-color)}.rgba-gradient{background:var(--canvas-bg-color);color:var(--color)}.paper-landing{overflow-x:clip}.paper-wrap{width:min(1076px,calc(100% - 64px));margin-left:auto;margin-right:auto;padding-left:0;padding-right:0}.paper-eyebrow{font-family:var(--font-mono);font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-soft);font-weight:500}.paper-hero{max-width:1140px;margin:0 auto;padding:40px 32px 0;display:grid;grid-template-columns:.95fr 1.05fr;gap:48px;align-items:center}.paper-hero h1.hero-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.6rem,5.4vw,4.4rem);line-height:1.02;letter-spacing:-.025em;margin:14px 0 0;color:var(--color);text-wrap:balance}.paper-hero h1 .accent{color:var(--accent);font-style:italic}.hero-subtitle{margin-top:18px;font-size:18px;line-height:1.55;color:var(--ink-soft);max-width:30em;text-wrap:pretty}.paper-cta-row{display:flex;flex-wrap:wrap;gap:18px;align-items:center;margin-top:26px}.btn-hero{display:inline-flex;align-items:center;font-family:var(--font-sans);border:1px solid var(--color);background:var(--color);color:var(--canvas-bg-color);padding:12px 20px;font-size:14px;font-weight:500;border-radius:var(--radius-control);letter-spacing:-.005em;transition:background .15s,border-color .15s,transform .1s}.btn-hero:hover{background:var(--primary-hover-bg);border-color:var(--primary-hover-bg);color:var(--canvas-bg-color);text-decoration:none;transform:translateY(-1px)}.btn-hero-ghost{background:0 0;border:1px solid var(--border-color);color:var(--color);padding:12px 20px;font-size:14px;font-weight:500;border-radius:var(--radius-control)}.btn-hero-ghost:hover{background:var(--hover-bg-color);border-color:var(--color);color:var(--color)}.paper-link-arrow{font-size:14px;color:var(--ink-soft);text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--border-color)}.paper-link-arrow:hover{color:var(--color);text-decoration-color:var(--color)}.paper-reassure{list-style:none;margin:16px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:6px 18px;font-size:13px;color:var(--ink-muted)}.paper-reassure li{white-space:nowrap}@media (min-width:1100px){.paper-reassure{flex-wrap:nowrap}}.paper-reassure li::before{content:"\2713";color:var(--status-ready);margin-right:7px;font-size:14px;font-weight:600}.paper-frame{border-radius:var(--radius-surface);background:var(--paper-card);border:1px solid var(--border-color);box-shadow:0 24px 60px rgba(26,24,21,.1),0 2px 6px rgba(26,24,21,.04);overflow:hidden}.dark-mode .paper-frame{box-shadow:0 24px 60px rgba(0,0,0,.45)}.paper-frame-bar{display:flex;align-items:center;gap:8px;height:34px;padding:0 12px;border-bottom:1px solid var(--border-soft);background:var(--paper-bg-alt);font-family:var(--font-mono);font-size:11px;color:var(--ink-muted)}.paper-frame-bar .dots{display:flex;gap:5px}.paper-frame-bar .dots i{width:9px;height:9px;border-radius:50%;background:color-mix(in srgb,var(--ink-faint) 45%,transparent)}.paper-frame-bar .url{flex:1;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-frame img{display:block;width:100%;height:auto}.paper-hero-shot .paper-frame{width:100%}.hero-mock{display:grid;grid-template-columns:168px 1fr;min-height:360px;font-family:var(--font-sans);text-align:left}.hero-mock-tree{background:var(--paper-bg-alt);border-right:1px solid var(--border-soft);padding:12px 8px}.hero-mock-tree-title{font-size:11px;font-weight:600;color:var(--ink-muted);padding:0 8px 8px}.hero-mock-row{display:flex;align-items:center;gap:7px;height:26px;padding:0 8px;border-radius:var(--radius-control);font-size:12.5px;color:var(--ink-soft);white-space:nowrap}.hero-mock-row svg{width:14px;height:14px;flex-shrink:0;fill:none;stroke:var(--ink-muted);stroke-width:1.2;stroke-linejoin:round}.hero-mock-row.is-active{background:rgba(128,128,128,.15);color:var(--color);font-weight:500}.hero-mock-doc{min-width:0;background:var(--paper-card)}.hero-mock-doc-bar{display:flex;align-items:center;justify-content:space-between;height:40px;padding:0 18px;border-bottom:1px solid var(--border-soft);font-size:13px}.hero-mock-doc-bar span{font-size:12px;color:var(--ink-muted)}.hero-mock-doc-body{padding:18px 22px 22px;font-size:13.5px;line-height:1.6;color:var(--color)}.hero-mock-doc-body p{margin:0 0 10px}.hero-mock-h1{font-size:22px;font-weight:600;letter-spacing:-.01em;padding-bottom:8px;margin-bottom:12px;border-bottom:1px solid var(--border-soft)}.hero-mock-h2{font-size:15px;font-weight:600;margin:16px 0 8px}.hero-mock-doc-body pre{margin:0 0 12px;padding:10px 12px;background:var(--paper-bg-alt);border:1px solid var(--border-soft);border-radius:var(--radius-control);font-family:var(--font-mono);font-size:12px;line-height:1.6;color:var(--color)}.hero-mock .redact{background:var(--accent-soft);color:var(--accent);border-radius:4px;padding:1px 5px;font-family:var(--font-mono);font-size:.88em}@media (max-width:575px){.hero-mock{grid-template-columns:1fr;min-height:0}.hero-mock-tree{display:none}}.paper-proof{margin-top:48px;padding-top:18px;padding-bottom:18px;border-top:1px solid var(--border-color);display:flex;flex-wrap:wrap;gap:12px 30px;align-items:center}.paper-proof-venues{display:flex;gap:22px;align-items:center;font-family:var(--font-serif);font-size:22px;letter-spacing:-.01em;color:var(--ink-soft)}.paper-proof-venues i{font-style:normal;font-family:var(--font-sans);font-size:14px;color:var(--ink-faint)}.paper-stats{margin-top:40px;padding-top:16px;border-top:1px solid var(--border-color)}.paper-stats-meta-left{display:inline-flex;align-items:center;gap:8px}.paper-stats-meta{display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:14px}.paper-stats-meta-right{display:inline-flex;align-items:center;gap:6px}.paper-stats-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--status-ready)}.paper-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color)}.paper-stat-card{display:flex;flex-direction:column;padding:22px 26px 20px 0;margin-right:26px;border-right:1px solid var(--border-color);min-width:0}.paper-stat-card:last-child{border-right:0;margin-right:0;padding-right:0}.paper-stat-value{font-size:44px;font-weight:500;letter-spacing:-.04em;line-height:1;color:var(--color);font-variant-numeric:tabular-nums}.paper-stat-label{margin-top:6px;font-size:13px;color:var(--ink-soft)}.paper-stat-value.is-pending{color:var(--ink-faint)}.paper-stat-bars{display:block;width:100%;height:34px;margin-top:18px;overflow:visible}.paper-stat-bars rect{fill:color-mix(in srgb,var(--ink-muted) 55%,transparent)}.paper-stat-bars rect.is-latest{fill:var(--accent)}.paper-stat-delta{margin-top:10px;font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}.paper-section-head{margin-top:88px;display:grid;grid-template-columns:1fr 1.4fr;gap:40px;align-items:start}.paper-section-head>p{padding-top:34px}@media (max-width:991px){.paper-section-head>p{padding-top:0}}.paper-qa h2,.paper-section-head h2{font-family:var(--font-serif);font-weight:400;font-size:clamp(2rem,4.2vw,3.25rem);line-height:1.05;letter-spacing:-.02em;margin:10px 0 0;color:var(--color);text-wrap:balance}.paper-feature-title em,.paper-qa h2 em,.paper-section-head h2 em{font-style:italic;color:var(--accent)}.paper-section-head p{font-size:15px;color:var(--ink-soft);max-width:32em;margin:0;line-height:1.6}.paper-preview{margin-top:28px;display:grid;grid-template-columns:1fr 1fr;gap:14px}.paper-preview .pane{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);overflow:hidden;min-width:0}.paper-preview .pane-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid var(--border-soft)}.paper-preview .pane-label{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);white-space:nowrap}.paper-preview .pane-url{font-family:var(--font-mono);font-size:11px;color:var(--ink-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.paper-preview .pane-body{padding:16px 18px;font-family:var(--font-mono);font-size:13px;line-height:1.9;color:var(--ink-soft)}.paper-preview .pane-body strong{color:var(--color);font-weight:600}.paper-preview .tok{color:var(--ink-faint)}.paper-preview .term{background:color-mix(in srgb,var(--status-error) 9%,transparent);color:inherit;border-radius:4px;padding:0 4px;text-decoration:line-through;text-decoration-color:color-mix(in srgb,var(--status-error) 60%,transparent)}.dark-mode .paper-preview .term{background:color-mix(in srgb,var(--status-error) 16%,transparent)}.paper-preview .redact{background:var(--accent-soft);color:var(--accent);border-radius:4px;padding:0 5px}.paper-features{margin-top:28px;padding-top:28px;border-top:1px solid var(--border-color);display:grid;grid-template-columns:5fr 7fr;gap:48px;align-items:start}.paper-features-list{display:flex;flex-direction:column}.paper-feature{border-left:2px solid var(--border-soft);padding:16px 0 16px 20px;transition:border-color .2s}.paper-feature.is-on{border-left-color:var(--accent)}.paper-feature-tab{all:unset;display:block;position:relative;width:100%;cursor:pointer;color:inherit;border-radius:4px}.paper-feature:not(.is-on) .paper-feature-tab::after{content:"";position:absolute;right:4px;bottom:12px;width:8px;height:8px;border-right:1.5px solid var(--ink-muted);border-bottom:1.5px solid var(--ink-muted);transform:rotate(45deg)}.paper-feature:not(.is-on) .paper-feature-tab:hover::after{border-color:var(--color)}.paper-feature.is-on .paper-feature-tab::after{content:"";position:absolute;right:4px;bottom:10px;width:8px;height:8px;border-right:1.5px solid var(--color);border-bottom:1.5px solid var(--color);transform:rotate(-135deg)}.paper-feature-tab:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.paper-feature-tab .paper-eyebrow{display:block;margin-bottom:6px}.paper-feature-title{display:block;font-family:var(--font-serif);font-size:24px;line-height:1.15;letter-spacing:-.015em;color:var(--color);transition:color .2s}.paper-feature-title em{color:inherit}.paper-feature-tab:hover .paper-feature-title,.paper-feature.is-on .paper-feature-title{color:var(--color)}.paper-feature.is-on .paper-feature-title em{color:var(--accent)}.paper-feature-more{display:none}.paper-feature.is-on .paper-feature-more{display:block}.paper-feature-more .paper-feature-title-m{display:none}.paper-feature-more p{font-size:15px;color:var(--ink-soft);line-height:1.55;margin:10px 0 12px;max-width:30em}.paper-features-stage{min-width:0}.paper-feature-panel{animation:paper-feature-in .25s ease-out}@keyframes paper-feature-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion:reduce){.paper-feature-panel{animation:none}}.paper-crop{position:relative;height:380px;overflow:hidden;background:var(--paper-card)}.paper-crop img{position:absolute;left:0;top:0;max-width:none;height:auto;display:block}.paper-crop-anonymize img{width:213%;transform:translate(-1.5%,-18%)}.paper-crop-review img{width:213%;transform:translate(0,-6.5%)}.paper-crop-manage img{width:213%;transform:translate(-12.3%,-6.5%)}.paper-qa{margin-top:88px;display:grid;grid-template-columns:1fr 1.4fr;gap:40px;align-items:start}.paper-qa h2{margin-bottom:12px}.paper-qa .q{border-top:1px solid var(--border-color);padding:18px 0}.paper-qa .q:last-child{border-bottom:1px solid var(--border-color)}.paper-qa .q a::after{content:none;display:inline-block;width:7px;height:7px;margin-left:10px;border-right:1.5px solid var(--ink-muted);border-bottom:1.5px solid var(--ink-muted);transform:translateY(-2px) rotate(-45deg);transition:transform .15s,border-color .15s}.paper-qa .q a:hover::after{border-color:var(--accent);transform:translate(3px,-2px) rotate(-45deg)}.paper-qa .q a:hover{text-decoration:underline;text-underline-offset:3px}.paper-qa .q a{display:block;font-weight:500;font-size:15px;color:var(--color);margin-bottom:6px}.paper-qa .q a:hover{color:var(--accent);text-decoration:none}.paper-qa .q p{margin:0;font-size:14px;color:var(--ink-soft);line-height:1.6}@media (max-width:1100px){.paper-hero-shot .paper-frame{width:100%}}@media (max-width:991px){.paper-hero{grid-template-columns:1fr;gap:36px;padding-top:40px}.paper-qa,.paper-section-head{grid-template-columns:1fr;gap:16px}.paper-stats-grid{grid-template-columns:1fr 1fr}.paper-stat-card:nth-child(2n){border-right:0;margin-right:0;padding-right:0}.paper-stat-card:nth-child(-n+2){border-bottom:1px solid var(--border-color)}}@media (max-width:767px){.paper-wrap{width:calc(100% - 40px)}.paper-hero{padding-left:20px;padding-right:20px}.paper-reassure{flex-direction:column;gap:6px}.paper-hero h1.hero-title{font-size:clamp(2.4rem,11vw,3.25rem)}.hero-subtitle{font-size:16px}.paper-cta-row .btn-hero{width:100%;justify-content:center}.paper-proof{margin-top:48px}.paper-stat-card{padding:18px 16px 16px 0;margin-right:16px}.paper-stat-value{font-size:40px}.paper-qa,.paper-section-head{margin-top:72px}.paper-preview{grid-template-columns:1fr}.paper-preview .pane-body{font-size:12px;line-height:1.8}.paper-features{display:flex;flex-wrap:wrap;gap:0;padding-top:0;border-top:0;margin-top:24px}.paper-feature,.paper-features-list,.paper-features-stage{display:contents}.paper-feature-tab{order:1;flex:1 1 0;width:auto;padding:12px 0 10px;border-bottom:2px solid var(--border-color);border-radius:0;text-align:left}.paper-feature.is-on .paper-feature-tab{border-bottom-color:var(--color)}.paper-feature-tab .paper-eyebrow{margin:0;font-size:11px;letter-spacing:.08em}.paper-feature-tab .paper-feature-title{display:none}.paper-feature-panel{order:2;width:100%;margin-top:20px;animation:none}.paper-crop{height:240px}.paper-crop-anonymize img{width:230%;transform:translate(-1.5%,-8%)}.paper-crop-review img{width:240%;transform:translate(-1%,-8%)}.paper-crop-manage img{width:220%;transform:translate(-3%,-9%)}.paper-feature-more{order:3;width:100%}.paper-feature.is-on .paper-feature-more{margin-top:18px}.paper-feature-more .paper-feature-title-m{display:block;font-size:28px;line-height:1.1}.paper-feature-more p{max-width:none}.paper-footer-inner>.paper-footer-brand{grid-column:1/-1}.paper-footer-foot{flex-direction:column;gap:6px}}.dropdown-menu{background-color:var(--paper-card,var(--main-bg-color))!important;color:var(--color)!important;border:1px solid var(--border-color)!important;border-radius:8px!important;box-shadow:0 12px 28px rgba(26,24,21,.08),0 2px 6px rgba(26,24,21,.05)!important;padding:6px!important;font-family:var(--font-sans);font-size:13px;min-width:11rem}.dropdown-menu .dropdown-item{color:var(--color)!important;padding:7px 10px!important;border-radius:5px;font-size:13px}.dropdown-menu .dropdown-item:focus,.dropdown-menu .dropdown-item:hover{background:var(--hover-bg-color)!important;color:var(--color)!important}.dropdown-menu .dropdown-divider{border-top-color:var(--border-color)!important;margin:4px 2px}.dropdown-menu .dropdown-header{color:var(--ink-muted);font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.body,.file-content,.markdown-body{background-color:var(--main-bg-color)!important;color:var(--color)!important}.markdown-body table tr{background-color:var(--main-bg-color);border-top:1px solid var(--border-color)}.markdown-body table tr:nth-child(2n){background-color:var(--paper-bg-alt)}.markdown-body table td,.markdown-body table th{border:1px solid var(--border-color)}.file-content{padding:4px 7px;text-align:left;min-height:100%}.file-card{margin:16px 20px 28px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);overflow:clip}.file-card:not(.is-code) .file-card-head{position:sticky;top:0;z-index:2;border-radius:var(--radius-surface) var(--radius-surface) 0 0}.file-card.is-code{display:flex;flex-direction:column;height:calc(100% - 32px);margin-bottom:16px}.file-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:46px;padding:6px 8px 6px 16px;border-bottom:1px solid var(--border-color);background:var(--paper-card)}.dark-mode .file-card-head{background:var(--paper-card)}.file-card-meta{font-size:13px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}.file-card-meta .sep{margin:0 8px;color:var(--ink-faint)}.file-card-head .btn{height:32px;min-height:32px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:500;padding:0 10px}.file-card-head .btn-seg:not(.view-switch){gap:2px;padding-left:8px;margin-left:4px;border-left:1px solid var(--border-color)}.file-card-head .btn-seg:not(.view-switch)>.btn{border:0!important;border-radius:var(--radius-control)!important;margin:0!important;background:0 0!important;color:var(--ink-soft)}.file-card-head .btn-seg:not(.view-switch)>.btn:hover{background:var(--hover-bg-color)!important;color:var(--color)}.file-card-actions>.btn-seg:first-child,.file-card-head .btn-seg:not(.view-switch):has(>.btn:only-child){border-left:0;padding-left:0;margin-left:0}.file-card-head .btn.btn-icon-only{width:32px;padding:0!important;color:var(--ink-soft)}.file-card-head .btn.btn-icon-only:hover{color:var(--color);background:var(--hover-bg-color)!important}.file-card .file-card-body{flex:1 1 auto;min-height:0}.file-card-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.file-card-path{display:none;align-items:center;gap:2px;min-width:0}.file-card-path .paths{overflow:hidden}.file-card-path+.file-card-meta,.file-card-path+.view-switch{margin-left:8px}.status-bar.is-redundant{display:none}.file-card-path{display:inline-flex}@media (max-width:767px){.file-card-head{flex-wrap:wrap;row-gap:0}.file-card-head .file-card-meta{order:3;flex-basis:100%;margin:0 0 6px!important;padding:0!important;border:0!important;font-size:12px;color:var(--ink-muted)}.file-card-head .file-card-meta .meta-extra{display:inline!important}.file-card-head .btn-ghost{color:var(--ink-soft)}.file-card-head .breadcrumb-root,.file-card-head .breadcrumb-root+.breadcrumb-item::before{display:none!important}.file-card-head .breadcrumb-item{padding-left:0}.file-card-head{gap:6px;padding:6px 8px 6px 16px}.file-card-actions{gap:2px}.file-card-actions .btn-seg{margin-left:4px}}@media (min-width:992px){.file-card-path+.file-card-meta{padding-left:14px;border-left:1px solid var(--border-color)}.file-card{margin-top:20px}.file-card.is-code{height:calc(100% - 36px)}}.card-files-btn{margin-right:4px}.leftCol-zip{display:inline-flex;align-items:center;gap:8px;margin-bottom:6px;font-size:13px;font-weight:500;color:var(--ink-soft);text-decoration:none}.leftCol-zip:hover{color:var(--color);text-decoration:none}.leftCol-zip .ui-icon{width:15px;height:15px}.file-card-actions .btn-seg{margin-left:6px}.file-card-head .btn.btn-find,.file-card-head .btn.btn-goto{padding:0 10px;gap:6px;font-weight:400}@media (max-width:1180px){.file-card-head .btn-find .btn-find-label,.file-card-head .btn-goto .btn-find-label{display:none}.file-card-head .btn-find,.file-card-head .btn-goto{width:32px;padding:0!important}}@media (max-width:991px){.file-card-head .btn-find{width:32px;padding:0!important}.file-card-head .btn-find-label{display:none}}@media (max-width:767px){.file-card-head .btn-find{width:40px}}.ace_search{top:8px!important;right:12px!important;left:auto!important;background:var(--paper-card)!important;color:var(--color)!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-surface)!important;box-shadow:0 8px 24px rgba(26,24,21,.14);padding:6px!important;font-family:var(--font-sans)!important;font-size:13px!important}.ace_search_field{background:var(--input-bg)!important;color:var(--input-color)!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important;margin-right:4px!important;height:28px!important;padding:0 8px!important;font-family:var(--font-sans)!important}.ace_search_field:focus{border-color:var(--ink-muted)!important;outline:0;box-shadow:0 0 0 3px var(--hover-bg-color)}.ace_button,.ace_searchbtn{background:0 0!important;color:var(--ink-soft)!important;border:0!important;border-radius:var(--radius-control)!important;width:28px;height:28px!important;line-height:28px!important}.ace_button:hover,.ace_searchbtn:hover{background:var(--hover-bg-color)!important;color:var(--color)!important}.ace_button.checked{border-color:var(--accent)!important;color:var(--accent)!important}.ace_search_counter{color:var(--ink-soft)!important;font-size:13px;margin:0 8px 0 4px!important;font-variant-numeric:tabular-nums}.ace_search{display:flex;flex-wrap:nowrap;align-items:center;gap:6px;max-width:none!important;width:auto!important}.dark-mode .ace_search{background:var(--paper-card)!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:var(--accent)!important}.ace_search .ace_button[action=searchInSelection],.ace_search .ace_button[action=toggleReplace],.ace_search .ace_replace_form,.ace_search .ace_searchbtn[action=findAll]{display:none!important}.ace_search_field{width:200px!important}.ace_search_form{display:flex;align-items:center;margin:0!important;float:none!important}.ace_search_options{display:flex!important;align-items:center;gap:4px;margin:0!important;float:none!important;text-align:left!important}.ace_search_options .ace_button{width:28px;height:28px!important;line-height:26px!important;padding:0!important;border-radius:var(--radius-control)!important;font-size:12px!important;text-align:center}.ace_searchbtn_close{position:static!important;margin-left:2px}.ace_editor .ace_marker-layer .ace_selected-word{background:color-mix(in srgb,#e0a94a 30%,transparent)!important;border:0!important;border-radius:2px}.ace_editor.ace-chrome .ace_marker-layer .ace_selection,.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:#f2b544!important;box-shadow:0 0 0 1px #b7791f;border-radius:2px}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:#b7791f!important;box-shadow:0 0 0 1px #f2b544}.ace_editor .ace_indent-guide-active{background:0 0!important;box-shadow:inset 1px 0 0 var(--border-color)!important}.ace_searchbtn_close{filter:none!important;opacity:.6}.ace_prompt_container{background:var(--paper-card)!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-surface)!important;box-shadow:0 12px 32px rgba(26,24,21,.16)!important}.btn.btn-ghost{background:0 0!important;border-color:transparent!important;color:var(--ink-muted)}.btn.btn-ghost:hover{background:var(--hover-bg-color)!important;color:var(--color)}.btn.btn-ghost.is-on{color:var(--color);background:var(--hover-bg-color)!important;box-shadow:none}.btn.btn-ghost:focus:not(:focus-visible):not(.is-on):not(:hover){background:0 0!important;box-shadow:none}.dark-mode .file-card-head .btn-seg .btn{background:0 0!important;border-color:var(--border-color)!important;color:var(--color);font-weight:500}.file-card-head .btn.btn-find,.file-card-head .btn.btn-goto{color:var(--ink-soft)}.file-card-head>.view-switch{gap:2px;padding:2px;border-radius:calc(var(--radius-control) + 2px);background:var(--hover-bg-color)}.file-card-head>.view-switch .btn{height:28px;min-height:28px;margin:0!important;border:0!important;border-radius:var(--radius-control)!important;background:0 0!important;color:var(--ink-soft);font-weight:500}.file-card-head>.view-switch .btn:hover{color:var(--color)}.file-card-head>.view-switch .btn.is-on{background:var(--paper-card)!important;color:var(--color);box-shadow:0 1px 2px rgba(26,24,21,.1),0 0 0 1px var(--border-color);cursor:default}.dark-mode .file-card-head>.view-switch .btn.is-on{background:#3d3934!important;box-shadow:none}.status-bar .btn.btn-ghost.btn-zip{color:var(--ink-soft)}.copy-path-btn{margin-left:-4px}@media (max-width:767px){.file-card-head .btn,.file-card-head .btn.btn-icon-only{height:40px;min-height:40px}.file-card-head .btn.btn-icon-only{width:40px}.copy-path-btn{display:none!important}}.file-card .ace_editor{height:100%!important}@media (max-width:767px){.file-card{margin:0;border:0;border-radius:0}.file-card.is-code{height:100%}.file-card-head{position:sticky;top:0;z-index:2;padding-left:16px}}.dir-card{margin:20px 20px 0;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);overflow:hidden}.dir-card+.file-card{margin-top:16px}.dir-card-head{display:flex;align-items:baseline;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt)}.dark-mode .dir-card-head{background:#35312c}.dir-card-title{font-size:14px;font-weight:600;color:var(--color)}.dir-card-sub{font-size:13px;color:var(--ink-muted)}.dir-list{list-style:none;margin:0;padding:4px 0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:0}.dir-row{display:flex;align-items:center;gap:10px;width:100%;height:34px;padding:0 16px;border:0;background:0 0;font:inherit;font-size:14px;color:var(--color);text-align:left;text-decoration:none;cursor:pointer}.dir-row:hover{background:var(--hover-bg-color);color:var(--color);text-decoration:none}.dir-row:hover .dir-name{text-decoration:underline;text-underline-offset:3px}.dir-row.is-current .dir-name{font-weight:600}.dir-icon{width:16px;height:16px;flex-shrink:0;fill:none;stroke:var(--ink-muted);stroke-width:1.2;stroke-linejoin:round}.dir-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dir-meta{flex-shrink:0;font-size:12px;color:var(--ink-muted);font-variant-numeric:tabular-nums}.dir-list:not(.is-expanded)>li:nth-child(n+9){display:none}.dir-more{display:block;width:100%;height:40px;border:0;border-top:1px solid var(--border-soft);background:0 0;font:inherit;font-size:13px;font-weight:500;color:var(--link-color);cursor:pointer}.dir-more:hover{background:var(--hover-bg-color)}@media (max-width:767px){.dir-card{margin:0;border:0;border-bottom:1px solid var(--border-color);border-radius:0}.dir-list{grid-template-columns:1fr}.dir-card+.file-card{margin-top:0}}.explorer-content .file-card .file-content.markdown-body{max-width:920px;margin:0;min-height:0;padding:28px 40px 48px;background:var(--paper-card)!important;font-family:var(--font-sans);font-size:15px;line-height:1.65}.explorer-content .markdown-body h1,.explorer-content .markdown-body h2{font-family:var(--font-sans);font-weight:600;letter-spacing:-.01em;border-bottom:1px solid var(--border-color);padding-bottom:.3em}.explorer-content .markdown-body h1{font-size:28px;margin-top:0}.explorer-content .markdown-body h2{font-size:21px;margin-top:32px}.explorer-content .markdown-body h3{font-size:17px;font-weight:600;margin-top:24px}.explorer-content .markdown-body h4{font-size:15px;font-weight:600}.explorer-content .markdown-body img{border-radius:var(--radius-control)}.explorer-content .markdown-body p>a:only-child>img,.explorer-content .markdown-body p>img:only-child{border:1px solid var(--border-color)}.explorer-content .markdown-body code,.explorer-content .markdown-body pre{font-family:var(--font-mono);font-size:13px}.explorer-content .markdown-body pre{background:var(--paper-bg-alt)!important;border:1px solid var(--border-soft);border-radius:var(--radius-control);padding:14px 16px;line-height:1.55}.explorer-content .markdown-body :not(pre)>code{background:var(--hover-bg-color);border-radius:4px;padding:.15em .4em;font-size:.86em}@media (max-width:767px){.explorer-content .file-card .file-content.markdown-body{padding:20px 16px 72px;font-size:15px}.explorer-content .markdown-body h1{font-size:24px}}.ace_editor.ace-chrome,.ace_editor.ace-nord-dark{background:var(--paper-card)!important;color:var(--color)!important}.ace_editor.ace-chrome .ace_gutter,.ace_editor.ace-nord-dark .ace_gutter{background:color-mix(in srgb,var(--paper-bg-alt) 55%,var(--paper-card))!important;color:var(--ink-muted)!important;border-right:1px solid var(--border-soft)}.ace_editor .ace_gutter-cell{padding-right:14px;padding-left:12px;font-size:12px}@media (max-width:767px){.ace_editor .ace_gutter-cell{padding-right:8px;padding-left:6px}}.ace_editor .ace_gutter-active-line{background:0 0!important}.ace_editor .ace_indent-guide{background:0 0!important;box-shadow:inset 1px 0 0 var(--border-soft)}.ace_editor .ace_fold-widget{opacity:0;transition:opacity .15s}.ace_editor .ace_fold-widget.ace_closed,.ace_editor .ace_gutter:hover .ace_fold-widget{opacity:.7}.ace_editor .ace_marker-layer .ace_selection{background:var(--accent-soft)!important}.ace-chrome .ace_keyword,.ace-chrome .ace_storage{color:#b4362a}.ace-chrome .ace_string{color:#2b5a3e}.ace-chrome .ace_constant.ace_language,.ace-chrome .ace_constant.ace_numeric,.ace-chrome .ace_support.ace_constant{color:#1f6a64}.ace-chrome .ace_comment{color:#7a746a;font-style:italic}.ace-chrome .ace_entity.ace_name.ace_function,.ace-chrome .ace_support.ace_function{color:#6b45b8}.ace-chrome .ace_variable.ace_parameter{color:#8a5a12;font-style:normal}.ace-chrome .ace_entity.ace_name.ace_tag,.ace-chrome .ace_meta.ace_tag{color:#116329}.ace-chrome .ace_entity.ace_other.ace_attribute-name{color:#0550ae}.ace-chrome .ace_identifier,.ace-chrome .ace_paren,.ace-chrome .ace_punctuation,.ace-chrome .ace_variable{color:var(--color)}.ace-nord-dark .ace_keyword,.ace-nord-dark .ace_storage{color:#f2897d}.ace-nord-dark .ace_string{color:#9dd3a8}.ace-nord-dark .ace_constant.ace_language,.ace-nord-dark .ace_constant.ace_numeric,.ace-nord-dark .ace_support.ace_constant{color:#7cc7be}.ace-nord-dark .ace_comment{color:#9a948a;font-style:italic}.ace-nord-dark .ace_entity.ace_name.ace_function,.ace-nord-dark .ace_support.ace_function{color:#c9a8f5}.ace-nord-dark .ace_variable.ace_parameter{color:#e0a94a;font-style:normal}.ace-nord-dark .ace_entity.ace_name.ace_tag,.ace-nord-dark .ace_meta.ace_tag{color:#7ee787}.ace-nord-dark .ace_entity.ace_other.ace_attribute-name{color:#79c0ff}.ace-nord-dark .ace_identifier,.ace-nord-dark .ace_paren,.ace-nord-dark .ace_punctuation,.ace-nord-dark .ace_variable{color:var(--color)}.badge{font-size:75%;font-weight:500;padding:3px 8px;border-radius:10px;vertical-align:middle}.warning-feedback{width:100%;margin-top:.25rem;font-size:80%;color:#dc8e35}.dropdown-item:focus,.dropdown-item:hover{background-color:var(--hover-bg-color)}.card-header .btn,.card-header .btn:focus,.card-header .btn:hover{border:none!important;background:0 0!important}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:var(--button-height);padding:7px var(--button-padding-x);font:500 var(--button-font-size)/var(--button-line-height) var(--font-sans);vertical-align:middle;max-width:100%;transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease;color:var(--btn-color);border:1px solid var(--border-color);background:var(--paper-card);border-radius:var(--radius-button);white-space:nowrap}.btn:active,.btn:hover{color:var(--btn-color);border:1px solid var(--border-color);background-color:var(--hover-bg-color);box-shadow:none!important}.btn.btn-primary{color:var(--primary-color);background-color:var(--primary-bg)}.btn.btn-primary:active,.btn.btn-primary:hover{color:var(--primary-active-color);background-color:var(--primary-hover-bg)!important}.btn::placeholder{color:var(--color);opacity:1}.white_border{border:1px solid #fff;color:#fff}.leftCol{display:flex;position:relative;flex:0 0 auto;width:100%;z-index:9;flex-direction:column}.card,.leftCol,.sidePanel{background-color:var(--sidebar-bg-color);border:1px solid var(--border-color)}@media (min-width:768px){.leftCol{flex:0 0 var(--sidebar-w,280px);width:var(--sidebar-w,280px);height:100%}}.leftCol-search{padding:10px 12px 0}.tree-search-box{display:flex;align-items:center;gap:8px;background:var(--canvas-bg-color);border:1px solid var(--border-color);border-radius:8px;padding:7px 10px;transition:border-color .15s}.tree-search-box:focus-within{border-color:var(--ink-muted)}.tree-search-icon{font-size:12px;color:var(--ink-muted);flex-shrink:0}.tree-search-input{flex:1;border:none;outline:0;background:0 0;font-family:var(--font-sans);font-size:13px;color:var(--color);min-width:0}.tree-search-input::placeholder{color:var(--ink-muted)}.tree-search-kbd{font-family:var(--font-sans);font-size:11px;color:var(--ink-muted);background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:4px;padding:1px 6px;line-height:1.4;flex-shrink:0}.tree-search-clear{background:0 0;border:none;color:var(--ink-muted);font-size:16px;cursor:pointer;padding:0 2px;line-height:1}.tree-search-clear:hover{color:var(--color)}.leftCol-project-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 4px;gap:8px}.project-name{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--color);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.project-file-count{font-family:var(--font-sans);font-size:12px;color:var(--ink-muted);white-space:nowrap;flex-shrink:0;margin-left:auto}.files{position:relative;display:block;overflow:auto;height:100%;padding:4px 6px}.files ul{list-style:none;margin:0;padding:0}.files li ul{padding-left:8px;margin-left:7px;border-left:1px solid var(--border-color)}.files .folder>ul{display:none}.files .folder.open>ul{display:block}.files .file,.files .folder{position:relative;color:var(--ink-soft);cursor:pointer;word-break:break-all}.files a{display:flex;align-items:center;gap:5px;min-height:26px;padding:2px 8px 2px 4px;font-family:var(--font-sans);font-size:13px;color:var(--ink-soft);border-radius:var(--radius-control);text-decoration:none;line-height:1.5;position:relative}.files a:hover{background:var(--hover-bg-color);color:var(--color);text-decoration:none}.files:focus{outline:0}.files a.tree-focused{background:var(--hover-bg-color);color:var(--color)}.files .file.active>a::before{content:"";position:absolute;left:0;top:4px;bottom:4px;width:2px;border-radius:0 2px 2px 0;background:var(--accent)}.files .file.active>a{border-radius:0 var(--radius-control) var(--radius-control) 0;background:rgba(128,128,128,.12);color:var(--color);font-weight:500}.files .file.active>a .tree-icon-file{color:var(--color)}.files .file.active>a .tree-count{color:var(--ink-muted)}.tree-icon-file,.tree-icon-folder{display:inline-block;flex-shrink:0}.tree-icon-folder{width:14px;height:12px;color:var(--ink-muted);opacity:.75}.tree-icon-folder svg{display:block;width:100%;height:100%;fill:currentColor;fill-opacity:0;stroke:currentColor;stroke-width:1.2;stroke-linejoin:round}.files .folder.open>a .tree-icon-folder{color:var(--ink-muted);opacity:1}.files .folder.open>a .tree-icon-folder svg{fill-opacity:.12}.tree-icon-file{width:16px;font-size:14px;text-align:center;color:var(--ink-muted)}.tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:12px;height:14px;flex-shrink:0;opacity:.8}.tree-toggle::after{content:"";display:block;width:6px;height:6px;border-right:1.5px solid var(--ink-muted);border-bottom:1.5px solid var(--ink-muted);transform:translateX(-1px) rotate(-45deg);transition:transform .12s ease}.files .folder.open>a .tree-toggle::after{transform:translateY(-2px) rotate(45deg)}.tree-spacer{display:inline-block;width:12px;flex-shrink:0}.tree-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.files a:not(:hover):not(.tree-focused) .tree-count{opacity:0}.tree-count{transition:opacity .12s;font-family:var(--font-sans);font-size:12px;font-variant-numeric:tabular-nums;color:var(--ink-muted);flex-shrink:0;margin-left:auto;padding-left:8px;opacity:.7}.files .folder>a{color:var(--color)}.files .file::before,.files .folder::before{display:none}.files .folder>a::after{display:none}.tree-search-path{color:var(--ink-muted);font-size:11px}.tree-search-empty{padding:16px 12px;color:var(--ink-muted);font-size:13px;font-family:var(--font-sans);text-align:center}.leftCol{background:var(--paper-bg-alt,#f3f1ec);border:0;border-right:1px solid var(--border-color)}.dark-mode .leftCol{background:var(--paper-bg-alt)}.dark-mode .explorer-content .markdown-body img{filter:brightness(.8)}.dark-mode .hero-mock-tree{background:#1f1d1a}.dark-mode .hero-mock-row.is-active{background:rgba(255,255,255,.1)}.dark-mode .files .file.active>a{background:rgba(255,255,255,.1)}.status-bar{background:var(--canvas-bg-color);border-bottom:1px solid var(--border-color);padding:8px 20px;min-height:48px}.paths .breadcrumb-item,.paths .breadcrumb-item a{font-family:var(--font-sans);font-size:14px;color:var(--ink-muted);letter-spacing:0}.paths .breadcrumb-root a{color:var(--link-color)}.paths .breadcrumb-root a:hover{text-decoration:underline;text-underline-offset:3px}.paths .breadcrumb-item.active,.paths .breadcrumb-item:last-child{color:var(--color);font-family:var(--font-sans);font-size:14px;font-weight:600;letter-spacing:0}.status-files-btn{display:none!important}.leftCol-resizer{display:none}@media (min-width:992px){.leftCol-resizer{display:block;position:absolute;top:0;bottom:0;left:calc(var(--sidebar-w,280px) - 3px);width:6px;cursor:col-resize;z-index:10}.leftCol-resizer:hover,body.is-resizing .leftCol-resizer{background:color-mix(in srgb,var(--accent) 35%,transparent)}body.is-resizing{cursor:col-resize;user-select:none}}.leftCol-hide{display:none;align-items:center;justify-content:center;width:26px;height:26px;margin-right:-6px;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-muted);cursor:pointer}.leftCol-hide:hover{background:var(--hover-bg-color);color:var(--color)}.leftCol-hide .ui-icon{width:16px;height:16px}@media (min-width:992px){.leftCol-hide{display:inline-flex}.explorer-page.files-hidden .leftCol{display:none!important}.explorer-page.files-hidden .status-files-btn{display:inline-flex!important}}@media (max-width:991px){.status-files-btn{display:inline-flex!important;flex-shrink:0}.explorer-page .sidebar-toggle{display:none!important}}@media (max-width:767px){.status-bar .btn,.status-bar .btn.btn-icon-only{min-height:40px}.status-bar .btn.btn-icon-only{width:40px}}.dark-mode .status-bar .btn{background:var(--paper-card)}.btn-seg{display:inline-flex}.btn-seg>.btn{border-radius:0;margin:0}.btn-seg>.btn+.btn{margin-left:-1px}.btn-seg>.btn:first-child{border-radius:var(--radius-control) 0 0 var(--radius-control)}.btn-seg>.btn:last-child{border-radius:0 var(--radius-control) var(--radius-control) 0}.btn-seg>.btn:focus-visible,.btn-seg>.btn:hover{position:relative;z-index:1}.status-bar .btn{height:32px;min-height:32px;display:inline-flex;align-items:center;gap:6px}.status-bar .btn.btn-icon-only{padding-left:0;padding-right:0;width:32px;justify-content:center}.status-files-btn{gap:6px}@media (max-width:991px){.status-files-btn{height:40px!important;min-height:40px!important;padding:0 12px;border-radius:var(--radius-control)}}@media (max-width:767px){.file-card.is-code.no-wrap{position:relative}.file-card.is-code.no-wrap::after{content:"";position:absolute;top:46px;right:0;bottom:0;width:44px;pointer-events:none;background:linear-gradient(to right,transparent,var(--paper-card) 85%);z-index:20}.file-card-head .btn-goto{display:none!important}}@media (max-width:575px){.status-bar .btn-zip{display:none!important}.navbar .nav-support{display:none!important}.navbar .nav-signin-m{margin-left:auto}}.ace_editor .ace_gutter-cell{cursor:pointer}.ace_editor .ace_gutter-cell:hover{color:var(--color)}.status-bar .file-meta{flex:0 0 auto;margin-right:auto;font-size:12px;color:var(--ink-muted);white-space:nowrap;font-variant-numeric:tabular-nums}@media (max-width:575px){.status-bar .file-meta{display:none}}.paths .breadcrumb-item+.breadcrumb-item::before{content:'/';color:var(--ink-faint);padding:0 6px 0 3px}.last-update{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);padding:8px 10px;border-top:1px solid var(--border-color);letter-spacing:.02em}.ace_editor{height:100%}.ace_scroller{cursor:default}.status-bar{display:flex;align-items:center;gap:12px;background-color:var(--canvas-bg-color);padding:10px 14px;margin:0;border-bottom:1px solid var(--border-color);border-radius:0;flex-shrink:0}.status-bar-actions{display:flex;align-items:center;margin-left:auto;gap:10px;flex-shrink:0}.status-bar-actions .btn{display:inline-flex;align-items:center;gap:6px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);border-radius:6px;padding:4px 10px;font-size:13px;min-height:30px}.status-bar-actions .btn:hover{background:var(--hover-bg-color);color:var(--color)}.status-bar-actions .btn.btn-active{border-color:currentColor;font-weight:600}.paths{flex:0 1 auto;min-width:0;padding:0;margin:0;background-color:initial;border:none;border-radius:0;align-items:center;overflow-x:auto;white-space:nowrap;flex-wrap:nowrap}.paths::-webkit-scrollbar{display:none}.paths a{color:var(--color)}.paths .breadcrumb-item{padding-left:3px}.paths .breadcrumb-item::before{padding-right:3px}.content{height:100%;overflow:scroll;padding-top:42px;background-color:var(--main-bg-color);text-align:center}.image-content{max-width:100%;max-height:100%;text-align:center;padding:4px}.pdf-viewer{display:flex;flex-direction:column;height:100%;min-height:0;background-color:var(--main-bg-color)}.pdf-toolbar{display:flex;align-items:center;gap:4px;flex:0 0 auto;padding:4px 8px;border-bottom:1px solid var(--border-color,rgba(0,0,0,.12))}.pdf-toolbar-group{display:inline-flex;align-items:center;gap:4px}.pdf-toolbar-right{margin-left:auto}.pdf-toolbar-btn{border:0;background:0 0;color:inherit;padding:4px 8px;border-radius:4px;font-size:13px;line-height:1.2;cursor:pointer}.pdf-toolbar-btn:hover:not(:disabled){background-color:rgba(128,128,128,.18)}.pdf-toolbar-btn:disabled{opacity:.4;cursor:default}.pdf-toolbar-zoom{min-width:52px;font-variant-numeric:tabular-nums}.pdf-toolbar-page{width:3.5em;text-align:right;padding:2px 4px;font-size:13px;color:inherit;background:0 0;border:1px solid var(--border-color,rgba(0,0,0,.2));border-radius:4px}.pdf-toolbar-count{font-size:13px;opacity:.75;font-variant-numeric:tabular-nums}.pdf-pages{flex:1 1 auto;min-height:0;overflow:auto;padding:8px 0}.pdf-viewer-page{margin:0 auto 12px}.pdf-viewer-page canvas{display:block;box-shadow:0 1px 4px rgba(0,0,0,.25)}.pdf-viewer-error{padding:24px;text-align:center;font-weight:700}.html-doc{display:block;height:100%}.html-doc-frame{display:block;width:100%;height:100%;border:0;background-color:#fff}.file-error{font-weight:700;font-size:50px;text-align:center}pdfviewer{display:block;padding:15px}pdfpageviewer{display:block;width:100%;margin-bottom:10px}notebook{display:block;text-align:left;padding:15px}@media (min-width:768px){notebook{padding-left:100px}}.nb-output td,.nb-output th{border:1px solid var(--border-color)!important}.paper-faq{max-width:1140px;margin:0 auto;padding:48px 32px 80px;font-family:var(--font-sans)}.paper-faq-hero{padding-bottom:40px;border-bottom:1px solid var(--border-color);margin-bottom:36px}.paper-faq-eyebrow{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);letter-spacing:.14em;text-transform:uppercase;margin-bottom:32px}.paper-faq-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.6rem,5.5vw,4.6rem);line-height:1.03;letter-spacing:-.02em;color:var(--color);margin:0 0 22px;max-width:14em}.paper-faq-title em{font-style:italic;color:var(--color)}.paper-faq-lede{color:var(--ink-soft);font-size:18px;line-height:1.55;max-width:38em;margin:0}.paper-faq-lede a{color:var(--link-color);border-bottom:1px solid currentColor}.paper-faq-lede a:hover{color:var(--accent);border-bottom-color:var(--accent)}.paper-faq-body{display:grid;grid-template-columns:220px 1fr;gap:60px;align-items:start}.paper-faq-toc{position:sticky;top:32px;font-size:14px}.paper-faq-toc-head{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);letter-spacing:.14em;text-transform:uppercase;padding-bottom:10px;margin-bottom:6px;border-bottom:1px solid var(--border-color)}.paper-faq-toc nav{display:flex;flex-direction:column}.paper-faq-toc nav a{color:var(--ink-soft);padding:8px 0 8px 14px;border-left:2px solid transparent;text-decoration:none;margin-left:0}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover{color:var(--color);border-left-color:var(--color)}.faq-section{min-width:0}.faq-section>div{width:100%}.faq-section h4.faq-cat{font-family:var(--font-serif);font-weight:400;font-size:clamp(1.7rem,2.6vw,2.2rem);letter-spacing:-.015em;color:var(--color);margin:0 0 6px;padding-bottom:14px;border-bottom:1px solid var(--border-color)}.faq-section h4.faq-cat i{display:none}.faq-section .panel-group{margin-bottom:56px}.faq-section .panel-default .panel-title a{display:block;position:relative;padding:22px 0 22px 42px;font-family:var(--font-serif);font-weight:400;font-size:20px;line-height:1.35;letter-spacing:-.005em;color:var(--color);text-decoration:none;cursor:pointer}.faq-section .panel-default .panel-title a:hover{color:var(--accent)}.faq-section .panel-heading{background:0 0;padding:0!important;border:none}.faq-section .panel-default{background:0 0;border:none;border-bottom:1px solid var(--border-soft);border-radius:0;margin-bottom:0!important;box-shadow:none}.faq-section .panel-default .panel-title{margin-bottom:0}.faq-section .panel-default .panel-title a:after{content:"−";position:absolute;left:0;right:auto;top:22px;transform:none;font-family:var(--font-mono);font-size:18px;line-height:1.35;color:var(--ink-muted);float:none;margin:0;font-weight:400}.faq-section .panel-default .panel-title a.collapsed:after{content:"+"}.faq-section .panel-default .panel-body{padding:0 0 22px 42px!important;color:var(--ink-soft);font-size:15px;line-height:1.65;border-top:none;max-width:68ch}.table{color:var(--color)}.dropdown-divider,.table td,.table th,hr{border-color:var(--border-color)}.table.repositories td{word-break:break-word}loc{display:block;position:relative;width:100%;height:5px}loc .lang{float:left;height:100%}.rounded-circle.fa{display:inline-block;font-size:75px;height:140px;width:140px;border:4px solid;line-height:140px;text-align:center}.featurette-divider{margin:4.5rem 0;border-color:var(--border-color)}.featurette-heading{font-family:var(--font-serif);font-weight:400;font-size:2rem;line-height:1.15;letter-spacing:-.02em;margin-bottom:14px;color:var(--color)}.dark-mode .shadow{box-shadow:none}.dark-mode .overflow-auto::-webkit-scrollbar{width:10px}.dark-mode .overflow-auto::-webkit-scrollbar-track{-webkit-box-shadow:none;background:inherit;border-radius:0}.dark-mode .overflow-auto::-webkit-scrollbar-thumb{border-radius:10px;border:1px solid var(--border-color);background-color:var(--hover-bg-color)}.dark-mode .rgba-gradient{background:linear-gradient(135deg,#2a2d4a,#151728)}.section-number{padding-right:10px;font-weight:700;font-style:italic}.org-subscript-child{font-size:85%}.highlighted-line{position:absolute;background:rgba(100,200,100,.5);z-index:20}code,pre{font-family:"Fira Code","Courier New",Courier,monospace;line-height:1.1;color:var(--color)}.diff-file,.diff-index,.diff-lines{background:rgba(172,172,172,.5)}.diff-add{background:rgba(100,200,100,.5)}.diff-remove{background:rgba(200,100,100,.5)}.dashboard-title{font-size:1.375rem;font-weight:600;letter-spacing:-.01em}.quota-row{display:flex;gap:16px;margin-bottom:16px;padding:0}.quota-item{flex:1;min-width:0}.quota-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px}.quota-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px;opacity:.7}.quota-value{font-size:.8125rem;font-weight:500;font-variant-numeric:tabular-nums}.quota-progress{height:6px;border-radius:3px;background:var(--hover-bg-color)}.quota-progress .progress-bar{border-radius:3px}.filter-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;font-size:.8125rem;font-weight:500;background:var(--primary-bg);color:var(--primary-color);border-radius:12px;line-height:1.4}.filter-chip-close{display:inline-flex;align-items:center;justify-content:center;background:0 0;border:none;color:inherit;opacity:.7;cursor:pointer;padding:0;min-width:24px;min-height:24px;margin:-6px -6px -6px 0;font-size:1rem;line-height:1;border-radius:999px}.filter-chip-close:hover{opacity:1}.repo-list{list-style:none;padding:0;margin:0}.repo-list-item{display:flex;align-items:flex-start;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--border-color);gap:12px}.repo-list-item:first-child{border-top:1px solid var(--border-color)}.repo-list-item.repo-inactive{opacity:.6}.repo-list-item-content{flex:1;min-width:0}.repo-list-item-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px}.repo-name{font-family:var(--font-mono);font-size:14px;font-weight:500;color:var(--color);word-break:break-word}.repo-name:hover{color:var(--accent)}.repo-source{font-size:.875rem;opacity:.8;display:flex;flex-wrap:wrap;align-items:center;gap:6px}.repo-date{opacity:.7}.repo-meta{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;font-size:.8125rem;opacity:.7}.repo-meta span{display:inline-flex;align-items:center;gap:4px;white-space:nowrap}.repo-list-item-actions{flex-shrink:0;padding-top:2px;margin-left:14px}.repo-list-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:48px 20px;opacity:.5;font-size:.9375rem;list-style:none;border-bottom:1px solid var(--border-color);border-top:1px solid var(--border-color)}.repo-list-empty i{font-size:1.75rem}.type-badge{display:inline-flex;align-items:center;gap:3px;font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px;padding:2px 8px;border-radius:4px;white-space:nowrap}.type-badge{font-family:var(--font-mono);letter-spacing:.06em;padding:2px 8px;border-radius:999px;border:1px solid var(--border-color);background:var(--paper-bg-alt,transparent)}.type-badge.type-pr,.type-badge.type-repo{background:var(--paper-bg-alt);color:var(--ink-muted);border-color:var(--border-color)}.dark-mode .type-badge.type-pr,.dark-mode .type-badge.type-repo{background:rgba(255,255,255,.06);color:var(--ink-muted);border-color:var(--border-color)}.type-badge.type-coauthor{background:rgba(99,102,241,.12);color:#4f46e5;border-color:rgba(99,102,241,.35);margin-left:6px}.dark-mode .type-badge.type-coauthor{background:rgba(129,140,248,.18);color:#7cc7be;border-color:rgba(165,180,252,.45)}.btn-group .btn:not(.btn-primary){background:var(--hover-bg-color)}.anonymize-page{display:flex;flex-direction:column;min-height:0}.anonymize-landing{flex:1 1 0;min-height:min-content;display:flex;align-items:center;justify-content:center;padding:20px}.anonymize-landing-inner{width:100%;max-width:600px}.anonymize-landing-inner h2{font-weight:700}.anonymize-workspace{display:flex;flex-direction:column;flex:1 1 0;min-height:0;background:var(--canvas-bg-color)}.anonymize-topbar{background:var(--paper-card);border-bottom:1px solid var(--border-color);padding:18px 28px;flex:0 0 auto}.anonymize-topbar-inner{max-width:1400px;margin:0 auto}.anonymize-topbar-head{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.anonymize-topbar-title{margin:4px 0 0;font-size:clamp(1.8rem,3vw,2.4rem)}.anonymize-split{flex:1 1 auto;display:flex;min-height:0}.anonymize-form-col{flex:0 0 auto;width:100%;min-width:0;background:var(--canvas-bg-color);border-right:1px solid var(--border-color);overflow-y:auto}.anonymize-form-col .paper-settings-main{max-width:720px;padding:28px 32px 40px;margin:0}.anonymize-form-col .paper-settings-section:first-child{padding-top:0}.anonymize-form-col .anonymize-submit-bar{position:sticky;bottom:0;background:var(--canvas-bg-color);padding:14px 0 0;border-top:1px solid var(--border-color);margin-top:20px}.anonymize-form-col .anonymize-submit-bar .btn{width:100%;padding:12px 16px;font-size:15px}.anonymize-preview-col{flex:1 1 auto;min-width:0;overflow:auto;background:var(--paper-bg-alt);padding:28px 32px}.anonymize-preview-head{display:flex;align-items:baseline;gap:12px;padding-bottom:12px;border-bottom:1px solid var(--border-color);margin-bottom:20px}.anonymize-preview-head .paper-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.anonymize-preview-sub{color:var(--ink-soft);font-size:13px}.anonymize-preview-body{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:28px 32px}.form-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}.form-grid-3{display:grid;grid-template-columns:2fr 1fr 1fr;gap:16px}@media (max-width:700px){.form-grid-2,.form-grid-3{grid-template-columns:1fr}}.paper-optional{font-family:var(--font-sans);font-weight:400;color:var(--ink-muted);text-transform:none;letter-spacing:0;font-size:11px;margin-left:6px}.paper-plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.paper-plan-card{position:relative;display:block;padding:18px 20px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease;margin:0}.paper-plan-card input[type=radio]{position:absolute;opacity:0;pointer-events:none}.paper-plan-card.selected{border-color:var(--color);box-shadow:0 0 0 2px var(--color) inset}.paper-plan-head{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.paper-plan-price{font-family:var(--font-serif);font-size:2rem;color:var(--color);margin:6px 0 4px}.paper-plan-price .paper-plan-per{font-family:var(--font-sans);font-size:12px;color:var(--ink-soft)}.paper-plan-desc{font-size:13px;color:var(--ink-soft)}.paper-plan-desc ul{padding-left:18px;margin:0}.paper-settings-copy{color:var(--ink-soft);font-size:13px;margin:0 0 14px}.anonymize-section-title{font-size:.875rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px;margin:18px 0 8px 0;padding-bottom:6px;border-bottom:1px solid var(--border-color);opacity:.7}.anonymize-section-title i{margin-right:4px}.anonymize-submit-bar{padding:12px 0 4px 0}.anonymize-submit-bar .btn{font-size:1rem;padding:10px 20px;font-weight:600}.anonymize-page .form-check{padding-top:4px;padding-bottom:4px}.anonymize-page .form-check-label{font-weight:500}.anonymize-preview-col{min-height:0;min-width:0}.pr-diff pre{overflow-x:auto;max-width:100%}@media (min-width:768px){.anonymize-form-col{height:100%;width:520px;flex:0 0 520px}.anonymize-preview-col{height:100%;flex:1}}@media (max-width:767px){.anonymize-workspace{flex:0 0 auto;min-height:0}.anonymize-topbar{padding:14px 16px}.anonymize-topbar-title{font-size:1.5rem;margin-top:2px}.anonymize-split{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}.anonymize-form-col{width:100%!important;flex:0 0 auto!important;height:auto!important;border-right:none;border-bottom:1px solid var(--border-color);overflow:visible}.anonymize-form-col .paper-settings-main{padding:18px 16px 24px;max-width:none}.anonymize-form-col .anonymize-submit-bar{position:sticky;bottom:0;margin:20px -16px 0;padding:12px 16px;background:var(--canvas-bg-color);border-top:1px solid var(--border-color);z-index:10}.anonymize-form-col .anonymize-submit-bar .btn{width:100%;padding:12px 16px;font-size:15px}.anonymize-preview-col{width:100%;flex:0 0 auto;height:auto!important;max-height:none;padding:18px 16px 24px;overflow:visible}.anonymize-preview-body{padding:16px;border-radius:8px}.anonymize-preview-body .pr-diff pre,.anonymize-preview-body pre{overflow-x:auto;max-width:100%}.anonymize-landing-inner{padding:16px}}@media (min-width:1280px){.anonymize-form-col{width:620px;flex:0 0 620px}}@media (max-width:767px){.btn{min-height:38px;padding:6px 14px;font-size:14px}.btn-sm{min-height:34px;padding:5px 10px}.navbar .nav-link{padding:8px 12px}.navbar .nav-link .fa,.navbar .nav-link .fab{font-size:24px;line-height:32px}.container.page{padding-left:15px;padding-right:15px;max-width:100%}h1{font-size:1.5rem}h2{font-size:1.375rem}h3{font-size:1.125rem}.repo-list-item{flex-direction:column}.repo-list-item-actions{align-self:flex-start}.quota-row{flex-direction:column;gap:10px;padding:10px 12px}.dropdown-menu{position:absolute;right:0;left:auto}.form-group{margin-bottom:12px}.form-control{font-size:16px}.status-bar{gap:6px;padding:8px 10px}.status-bar .btn{font-size:12px;padding:4px 10px;min-height:32px}.status-bar .paths{font-size:12px;overflow-x:auto;flex-wrap:nowrap;white-space:nowrap;-webkit-overflow-scrolling:touch}.status-bar .paths::-webkit-scrollbar{display:none}.status-bar-actions{flex-shrink:0;gap:4px}#home .display-4{font-size:1.75rem}#home h4.subtext-header{font-size:1rem}.progress:not(.quota-progress){min-width:120px!important}.badge{font-size:70%}.col-2.input-group{width:100%!important;max-width:100%;flex:0 0 100%}.color-text-secondary span{display:inline-block;margin-bottom:2px}.anonymize-landing{align-items:flex-start;padding-top:60px}.anonymize-page .container-fluid>.row{height:auto!important;min-height:100%}.anonymize-form-col{width:100%!important;flex:0 0 100%!important;border-right:none;border-top:1px solid var(--border-color)}.anonymize-preview-col{height:auto!important;max-height:none}.anonymize-section-title{font-size:.8125rem;margin:12px 0 8px 0}.anonymize-submit-bar{position:sticky;bottom:0;background:var(--sidebar-bg-color);padding:10px 0;margin:0 -12px;padding-left:12px;padding-right:12px;border-top:1px solid var(--border-color);z-index:10}.anonymize-submit-bar .btn{font-size:1rem;padding:12px 20px}.anonymize-page .form-check{padding-top:6px;padding-bottom:6px;min-height:36px}.anonymize-page .form-check-label{font-size:.9375rem}.anonymize-page .card-body{padding:12px}.pr-title{font-size:1.125rem}.pr-diff pre{font-size:.75rem}.pr-comment .mb-1 h5{font-size:1rem}.nav-tabs .nav-link{padding:8px 12px;font-size:.875rem}}@media (min-width:768px) and (max-width:991px){.container.page{max-width:100%;padding-left:15px;padding-right:15px}.leftCol{flex:0 0 250px;width:250px}}.explorer-page{display:flex;height:100%;width:100%;background:var(--canvas-bg-color);position:relative}.explorer-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden;background:var(--canvas-bg-color)}.explorer-content{flex:1 1 auto;min-height:0;width:100%;overflow:auto}.leftCol-close,.leftCol-head{display:none}.leftCol-body{flex:1 1 auto;min-height:0;overflow:auto}.leftCol-foot{padding:8px 12px;border-top:1px solid var(--border-color);background:var(--paper-bg-alt)}.leftCol-foot .last-update{display:block;border-top:none;padding:0;font-family:var(--font-sans);font-size:13px;font-weight:400;letter-spacing:0;line-height:1.5;color:var(--ink-muted)}.leftCol-foot{padding:10px 14px}.leftCol-backdrop{display:none}.paper-inline-warning{display:flex;align-items:flex-start;gap:8px;margin:8px;padding:10px 12px;background:rgba(138,107,30,.08);border:1px solid rgba(138,107,30,.35);border-radius:8px;color:var(--color);font-size:13px;line-height:1.4}.paper-inline-warning i{color:var(--status-progress);margin-top:2px;flex-shrink:0}.dark-mode .paper-inline-warning i{color:var(--status-progress)}@media (min-width:768px){.leftCol-body{padding:8px 4px}}.sidebar-toggle{display:none;align-items:center;gap:8px;position:fixed;bottom:18px;right:18px;z-index:1100;padding:10px 14px;background:var(--color);color:var(--canvas-bg-color);border:1px solid var(--color);border-radius:999px;font-family:var(--font-sans);font-size:13px;font-weight:500;box-shadow:0 6px 20px rgba(0,0,0,.15);cursor:pointer;outline:0}.sidebar-toggle:hover{background:var(--primary-hover-bg)}.sidebar-toggle:focus{outline:0}.sidebar-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media (max-width:991px){.sidebar-toggle{display:inline-flex}.leftCol{position:fixed;top:0;left:0;bottom:0;width:min(86vw,340px);max-width:340px;z-index:1200;border:none;border-right:1px solid var(--border-color);transform:translateX(0);transition:transform .22s ease;box-shadow:4px 0 24px rgba(0,0,0,.18);background:var(--sidebar-bg-color)}.leftCol.collapsed{transform:translateX(-105%);box-shadow:none;pointer-events:none}.leftCol-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt)}.leftCol-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.leftCol-close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;background:0 0;border:1px solid var(--border-color);border-radius:8px;color:var(--ink-muted);cursor:pointer}.leftCol-close:hover{color:var(--color);border-color:var(--color)}.leftCol-foot{display:block}.leftCol-body{padding:4px 0}.leftCol-backdrop{display:block;position:fixed;inset:0;background:rgba(0,0,0,.32);z-index:1150;backdrop-filter:blur(2px)}.dark-mode .leftCol-backdrop{background:rgba(0,0,0,.5)}}.admin-nav{display:flex;gap:4px;padding:4px;margin:8px 0 20px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap}.admin-nav a{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:7px;color:var(--ink-muted);text-decoration:none;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;transition:background-color .15s,color .15s;white-space:nowrap}.admin-nav a:hover{background:var(--paper-bg-alt);color:var(--color)}.admin-nav a.active{background:var(--color);color:var(--paper-card)}.admin-nav a i{font-size:12px;width:14px;text-align:center}@media (max-width:767px){.admin-nav{flex-direction:column}.admin-nav a{justify-content:flex-start}}.admin-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:16px}.admin-stat-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px}.admin-stat-card .stat-value{font-family:var(--font-serif);font-size:1.375rem;font-weight:400;line-height:1.1;color:var(--color)}.admin-stat-card .stat-label{font-family:var(--font-mono);font-size:10px;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;margin-top:4px}.admin-toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:16px 0 12px}.admin-toolbar .form-control{max-width:320px}@media (max-width:767px){.admin-toolbar .form-control{max-width:100%;width:100%}}.pagination-compact{display:inline-flex;align-items:center;gap:6px;flex-wrap:nowrap;font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}.pagination-compact input{width:60px;text-align:center;flex-shrink:0}.pagination-compact .btn{padding:4px 10px;min-height:34px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);flex-shrink:0}.pagination-compact>span{white-space:nowrap}.admin-list-item{display:flex;align-items:flex-start;gap:12px;padding:12px 0;border-bottom:1px solid var(--border-color)}.admin-list-item:last-child{border-bottom:none}.admin-list-item .item-main{flex:1;min-width:0}.admin-list-item .item-title{font-size:1rem;font-weight:600;margin-bottom:4px;word-break:break-word}.admin-list-item .item-meta{font-size:.875rem;opacity:.8}.admin-list-item .item-meta span{margin-right:12px;display:inline-block;margin-bottom:2px}.admin-list-item .item-actions{flex-shrink:0}@media (max-width:767px){.admin-list-item{flex-direction:column;gap:8px}.admin-list-item .item-actions{align-self:flex-start}}.status-badge{display:inline-block;font-size:11px;font-weight:600;padding:2px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.3px;vertical-align:middle}.status-badge{font-family:var(--font-sans);font-weight:500;letter-spacing:.02em;border:1px solid transparent;text-transform:none;font-size:12px;padding:1px 9px 2px;border-radius:999px}.status-badge.status-ready{background:#e8f3ec;color:#2f7a44;border-color:#cfe6d6}.status-badge.status-error{background:#fbe7e7;color:#b42318;border-color:#f3c7c7}.status-badge.status-download,.status-badge.status-preparing{background:#e4f0ee;color:#1f6a64;border-color:#d4d8fa}.status-badge.status-expired,.status-badge.status-expiring,.status-badge.status-removed,.status-badge.status-removing{background:var(--paper-bg-alt);color:var(--ink-muted);border-color:var(--border-color)}.status-badge.status-active{background:#e8f3ec;color:#2f7a44;border-color:#cfe6d6}.queue-job-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 18px;margin-bottom:10px}.queue-job-card .job-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;flex-wrap:wrap;gap:8px}.queue-job-card .job-id{font-family:var(--font-mono);font-size:.9375rem;color:var(--color);word-break:break-all;display:flex;align-items:center;gap:10px}.queue-job-card .job-timestamps{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);display:flex;flex-wrap:wrap;gap:14px}.queue-job-card .job-actions{display:flex;gap:8px;margin-top:10px}.queue-job-card .job-actions .btn{font-size:12px;padding:4px 12px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color)}.admin-section-header{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:24px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border-color)}.admin-section-header h2{margin:0;font-family:var(--font-mono);font-size:11px;font-weight:500;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em}.admin-section-header h2 i{margin-right:6px;color:var(--ink-muted)}.admin-section-header .section-count{background:var(--paper-bg-alt);color:var(--ink-muted);border:1px solid var(--border-color);font-family:var(--font-mono);font-size:11px;padding:1px 10px;border-radius:999px}.user-detail-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:22px;margin-bottom:24px}.user-detail-card .user-header{display:flex;align-items:center;gap:16px;margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--border-color);flex-wrap:wrap}.user-detail-card .user-header img{border-radius:50%}.user-detail-card .user-header h2{margin:0;font-family:var(--font-serif);font-weight:400;font-size:1.5rem;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.user-detail-grid{display:grid;grid-template-columns:160px 1fr;gap:10px 20px;font-size:.9375rem}.user-detail-grid .detail-label{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);white-space:nowrap;padding-top:2px}.user-detail-grid .detail-value{word-break:break-all;color:var(--color)}@media (max-width:767px){.user-detail-grid{grid-template-columns:1fr}.user-detail-grid .detail-label{margin-top:8px;border-top:1px solid var(--border-color);padding-top:8px}.user-detail-grid .detail-label:first-child{border-top:none;margin-top:0;padding-top:0}}.paper-empty{min-height:calc(100vh - 60px);display:flex;align-items:center;justify-content:center;padding:40px 24px}.paper-empty-inner{max-width:560px;text-align:left}.paper-empty-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.4rem,6vw,4.4rem);line-height:1.02;letter-spacing:-.025em;margin:18px 0 20px;color:var(--color)}.paper-empty-title em{color:var(--accent);font-style:italic}.paper-empty-lede{font-size:18px;line-height:1.55;color:var(--ink-soft);text-wrap:pretty}.paper-empty-cta{display:flex;gap:10px;margin-top:28px;flex-wrap:wrap}.paper-page{max-width:1100px;margin:0 auto;padding:32px 32px 80px}@media (max-width:991px){.paper-page,.paper-page.container,.paper-page.container-fluid{padding:20px 18px 60px!important}.paper-page .row{margin-left:0;margin-right:0}.paper-page .row>.w-100,.paper-page .row>[class*=col]{padding-left:0;padding-right:0}.navbar.navbar-expand-lg{padding:10px 18px!important}.navbar .navbar-collapse{margin:10px -18px 0;padding:12px 18px 16px;border-top:1px solid var(--border-color);background:var(--header-bg-color)}.navbar .navbar-nav{align-items:stretch!important;gap:2px}.navbar .navbar-nav .nav-item,.navbar .navbar-nav .nav-link{width:100%}.navbar .nav-link{display:flex!important;align-items:center;justify-content:flex-start;margin:0!important;padding:12px 4px!important;border-bottom:1px solid var(--border-color)!important;border-radius:0;font-size:14px;line-height:1.2;gap:0;min-height:44px;box-sizing:border-box}.navbar .nav-link:not(.btn-signin):not(.user-chip){color:var(--color)!important}.navbar .nav-link .fa,.navbar .nav-link .fab,.navbar .nav-link .fas,.navbar .nav-link i{display:inline-flex!important;align-items:center;justify-content:center;width:28px;flex-shrink:0;font-size:16px;line-height:1;margin:0 10px 0 0!important}.navbar .navbar-nav .nav-item:last-child .nav-link,.navbar .navbar-nav>.nav-link:last-child{border-bottom-color:transparent!important}.navbar .nav-link.active{color:var(--color)!important;border-bottom-color:var(--border-color)!important}.navbar .nav-item>.nav-link:not(.btn-signin):not(.user-chip)::before{content:'';display:inline-block;width:28px;margin-right:10px;flex-shrink:0}.navbar .nav-link.nav-icon::before,.navbar .nav-link:has(>i)::before{display:none!important}.navbar .nav-link.nav-icon{width:100%!important;height:auto!important;justify-content:flex-start!important;padding:12px 4px!important}.navbar .nav-link.nav-icon::after{content:attr(title);font-family:var(--font-sans);font-size:14px;color:var(--color)}.navbar .btn-signin{display:inline-flex!important;width:auto!important;align-self:flex-start;justify-content:center;height:40px;padding:0 18px!important;margin:14px 0 0!important;border-radius:8px;border-bottom:none!important}.navbar .btn-signin i{margin:0 8px 0 0!important;width:auto!important}.navbar .user-chip-wrap{width:100%}.navbar .user-chip{display:flex!important;width:100%;margin:0!important;padding:10px 4px!important;background:0 0!important;border:none!important;border-bottom:1px solid var(--border-color)!important;border-radius:0!important;justify-content:flex-start;gap:0;min-height:44px}.navbar .user-chip::before{display:none!important}.navbar .user-chip img{width:28px!important;height:28px!important;margin-right:10px!important;flex-shrink:0}.navbar .user-chip .user-chip-name{font-size:14px;font-weight:500;color:var(--color)!important}.navbar .user-chip.dropdown-toggle::after{margin-left:auto;color:var(--ink-muted)}.navbar .user-chip-wrap .dropdown-menu{position:static!important;float:none;width:100%;margin:0!important;padding:0!important;background:0 0!important;border:none!important;box-shadow:none!important;transform:none!important}.navbar .user-chip-wrap .dropdown-menu .dropdown-item{padding:10px 4px 10px 42px!important;font-size:14px;color:var(--ink-muted)!important;background:0 0!important;border-bottom:1px solid var(--border-color);text-align:left!important}.navbar .user-chip-wrap .dropdown-menu .dropdown-item:last-child{border-bottom:none}.navbar .user-chip-wrap .dropdown-divider{display:none}.navbar-brand{border-right:none!important;padding-right:0!important;margin-right:0!important}.dark-mode .navbar .navbar-collapse{background:var(--header-bg-color);border-top-color:var(--border-color)}.dark-mode .navbar .nav-link{border-bottom-color:var(--border-color)!important}.dark-mode .navbar-toggler .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' d='M4 8h22M4 15h22M4 22h22'/%3E%3C/svg%3E")!important}}.status-pill{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:999px;background:var(--paper-bg-alt);border:1px solid var(--border-color);color:var(--ink-soft);font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase}.paper-progress{position:relative;height:auto;background:0 0;margin:18px 0 8px}.paper-progress::before{content:"";display:block;height:8px;background:var(--paper-bg-alt);border-radius:999px}.paper-progress .paper-progress-bar{position:absolute;top:0;left:0;height:8px;background:var(--color);border-radius:999px;transition:width .4s ease;min-width:4px}.paper-progress .paper-progress-label{display:flex;justify-content:space-between;align-items:center;margin-top:10px;font-family:var(--font-mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted)}.paper-progress .paper-progress-pct{color:var(--color)}.paper-progress.paper-progress-ready .paper-progress-bar{background:var(--color)}.paper-error-card{margin-top:18px;padding:20px 22px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-left:3px solid var(--status-error);border-radius:10px;color:var(--color)}.dark-mode .paper-error-card{border-left-color:var(--status-error)}.paper-ratelimit-card{margin-top:18px;padding:20px 22px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-left:3px solid var(--status-progress);border-radius:10px;color:var(--color)}.dark-mode .paper-ratelimit-card{border-left-color:var(--status-progress)}.paper-ratelimit-head{display:flex;align-items:flex-start;gap:14px}.paper-ratelimit-head>i{font-size:18px;color:var(--status-progress);margin-top:4px;flex-shrink:0}.dark-mode .paper-ratelimit-head>i{color:var(--status-progress)}.paper-ratelimit-card strong{color:var(--color)}.paper-ratelimit-card{text-align:left;border-left-color:var(--status-progress)}.dark-mode .paper-ratelimit-card{border-left-color:var(--status-progress)}.paper-ratelimit-head>.ui-icon{width:18px;height:18px;margin-top:3px;flex-shrink:0;color:var(--status-progress)}.paper-ratelimit-card .paper-error-title{font-family:var(--font-sans);font-size:17px;font-weight:600}.paper-ratelimit-card .paper-error-msg{font-family:var(--font-sans);white-space:normal;margin-left:32px}.paper-ratelimit-card .paper-error-eyebrow{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.status-pill-ratelimit{border-color:var(--status-progress);color:var(--status-progress)}.dark-mode .status-pill-ratelimit{border-color:var(--status-progress);color:var(--status-progress)}.status-ratelimit{background:var(--status-progress)}.dark-mode .status-ratelimit{background:var(--status-progress)}.paper-error-head{display:flex;align-items:flex-start;gap:14px}.paper-error-head>i{font-size:18px;color:var(--status-error);margin-top:4px;flex-shrink:0}.dark-mode .paper-error-head>i{color:var(--status-error)}.paper-error-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}.paper-error-title{font-family:var(--font-serif);font-size:1.375rem;line-height:1.2;margin-top:2px;color:var(--color)}.paper-error-msg{margin:12px 0 0;font-size:14px;line-height:1.55;color:var(--ink-soft);font-family:var(--font-mono);word-break:break-word;white-space:pre-wrap}.paper-error-hints{margin:14px 0 0;padding-left:18px;color:var(--ink-soft);font-size:14px;line-height:1.6}.paper-error-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}.paper-detail-grid{display:grid;grid-template-columns:160px 1fr;gap:10px 20px;margin-top:18px;font-size:.9375rem}.paper-detail-grid .detail-label{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);white-space:nowrap;padding-top:2px}.paper-detail-grid .detail-value{word-break:break-all;color:var(--color)}.paper-detail-grid .detail-value a{color:var(--color);border-bottom:1px solid var(--border-color)}.paper-detail-grid .detail-value a:hover{border-bottom-color:var(--color)}@media (max-width:700px){.paper-detail-grid{grid-template-columns:1fr;gap:4px 0}.paper-detail-grid .detail-label{padding-top:8px}}.paper-support-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin:18px 0 28px}.paper-support-card{display:flex;flex-direction:column;gap:10px;padding:20px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;color:var(--color);text-decoration:none;transition:border-color .15s ease,transform .15s ease}.paper-support-card:hover{border-color:var(--color);text-decoration:none;color:var(--color)}.paper-support-card .paper-support-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);display:flex;align-items:center;gap:8px}.paper-support-card p{margin:0;font-size:14px;line-height:1.55;color:var(--ink-soft)}.paper-support-card .paper-support-cta{margin-top:auto;font-size:13px;font-weight:500;color:var(--color);display:inline-flex;align-items:center;gap:6px}.paper-support-card:hover .paper-support-cta i{transform:translateX(2px)}.paper-support-card .paper-support-cta i{transition:transform .15s ease}.pr-page{min-height:100%;background:var(--canvas-bg-color)}.pr-page-inner{padding-top:24px;padding-bottom:60px}.pr-header{margin:6px 0 18px}.pr-title{margin:4px 0 10px;font-size:clamp(1.6rem,3vw,2.4rem);line-height:1.15;word-break:break-word}.pr-header-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}.pr-header-meta .pr-meta-item{display:inline-flex;align-items:center;gap:6px}.pr-header-meta .pr-meta-item i{color:var(--ink-muted)}.pr-body-card{margin:18px 0 24px;padding:20px 22px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px}.pr-body-card .paper-section-eyebrow{margin-bottom:12px}.paper-tabs{display:flex;flex-wrap:wrap;gap:0;border-bottom:1px solid var(--border-color);margin:18px 0 0}.paper-tabs .paper-tab{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;background:0 0;border:0;border-bottom:2px solid transparent;font-family:var(--font-sans);font-size:14px;font-weight:500;color:var(--ink-muted);cursor:pointer;transition:color .15s ease,border-color .15s ease;margin-bottom:-1px}.paper-tabs .paper-tab:hover{color:var(--color)}.paper-tabs .paper-tab.active{color:var(--color);border-bottom-color:var(--color)}.paper-tabs .paper-tab i{color:inherit;opacity:.85}.paper-tabs .paper-tab:active,.paper-tabs .paper-tab:focus,.paper-tabs .paper-tab:focus-visible{outline:0;box-shadow:none}.paper-tabs .paper-tab:focus-visible{color:var(--color);background:var(--hover-bg-color);border-radius:6px 6px 0 0}.pr-diff{margin:16px 0 28px;display:flex;flex-direction:column;gap:16px}.diff-file-block{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;overflow:hidden}.diff-file-header{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--paper-bg-alt);border-bottom:1px solid var(--border-color);font-family:var(--font-mono);font-size:13px;color:var(--color)}.diff-file-icon{color:var(--ink-muted);flex-shrink:0}.diff-file-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:break-all}.diff-file-status{flex-shrink:0;padding:2px 8px;border-radius:999px;font-size:10px;letter-spacing:.12em;text-transform:uppercase;border:1px solid var(--border-color);color:var(--ink-muted);background:var(--paper-card)}.diff-file-status-added{color:#2f6b3e;border-color:rgba(47,107,62,.35);background:rgba(47,107,62,.08)}.diff-file-status-deleted{color:var(--status-error);border-color:rgba(161,58,46,.35);background:rgba(161,58,46,.08)}.diff-file-status-renamed{color:var(--status-progress);border-color:rgba(138,107,30,.35);background:rgba(138,107,30,.08)}.dark-mode .diff-file-status-added{color:#a7e2a7;border-color:rgba(167,226,167,.35);background:rgba(167,226,167,.08)}.dark-mode .diff-file-status-deleted{color:var(--status-error);border-color:rgba(255,139,123,.35);background:rgba(255,139,123,.08)}.dark-mode .diff-file-status-renamed{color:var(--status-progress);border-color:rgba(255,211,122,.35);background:rgba(255,211,122,.08)}.diff-file-table{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:13px;line-height:1.55;table-layout:fixed}.diff-file-table tr.diff-row{vertical-align:top}.diff-file-table .diff-gutter{width:48px;padding:0 10px;text-align:right;color:var(--ink-muted);background:var(--paper-bg-alt);border-right:1px solid var(--border-color);user-select:none;font-variant-numeric:tabular-nums;vertical-align:top;white-space:nowrap}.diff-file-table .diff-gutter-new{border-right:1px solid var(--border-color)}.diff-file-table .diff-sign{width:18px;padding:0 6px;text-align:center;color:var(--ink-muted);background:var(--paper-bg-alt);user-select:none;vertical-align:top}.diff-file-table .diff-code{padding:1px 12px;white-space:pre-wrap;word-break:break-word;color:var(--color)}.diff-row-hunk td{background:var(--paper-bg-alt)!important;color:var(--ink-muted);border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);font-size:12px;letter-spacing:.02em;padding-top:6px;padding-bottom:6px}.diff-row-hunk .diff-code{color:var(--ink-muted)}.diff-row-add .diff-code{background:rgba(47,107,62,.1);color:#1f4a2a}.diff-row-add .diff-gutter,.diff-row-add .diff-sign{background:rgba(47,107,62,.06);color:#2f6b3e}.diff-row-remove .diff-code{background:rgba(161,58,46,.1);color:#6e1f1a}.diff-row-remove .diff-gutter,.diff-row-remove .diff-sign{background:rgba(161,58,46,.06);color:var(--status-error)}.dark-mode .diff-row-add .diff-code{background:rgba(167,226,167,.1);color:#c9f0c9}.dark-mode .diff-row-add .diff-gutter,.dark-mode .diff-row-add .diff-sign{background:rgba(167,226,167,.06);color:#a7e2a7}.dark-mode .diff-row-remove .diff-code{background:rgba(255,139,123,.1);color:#ffc9c0}.dark-mode .diff-row-remove .diff-gutter,.dark-mode .diff-row-remove .diff-sign{background:rgba(255,139,123,.06);color:var(--status-error)}@media (max-width:700px){.diff-file-table{font-size:12px}.diff-file-table .diff-gutter{width:36px;padding:0 6px}.diff-file-table .diff-sign{width:14px;padding:0 4px}.diff-file-table .diff-code{padding:1px 8px}}.pr-comments{list-style:none;margin:16px 0 28px;padding:0;display:flex;flex-direction:column;gap:12px}.pr-comment{padding:16px 18px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px}.pr-comment-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;padding-bottom:8px;margin-bottom:8px;border-bottom:1px solid var(--border-color)}.pr-comment-author{font-weight:600;color:var(--color);font-size:14px;display:inline-flex;align-items:center;gap:6px}.pr-comment-author i{color:var(--ink-muted);font-size:12px}.pr-comment-date{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);letter-spacing:.02em}.pr-comment-body{color:var(--color);font-size:14px;line-height:1.6}.pr-comment-body :last-child{margin-bottom:0}@media (max-width:700px){.pr-page-inner{padding-top:14px;padding-bottom:40px}.pr-body-card{padding:14px 14px;border-radius:8px}.pr-comment{padding:12px 14px}.paper-tabs .paper-tab{padding:10px 12px;font-size:13px}.pr-diff pre{font-size:12px;padding:12px}}.toast{background-color:var(--paper-card)!important;color:var(--color)!important;border:1px solid var(--border-color)!important;border-radius:10px!important;box-shadow:0 8px 24px rgba(0,0,0,.08);backdrop-filter:none;min-width:280px;max-width:calc(100vw - 24px)}.toast .toast-header{background:0 0!important;color:var(--color)!important;border-bottom:1px solid var(--border-color)!important;font-family:var(--font-sans);padding:10px 12px}.toast .toast-header strong{font-weight:600;color:var(--color);letter-spacing:.01em}.toast .toast-header .close{color:var(--ink-muted)!important;text-shadow:none;opacity:.9;font-size:1.25rem}.toast .toast-header .close:hover{color:var(--color)!important;opacity:1}.toast .toast-body{color:var(--color)!important;padding:12px;font-size:14px;line-height:1.45}.dark-mode .toast{box-shadow:0 8px 24px rgba(0,0,0,.5)}.paper-crumbs{font-family:var(--font-sans);font-size:12px;color:var(--ink-muted);letter-spacing:0}.paper-crumbs .here,.paper-crumbs a{color:var(--color);text-decoration:none}.paper-page-title{font-family:var(--font-sans);font-weight:600;font-size:32px;line-height:1.2;letter-spacing:-.035em;margin:8px 0 6px;color:var(--color)}.paper-page-title em{color:inherit;font-style:normal}.paper-page-lede{color:var(--ink-soft);font-size:16px;max-width:640px;line-height:1.55}.paper-meta-rule{display:flex;flex-wrap:wrap;gap:16px 24px;align-items:center;margin:22px 0 18px;padding:12px 0;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);letter-spacing:.02em}.paper-meta-rule b{color:var(--color);font-weight:500}.paper-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:10px;padding:20px 22px}.dark-mode .paper-card{background:var(--admin-stat-bg)}.paper-section-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);font-weight:500;margin-bottom:10px;display:block}.paper-section-title{font-family:var(--font-serif);font-weight:400;font-size:22px;letter-spacing:-.01em;margin:0 0 8px}.paper-kbd{display:inline-block;font-family:var(--font-mono);font-size:11px;padding:1px 6px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:3px;background:var(--paper-card,#fff);color:var(--color)}.paper-pill{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:11px;padding:2px 8px;border:1px solid var(--border-color);border-radius:999px;color:var(--ink-soft);background:var(--paper-card,#fff);letter-spacing:.04em}.paper-pill.accent{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}.paper-pill.good{border-color:#2f6b3e;color:#2f6b3e}.paper-pill.warn{border-color:var(--status-progress);color:var(--status-progress)}.paper-pill.bad{border-color:var(--status-error);color:var(--status-error)}.paper-row{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start;padding:18px 0;border-bottom:1px solid var(--border-color)}.paper-row:first-child{border-top:1px solid var(--border-color)}.paper-row .row-title{font-family:var(--font-serif);font-size:20px;letter-spacing:-.01em;line-height:1.2}.paper-row .row-title a{color:var(--color)}.paper-row .row-meta{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);letter-spacing:.03em;margin-top:6px;display:flex;flex-wrap:wrap;gap:14px}.paper-row .row-actions{display:flex;gap:6px;flex-shrink:0}.paper-form-step{display:grid;grid-template-columns:48px 1fr;gap:20px;padding:22px 0;border-bottom:1px solid var(--border-color)}.paper-form-step:first-child{border-top:1px solid var(--border-color)}.paper-form-step .step-num{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);padding-top:4px;letter-spacing:.02em}.paper-form-step .step-title{font-family:var(--font-serif);font-size:22px;letter-spacing:-.01em;line-height:1.2;margin-bottom:4px}.paper-form-step .step-sub{color:var(--ink-soft);font-size:14px;margin-bottom:14px;text-wrap:pretty}.paper-field-label{font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--color);margin-bottom:6px;display:block}.paper-switch-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;gap:16px}.paper-switch-row+.paper-switch-row{border-top:1px dashed var(--border-soft)}.paper-switch-row .sw-label{font-weight:500}.paper-switch-row .sw-sub{display:block;font-size:13px;color:var(--ink-muted);margin-top:2px}.paper-page .custom-select,.paper-page .form-control{border-radius:6px;border-color:var(--border-color);font-size:14px;padding:8px 12px;height:auto;background:var(--input-bg);color:var(--input-color)}.paper-page .custom-select:focus,.paper-page .form-control:focus{color:var(--input-color);background:var(--input-bg);border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.custom-select:focus,.form-control:focus,input:focus,select:focus,textarea:focus{color:var(--input-color)!important;background-color:var(--input-bg)!important}.form-control::selection,input::selection,textarea::selection{background:var(--accent-soft);color:var(--input-color)}.dark-mode .form-control::selection,.dark-mode input::selection,.dark-mode textarea::selection{background:rgba(124,199,190,.3);color:var(--input-color)}.btn.btn-ink{background:var(--color);color:var(--canvas-bg-color);border:1px solid var(--color);font-weight:500;border-radius:6px;padding:8px 14px}.btn.btn-ink:hover{background:var(--primary-hover-bg);color:var(--canvas-bg-color);border-color:var(--primary-hover-bg)}.btn.btn-accent{background:var(--accent);color:#fff;border:1px solid var(--accent);font-weight:500;border-radius:6px}.btn.btn-accent:hover{background:#17544f;border-color:#17544f;color:#fff}.paper-page .dashboard-filter-row{display:flex;align-items:center;gap:10px;padding:14px 0;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);margin:18px 0 0;flex-wrap:wrap}.paper-page .dashboard-filter-row .form-control{height:38px;background:var(--paper-card)}.paper-page .dashboard-filter-controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.paper-page .dashboard-filter-row .btn-group .btn,.paper-page .dashboard-filter-row .dropdown-toggle{background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);height:38px;padding:0 14px;font-size:14px;font-weight:500;border-radius:var(--radius-control,6px);display:inline-flex;align-items:center;gap:6px;line-height:1}.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-label{color:var(--ink-muted);font-weight:500}.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-label::after{content:"·";margin-left:6px;color:var(--ink-faint)}.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-value{font-weight:500}.paper-page .dashboard-filter-row .dropdown-toggle .fas{font-size:11px;color:var(--ink-muted)}.paper-page .dashboard-filter-row .btn-group .btn:hover,.paper-page .dashboard-filter-row .dropdown-toggle:hover{background:var(--hover-bg-color);border-color:var(--border-color);color:var(--color)}.paper-page .dashboard-filter-row .btn-group{gap:0}.paper-page .dashboard-filter-row .btn-group .btn{border-radius:0;border-right-width:0}.paper-page .dashboard-filter-row .btn-group .btn:first-child{border-top-left-radius:6px;border-bottom-left-radius:6px}.paper-page .dashboard-filter-row .btn-group .btn:last-child{border-top-right-radius:6px;border-bottom-right-radius:6px;border-right-width:1px}.paper-page .dashboard-filter-row .btn-group .btn.btn-primary{background:var(--color);color:var(--canvas-bg-color);border-color:var(--color)}.paper-page .dashboard-filter-row .search-wrap{flex:1 1 280px;min-width:220px}.paper-page .quota-row{display:grid;grid-template-columns:repeat(3,1fr);gap:40px;margin:24px 0 0;padding:0;background:0 0;border:none;border-radius:0}.paper-page .quota-label{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);opacity:1}.paper-page .quota-value{font-family:var(--font-mono);font-size:13px;font-variant-numeric:tabular-nums;color:var(--color)}.paper-page .quota-value .quota-unlimited-tag{font-family:var(--font-sans);font-size:12px;color:var(--ink-muted);margin-left:4px}.paper-page .quota-track{height:6px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:999px;overflow:hidden;width:100%;min-width:0}.paper-page .quota-track .quota-fill{height:100%;min-width:2px;border-radius:999px;background:var(--color);transition:width .3s ease}.paper-page .quota-track.quota-warn .quota-fill{background:var(--status-progress)}.paper-page .quota-track.quota-danger .quota-fill{background:var(--status-error)}.paper-page .quota-track.quota-unlimited{background:repeating-linear-gradient(-45deg,var(--paper-bg-alt) 0 4px,var(--border-soft,var(--border-color)) 4px 6px)}.paper-page .repo-list{margin-top:8px}.paper-page .repo-list-item{padding:20px 0;gap:16px;border-bottom:1px solid var(--border-soft,var(--border-color))}.paper-page .repo-list-item:first-child{border-top:none}.paper-page .repo-list-item-header{gap:10px;margin-bottom:6px}.paper-page .repo-name{font-family:var(--font-mono);font-size:15px;color:var(--color)}.paper-page .repo-source{font-size:13px;color:var(--ink-soft);opacity:1;gap:10px}.paper-page .repo-source a{color:var(--ink-soft);border-bottom:1px dotted var(--border-color)}.paper-page .repo-source a:hover{color:var(--color)}.paper-page .repo-meta{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);opacity:1;gap:16px;margin-top:10px}.paper-page .type-badge{font-family:var(--font-mono);font-size:11px;padding:3px 8px;letter-spacing:.1em}.paper-page .repo-list-item-actions .dropdown-toggle{background:var(--paper-card);border:1px solid var(--border-color);color:var(--color);border-radius:6px;font-size:13px;padding:6px 12px;height:auto}.paper-settings{max-width:1180px;margin:0 auto;padding:48px 32px 80px}.paper-settings .paper-page-title{max-width:14em}.paper-settings-body{display:grid;grid-template-columns:220px 1fr;gap:60px;margin-top:32px;border-top:1px solid var(--border-color);padding-top:28px}.paper-settings-toc{position:sticky;top:32px;align-self:start}.paper-settings-toc-head{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:12px}.paper-settings-toc nav{display:flex;flex-direction:column;gap:2px}.paper-settings-toc nav a{padding:8px 0 8px 14px;border-left:2px solid transparent;color:var(--ink-soft);font-size:14px;text-decoration:none}.paper-settings-toc nav a.active,.paper-settings-toc nav a:hover{color:var(--color);border-left-color:var(--color)}.paper-settings-main{min-width:0;max-width:720px}.paper-settings-section{padding:28px 0;border-bottom:1px solid var(--border-soft,var(--border-color))}.paper-settings-section:first-child{padding-top:0}.paper-settings-section:last-of-type{border-bottom:none}.paper-settings-section .form-group{margin-bottom:18px}.paper-settings-section .form-check{padding:10px 0 10px 28px;border-bottom:1px solid var(--border-soft,var(--border-color))}.paper-settings-section .form-check:last-child{border-bottom:none}.paper-settings-section .form-check-label{font-family:var(--font-sans);font-size:14px;font-weight:500;color:var(--color);cursor:pointer}.paper-settings-section .form-check-input{cursor:pointer;width:16px;height:16px;margin-top:3px;margin-left:-28px;accent-color:var(--color)}.paper-settings-section .form-text{color:var(--ink-soft);font-size:13px;margin-top:4px;max-width:48em}.paper-settings-section .paper-section-copy{margin:-4px 0 14px}.paper-settings-footer{padding-top:24px;display:flex;flex-direction:column;gap:14px}.paper-settings-footer .btn-ink{align-self:flex-start}.paper-settings-actions{display:flex;align-items:center;flex-wrap:wrap;gap:14px}.paper-settings-hint{font-size:13px;color:var(--ink-muted)}.paper-inline-alert{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border-radius:var(--radius-control,6px);border:1px solid var(--border-color);background:var(--paper-bg-alt);font-size:13px;color:var(--color)}.paper-inline-alert i{margin-top:2px;color:var(--ink-muted)}.paper-inline-alert-error{border-color:var(--status-error);background:var(--status-error-soft)}.paper-inline-alert-error i{color:var(--status-error)}.paper-account-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border:1px solid var(--border-color);border-radius:var(--radius-surface,10px);background:var(--paper-card);margin-bottom:20px}.paper-account-card img{flex-shrink:0}.paper-account-identity{flex:1 1 auto;min-width:0}.paper-account-name{font-family:var(--font-mono);font-size:14px;font-weight:500;color:var(--color)}.paper-account-meta{font-size:13px;color:var(--ink-muted);margin-top:2px}.paper-danger-zone{padding:16px;border:1px solid var(--status-error);border-radius:var(--radius-surface,10px);background:var(--status-error-soft)}.paper-danger-head{font-weight:600;font-size:14px;color:var(--status-error);margin-bottom:6px}.paper-danger-copy{font-size:13px;color:var(--ink-soft);max-width:52em;margin-bottom:12px}.paper-danger-zone .paper-inline-alert{margin-bottom:12px}.btn.btn-outline-danger{background:0 0;color:var(--status-error);border:1px solid var(--status-error);font-weight:500;border-radius:var(--radius-control,6px);padding:8px 14px}.btn.btn-outline-danger:hover{background:var(--status-error);color:#fff;border-color:var(--status-error)}.navbar .user-menu{min-width:260px}.navbar .user-menu .dropdown-item{display:flex;align-items:flex-start;gap:10px;padding:8px 10px!important}.navbar .user-menu .dropdown-item i{width:16px;margin-top:3px;font-size:13px;color:var(--ink-muted);text-align:center}.navbar .user-menu .dropdown-item span{display:flex;flex-direction:column;line-height:1.3}.navbar .user-menu .dropdown-item small{font-size:12px;color:var(--ink-muted)}.navbar .user-menu .dropdown-header{font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);padding:8px 10px 6px}.paper-field-label{font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--color);margin-bottom:6px;display:block}@media (max-width:900px){.paper-settings-body{grid-template-columns:1fr;gap:24px}.paper-settings-toc{position:static}.paper-settings-toc nav{flex-direction:row;flex-wrap:wrap;gap:8px}.paper-settings-toc nav a{border-left:none;border-bottom:2px solid transparent;padding:4px 0}.paper-settings-toc nav a.active,.paper-settings-toc nav a:hover{border-bottom-color:var(--color);border-left:none}}.paper-table{display:block;margin-top:20px;background:0 0;border:none;border-top:1px solid var(--border-color);border-radius:0;overflow:visible}.paper-table .paper-table-head,.paper-table .paper-table-row{display:grid;grid-template-columns:minmax(280px,2.4fr) minmax(140px,1fr) 140px 90px 140px 52px;align-items:center;gap:18px;padding:12px 4px}.paper-table.paper-table-conferences .paper-table-head,.paper-table.paper-table-conferences .paper-table-row{grid-template-columns:minmax(280px,2.4fr) 140px 90px 220px 52px}.paper-table.paper-table-repos .paper-table-head,.paper-table.paper-table-repos .paper-table-row{grid-template-columns:minmax(280px,2.4fr) minmax(140px,1fr) 90px 140px 52px}.paper-table.paper-table-repos.has-bulk .paper-table-head,.paper-table.paper-table-repos.has-bulk .paper-table-row{grid-template-columns:28px minmax(280px,2.4fr) minmax(140px,1fr) 90px 140px 52px;gap:12px}.paper-table .admin-users-row{grid-template-columns:28px minmax(280px,2.4fr) 90px 120px 100px 52px!important;gap:12px!important}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(320px,3fr) 190px 90px 170px 52px;gap:20px;padding:8px 4px}.paper-table.paper-table-dashboard .cell-anon .anon-text{gap:2px}.paper-table.paper-table-dashboard .cell-anon .repo-name{padding:0}.paper-table .paper-table-head{background:0 0;border-bottom:1px solid var(--border-color);font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);padding-top:8px;padding-bottom:8px}.paper-table .paper-table-head .sortable{cursor:pointer;user-select:none;display:inline-flex;align-items:center;gap:6px;background:0 0;border:none;padding:2px 0;margin:0;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;border-radius:4px}.paper-table .paper-table-head .sortable:hover{color:var(--color)}.paper-table .paper-table-head .sortable.active{color:var(--color)}.paper-table .paper-table-head .sortable i{font-size:10px}.paper-table .paper-table-head .num,.paper-table .paper-table-row .num{text-align:right}.paper-table .paper-table-head .num .sortable{flex-direction:row-reverse}.paper-table .paper-table-row{border-top:1px solid var(--border-soft,var(--border-color));font-size:14px;transition:background-color .12s ease}.paper-table .paper-table-row:hover{background:var(--hover-bg-color)}.paper-table .paper-table-row.row-clickable{cursor:pointer}.paper-table .paper-table-row.repo-inactive>:not(.cell-actions){opacity:.55}.paper-table .paper-table-row.repo-error{background:var(--status-error-soft);border-left:2px solid var(--status-error);padding-left:6px}.paper-table .paper-table-row.repo-error:hover{background:var(--status-error-soft-hover)}.cell-status .status-sub.status-sub-error,.paper-table .cell-status .status-sub-error{color:var(--status-error)}.paper-table .cell-status .status-sub-warn{color:var(--status-progress)}.paper-table .cell-anon{display:flex;align-items:center;gap:12px;min-width:0}.paper-table .cell-anon .type-badge{flex-shrink:0;min-width:52px;justify-content:center}.paper-table .cell-anon .anon-text{min-width:0;display:flex;flex-direction:column;gap:3px}.paper-table .cell-anon .anon-title{display:flex;align-items:center;gap:8px;min-width:0}.paper-table .cell-anon .anon-title .type-badge{margin-left:0}.paper-table .cell-anon .repo-name{font-family:var(--font-mono);font-size:14px;color:var(--color);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;max-width:100%;padding:2px 0}.paper-table .cell-anon .repo-name-static{color:var(--ink-muted);font-style:italic}.paper-table .cell-anon .anon-sub{font-family:var(--font-sans);font-size:13px;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:10px;min-width:0}.paper-table .cell-anon .anon-sub .anon-source{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-anon .anon-sub .conf-tag{flex:0 1 auto;min-width:72px;max-width:50%}.paper-table .cell-anon .anon-sub a{color:var(--ink-muted);border-bottom:1px dotted var(--border-color);padding:4px 0;margin:-4px 0}.paper-table .cell-anon .anon-sub a:hover{color:var(--color)}.paper-table .cell-anon .anon-sub .commit-hash{font-family:var(--font-mono);font-size:12px}.paper-table .cell-conf{font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-conf span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-status{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;font-size:14px;color:var(--color);min-width:0}.paper-table .cell-status .status-line{display:inline-flex;align-items:center;gap:8px}.paper-table .cell-status .status-sub{flex-basis:100%;font-family:var(--font-sans);font-size:13px;line-height:1.3;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-views{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--color)}.paper-table .cell-expires{font-family:var(--font-sans);font-size:13px;color:var(--ink-soft)}.paper-table .cell-expires .expires-never{color:var(--ink-muted)}.paper-table .cell-expires .expires-past{color:var(--ink-muted)}.paper-table .empty-dash{color:var(--ink-faint)}.paper-table .cell-actions{display:flex;justify-content:flex-end}.status-dot{width:8px;height:8px;border-radius:999px;display:inline-block;flex-shrink:0;background:var(--status-inactive)}.status-dot.status-ready{background:var(--status-ready)}.status-dot.status-error{background:var(--status-error)}.status-dot.status-preparing,.status-dot.status-progress{background:var(--status-progress)}.status-dot.status-expired,.status-dot.status-removed{background:var(--status-inactive)}.dashboard-page{padding-bottom:96px}.dashboard-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;margin-top:12px;font-size:13px;color:var(--ink-muted)}.dashboard-meta .dashboard-count{font-variant-numeric:tabular-nums}.btn-link-inline{background:0 0;border:none;padding:2px 4px;margin:-2px 0;font:inherit;color:var(--link-color);cursor:pointer;border-radius:4px}.btn-link-inline:hover{color:var(--link-hover-color);text-decoration:underline}.btn.btn-outline-ink{background:0 0;color:var(--color);border:1px solid var(--border-color);font-weight:500;border-radius:var(--radius-control,6px);padding:8px 14px}.btn.btn-outline-ink:hover{background:var(--hover-bg-color);color:var(--color);border-color:var(--ink-faint)}.paper-page .dropdown-menu .dropdown-item-danger,.paper-page .dropdown-menu .dropdown-item-danger i{color:var(--status-error)}.paper-page .dropdown-menu .dropdown-item-danger:focus,.paper-page .dropdown-menu .dropdown-item-danger:hover{background:var(--status-error-soft-hover);color:var(--status-error)}.paper-page .dropdown-menu button.dropdown-item{width:100%;text-align:left;background:0 0;border:none}.paper-page .dropdown-menu .dropdown-divider{border-top-color:var(--border-soft,var(--border-color));margin:4px 0}.paper-table .paper-table-skeleton{pointer-events:none}.skeleton{display:block;background:var(--paper-bg-alt);border-radius:4px;position:relative;overflow:hidden}.skeleton::after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,var(--hover-bg-color),transparent);animation:skeleton-shimmer 1.4s infinite}.skeleton-line{height:12px}.skeleton-line+.skeleton-line{margin-top:8px}.skeleton-line-sm{height:10px}.skeleton-badge{width:52px;height:22px;border-radius:999px;flex-shrink:0}@keyframes skeleton-shimmer{to{transform:translateX(100%)}}@media (prefers-reduced-motion:reduce){.skeleton::after{animation:none}}.paper-table .paper-table-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:56px 20px;color:var(--ink-muted);text-align:center;border-top:1px solid var(--border-soft,var(--border-color))}.paper-table .paper-table-empty>i{font-size:1.5rem;color:var(--ink-faint)}.paper-table .paper-table-empty-actions{display:flex;gap:8px;margin-top:8px}.bulk-bar{display:flex;align-items:center;gap:8px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:8px;padding:8px 12px;margin:8px 0}.paper-page.admin-page .paper-page-title{font-size:1.5rem;margin:4px 0}.paper-page.admin-page .paper-page-lede{display:none}.paper-page.admin-page .paper-crumbs{margin-bottom:4px}.paper-page.admin-page .admin-nav{margin:6px 0 12px}.admin-filter-toolbar{display:flex;flex-direction:column;gap:6px;margin:8px 0 12px;position:sticky;top:0;z-index:4;background:var(--canvas-bg-color);padding:6px 0;border-bottom:1px solid var(--border-soft,var(--border-color));box-shadow:0 1px 0 var(--canvas-bg-color)}@media (max-width:900px){.admin-filter-toolbar{position:static;box-shadow:none}}.admin-filter-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.admin-filter-row .search-wrap{flex:1;min-width:220px}.admin-filter-row .search-wrap .form-control{height:34px}.admin-filter-inline{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--ink-muted)}.admin-filter-inline>label{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-muted);margin:0;white-space:nowrap}.admin-filter-inline .form-control-sm{height:28px;padding:2px 8px;font-size:12px}.admin-filter-inline input[type=search].form-control-sm,.admin-filter-inline input[type=text].form-control-sm{width:130px}.admin-filter-inline input[type=date].form-control-sm{width:130px}.admin-filter-inline select.form-control-sm{width:auto;min-width:88px}.admin-filter-inline .arrow{color:var(--ink-muted);font-size:11px}.admin-filter-spacer{flex:1}.admin-active-chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:12px;color:var(--ink-muted)}.admin-active-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 4px 2px 10px;border:1px solid var(--border-color);border-radius:999px;background:var(--hover-bg-color);font-size:12px;color:var(--color)}.admin-active-chip .key{color:var(--ink-muted);font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.06em}.admin-active-chip button{background:0 0;border:none;cursor:pointer;padding:0 4px;color:var(--ink-muted);font-size:12px;border-radius:50%}.admin-active-chip button:hover{color:#b42318}.admin-search-hint{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);pointer-events:none;border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;background:var(--paper-card)}.admin-filter-row .search-wrap{position:relative}.admin-filter-row .search-wrap .form-control{padding-right:40px}.paper-table-row.row-selected{background:rgba(47,122,68,.06)}.queue-state-pills{display:inline-flex;gap:6px;margin-left:8px;flex-wrap:wrap}.queue-state-pills .pill{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--ink-muted)}.queue-state-pills .pill-active{color:var(--status-progress);border-color:rgba(196,138,46,.4)}.queue-state-pills .pill-failed{color:#b42318;border-color:rgba(180,35,24,.4)}.queue-state-pills .pill-completed{color:#2f7a44;border-color:rgba(47,122,68,.3)}.queue-state-pills .pill-waiting{color:var(--ink-soft)}.queue-state-pills .pill-delayed{color:var(--ink-muted)}.admin-summary{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:8px 0 12px;font-size:13px}.admin-summary .summary-total{font-family:var(--font-serif);font-size:1rem;color:var(--color);margin-right:8px}.admin-summary .summary-meta{color:var(--ink-muted);font-size:12px;margin-right:8px}.admin-summary .summary-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--ink-muted);cursor:pointer;font-size:12px;user-select:none;transition:background-color .1s,border-color .1s}.admin-summary .summary-pill:hover{background:var(--hover-bg-color)}.admin-summary .summary-pill .count{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--color)}.admin-summary .summary-pill.active{background:var(--primary-bg);color:var(--primary-color);border-color:var(--primary-bg)}.admin-summary .summary-pill.active .count{color:inherit}.admin-summary .summary-pill.error .count{color:#b42318}.admin-summary .summary-pill.warn .count{color:var(--status-progress)}.admin-summary .summary-pill.ok .count{color:#2f7a44}.dark-mode .admin-summary .summary-pill.error .count{color:#f08a82}.dark-mode .admin-summary .summary-pill.warn .count{color:var(--status-progress)}.dark-mode .admin-summary .summary-pill.ok .count{color:#7dc894}.dark-mode .queue-state-pills .pill-active{color:var(--status-progress);border-color:rgba(232,185,112,.4)}.dark-mode .queue-state-pills .pill-failed{color:#f08a82;border-color:rgba(240,138,130,.4)}.dark-mode .queue-state-pills .pill-completed{color:#7dc894;border-color:rgba(125,200,148,.3)}.job-progress{flex:1;height:6px;background:var(--border-soft,var(--border-color));border-radius:999px;overflow:hidden;margin-left:12px;max-width:200px}.job-progress-bar{height:100%;background:#2f7a44;transition:width .3s}.paper-table-empty{padding:48px 4px;text-align:center;color:var(--ink-muted);display:flex;flex-direction:column;align-items:center;gap:8px;border-top:1px solid var(--border-soft,var(--border-color))}.paper-table-empty>i{font-size:1.5rem}.q-header{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:4px}.q-header-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.q-range-btns{display:inline-flex;border:1px solid var(--border-color);border-radius:8px;overflow:hidden}.q-range-btns .btn{border:none;border-radius:0;padding:5px 14px;font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);background:var(--paper-card)}.q-range-btns .btn+.btn{border-left:1px solid var(--border-color)}.q-range-btns .btn.active{background:var(--color);color:var(--paper-card)}.q-cards{display:flex;gap:10px;margin:16px 0;flex-wrap:wrap}.q-card{flex:1;min-width:140px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:14px 16px;cursor:pointer;transition:border-color .15s,box-shadow .15s}.q-card:hover{border-color:var(--ink-muted)}.q-card.selected{border-color:var(--color);box-shadow:0 0 0 1px var(--color)}.q-card-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.q-dot{width:8px;height:8px;border-radius:50%;background:#2f7a44;flex-shrink:0}.q-dot-red{background:#b42318}.dark-mode .q-dot{background:#7dc894}.dark-mode .q-dot-red{background:#f08a82}.q-card-name{font-family:var(--font-mono);font-size:12px;font-weight:500;color:var(--ink-muted);letter-spacing:.04em}.q-card-count{font-family:var(--font-serif);font-size:2rem;font-weight:400;line-height:1.1;color:var(--color);margin-bottom:6px}.q-card-sub{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);letter-spacing:.04em}.q-card-bar{height:4px;background:var(--border-color);border-radius:2px;overflow:hidden;margin-top:6px}.q-card-bar-fill{height:100%;background:#1f6a64;border-radius:2px;transition:width .3s}.dark-mode .q-card-bar-fill{background:#7cc7be}.q-detail{display:grid;grid-template-columns:1fr 320px;gap:16px;margin-bottom:20px}.q-throughput{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px}.q-chart-wrap{position:relative}.q-throughput canvas{width:100%;display:block}.q-chart-tooltip{position:absolute;pointer-events:none;background:var(--paper-card);border:1px solid var(--border-color);border-radius:6px;padding:6px 10px;font-family:var(--font-mono);font-size:11px;line-height:1.6;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.15);z-index:10}.q-chart-tooltip .q-tip-time{color:var(--ink-muted);margin-bottom:2px}.q-chart-tooltip .q-tip-completed{color:#1f6a64}.q-chart-tooltip .q-tip-failed{color:#b42318}.dark-mode .q-chart-tooltip .q-tip-completed{color:#7cc7be}.dark-mode .q-chart-tooltip .q-tip-failed{color:#f08a82}.q-chart-crosshair{position:absolute;top:0;width:1px;height:100%;background:var(--ink-muted);opacity:.3;pointer-events:none;z-index:9}.q-stats-panel{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px;display:flex;flex-direction:column}.q-section-label{font-family:var(--font-mono);font-size:11px;font-weight:500;color:var(--ink-muted);text-transform:lowercase;letter-spacing:.04em;margin-bottom:14px}.q-section-right{float:right;text-transform:uppercase;font-size:10px;letter-spacing:.08em}.q-legend-completed{color:#1f6a64}.q-legend-failed{color:#b42318;margin-left:8px}.q-legend-exec{color:var(--status-progress);margin-left:8px;letter-spacing:2px}.dark-mode .q-legend-completed{color:#7cc7be}.dark-mode .q-legend-failed{color:#f08a82}.dark-mode .q-legend-exec{color:var(--status-progress)}.q-chart-tooltip .q-tip-exec{color:var(--status-progress)}.dark-mode .q-chart-tooltip .q-tip-exec{color:var(--status-progress)}.q-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 20px;flex:1}.q-stat-label{font-family:var(--font-mono);font-size:10px;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:2px}.q-stat-value{font-family:var(--font-serif);font-size:1.25rem;font-weight:400;color:var(--color);line-height:1.2}.q-stats-actions{display:flex;gap:8px;margin-top:16px;padding-top:14px;border-top:1px solid var(--border-color)}.q-stats-actions .btn{font-family:var(--font-mono);font-size:11px;padding:6px 14px;border-radius:8px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color)}.q-jobs{background:var(--paper-card);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px}.q-jobs-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:12px}.q-state-filters{display:inline-flex;gap:6px;flex-wrap:wrap}.q-state-toggle{cursor:pointer;display:inline-flex;align-items:center}.q-state-toggle input{display:none}.q-state-chip{font-family:var(--font-mono);font-size:11px;padding:3px 10px;border-radius:10px;border:1px solid var(--border-color);background:var(--paper-card);color:var(--ink-muted);transition:opacity .15s,background .15s;text-transform:capitalize}.q-state-toggle input:checked+.q-state-chip{opacity:1}.q-state-toggle input:not(:checked)+.q-state-chip{opacity:.4;text-decoration:line-through}.q-state-badge,.q-state-chip{font-family:var(--font-mono);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.3px}.q-state-badge{display:inline-block;padding:2px 8px;border-radius:8px}.q-state-active{background:#dbeafe;color:#1d4ed8}.q-state-waiting{background:#fef3c7;color:#92400e}.q-state-delayed{background:#e0e7ff;color:#4338ca}.q-state-failed{background:#fee2e2;color:#b91c1c}.q-state-completed{background:#dcfce7;color:#166534}.dark-mode .q-state-active{background:rgba(59,130,246,.2);color:#93bbfd}.dark-mode .q-state-waiting{background:rgba(251,191,36,.15);color:#fbbf24}.dark-mode .q-state-delayed{background:rgba(99,102,241,.2);color:#7cc7be}.dark-mode .q-state-failed{background:rgba(239,68,68,.2);color:#fca5a5}.dark-mode .q-state-completed{background:rgba(34,197,94,.15);color:#86efac}.q-cell-state{width:120px}.q-delay-hint{display:block;font-size:10px;color:var(--ink-muted);margin-top:2px;font-family:var(--font-mono)}.q-search-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}.q-search-row .form-control{max-width:320px;height:34px}.q-auto-refresh{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);display:inline-flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap}.q-table{width:100%;border-collapse:collapse}.q-table thead th{font-family:var(--font-mono);font-size:10px;font-weight:500;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;padding:8px 10px;border-bottom:1px solid var(--border-color);text-align:left;white-space:nowrap}.q-table tbody tr{border-bottom:1px solid var(--border-color);transition:background .1s}.q-table tbody tr:hover{background:var(--paper-bg-alt)}.q-table tbody tr:last-child{border-bottom:none}.q-table tbody td{padding:10px 10px;font-size:.875rem;vertical-align:middle}.q-cell-id{font-family:var(--font-mono);font-size:12px;white-space:nowrap}.q-cell-id{display:flex;align-items:center;gap:8px}.q-chevron{font-size:10px;color:var(--ink-muted);transition:transform .15s;flex-shrink:0}.q-chevron-open{transform:rotate(90deg)}.q-cell-id a{color:var(--color)}.q-cell-payload{font-family:var(--font-mono);font-size:12px;color:var(--color)}.q-payload-detail{color:var(--ink-muted)}.q-cell-num{font-family:var(--font-mono);font-size:12px;text-align:center;white-space:nowrap}.q-cell-progress{min-width:120px}.q-progress-wrap{display:flex;align-items:center;gap:8px}.q-progress-bar{flex:1;height:6px;border-radius:3px;overflow:hidden;background-image:linear-gradient(to right,#1f6a64 var(--pct,0),var(--border-color) var(--pct,0))}.dark-mode .q-progress-bar{background-image:linear-gradient(to right,#7cc7be var(--pct,0),var(--border-color) var(--pct,0))}.q-progress-label{font-family:var(--font-mono);font-size:11px;color:var(--ink-muted);white-space:nowrap;min-width:32px;text-align:right}.q-cell-actions{text-align:right;white-space:nowrap}.q-cell-actions .btn{padding:3px 8px;font-size:11px;background:0 0;border:1px solid var(--border-color);color:var(--ink-muted)}.q-cell-actions .btn:hover{color:var(--color)}.q-row-failed{background:rgba(180,35,24,.04)}.dark-mode .q-row-failed{background:rgba(240,138,130,.06)}.q-error-detail{padding:4px 10px 12px;font-size:12px}.q-error-reason{color:#b42318;font-size:.8125rem;margin-bottom:4px}.dark-mode .q-error-reason{color:#f08a82}.q-toast-error{background:#fbe7e7;color:#b42318;border:1px solid #f3c7c7;border-radius:8px;padding:10px 16px;margin-bottom:12px;font-family:var(--font-mono);font-size:12px;cursor:pointer;display:flex;align-items:center;gap:8px}.dark-mode .q-toast-error{background:rgba(240,138,130,.1);color:#f08a82;border-color:rgba(240,138,130,.25)}.q-error-stack{font-size:.75rem;max-height:120px;overflow:auto;margin:4px 0 0;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:6px;padding:8px 10px}.q-row-expanded>td{border-bottom-color:transparent}.q-detail-row>td{padding:0 10px 14px!important;border-bottom:1px solid var(--border-color)}.q-job-detail{background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:8px;padding:14px 16px}.q-job-detail-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px 20px;margin-bottom:10px}.q-job-detail-item{display:flex;flex-direction:column;gap:2px}.q-job-detail-label{font-family:var(--font-mono);font-size:10px;font-weight:500;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em}.q-job-detail-value{font-family:var(--font-mono);font-size:12px;color:var(--color);word-break:break-all}.q-job-detail-value a{color:var(--color)}.q-job-detail-error{margin:8px 0}.q-job-detail-actions{display:flex;gap:8px;margin-top:12px;padding-top:10px;border-top:1px solid var(--border-color)}.q-job-detail-actions .btn{font-family:var(--font-mono);font-size:11px;padding:5px 12px;border-radius:6px;background:var(--paper-card);border:1px solid var(--border-color);color:var(--color)}@media (max-width:900px){.q-detail{grid-template-columns:1fr}.q-cards{flex-direction:column}.q-card{min-width:100%}.q-jobs-header{flex-direction:column;align-items:flex-start}.q-tabs{flex-wrap:wrap}.q-search-row{flex-wrap:wrap}.q-search-row .form-control{max-width:100%;flex:1}.q-table{font-size:12px}.q-header{flex-direction:column}.q-job-detail-grid{grid-template-columns:1fr 1fr}}@media (max-width:900px){.paper-table .paper-table-head{display:none}.paper-table .admin-users-row,.paper-table .paper-table-row,.paper-table.paper-table-conferences .paper-table-row,.paper-table.paper-table-repos .paper-table-row{display:flex!important;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:14px 0;grid-template-columns:none!important}.paper-table .cell-anon{flex:1 1 calc(100% - 52px);min-width:0;order:1}.paper-table .cell-actions{flex:0 0 auto;order:2;align-self:center}.paper-table .cell-conf,.paper-table .cell-expires,.paper-table .cell-status,.paper-table .cell-views{order:3;font-size:13px;color:var(--ink-muted);display:inline-flex;align-items:center;gap:6px;text-align:left}.paper-table .cell-status{flex-wrap:nowrap;gap:6px;min-width:0}.paper-table .cell-status .status-sub{flex-basis:auto;font-size:13px;line-height:inherit}.paper-table .cell-status .status-sub::before{content:"·";margin-right:6px;color:var(--ink-faint)}.paper-table .cell-conf{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-table .cell-views.num{text-align:left}.paper-table .paper-table-skeleton .cell-expires,.paper-table .paper-table-skeleton .cell-status,.paper-table .paper-table-skeleton .cell-views{flex:1 1 30%}.paper-table .cell-views::before{content:'\f06e';font-family:'Font Awesome 5 Free';font-weight:900;font-size:10px;color:var(--ink-muted)}.paper-table .cell-expires::before{content:'\f017';font-family:'Font Awesome 5 Free';font-weight:400;font-size:10px;color:var(--ink-muted)}.paper-table .cell-conf::before{content:'\f51c';font-family:'Font Awesome 5 Free';font-weight:900;font-size:10px;color:var(--ink-muted)}.paper-table-dashboard .cell-conf::before{content:none}.paper-table .cell-conf:has(.empty-dash),.paper-table .cell-expires:has(.empty-dash){display:none}.paper-table .cell-expires::before,.paper-table .cell-views::before{color:var(--ink-faint)}}.paper-page .dropdown-menu{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface,10px);box-shadow:0 12px 28px rgba(26,24,21,.08);padding:6px;min-width:220px;font-family:var(--font-sans);font-size:14px;color:var(--color);margin-top:6px}.paper-page .dropdown-menu .dropdown-header{font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);padding:8px 10px 6px;font-weight:500}.paper-page .dropdown-menu .dropdown-item{border-radius:6px;padding:8px 10px;color:var(--color);font-size:14px;display:flex;align-items:center;gap:8px}.paper-page .dropdown-menu .dropdown-item:focus,.paper-page .dropdown-menu .dropdown-item:hover{background:var(--hover-bg-color);color:var(--color)}.paper-page .dropdown-menu .dropdown-item i{width:16px;font-size:13px;color:var(--ink-muted)}.paper-page .dropdown-menu .form-check.dropdown-item{padding:8px 10px 8px 36px!important;position:relative}.paper-page .dropdown-menu .form-check.dropdown-item .form-check-input{position:absolute;left:12px;top:50%;transform:translateY(-50%);margin:0;width:16px;height:16px;accent-color:var(--color);cursor:pointer}.paper-page .dropdown-menu .form-check.dropdown-item .form-check-label{font-family:var(--font-sans);font-size:14px;color:var(--color);font-weight:400;margin:0;cursor:pointer}.btn.btn-icon-dots{width:32px;height:32px;padding:0;border:1px solid transparent;background:0 0;color:var(--ink-muted);border-radius:6px;display:inline-flex;align-items:center;justify-content:center;line-height:1}.btn.btn-icon-dots:focus,.btn.btn-icon-dots:hover,.btn.btn-icon-dots[aria-expanded=true]{background:var(--hover-bg-color);border-color:var(--border-color);color:var(--color)}.btn.btn-icon-dots::after{display:none}.btn.btn-icon-dots i{font-size:15px}.paper-page .repo-list-empty{border-top:none;padding:56px 20px;color:var(--ink-muted)}.paper-page .filter-chip{background:var(--paper-bg-alt);color:var(--color);border:1px solid var(--border-color);font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:4px 10px;border-radius:999px}@media (max-width:768px){.paper-page .paper-page-title{font-size:clamp(1.4rem,6vw,1.9rem);margin:4px 0 2px}.paper-page .paper-page-lede{font-size:13px;line-height:1.4;margin-bottom:0}.paper-page .btn-ink{padding:6px 12px;font-size:13px}.paper-page .quota-row{grid-template-columns:1fr;gap:12px;padding:0;margin:16px 0 0}.paper-page .dashboard-filter-row{padding:10px 0;margin-top:12px;gap:8px}.paper-page .quota-header{flex-direction:row;align-items:baseline;gap:8px}.paper-page .quota-value{white-space:nowrap}.paper-page .dashboard-filter-controls{width:100%}.paper-page .dashboard-filter-controls .btn-group{flex:1 1 100%;display:flex}.paper-page .dashboard-filter-controls .btn-group .btn{flex:1;justify-content:center}.paper-page .dashboard-filter-controls .dropdown{flex:1 1 0;min-width:0}.paper-page .dashboard-filter-controls .dropdown .dropdown-toggle{width:100%;justify-content:space-between}.paper-page .dashboard-filter-controls .dropdown .filter-btn-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}@media (max-width:480px){.paper-page .paper-page-lede{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}}.empty-state{text-align:center;padding:40px 20px;opacity:.6}.empty-state i{font-size:2rem;margin-bottom:10px;display:block}.paper-page .progress{height:10px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:999px;overflow:hidden;margin-bottom:20px}.paper-page .progress .progress-bar{background:var(--color);color:var(--canvas-bg-color);font-family:var(--font-mono);font-size:10px;line-height:10px}.paper-page .progress .progress-bar.bg-success{background:var(--status-ready)!important}.paper-page .progress .progress-bar.bg-warning{background:var(--status-progress)!important;color:#fff}.paper-page .progress .progress-bar.bg-danger{background:var(--status-error)!important}.paper-page .progress .progress-bar.bg-dark{background:var(--ink-faint)!important}.paper-page .form>h5,.paper-page form>h5,.paper-page>h5{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin:24px 0 8px}.paper-page .accordion .card{background:var(--paper-card,var(--sidebar-bg-color));border:1px solid var(--border-color);border-radius:8px!important;margin-bottom:8px;overflow:hidden}.paper-page .accordion .card+.card{border-top:1px solid var(--border-color)}.paper-page .accordion .card-header{background:0 0;border:none;padding:0}.paper-page .accordion .card-header .btn{color:var(--color)!important;font-family:var(--font-sans);font-weight:500;font-size:14px;padding:14px 18px!important;background:0 0!important;border:none!important;position:relative}.paper-page .accordion .card-header .btn::after{content:"+";position:absolute;right:18px;font-family:var(--font-serif);font-size:20px;color:var(--ink-muted)}.paper-page .accordion .card-header .btn:not(.collapsed)::after{content:"–"}.paper-page .accordion .card-body{padding:6px 18px 18px!important;border-top:1px solid var(--border-soft)}.paper-page .accordion .form-check-label{color:var(--color);font-weight:500;font-size:14px}.paper-page .accordion .form-text{color:var(--ink-muted);font-size:12px;margin-top:2px}.faq-section .panel-body code{font-family:var(--font-mono);font-size:13px;padding:1px 6px;background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:3px;color:var(--color)}.faq-section .panel-body a{color:var(--accent)}@media (max-width:820px){.paper-faq-body{grid-template-columns:1fr;gap:24px}.paper-faq-toc{position:static}}.paper-footer{margin-top:80px;padding:56px max(20px,calc((100% - 1076px)/ 2)) 28px;border-top:1px solid var(--border-color);background:var(--canvas-bg-color);font-family:var(--font-sans)}.paper-footer-inner{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr;gap:40px}.paper-footer-brand .paper-footer-mark{font-family:var(--font-serif);font-size:22px;letter-spacing:-.01em;color:var(--color)}.paper-footer-brand .paper-footer-mark em{font-style:italic;font-weight:400}.paper-footer-tag{margin-top:10px;color:var(--ink-soft);font-size:14px;line-height:1.55;max-width:340px}.paper-footer-head{font-family:var(--font-mono);font-size:12px;color:var(--ink-soft);letter-spacing:.1em;text-transform:uppercase;margin-bottom:14px}.paper-footer-col a{display:block;color:var(--color);font-size:14px;padding:4px 0;text-decoration:none}.paper-footer-col a:hover{color:var(--accent)}.paper-footer-rule{height:1px;background:var(--border-color);margin:40px 0 16px}.paper-footer-foot{display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px;font-size:13px;color:var(--ink-muted);font-family:var(--font-sans);letter-spacing:0}.paper-footer-foot .paper-footer-meta{font-family:var(--font-serif);font-style:italic;font-size:14px;letter-spacing:0;color:var(--ink-soft)}@media (max-width:768px){.paper-footer-inner{grid-template-columns:1fr 1fr}}.file.folder.truncated .truncated-warning{color:var(--status-progress);margin-left:4px;font-size:.85em;flex-shrink:0}.file.folder.truncated>a{color:var(--status-progress)}body{--bucket-error-bg:#FCEDED;--bucket-error-fg:#8A1F1F;--bucket-error-bd:#F0C4C4;--bucket-error-dot:#B53737;--bucket-warn-bg:#FFF3DF;--bucket-warn-fg:#7A4D00;--bucket-warn-bd:#F0D8A0;--bucket-warn-dot:#B07A2F;--bucket-info-bg:#EAF2EC;--bucket-info-fg:#2C5D3A;--bucket-info-bd:#C5DCCD;--bucket-info-dot:#5B8D6B;--card-shadow:0 1px 2px rgba(26, 24, 21, 0.04),0 4px 14px rgba(26, 24, 21, 0.05);--card-shadow-hover:0 1px 2px rgba(26, 24, 21, 0.06),0 8px 24px rgba(26, 24, 21, 0.08)}.dark-mode{--bucket-error-bg:rgba(255, 139, 123, 0.10);--bucket-error-fg:var(--status-error);--bucket-error-bd:rgba(255, 139, 123, 0.28);--bucket-error-dot:var(--status-error);--bucket-warn-bg:rgba(255, 211, 122, 0.10);--bucket-warn-fg:var(--status-progress);--bucket-warn-bd:rgba(255, 211, 122, 0.28);--bucket-warn-dot:var(--status-progress);--bucket-info-bg:rgba(152, 200, 168, 0.10);--bucket-info-fg:#98C8A8;--bucket-info-bd:rgba(152, 200, 168, 0.28);--bucket-info-dot:#98C8A8;--card-shadow:0 1px 2px rgba(0, 0, 0, 0.35),0 6px 18px rgba(0, 0, 0, 0.30);--card-shadow-hover:0 2px 4px rgba(0, 0, 0, 0.45),0 12px 28px rgba(0, 0, 0, 0.40)}.errors-page .errors-header{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:4px 0 8px}.errors-page .errors-actions{display:flex;gap:6px}.errors-page .kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin:14px 0 18px}.errors-page .kpi-card{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--card-shadow);transition:box-shadow 160ms ease,transform 160ms ease}.errors-page .kpi-card:hover{box-shadow:var(--card-shadow-hover)}.errors-page .kpi-label{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted)}.errors-page .kpi-value{font-family:var(--font-serif);font-size:2.6rem;font-weight:400;line-height:1;margin-top:2px;color:var(--color)}.errors-page .kpi-card.kpi-error .kpi-value{color:var(--bucket-error-fg)}.errors-page .kpi-card.kpi-warn .kpi-value{color:var(--bucket-warn-fg)}.errors-page .kpi-card.kpi-info .kpi-value{color:var(--bucket-info-fg)}.errors-page .kpi-sub{font-size:.75rem;color:var(--ink-muted);font-style:italic}.errors-page .kpi-sub.up{color:var(--bucket-error-fg);font-style:normal}.errors-page .kpi-sub.down{color:var(--bucket-info-fg);font-style:normal}.errors-page .dropped-warn{color:var(--bucket-error-fg);font-weight:600;font-style:normal}.errors-page .volume-chart{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;margin-bottom:18px;box-shadow:var(--card-shadow)}.errors-page .volume-head{display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:.6875rem;color:var(--ink-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}.errors-page .volume-legend{display:flex;gap:14px;align-items:center;text-transform:none;letter-spacing:0;font-family:var(--font-sans)}.errors-page .volume-legend .dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}.errors-page .dot.dot-error{background:var(--bucket-error-dot)}.errors-page .dot.dot-warn{background:var(--bucket-warn-dot)}.errors-page .dot.dot-info{background:var(--bucket-info-dot)}.errors-page .volume-bars{display:flex;align-items:flex-end;gap:4px;height:80px}.errors-page .volume-bar{display:flex;flex-direction:column-reverse;flex:1 1 0;min-width:6px;height:100%}.errors-page .volume-bar .seg{display:block;width:100%}.errors-page .seg.seg-error{background:var(--bucket-error-dot)}.errors-page .seg.seg-warn{background:var(--bucket-warn-dot)}.errors-page .seg.seg-info{background:var(--bucket-info-dot);opacity:.7}.errors-page .errors-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}.errors-page .seg-tabs{display:inline-flex;border:1px solid var(--border-color);border-radius:8px;overflow:hidden;background:var(--paper-card)}.errors-page .seg-tabs button{background:0 0;border:none;padding:6px 14px;font-size:.875rem;cursor:pointer;color:var(--color);font-family:var(--font-sans)}.errors-page .seg-tabs button+button{border-left:1px solid var(--border-color)}.errors-page .seg-tabs button.active{background:var(--primary-bg);color:var(--primary-color)}.errors-page .errors-toolbar .search-wrap{position:relative;flex:1 1 320px;min-width:220px}.errors-page .errors-toolbar .search-wrap input{width:100%;padding:6px 10px 6px 32px;border:1px solid var(--border-color);border-radius:8px;font-family:var(--font-mono);font-size:.8125rem;background:var(--input-bg);color:var(--input-color)}.errors-page .errors-toolbar .search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--ink-muted);font-size:.875rem}.errors-page .errors-toolbar .filter-count{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:.75rem;color:var(--ink-muted)}.errors-page .errors-toolbar .select-wrap{display:inline-flex;align-items:center;gap:6px}.errors-page .errors-toolbar .select-wrap label{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);margin:0}.errors-page .errors-toolbar .select-wrap select{background:var(--input-bg);color:var(--input-color);border:1px solid var(--border-color);border-radius:8px}.errors-page .errors-toolbar .autoref{display:inline-flex;gap:6px;align-items:center;font-size:.8125rem;color:var(--color);cursor:pointer;margin:0}.errors-page .btn-icon{width:32px;padding:4px 0}.errors-page .errors-list{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;box-shadow:var(--card-shadow)}.errors-page .errors-list-head,.errors-page .errors-row-main{display:grid;grid-template-columns:110px 100px 100px 1fr 60px 70px;gap:16px;padding:18px 20px;align-items:start}.errors-page .errors-list-head{padding:12px 20px;align-items:center}.errors-page .errors-list-head{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt)}.errors-page .errors-row{border-bottom:1px solid var(--border-soft)}.errors-page .errors-row:last-child{border-bottom:none}.errors-page .errors-row.open{background:var(--paper-bg-alt)}.errors-page .errors-row-main{cursor:pointer}.errors-page .errors-row-main:hover{background:var(--hover-bg-color)}.errors-page .col-when .when-rel{font-size:.9375rem;color:var(--color);font-variant-numeric:tabular-nums}.errors-page .col-when .when-abs{font-family:var(--font-mono);font-size:.75rem;color:var(--ink-muted);font-variant-numeric:tabular-nums;margin-top:2px}.errors-page .col-sev{display:flex;align-items:center;gap:8px}.errors-page .sev-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.errors-page .sev-dot.sev-error{background:var(--bucket-error-dot)}.errors-page .sev-dot.sev-warn{background:var(--bucket-warn-dot)}.errors-page .sev-dot.sev-info{background:var(--bucket-info-dot)}.errors-page .sev-label{font-family:var(--font-mono);font-size:.75rem;letter-spacing:.08em;color:var(--color)}.errors-page .pill-module{display:inline-block;font-family:var(--font-mono);font-size:.75rem;background:var(--paper-bg-alt);color:var(--ink-soft);border:1px solid var(--border-color);padding:3px 10px;border-radius:6px}.errors-page .col-msg .msg-code{font-family:var(--font-mono);font-size:1rem;color:var(--color);font-weight:600}.errors-page .col-msg .msg-context{color:var(--ink-muted);font-style:italic;font-size:.875rem;margin-left:10px}.errors-page .col-msg .msg-detail{color:var(--ink-soft);font-family:var(--font-mono);font-size:.8125rem;margin-left:10px}.errors-page .col-msg .msg-url{color:var(--ink-muted);font-family:var(--font-mono);font-size:.8125rem;margin-top:6px;word-break:break-all}.errors-page .count-pill{display:inline-block;padding:2px 8px;border-radius:4px;background:var(--primary-bg);color:var(--primary-color);font-family:var(--font-mono);font-size:.75rem;font-variant-numeric:tabular-nums}.errors-page .count-pill.count-pill-muted{background:0 0;color:var(--ink-muted);border:1px solid var(--border-color)}.errors-page .status-pill{display:inline-block;padding:2px 10px;border-radius:4px;font-family:var(--font-mono);font-size:.8125rem;font-variant-numeric:tabular-nums;border:1px solid transparent}.errors-page .status-pill.status-error{background:var(--bucket-error-bg);color:var(--bucket-error-fg);border-color:var(--bucket-error-bd)}.errors-page .status-pill.status-warn{background:var(--bucket-warn-bg);color:var(--bucket-warn-fg);border-color:var(--bucket-warn-bd)}.errors-page .status-pill.status-info{background:var(--bucket-info-bg);color:var(--bucket-info-fg);border-color:var(--bucket-info-bd)}.errors-page .errors-row-detail{padding:0 16px 16px}.errors-page .detail-tabs{display:flex;gap:4px;border-bottom:1px solid var(--border-color);margin-bottom:12px}.errors-page .detail-tabs button{background:0 0;border:0;border-bottom:2px solid transparent;padding:8px 12px;font-size:.875rem;cursor:pointer;color:var(--ink-muted);font-family:var(--font-sans)}.errors-page .detail-tabs button.active{color:var(--color);border-bottom-color:var(--color)}.errors-page .detail-body{display:grid;grid-template-columns:1fr 260px;gap:20px}.errors-page .detail-main pre{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;font-family:var(--font-mono);font-size:.875rem;line-height:1.7;color:var(--color);max-height:26em;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;margin:0;box-shadow:var(--card-shadow)}.errors-page .related-list{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;max-height:22em;overflow:auto;box-shadow:var(--card-shadow)}.errors-page .stack-pre{line-height:1.45;white-space:pre;overflow-x:auto}.errors-page .related-row{display:flex;gap:10px;padding:6px 12px;align-items:center;border-bottom:1px solid var(--border-soft);font-size:.8125rem}.errors-page .related-row:last-child{border-bottom:none}.errors-page .detail-actions{display:flex;gap:6px;align-items:center;margin-top:10px}.errors-page .copy-hint{font-size:.75rem;color:var(--bucket-info-fg)}.errors-page .detail-aside{background:var(--paper-card);border:1px solid var(--border-color);border-radius:12px;padding:18px;box-shadow:var(--card-shadow)}.errors-page .aside-block+.aside-block{margin-top:16px}.errors-page .aside-label{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted);margin-bottom:4px}.errors-page .aside-value{font-family:var(--font-serif);font-size:1rem;color:var(--color);word-break:break-word;line-height:1.3}.errors-page .aside-value.mono{font-family:var(--font-mono);font-size:.8125rem}.errors-page .aside-sub{color:var(--ink-muted);font-style:italic;font-size:.8125rem;font-family:var(--font-sans)}.errors-page .errors-pager{display:flex;justify-content:space-between;align-items:center;padding:10px 0;color:var(--ink-muted);font-size:.875rem}@media (max-width:720px){.errors-page .errors-list-head,.errors-page .errors-row-main{grid-template-columns:80px 80px 1fr 60px}.errors-page .errors-list-head .col-mod,.errors-page .errors-list-head .col-sev,.errors-page .errors-row-main .col-mod,.errors-page .errors-row-main .col-sev{display:none}.errors-page .detail-body{grid-template-columns:1fr}}.overview-page .ov-header{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:2px}.overview-page .ov-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:6px;vertical-align:middle}.overview-page .ov-dot-ok{background:#2f7a44}.overview-page .ov-dot-warn{background:var(--status-progress)}.overview-page .ov-dot-idle{background:var(--ink-muted)}.dark-mode .overview-page .ov-dot-ok{background:#7dc894}.dark-mode .overview-page .ov-dot-warn{background:var(--status-progress)}.overview-page .ov-kpi-row{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin:16px 0 22px}.overview-page .ov-kpi-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--card-shadow);transition:box-shadow 160ms ease}.overview-page .ov-kpi-card:hover{box-shadow:var(--card-shadow-hover)}.overview-page .ov-kpi-label{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted)}.overview-page .ov-kpi-value{font-family:var(--font-serif);font-size:2.6rem;font-weight:400;line-height:1;margin-top:2px;color:var(--color)}.overview-page .ov-kpi-value.ov-val-warn{color:#b42318}.dark-mode .overview-page .ov-kpi-value.ov-val-warn{color:#f08a82}.overview-page .ov-kpi-sub{font-size:.75rem;color:var(--ink-muted);margin-top:2px}.overview-page .ov-daily-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:-4px 0 22px}.overview-page .ov-daily-card{background:var(--primary-bg);border:1px solid var(--border-color);border-radius:12px;padding:16px 20px;box-shadow:var(--card-shadow)}.overview-page .ov-daily-label{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-muted)}.overview-page .ov-daily-value{font-family:var(--font-serif);font-size:2.5rem;font-weight:400;line-height:1;color:var(--accent,#1f6a64);margin-top:6px}.overview-page .ov-daily-sub{font-size:.75rem;color:var(--ink-muted);margin-top:5px}.dark-mode .overview-page .ov-daily-card{background:var(--paper-card)}.dark-mode .overview-page .ov-daily-value{color:#7cc7be}.overview-page .ov-chart-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-bottom:18px}.overview-page .ov-chart-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px 14px;box-shadow:var(--card-shadow)}.overview-page .ov-chart-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.overview-page .ov-chart-title{font-family:var(--font-serif);font-size:1rem}.overview-page .ov-chart-legend{font-family:var(--font-mono);font-size:.6875rem;color:var(--ink-muted);display:flex;align-items:center;gap:5px}.overview-page .ov-dot-legend{display:inline-block;width:8px;height:8px;border-radius:50%}.overview-page .ov-dot-accent{background:var(--accent,#1f6a64)}.overview-page .ov-dot-ok-fill{background:#2f7a44}.overview-page .ov-dot-user-fill{background:#8b5e2e}.overview-page .ov-dot-new-user-fill{background:var(--status-progress)}.dark-mode .overview-page .ov-dot-ok-fill{background:#7dc894}.dark-mode .overview-page .ov-dot-user-fill{background:var(--status-progress)}.dark-mode .overview-page .ov-dot-new-user-fill{background:var(--status-progress)}.overview-page .ov-spark-bars{display:flex;align-items:flex-end;gap:3px;height:140px}.overview-page .ov-spark-col{flex:1;display:flex;align-items:flex-end;height:100%;position:relative}.overview-page .ov-spark-fill{width:100%;background:var(--accent,#1f6a64);border-radius:2px 2px 0 0;transition:height .3s ease;opacity:.55}.overview-page .ov-spark-fill-alt{background:#2f7a44}.overview-page .ov-spark-fill-user{background:#8b5e2e}.overview-page .ov-spark-fill-new-user{background:var(--status-progress)}.dark-mode .overview-page .ov-spark-fill-alt{background:#7dc894}.dark-mode .overview-page .ov-spark-fill-user{background:var(--status-progress)}.dark-mode .overview-page .ov-spark-fill-new-user{background:var(--status-progress)}.overview-page .ov-spark-col:hover .ov-spark-fill{opacity:1}.overview-page .ov-spark-col.has-tip::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 10px);transform:translateX(-50%) translateY(4px);z-index:10;padding:6px 8px;border-radius:6px;background:var(--color);color:var(--canvas-bg-color);font-family:var(--font-mono);font-size:.6875rem;line-height:1;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity 120ms ease,transform 120ms ease}.overview-page .ov-spark-col.has-tip::before{content:"";position:absolute;left:50%;bottom:calc(100% + 4px);transform:translateX(-50%) translateY(4px);z-index:10;border:5px solid transparent;border-top-color:var(--color);opacity:0;pointer-events:none;transition:opacity 120ms ease,transform 120ms ease}.overview-page .ov-spark-col.has-tip:hover::after,.overview-page .ov-spark-col.has-tip:hover::before{opacity:1;transform:translateX(-50%) translateY(0)}.overview-page .ov-spark-x{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:.6875rem;color:var(--ink-muted);margin-top:6px}.overview-page .ov-triple-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:18px}.overview-page .ov-panel-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 20px;box-shadow:var(--card-shadow)}.overview-page .ov-panel-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px}.overview-page .ov-panel-title{font-family:var(--font-serif);font-size:1rem}.overview-page .ov-panel-meta{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-muted)}.overview-page .ov-panel-foot{margin-top:12px;padding-top:10px;border-top:1px solid var(--border-color);font-size:.8125rem}.overview-page .ov-panel-foot a{color:var(--link-color);text-decoration:none}.overview-page .ov-panel-foot a:hover{text-decoration:underline}.overview-page .ov-performance{min-width:0;margin:22px 0}.overview-page .ov-performance-head{align-items:center;flex-wrap:wrap;gap:12px 20px}.overview-page .ov-performance-head h2{margin:0 0 4px}.overview-page .ov-performance-note{margin:0 0 12px;font-size:12px;line-height:1.5;color:var(--ink-muted)}.overview-page .ov-performance-head .ov-performance-note{margin:0}.overview-page .ov-performance-window{display:flex;align-items:center;gap:8px;flex-shrink:0}.overview-page .ov-performance-window label{margin:0;font-size:12px;color:var(--ink-muted)}.overview-page .ov-performance-window select{width:auto}.overview-page .ov-performance-tabs{margin:0 0 16px;gap:4px}.overview-page .ov-performance-tabs .paper-tab{white-space:nowrap}.overview-page .ov-metrics-scroll{max-width:100%;overflow-x:auto;border:1px solid var(--border-color);border-radius:var(--radius-control,8px)}.overview-page .ov-metrics-table{width:100%;border-collapse:collapse;color:var(--color);font-size:13px;line-height:1.5;font-variant-numeric:tabular-nums}.overview-page .ov-metrics-table td,.overview-page .ov-metrics-table th{padding:10px 12px;border-bottom:1px solid var(--border-color);vertical-align:middle;white-space:nowrap}.overview-page .ov-metrics-table thead th{background:var(--paper-bg-alt);color:var(--ink-muted);font-size:12px;font-weight:500}.overview-page .ov-metrics-table .num{text-align:right}.overview-page .ov-metrics-table tr:last-child>*{border-bottom:0}.overview-page .ov-metrics-table thead tr:last-child>*{border-bottom:1px solid var(--border-color)}.overview-page .ov-metrics-table tbody tr:hover{background:var(--hover-bg-color)}.overview-page .ov-metrics-table .ov-metric-name{min-width:190px;max-width:280px;text-align:left;white-space:normal;overflow-wrap:anywhere;font-weight:500}.overview-page .ov-metrics-table small{display:block;margin-top:3px;font-size:11px;font-weight:400;color:var(--ink-muted)}.overview-page .ov-metrics-table small .ui-icon{width:12px;height:12px}.overview-page .ov-metric-warning-row{background:rgba(196,138,46,.07)}.overview-page .ov-metric-warn,.overview-page .ov-metrics-table small.ov-metric-warn{color:#946519}.dark-mode .overview-page .ov-metric-warn,.dark-mode .overview-page .ov-metrics-table small.ov-metric-warn{color:var(--status-progress)}.overview-page .ov-performance-message{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:10px 12px;margin:0 0 16px;border:1px solid var(--border-color);border-radius:var(--radius-control,8px);font-size:13px;line-height:1.5}.overview-page .ov-performance-message span{flex:1;min-width:160px}.overview-page .ov-performance-warning{background:rgba(196,138,46,.07)}.overview-page .ov-performance-empty{margin:0;padding:24px 12px;text-align:center;font-size:13px;color:var(--ink-muted)}.overview-page .ov-performance-help{margin-top:16px;font-size:12px;line-height:1.6;color:var(--ink-muted)}.overview-page .ov-performance-help summary{width:fit-content;cursor:pointer}.overview-page .ov-performance-help p{margin:8px 0 0;max-width:80ch}@media (max-width:575px){.overview-page .ov-performance{padding:16px 12px}.overview-page .ov-performance-window{width:100%}.overview-page .ov-performance-window select{flex:1;min-width:0}.overview-page .ov-performance-tabs .paper-tab{flex:1;padding:10px 8px}}.overview-page .ov-stacked-bar{display:flex;height:18px;border-radius:6px;overflow:hidden;margin-bottom:14px}.overview-page .ov-bar-seg{min-width:2px;transition:width .4s ease}.overview-page .ov-bar-ready{background:#2f7a44}.overview-page .ov-bar-preparing{background:var(--status-progress)}.overview-page .ov-bar-error{background:#b42318}.overview-page .ov-bar-expired{background:#8a857c}.overview-page .ov-bar-removed{background:#b5afa2}.dark-mode .overview-page .ov-bar-ready{background:#7dc894}.dark-mode .overview-page .ov-bar-preparing{background:var(--status-progress)}.dark-mode .overview-page .ov-bar-error{background:#f08a82}.dark-mode .overview-page .ov-bar-expired{background:#6b665e}.dark-mode .overview-page .ov-bar-removed{background:#4a453e}.overview-page .ov-bar-legend{display:flex;flex-direction:column;gap:6px}.overview-page .ov-legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem;text-decoration:none;color:inherit}.overview-page .ov-legend-item:hover{text-decoration:none;color:inherit}.overview-page .ov-swatch{display:inline-block;width:12px;height:12px;border-radius:3px;flex-shrink:0}.overview-page .ov-legend-n{margin-left:auto;font-family:var(--font-mono);font-size:.8125rem;font-weight:600}.overview-page .ov-error-bars{display:flex;flex-direction:column;gap:12px}.overview-page .ov-ebar-row{display:flex;align-items:center;gap:10px}.overview-page .ov-ebar-label{width:30px;flex-shrink:0;font-family:var(--font-mono);font-size:.75rem;text-transform:uppercase;color:var(--ink-muted)}.overview-page .ov-ebar-track{flex:1;height:10px;background:var(--hover-bg-color);border-radius:5px;overflow:hidden}.overview-page .ov-ebar-fill{height:100%;border-radius:5px;transition:width .4s ease}.overview-page .ov-ebar-error{background:#b42318}.overview-page .ov-ebar-warn{background:var(--status-progress)}.overview-page .ov-ebar-info{background:var(--accent,#1f6a64)}.dark-mode .overview-page .ov-ebar-error{background:#f08a82}.dark-mode .overview-page .ov-ebar-warn{background:var(--status-progress)}.overview-page .ov-ebar-n{width:36px;text-align:right;font-family:var(--font-mono);font-size:.8125rem;font-weight:600;flex-shrink:0}.overview-page .ov-routes-table{display:flex;flex-direction:column}.overview-page .ov-route-row{display:grid;grid-template-columns:1fr 60px 60px 60px;padding:8px 0;border-bottom:1px solid var(--border-soft,var(--border-color));text-decoration:none;color:inherit;font-size:.875rem;align-items:center}.overview-page .ov-route-row:last-child{border-bottom:none}.overview-page .ov-route-row:hover{background:var(--hover-bg-color);text-decoration:none;color:inherit}.overview-page .ov-route-head{font-family:var(--font-mono);font-size:.6875rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-muted);border-bottom:1px solid var(--border-color)}.overview-page .ov-route-head:hover{background:0 0}.overview-page .ov-route-name{font-family:var(--font-mono);font-size:.8125rem}.overview-page .ov-route-n{text-align:right;font-family:var(--font-mono);font-size:.8125rem}.overview-page .ov-route-lat{color:var(--ink-muted)}.overview-page .ov-n-bad{color:#b42318;font-weight:600}.dark-mode .overview-page .ov-n-bad{color:#f08a82}.overview-page .ov-services-card{background:var(--paper-card,#fff);border:1px solid var(--border-color);border-radius:12px;padding:18px 22px;box-shadow:var(--card-shadow);margin-bottom:18px}.overview-page .ov-services-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px}.overview-page .ov-services-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px}.overview-page .ov-svc{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border:1px solid var(--border-soft,var(--border-color));border-radius:10px}.overview-page .ov-svc-dot{width:10px;height:10px;border-radius:50%;margin-top:3px;flex-shrink:0}.overview-page .ov-svc-info{display:flex;flex-direction:column;gap:1px;flex:1}.overview-page .ov-svc-name{font-family:var(--font-mono);font-size:.875rem;font-weight:600}.overview-page .ov-svc-meta{font-size:.75rem;color:var(--ink-muted)}.overview-page .ov-svc-detail{font-size:.75rem;color:var(--ink-muted);white-space:nowrap}@media (max-width:900px){.overview-page .ov-kpi-row{grid-template-columns:repeat(3,1fr)}.overview-page .ov-daily-row{grid-template-columns:repeat(3,1fr)}.overview-page .ov-triple-row{grid-template-columns:1fr}}@media (max-width:720px){.overview-page .ov-kpi-row{grid-template-columns:repeat(2,1fr)}.overview-page .ov-daily-row{grid-template-columns:1fr}.overview-page .ov-chart-row{grid-template-columns:1fr}.overview-page .ov-header{flex-direction:column;gap:8px}}@media (max-width:480px){.overview-page .ov-kpi-row{grid-template-columns:1fr 1fr}.overview-page .ov-services-grid{grid-template-columns:1fr 1fr}}.dashboard-heading-actions{display:flex;flex-direction:column;align-items:flex-end;gap:8px;padding-bottom:12px}.dashboard-heading-actions .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px}.dashboard-connections-link{display:inline-flex;align-items:center;gap:7px;min-height:32px;color:var(--ink-muted);font-size:12px;line-height:1.5}.dashboard-connections-link:hover{color:var(--accent);text-decoration:none}.dashboard-connections-link:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:2px}@media (max-width:767px){.dashboard-heading-actions{align-items:flex-start;padding-bottom:0;gap:4px}.dashboard-connections-link{min-height:44px}}.paper-settings-connection-link{display:flex;align-items:center;gap:12px;padding:18px 0;color:var(--color);font-size:14px}.paper-settings-connection-link>i{color:var(--ink-muted);font-size:18px}.paper-settings-connection-link>span:first-of-type{flex:1}.paper-settings-connection-link small{display:block;margin-top:4px;color:var(--ink-muted);font-size:12px}.paper-settings-connection-link:hover{color:var(--accent);text-decoration:none}.paper-settings-connection-link:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.connections-heading{margin-bottom:36px}.connections-heading .paper-page-title{font-size:clamp(38px,4.5vw,56px);margin-bottom:12px}.connections-heading .paper-page-lede{font-size:15px}.connections-grid{display:grid;grid-template-columns:1.15fr 1fr;gap:56px;align-items:start;border-top:1px solid var(--border-color);padding-top:28px}.connections-grid>section{min-width:0}.connection-title,.connection-top{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.connection-top{margin-bottom:24px;min-height:28px}.connection-top>i{color:var(--ink-muted);font-size:24px}.connection-top>.fa-key{font-size:18px}.connection-title{margin-bottom:10px}.connections-page h2{font-family:inherit;font-size:22px;font-weight:600;letter-spacing:-.5px;margin:0}.connections-page p{font-size:14px;line-height:1.65;color:var(--ink-muted)}.connection-badge{background:var(--accent-soft);color:var(--accent);font-size:11px;font-weight:600;border-radius:5px;padding:4px 8px;white-space:nowrap}.connection-badge-muted{background:var(--paper-bg-alt);color:var(--ink-muted)}.connection-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-muted)}.connection-status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}.connection-status.is-connected{color:var(--status-ready)}.connection-actions{display:flex;flex-wrap:wrap;align-items:center;gap:16px;margin:22px 0 16px}.connections-page .btn{font-size:13px;padding:9px 13px;white-space:normal}.connection-revoke p,.connections-page .connection-help{font-size:12px}.connection-link{display:inline-flex;align-items:center;gap:8px;border:0;padding:0;background:0 0;color:var(--accent);font-size:13px;line-height:1.6;font-weight:500;text-align:left;cursor:pointer}.connection-link:hover{color:var(--link-hover-color);text-decoration:underline}.connection-link:disabled{color:var(--ink-muted);opacity:.65;cursor:not-allowed;text-decoration:none}.connection-link:focus-visible{outline:2px solid var(--accent);outline-offset:5px}.connection-installations{list-style:none;padding:0;margin:18px 0;border-top:1px solid var(--border-soft)}.connection-installations li{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:14px 0;font-size:12px;border-bottom:1px solid var(--border-soft)}.connection-revoke{border-top:1px solid var(--border-soft);padding-top:18px;margin-top:24px}.connection-danger{color:var(--status-error)}.connection-resources{margin-top:36px;padding-top:28px;border-top:1px solid var(--border-color)}.connection-count{color:var(--ink-muted);font-size:12px}.connection-empty{display:flex;align-items:center;gap:18px;padding:20px 0;margin:22px 0}.connection-empty>i{font-size:22px;color:var(--ink-muted)}.connection-empty h3{font-size:14px;font-weight:600;margin:0 0 5px}.connection-empty p{font-size:12px;margin:0}.connection-empty>a{margin-left:auto;flex-shrink:0}.connection-resource{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:20px 0;border-bottom:1px solid var(--border-color)}.connection-resource-name{flex:1;min-width:180px;overflow-wrap:anywhere;font-size:14px}.connection-resource-name small{display:block;color:var(--ink-muted);font-size:11px;margin-top:5px}.connection-resource-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.connection-resource-actions .connection-help{flex-basis:100%}.connections-page .connection-footnote{display:flex;align-items:baseline;gap:10px;margin-top:22px;font-size:12px}@media (max-width:767px){.connections-grid{grid-template-columns:1fr;gap:28px}.connections-grid>section+section{border-top:1px solid var(--border-color);padding-top:28px}.connection-empty{flex-wrap:wrap}.connection-empty>div{flex:1;min-width:180px}.connection-empty>a{margin-left:40px}.connection-resource-actions{flex-basis:100%}}.paper-table-empty-actions .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px}.paper-table-empty-actions .btn>i{font-size:1em;line-height:1;color:inherit;margin-right:0!important}.auth-back{display:inline-flex;align-items:center;gap:10px;font-size:13px;color:var(--ink-muted);margin-bottom:48px}.auth-layout{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:start}.auth-intro,.auth-options{padding-top:28px}.auth-title-mobile{display:none}.auth-intro .paper-page-title{font-size:clamp(48px,5vw,76px);line-height:1.06;margin:18px 0 24px}.auth-intro .paper-page-lede{max-width:330px}.auth-options{display:grid;gap:28px}.auth-options h2{font-family:inherit;font-size:22px;font-weight:600;margin:0 0 10px}.auth-options p{font-size:14px;line-height:1.65;color:var(--ink-muted)}.auth-options .auth-existing{font-size:12px;border-left:2px solid var(--accent);padding-left:14px;margin:0}.auth-permissions{padding:6px 0 12px;list-style:none;font-size:13px;line-height:1.6}.auth-permissions li{display:flex;gap:10px;margin:12px 0}.auth-permissions i{color:var(--status-ready);margin-top:4px}.auth-continue{display:flex!important;align-items:center;justify-content:space-between;min-height:48px;padding:12px 16px!important}.auth-options small{display:block;font-size:12px;text-align:center;color:var(--ink-muted);margin-top:12px}.auth-legacy{border-top:1px solid var(--border-color);padding-top:24px}.auth-legacy h2{font-size:16px;margin:0}.auth-legacy p{font-size:13px}.source-url-label-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 16px;margin-bottom:8px}.source-url-label-row>.paper-field-label{font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--color);margin-bottom:6px;display:block}.repo-access{margin:0}.repo-access-details{margin:12px 0 24px}.source-url-label-row .repo-access-switch{padding:2px}.source-url-label-row .repo-access-switch button{padding:6px 10px;font-size:12px;line-height:20px}.repo-access-main{display:flex;align-items:center;gap:10px;padding:0 0 8px;flex-wrap:wrap}.repo-access-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.repo-access-switch{display:inline-flex;gap:3px;padding:3px;border:1px solid var(--border-color);border-radius:7px;flex-wrap:wrap}.repo-access-switch button{border:0;border-radius:4px;background:0 0;color:var(--ink-muted);padding:7px 10px;font:inherit;font-size:12px;cursor:pointer}.repo-access-switch button[aria-pressed=true]{background:var(--accent-soft);color:var(--accent);opacity:1;cursor:default;font-weight:600}.repo-access-switch button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.repo-access-details .btn{font-size:12px;padding:8px 12px}.repo-access-note{display:flex;gap:6px 16px;flex-wrap:wrap;padding:0;color:var(--ink-muted);font-size:11px;line-height:1.6}@media (max-width:767px){.auth-page{max-width:480px}.auth-back,.auth-intro .paper-eyebrow,.auth-title-desktop{display:none}.auth-title-mobile{display:inline}.auth-layout{grid-template-columns:1fr;gap:24px}.auth-intro,.auth-options{padding-top:0}.auth-intro .paper-page-title{font-size:40px;line-height:1.15;margin:6px 0 12px}.auth-intro .paper-page-lede{max-width:none;font-size:14px;margin:0}.auth-options{gap:24px}.auth-options .connection-top{margin-bottom:16px}.auth-options h2{font-size:18px}.auth-options p{font-size:13px;margin-bottom:12px}.auth-permissions{padding:0;margin:14px 0 20px;font-size:12px}.auth-permissions li{margin:8px 0}.auth-continue{font-size:14px}.auth-legacy{padding-top:20px}.auth-legacy h2{font-size:15px}.auth-legacy .connection-link{min-height:44px}.repo-access-controls{width:100%;gap:12px}}.text-muted{color:var(--ink-muted)!important}.anonymize-review-note,.draft-status{font-size:13px;color:var(--ink-muted)}.anonymize-review-note{padding:12px 0;margin:0}.anonymize-preview-status{padding:20px 32px;color:var(--ink-muted)}.anonymize-preview-status p{margin:0 0 12px}@media (max-width:575px){.dashboard-page .paper-page-lede{margin-bottom:8px}.dashboard-page .dashboard-heading-actions{gap:4px}.dashboard-page .dashboard-connections-link{min-height:32px}.dashboard-page .dashboard-filter-row{margin-top:12px}.anonymize-preview-status{padding:16px}}.paper-page .paper-page-title{font-size:32px}.paper-page.admin-page .paper-page-title{font-size:28px}.paper-page .paper-page-lede{font-size:14px;line-height:1.5}.paper-settings{padding-top:32px}.paper-settings-body{grid-template-columns:180px minmax(0,1fr);gap:40px;margin-top:24px;padding-top:24px;border-top:0}.paper-settings-main .paper-settings-section{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);padding:20px;margin-bottom:16px}.anonymize-page .paper-section-eyebrow,.paper-settings .paper-section-eyebrow{font-family:var(--font-sans);font-size:16px;font-weight:600;color:var(--color);text-transform:none;letter-spacing:-.01em;margin-bottom:12px}.paper-settings .paper-settings-toc-head{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.paper-settings-section .paper-section-copy{margin-top:0}.paper-settings .quota-row{gap:24px}.paper-settings .quota-header{flex-direction:column;align-items:flex-start;gap:4px}.paper-settings .quota-value{white-space:nowrap}.paper-settings .paper-settings-footer{padding:0 0 20px}.paper-settings .form-control{font-size:14px;border-color:var(--border-color);border-radius:var(--radius-control)}.paper-settings .form-check{padding-top:8px;padding-bottom:8px}.anonymize-page .btn-ink,.explorer-page .btn-ink,.paper-page .btn-ink{background:var(--accent);color:#fff;border-color:var(--accent)}.dark-mode .anonymize-page .btn-ink,.dark-mode .explorer-page .btn-ink,.dark-mode .paper-page .btn-ink{color:#1a1815}.anonymize-page .btn-ink:hover,.explorer-page .btn-ink:hover,.paper-page .btn-ink:hover{background:var(--accent);border-color:var(--accent);color:#fff;filter:brightness(.94)}.dark-mode .anonymize-page .btn-ink:hover,.dark-mode .explorer-page .btn-ink:hover,.dark-mode .paper-page .btn-ink:hover{color:#1a1815}.anonymize-page .btn-ink:active,.explorer-page .btn-ink:active,.paper-page .btn-ink:active{filter:brightness(.9)}input[type=date],select{color-scheme:light}.dark-mode input[type=date],.dark-mode select{color-scheme:dark}.draft-status{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;padding:10px 0}.draft-discard{padding:0;border:0;color:var(--accent);font:inherit;font-size:13px;text-decoration:underline;background:0 0;cursor:pointer}.conference-feedback{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px;margin-top:16px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);width:100%}.paper-table{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);margin-top:12px}.dashboard-page .paper-table-dashboard{width:100%}.paper-account-email{margin:20px 0}.paper-account-email .form-control{max-width:420px}.paper-settings .paper-account-email .btn[type=submit]{min-height:36px;padding:7px 18px;white-space:nowrap}.paper-table .paper-table-head{font-family:var(--font-sans);text-transform:none;letter-spacing:0;font-size:12px;background:var(--paper-bg-alt);border-radius:var(--radius-surface) var(--radius-surface) 0 0}.paper-table .paper-table-head,.paper-table .paper-table-row{padding-left:16px;padding-right:16px}.dashboard-page{--dashboard-space-xs:4px;--dashboard-space-sm:8px;--dashboard-space-md:12px;--dashboard-space-lg:16px;--dashboard-space-xl:24px;padding-top:var(--dashboard-space-xl)}.project-icon-definitions{position:absolute;width:0;height:0;overflow:hidden}.project-type-svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.project-empty-icon{width:28px;height:28px;color:var(--ink-faint)}.project-type-icon{display:inline-flex;color:var(--ink-muted);flex-shrink:0}.dashboard-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--dashboard-space-lg)}.dashboard-heading .paper-page-title{font-size:28px;margin-bottom:var(--dashboard-space-xs)}.dashboard-heading .paper-page-lede{display:none}.dashboard-heading .btn{min-height:40px;display:inline-flex;gap:var(--dashboard-space-sm);align-items:center}.dashboard-new-mobile{display:none}.dashboard-quota{position:relative;margin:0;padding:0;border:0;background:0 0}.dashboard-quota>summary{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:5px 10px;border:1px solid var(--border-color);border-radius:6px;background:var(--paper-card);color:var(--color);font-family:var(--font-sans);font-size:12px;cursor:pointer;list-style:none;white-space:nowrap}.dashboard-quota>summary::-webkit-details-marker{display:none}.dashboard-quota>summary:hover,.dashboard-quota[open]>summary{background:var(--paper-bg-alt);border-color:var(--ink-faint)}.quota-summary-label{font-weight:600}.quota-summary-value{color:var(--ink-muted);font-variant-numeric:tabular-nums}.quota-summary-ring{width:18px;height:18px;flex-shrink:0;fill:none;stroke-width:2.5}.quota-ring-track{stroke:var(--border-color)}.quota-ring-fill{stroke:var(--accent);transform:rotate(-90deg);transform-origin:50% 50%}.quota-summary-ring.quota-warn .quota-ring-fill{stroke:var(--status-progress)}.quota-summary-ring.quota-danger .quota-ring-fill{stroke:var(--status-error)}.quota-ring-unlimited{stroke:var(--accent);stroke-width:1.7}.quota-summary-chevron{width:14px;height:14px;margin-left:2px;color:var(--ink-muted);transition:transform .15s ease}.dashboard-quota[open] .quota-summary-chevron{transform:rotate(180deg)}.dashboard-quota .quota-panel{position:absolute;z-index:25;top:calc(100% + 8px);left:0;width:344px;max-width:calc(100vw - 36px);padding:16px;border:1px solid var(--border-color);border-radius:12px;background:var(--paper-card);box-shadow:0 12px 36px rgba(26,24,21,.16)}.quota-panel-heading{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.quota-panel-heading h2{font:600 15px var(--font-sans);margin:0}.quota-panel-heading .project-close{margin:-8px -8px -8px 0}.paper-page .quota-usage{display:grid;grid-template-columns:1fr;gap:16px;margin:0;padding:0}.quota-usage .quota-item+.quota-item{padding-top:16px;border-top:1px solid var(--border-soft,var(--border-color))}.quota-usage .quota-header{display:flex;align-items:center;gap:8px;margin-bottom:6px}.quota-usage .quota-label{display:inline-flex;align-items:center;gap:8px;font:500 12px var(--font-sans);letter-spacing:0;text-transform:none;color:var(--ink-muted)}.quota-usage .quota-label .project-type-svg{width:16px;height:16px}.quota-usage .quota-value{display:flex;align-items:baseline;gap:4px;font:12px var(--font-sans);font-variant-numeric:tabular-nums;color:var(--ink-muted);margin-bottom:8px}.quota-usage .quota-value strong{font-size:20px;line-height:1.2;font-weight:600;color:var(--color)}.quota-percent,.quota-usage .quota-unlimited-tag{margin-left:auto;padding:2px 6px;border-radius:4px;background:var(--paper-bg-alt);font:11px var(--font-sans);color:var(--ink-muted);white-space:nowrap}.quota-warn>.quota-header .quota-percent{color:var(--status-progress)}.quota-danger>.quota-header .quota-percent{color:var(--status-error)}.quota-usage .quota-track{height:5px;border:0;background:var(--paper-bg-alt)}.quota-usage .quota-track .quota-fill{min-width:0;background:var(--accent)}.quota-remaining{margin:6px 0 0;color:var(--ink-muted);font-size:11px}.quota-remaining.quota-limit{color:var(--status-error)}.quota-loading{font-size:12px;color:var(--ink-muted);margin:12px 0 0}.dashboard-page .dashboard-filter-row{display:flex;align-items:center;gap:var(--dashboard-space-md);padding:0;margin:var(--dashboard-space-lg) 0 0;border:0;background:0 0}.dashboard-page .dashboard-filter-row .search-wrap{flex:1 1 240px;min-width:0}.dashboard-page .dashboard-filter-row .form-control{height:40px;padding:8px 12px;font-size:13px;background:var(--paper-card)}.dashboard-page .dashboard-filter-controls{display:flex;flex-wrap:nowrap;align-items:center;gap:var(--dashboard-space-sm)}.dashboard-extra-filters{display:flex;gap:var(--dashboard-space-sm)}.dashboard-filters-toggle{display:none}.dashboard-page .dashboard-filter-row .btn-group .btn{border:0;background:0 0;border-radius:6px;height:36px;padding:6px 10px}.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary{background:var(--accent-soft);color:var(--accent)}.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary:active,.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary:hover{background:var(--accent-soft)!important;color:var(--accent)}.dashboard-page .dashboard-filter-row .dropdown-toggle{height:36px;padding:6px 10px}.dashboard-type-filter .project-type-svg{display:none}.sort-direction{width:14px;height:14px}.sort-direction.ascending{transform:rotate(180deg)}.dashboard-page .dashboard-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--dashboard-space-sm) var(--dashboard-space-md);margin:var(--dashboard-space-md) 0 0;padding:0}.dashboard-view-tabs{display:flex;align-items:center;gap:var(--dashboard-space-lg)}.dashboard-view-short{display:none}.dashboard-view-tabs button{min-height:36px;border:0;border-bottom:2px solid transparent;border-radius:0;background:0 0;font:inherit;font-size:13px;color:var(--ink-muted);padding:4px 0}.dashboard-view-tabs button[aria-pressed=true]{color:var(--color);border-bottom-color:var(--color);font-weight:600}.attention-count{padding:1px 5px;margin-left:4px;border-radius:4px;background:var(--paper-bg-alt);font-size:11px;font-variant-numeric:tabular-nums}.attention-count.has-attention{color:var(--status-error)}.dashboard-count{margin-left:auto;font-size:12px;color:var(--ink-muted)}.dashboard-active-filters{display:flex;align-items:center;flex-wrap:wrap;gap:var(--dashboard-space-sm);width:100%}.dashboard-page .filter-chip{display:inline-flex;font-family:var(--font-sans);font-size:11px;text-transform:none;letter-spacing:0;border-radius:6px}.dashboard-page .filter-chip .project-type-svg{width:12px;height:12px;margin-left:4px}.dashboard-page .filter-chip-close{min-width:24px;min-height:24px;display:inline-flex;align-items:center;justify-content:center;margin:-2px -4px -2px 2px}.dashboard-page .btn-link-inline{color:var(--ink-muted);font-size:12px}.dashboard-page .paper-table-dashboard{margin-top:var(--dashboard-space-md);border-left:0;border-right:0;border-radius:0}.dashboard-page .paper-table-dashboard .paper-table-head,.dashboard-page .paper-table-dashboard .paper-table-row{grid-template-columns:minmax(280px,3fr) 180px 72px 148px 64px;gap:var(--dashboard-space-lg);padding:8px 12px}.dashboard-page .paper-table-dashboard .paper-table-head{border-radius:0}.dashboard-page .paper-table .cell-anon{gap:var(--dashboard-space-sm)}.dashboard-page .paper-table .anon-text{flex:1}.dashboard-page .paper-table .repo-name{font-family:var(--font-sans);font-size:14px;font-weight:600}.dashboard-page .paper-table .anon-sub{display:flex;align-items:center;gap:var(--dashboard-space-sm);min-width:0}.anonymous-id{font-size:12px;font-family:var(--font-mono);color:var(--ink-muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashboard-page .paper-table .type-badge{font-family:var(--font-sans);letter-spacing:0;font-size:11px;text-transform:none;border-radius:6px}.dashboard-page .paper-table .status-sub{font-size:12px}.dashboard-page .paper-table .cell-conf{font-family:var(--font-sans);letter-spacing:0;text-transform:none}.dashboard-page .conf-tag .project-type-svg{width:14px;height:14px}.dashboard-row-meta{display:contents}.dashboard-page .cell-actions{gap:0;align-items:center}.copy-link-inline,.dashboard-page .btn-icon-dots,.project-close{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:6px;background:0 0;color:var(--ink-muted);padding:0;width:32px;height:32px;min-width:32px;cursor:pointer}.copy-link-inline:focus-visible,.copy-link-inline:hover,.dashboard-page .btn-icon-dots:hover,.project-close:hover{background:var(--accent-soft);color:var(--accent)}.copy-link-inline:disabled{opacity:.5;cursor:wait}.mobile-meta-icon{display:none}.project-attention-action{display:inline-block;color:var(--accent);border:0;background:0 0;padding:0;font:inherit;font-size:12px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}.project-attention-action{text-align:left}.dashboard-page .cell-status .project-attention-action{flex-basis:100%}.project-details-row{width:100%;border-bottom:1px solid var(--border-color)}.project-details{padding:var(--dashboard-space-xl);background:var(--paper-bg-alt)}.project-details-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--dashboard-space-sm)}.project-details-heading h2{font:600 16px var(--font-sans);margin:0}.project-details-list{display:grid;grid-template-columns:1fr 1fr;gap:var(--dashboard-space-md) var(--dashboard-space-xl);margin:12px 0}.project-details-list dt{font-size:12px;color:var(--ink-muted);font-weight:500;margin-bottom:4px}.project-details-list dd{margin:0;font-size:13px;overflow-wrap:anywhere}.project-details-list code{color:var(--color);font-size:12px}.project-name-form{margin-top:var(--dashboard-space-lg);padding-top:var(--dashboard-space-lg);border-top:1px solid var(--border-color)}.project-name-form label{display:block;font-size:13px;font-weight:500;margin-bottom:4px}.project-name-form p,.project-progress-help{font-size:12px;color:var(--ink-muted);margin-bottom:8px}.project-name-controls{display:flex;gap:var(--dashboard-space-sm);max-width:560px}.project-name-controls .form-control{min-width:0;font-size:14px}.project-name-controls .btn{flex-shrink:0;min-height:40px}.project-name-form .project-name-error{color:var(--status-error)}.project-details-button{min-height:44px;padding:0 8px;border:0;border-radius:6px;background:0 0;font:inherit;font-size:12px;color:var(--ink-muted);cursor:pointer}.anonymize-page .anonymize-topbar-title{font-family:var(--font-sans);font-size:28px;font-weight:600}.anonymize-topbar{padding:16px 24px}.anonymize-form-col{background:var(--paper-bg-alt)}.anonymize-form-col .paper-settings-main{padding:20px 24px}.anonymize-form-col .paper-settings-section{padding:18px}.anonymize-form-col .paper-settings-section:first-child{padding-top:18px}.anonymize-form-col .anonymize-submit-bar{background:var(--paper-bg-alt);padding:12px 0}.anonymize-preview-head .paper-eyebrow{font-family:var(--font-sans);font-size:14px;font-weight:600;color:var(--color);text-transform:none;letter-spacing:0}.anonymize-preview-col{background:var(--canvas-bg-color);padding:24px}.anonymize-preview-head{flex-wrap:wrap;gap:6px 12px;margin-bottom:16px}.anonymize-preview-body{padding:24px}.anonymize-mobile-tabs,.anonymize-preview-empty,.mobile-preview-jump{display:none}.anonymize-page .repo-access-note{font-size:12px}@media (max-width:900px){.paper-settings-body{grid-template-columns:1fr;gap:18px}.paper-settings-toc{padding:0}.paper-settings-toc-head{display:none}.paper-table .paper-table-row{padding-left:14px;padding-right:14px}}@media (max-width:767px){.anonymize-page .paper-page-title,.paper-page .paper-page-title{font-size:24px}.paper-settings{padding:20px 16px 40px}.paper-settings-main .paper-settings-section{padding:16px}.anonymize-topbar{padding:12px 16px}.anonymize-topbar .paper-crumbs{display:none}.anonymize-mobile-tabs{display:flex;gap:4px;padding:8px 16px;border-bottom:1px solid var(--border-color);background:var(--paper-card)}.anonymize-mobile-tabs button{flex:1;font:inherit;font-size:13px;padding:10px;border:0;border-radius:6px;background:0 0;color:var(--ink-muted)}.anonymize-mobile-tabs button[aria-selected=true]{background:var(--accent-soft);color:var(--accent);font-weight:600}.anonymize-page:not(.mobile-preview-active) .anonymize-preview-col{display:none}.mobile-preview-active .anonymize-form-col{display:none}.mobile-preview-active .anonymize-preview-empty{display:block;padding:24px 16px;color:var(--ink-muted)}.anonymize-form-col .paper-settings-main{padding:16px}.anonymize-form-col .anonymize-submit-bar{background:var(--paper-bg-alt)}.mobile-preview-jump{display:block;width:100%;min-height:44px}.anonymize-preview-col{padding:16px}.anonymize-preview-body{padding:16px}}@media (max-width:575px){.paper-settings .quota-header{flex-direction:row;flex-wrap:wrap;align-items:baseline;gap:4px 8px}}@media (max-width:1100px) and (min-width:901px){.dashboard-page .dashboard-filter-row{flex-wrap:wrap}.dashboard-page .dashboard-filter-row .search-wrap{flex-basis:100%}.dashboard-page .dashboard-filter-controls{width:100%;justify-content:space-between}}@media (max-width:900px){.dashboard-page .paper-table.paper-table-dashboard .paper-table-row{gap:4px 8px;padding:8px 12px}.dashboard-page .paper-table .cell-anon{flex:1 1 calc(100% - 96px);min-width:0}.dashboard-page .paper-table .cell-actions{flex:0 0 88px}.dashboard-row-meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;order:3;width:100%}.dashboard-page .dashboard-row-meta .cell-status{flex:1 1 80px;flex-wrap:wrap;gap:2px 8px}.dashboard-page .dashboard-row-meta .cell-expires,.dashboard-page .dashboard-row-meta .cell-views{flex-shrink:0;font-size:12px}.dashboard-page .cell-expires::before,.dashboard-page .cell-views::before{content:none}.dashboard-page .mobile-meta-icon{display:inline-block;width:14px;height:14px}.dashboard-page .dashboard-row-meta .status-sub-date{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dashboard-page .dashboard-row-meta .status-sub-error,.dashboard-page .dashboard-row-meta .status-sub-warn{flex-basis:100%;white-space:normal;overflow-wrap:anywhere}.dashboard-page .dashboard-row-meta .status-sub::before{content:none}.dashboard-page .project-attention-action{min-height:44px;display:inline-flex;align-items:center}.dashboard-page .paper-table .anon-sub .commit-hash,.dashboard-page .paper-table .anon-sub .conf-tag{display:none}}@media (max-width:767px){.container.dashboard-page{padding-top:12px}.dashboard-heading .paper-page-title{font-size:24px}.dashboard-heading-actions .btn-ink{min-height:44px}.dashboard-new-label{display:none}.dashboard-new-mobile{display:inline}.dashboard-quota>summary{min-height:44px;gap:6px;padding:10px;font-size:12px;line-height:20px}.dashboard-quota .quota-panel{max-height:calc(100dvh - 184px);overflow-y:auto;overscroll-behavior:contain}.dashboard-page .dashboard-filter-row{flex-wrap:wrap;gap:8px;margin-top:12px}.dashboard-page .dashboard-filter-row .search-wrap{flex:1 1 0}.dashboard-page .dashboard-filter-row .form-control{height:44px}.dashboard-page .dashboard-filters-toggle{display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:8px;min-height:44px;background:0 0;border:0}.dashboard-page .dashboard-filter-controls{display:none;width:100%}.dashboard-page .dashboard-filter-controls.expanded{display:flex;flex-wrap:wrap;gap:8px}.dashboard-page .dashboard-filter-controls .btn-group{width:100%;display:flex}.dashboard-page .dashboard-filter-controls .btn-group .btn{flex:1;min-height:44px}.dashboard-extra-filters{display:flex;width:100%}.dashboard-page .dashboard-extra-filters .dropdown{flex:1;min-width:0}.dashboard-page .dashboard-extra-filters .dropdown-toggle{width:100%;height:44px}.dashboard-type-filter .type-filter-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dashboard-type-filter .project-type-svg{display:inline-block}.dashboard-page .dashboard-meta{margin-top:8px;gap:4px 8px}.dashboard-view-tabs{gap:16px}.dashboard-view-tabs button{min-width:44px;min-height:44px;font-size:12px}.dashboard-count{font-size:11px}.dashboard-count{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.dashboard-view-long{display:none}.dashboard-view-short{display:inline}.dashboard-active-filters{display:contents}.dashboard-page .filter-chip{font-size:11px;padding:0 8px;min-height:44px;gap:4px}.dashboard-page .filter-chip-close{min-width:44px;min-height:44px;margin:0}.dashboard-page .btn-link-inline{min-height:44px;font-size:12px}.dashboard-page .paper-table-dashboard{width:calc(100% + 36px);margin:8px -18px 0;border-left:0;border-right:0;border-radius:0}.dashboard-page .paper-table .repo-name{font-size:13px}.project-details{padding:16px 12px}.project-details-list{grid-template-columns:1fr;gap:12px}.project-name-controls{flex-wrap:wrap}.project-name-controls .form-control{flex-basis:100%}.project-name-controls .btn{min-height:44px}}@media (max-width:360px){.dashboard-attention-prefix{display:none}.dashboard-attention-prefix+span{text-transform:capitalize}}.ui-icon-definitions{position:absolute;width:0;height:0;overflow:hidden;pointer-events:none}.ui-icon{display:inline-block;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0;vertical-align:-3px}.ui-icon.sort-direction{width:14px;height:14px}.pr-title-row{display:flex;align-items:flex-start;gap:16px}.pr-title-row .pr-title{flex:1;min-width:0}.pr-title-row>.btn{flex-shrink:0}.ui-icon.record-type-icon{color:var(--ink-muted)}.btn .ui-icon,.dropdown-item .ui-icon{width:16px;height:16px}.btn-icon-dots .ui-icon{width:18px;height:18px}.anonymize-page .paper-page-title,.paper-page .paper-page-title{font-size:28px}.paper-faq,.paper-page{padding-top:24px}.paper-page .paper-page-lede{margin-bottom:16px;overflow-wrap:anywhere}.paper-page-title{overflow-wrap:anywhere}.anonymize-page .form-control,.paper-page .form-control{min-height:40px;font-family:var(--font-sans);font-size:14px;border-radius:var(--radius-control)}.anonymize-page textarea.form-control,.paper-page textarea.form-control{height:auto}.anonymize-page .btn,.paper-page .btn,.status-bar-actions .btn{min-height:var(--button-height);padding:7px var(--button-padding-x);font:500 var(--button-font-size)/var(--button-line-height) var(--font-sans);border-radius:var(--radius-button);gap:6px}.anonymize-page .btn.btn-sm,.paper-page .btn.btn-sm,.status-bar-actions .btn{min-height:var(--button-compact-height);padding:5px 10px;font-size:13px;border-radius:6px}.anonymize-page .btn-ink,.paper-page .btn-ink{min-height:40px;padding:9px 15px}.btn-hero>i,.btn>.project-type-svg,.btn>.ui-icon,.btn>i{width:16px;height:16px;margin:0!important;flex-shrink:0}.btn.dropdown-toggle::after{margin-left:0}.btn.disabled,.btn:disabled{cursor:not-allowed}.btn.disabled:hover,.btn:disabled:hover{filter:none}.paper-page .btn.btn-link{background:0 0;border-color:transparent}.auth-continue.btn{min-height:44px;padding:11px 16px!important}.anonymize-page .btn.btn-icon,.paper-page .btn.btn-icon{width:32px;height:32px;min-width:32px;min-height:32px;padding:0}.paper-page .btn.btn-icon-dots{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;min-height:32px;padding:0;border:0;background:0 0;color:var(--ink-muted)}.paper-page .btn-icon-dots:hover{background:var(--accent-soft);color:var(--accent)}.paper-page .btn-group .btn-primary:active,.paper-page .btn-group .btn-primary:hover{background:var(--accent-soft)!important;color:var(--accent)}.paper-page .paper-section-eyebrow{font-family:var(--font-sans);font-size:16px;font-weight:600;color:var(--color);text-transform:none;letter-spacing:0;margin-bottom:12px}.paper-page .paper-field-label{font-family:var(--font-sans);font-size:13px;letter-spacing:0}.paper-tabs .paper-tab{min-height:44px}.paper-pill,.status-pill{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none;border-radius:6px}.paper-meta-rule{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none;gap:12px 24px}.paper-meta-rule b:not(.commit-hash){font-family:var(--font-sans);font-weight:500}.connections-grid{gap:16px}.connections-grid>section{background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);padding:20px}.connections-heading{margin-bottom:24px}.claim-page .claim-form,.status-page>.paper-settings-section{padding:20px;margin-top:24px;border:1px solid var(--border-color);border-radius:var(--radius-surface);background:var(--paper-card)}.claim-page .claim-form{max-width:640px}.status-page .anonymize-submit-bar{background:0 0;gap:8px;flex-wrap:wrap}.anonymize-landing{padding-top:32px}.paper-settings .quota-usage{grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}.paper-settings .quota-usage .quota-item+.quota-item{padding-top:0;border-top:0;padding-left:16px;border-left:1px solid var(--border-color)}.paper-settings .quota-usage .quota-header{flex-direction:row;align-items:center;flex-wrap:wrap}.paper-settings .quota-usage .quota-value{white-space:normal;flex-wrap:wrap}.paper-faq-hero{padding-bottom:24px;margin-bottom:24px}.paper-faq-toc-head{font-family:var(--font-sans);font-size:12px;text-transform:none;letter-spacing:0;margin-bottom:8px}.paper-faq-eyebrow{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:14px}.paper-faq-title{font-family:var(--font-serif);font-weight:400;font-size:clamp(2.4rem,4.6vw,3.6rem);line-height:1.05;letter-spacing:-.02em;max-width:13em;margin-bottom:16px;text-wrap:balance}.paper-faq-title em{font-style:italic;color:var(--accent)}.paper-faq-lede{font-size:14px}.paper-faq-body{grid-template-columns:180px minmax(0,1fr);gap:40px}.faq-section h4.faq-cat{font:400 30px/1.15 var(--font-serif);letter-spacing:-.015em;padding-bottom:12px}.faq-section h4.faq-cat .ui-icon{display:none}.faq-section .panel-group{margin-bottom:32px}.faq-section .panel-default .panel-title a{font:500 15px/1.5 var(--font-sans);padding:16px 0 16px 28px;min-height:44px}.faq-section .panel-default .panel-body{padding-left:28px!important}.faq-section .panel-default .panel-title a:after{content:"";top:22px;width:8px;height:8px;border-right:1.5px solid var(--ink-soft);border-bottom:1.5px solid var(--ink-soft);transform:translateX(4px) rotate(45deg);transition:transform .15s ease}.faq-section .panel-default .panel-title a.collapsed:after{content:"";transform:translateX(2px) rotate(-45deg)}.faq-section .panel-default .panel-title a:hover:after{border-color:var(--color)}.paper-faq-toc nav a.active{color:var(--color);font-weight:500}.paper-empty-title{font:600 32px/1.2 var(--font-sans);letter-spacing:-.025em}.paper-empty-title em{font-style:normal}.btn-hero{justify-content:center;gap:8px;min-height:48px;padding:13px 20px;font-size:15px;line-height:20px;background:var(--accent);border-color:var(--accent);color:#fff;border-radius:var(--radius-button)}.dark-mode .btn-hero{background:#1f6a64;border-color:#1f6a64;color:#fff}.btn-hero:hover{background:var(--accent);color:#fff;filter:brightness(.94)}.dark-mode .btn-hero:hover{background:#24786f;border-color:#24786f;color:#fff;filter:none}.paper-table:not(.paper-table-dashboard){border-left:0;border-right:0;border-radius:0}.paper-table:not(.paper-table-dashboard) .paper-table-head{border-radius:0}.paper-table .cell-anon{gap:8px}.paper-table .cell-anon .anon-text{flex:1}.paper-table-conferences .cell-anon .repo-name{font-family:var(--font-sans);font-weight:600}.paper-table .anon-sub>a,.paper-table .anon-sub>span{min-width:0;overflow:hidden;text-overflow:ellipsis}.paper-table .anon-sub .ui-icon{width:14px;height:14px}.paper-page .filter-chip{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none;border-radius:6px}.paper-page .filter-chip-close .ui-icon{width:12px;height:12px}.admin-nav{gap:4px;border:0;border-bottom:1px solid var(--border-color);border-radius:0;padding:0 0 8px;background:0 0}.admin-nav a{font-family:var(--font-sans);font-size:13px;letter-spacing:0;text-transform:none;border-radius:6px;min-height:40px;padding:8px 12px}.admin-nav a.active{background:var(--accent-soft);color:var(--accent)}.admin-nav .ui-icon{width:16px;height:16px}.admin-filter-toolbar{background:0 0;border:0;padding:0}.admin-filter-inline>label,.admin-filter-inline>span{font-family:var(--font-sans);text-transform:none;letter-spacing:0}.admin-summary .summary-total{font-family:var(--font-sans);font-size:18px;font-weight:600}.admin-summary .summary-pill{font:500 13px/20px var(--font-sans);min-height:32px;padding:5px 10px;border-radius:6px}.admin-summary .summary-pill.active{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}.admin-summary .summary-pill .count{font-family:var(--font-sans)}.admin-active-chip{font-family:var(--font-sans);font-size:12px}.admin-active-chip button .ui-icon{width:12px;height:12px}.user-detail-card .user-header h2{font:600 20px/1.4 var(--font-sans)}.errors-page .errors-toolbar .select-wrap label,.errors-page .volume-head,.overview-page .ov-panel-meta,.paper-detail-grid .detail-label,.paper-progress .paper-progress-label,.status-page .paper-error-eyebrow,.user-detail-grid .detail-label{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.overview-page .ov-chart-title,.overview-page .ov-panel-title,.paper-error-title{font:600 16px/1.4 var(--font-sans)}.paper-error-head>.ui-icon,.paper-ratelimit-head>.ui-icon{margin-top:3px;color:var(--status-error)}.paper-ratelimit-head>.ui-icon{color:var(--status-progress)}.errors-page .seg-tabs{border-radius:var(--radius-control)}.errors-page .seg-tabs button{min-height:36px;padding:7px 12px;font:500 14px/20px var(--font-sans)}.errors-page .seg-tabs button.active{background:var(--accent-soft);color:var(--accent)}.errors-page .errors-list,.errors-page .kpi-card:hover,.errors-page .volume-chart,.overview-page .ov-chart-card,.overview-page .ov-panel-card{box-shadow:none}.errors-page .kpi-value,.overview-page .ov-daily-value,.overview-page .ov-kpi-value,.q-card-count,.user-hero-name{font-family:var(--font-sans);font-size:28px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.errors-page .kpi-label,.overview-page .ov-daily-label,.overview-page .ov-kpi-label,.q-card-name,.q-section-label,.user-hero-details .detail-label{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.overview-page .ov-daily-card{background:var(--paper-card);box-shadow:none;border-radius:var(--radius-surface)}.dark-mode .overview-page .ov-daily-value,.overview-page .ov-daily-value{color:var(--accent)}.errors-page .kpi-card,.overview-page .ov-kpi-card{box-shadow:none;border-radius:var(--radius-surface)}.q-card{border-radius:var(--radius-surface)}.q-card.selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}.q-range-btns{border-radius:var(--radius-control)}.q-job-detail-actions .btn,.q-range-btns .btn{font-family:var(--font-sans);font-size:13px}.q-range-btns .btn.active{background:var(--accent-soft);color:var(--accent)}@media (max-width:900px){.paper-settings .quota-usage{grid-template-columns:1fr;gap:16px}.paper-settings .quota-usage .quota-item+.quota-item{padding-left:0;border-left:0;border-top:1px solid var(--border-color);padding-top:16px}.paper-table:not(.paper-table-dashboard) .paper-table-row{gap:4px 8px!important;padding:8px 12px}.paper-table:not(.paper-table-dashboard) .cell-anon{flex:1 1 calc(100% - 52px);order:1}.paper-table:not(.paper-table-dashboard) .cell-actions{order:2;flex:0 0 44px}.paper-table .admin-users-row>[role=cell]:first-child,.paper-table.paper-table-repos.has-bulk .paper-table-row>[role=cell]:first-child{flex:0 0 24px;order:0}.paper-table .admin-users-row .cell-anon,.paper-table.paper-table-repos.has-bulk .cell-anon{flex-basis:calc(100% - 84px)}.paper-table:not(.paper-table-dashboard) .cell-anon .anon-sub{font-size:12px;gap:6px}.paper-table:not(.paper-table-dashboard) .cell-status .status-sub{font-size:12px}.paper-table .cell-expires::before,.paper-table .cell-views::before{content:none}.paper-table .cell-expires .mobile-meta-icon,.paper-table .cell-views .mobile-meta-icon{display:inline-block;width:14px;height:14px}.paper-faq-body{grid-template-columns:1fr;gap:24px}}@media (max-width:767px){.paper-faq,.paper-page{padding-top:20px}.paper-settings .paper-account-email .btn[type=submit]{min-height:40px;padding:9px 18px}.anonymize-page .paper-page-title,.paper-page .paper-page-title,.paper-page.admin-page .paper-page-title{font-size:24px}.paper-faq-title{font-size:40px}.anonymize-page .form-control,.paper-page .form-control{min-height:44px}.anonymize-page .btn,.connection-link,.draft-discard,.paper-faq-toc a,.paper-page .btn,.paper-settings-toc nav a{min-height:44px}.anonymize-page .btn,.paper-page .btn,.status-bar-actions .btn{min-height:44px;padding:11px 14px;font-size:14px;line-height:20px;white-space:normal}.anonymize-page .btn.btn-sm,.paper-page .btn.btn-sm{min-height:44px;padding:11px 12px;font-size:14px}.anonymize-page .btn.btn-icon,.copy-link-inline,.paper-page .btn.btn-icon,.paper-page .btn.btn-icon-dots,.project-close{width:44px;height:44px;min-width:44px;min-height:44px;padding:0}.dashboard-page .dashboard-filter-row .btn-group .btn,.dashboard-page .dashboard-filter-row .dropdown-toggle{height:auto;min-height:44px;font-size:14px;padding:11px 10px}.dashboard-heading-actions .btn-ink{padding:11px 16px}.dashboard-page .dashboard-filters-toggle{padding:11px 10px}.errors-page .btn-icon,.pr-title-row .btn,.status-bar-actions .btn{min-width:44px;min-height:44px;padding:0}.status-bar-actions .btn{width:44px;height:44px}.errors-page .seg-tabs button,.paper-tab{min-height:44px}.errors-page .seg-tabs button{padding:11px 12px}.status-bar-actions{flex-wrap:wrap;max-width:100%}.leftCol-close,.tree-search-clear{min-width:44px;min-height:44px}.anonymize-landing{padding:20px 18px}.anonymize-landing .repo-access-switch button,.source-url-label-row .repo-access-switch button{min-height:44px;padding:11px 10px;font-size:13px}.paper-settings-body{margin-top:16px;padding-top:0;gap:16px}.paper-settings-toc nav{gap:0 12px}.paper-settings-toc nav a{display:inline-flex;align-items:center}.paper-faq{padding-left:18px;padding-right:18px}.paper-faq-toc nav{display:flex;flex-direction:row;flex-wrap:wrap;gap:0 16px}.paper-faq-toc a{display:inline-flex;align-items:center;padding:8px 0;border-left:0;border-bottom:2px solid transparent;margin-left:0}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover{border-bottom-color:var(--accent);color:var(--accent)}.claim-page .claim-form,.status-page>.paper-settings-section{padding:16px;margin-top:16px}.admin-nav{flex-direction:row;flex-wrap:nowrap;overflow-x:auto;gap:4px;max-width:100%;margin:8px 0 16px}.admin-nav a{flex-shrink:0;min-height:44px}.admin-summary .summary-pill{min-height:44px;padding:11px 12px;font-size:14px}.admin-active-chip button{min-width:44px;min-height:44px}.admin-filter-row{gap:8px}.admin-filter-row .admin-filter-inline{gap:8px;flex-wrap:wrap}.errors-page .kpi-grid{grid-template-columns:1fr 1fr;gap:12px}.q-cards{flex-direction:row}.q-card{min-width:130px}.q-header-actions{flex-wrap:wrap;gap:8px}}.paper-page.container:not(.auth-page){max-width:1180px}.paper-page>.row{margin-left:0;margin-right:0}.paper-page>.row>.w-100,.paper-page>.row>[class*=col]{padding-left:0;padding-right:0}.anonymize-landing{align-items:flex-start}.status-page .paper-support-grid{margin-bottom:0}.explorer-content .ace_editor{font-family:var(--font-mono);line-height:1.6}.dark-mode .explorer-content .ace-nord-dark,.dark-mode .explorer-content .ace-nord-dark .ace_gutter{background:var(--main-bg-color)}.explorer-content .file-content.markdown-body{max-width:960px;padding:32px 40px 96px}.leftCol-project-header .project-name{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.file-error{font-size:inherit;font-weight:inherit;text-align:left}.file-error-card{width:100%;max-width:520px;margin-top:0}.file-error-msg{margin:12px 0 0;font-size:14px;line-height:1.55;color:var(--ink-soft)}.file-error-actions{display:flex;gap:8px;margin-top:16px}.file-error .paper-empty-title{font-size:20px;text-align:center}@media (min-width:992px){.leftCol-foot{padding-bottom:72px}}.explorer-content .ace-chrome,.explorer-content .ace-chrome .ace_gutter{background:var(--main-bg-color)}@media (max-width:991px){.explorer-content .file-content.markdown-body{padding:20px 16px 96px}.anonymize-landing{padding:20px 18px}.anonymize-landing-inner{padding:0}.paper-faq-toc nav,.paper-settings-toc nav{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}.paper-faq-toc nav a,.paper-settings-toc nav a{flex-shrink:0;white-space:nowrap}.paper-faq-body,.paper-settings-body{grid-template-columns:minmax(0,1fr)}.paper-faq-toc,.paper-settings-toc{position:static}.paper-faq-toc nav,.paper-settings-toc nav{display:flex;flex-direction:row;gap:0 16px}.paper-faq-toc nav a,.paper-settings-toc nav a{display:inline-flex;align-items:center;padding:8px 0;margin-left:0;border-left:0;border-bottom:2px solid transparent}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover,.paper-settings-toc nav a.active,.paper-settings-toc nav a:hover{border-bottom-color:var(--accent)}.paper-page .dashboard-filter-controls .dropdown .dropdown-toggle{justify-content:flex-start;gap:6px}.paper-page .dashboard-filter-controls .dropdown .dropdown-toggle::after{margin-left:auto}}@media (max-width:480px){.paper-page .paper-page-lede{display:block;-webkit-line-clamp:unset;overflow:visible}}.navbar .btn-signin,.navbar .btn-signin:hover{background:var(--accent);border-color:var(--accent);color:#fff!important}.dark-mode .navbar .btn-signin,.dark-mode .navbar .btn-signin:hover{color:#1a1815!important}.navbar .btn-signin:hover{filter:brightness(.94)}.paper-progress .paper-progress-bar,.paper-progress.paper-progress-ready .paper-progress-bar{background:var(--accent)}.paper-progress .paper-progress-label{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.paper-error-eyebrow,.paper-support-card .paper-support-eyebrow{font-family:var(--font-sans);font-size:12px;font-weight:500;letter-spacing:0;text-transform:none}.leftCol-foot .last-update{font-family:var(--font-sans);font-size:11px;letter-spacing:0}.paper-proof-venues span{font-family:var(--font-sans);font-size:18px;font-weight:600;letter-spacing:.02em;color:var(--ink-soft)}.markdown-body mark.redaction{background:var(--accent-soft);color:var(--accent);border-radius:3px;padding:0 2px}.ace_marker-layer .ace_redaction{position:absolute;background:var(--accent-soft);border-radius:3px}.leftCol-project-header{flex-wrap:wrap;row-gap:2px}.project-expiry{flex:1 0 100%;display:flex;align-items:center;gap:4px;font-size:12px;color:var(--ink-muted)}.project-expiry .ui-icon{width:12px;height:12px}.status-bar-actions .btn-icon-only{width:32px;padding:0;justify-content:center}.share-link-row{display:flex;gap:8px;margin-top:4px}.share-link-input{flex:1;min-width:0;font-family:var(--font-mono);font-size:14px;background:var(--paper-bg-alt)}.share-link-row .btn{flex-shrink:0;display:inline-flex;align-items:center;gap:6px;min-width:112px;justify-content:center}.share-link-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.share-link-actions .btn{display:inline-flex;align-items:center;gap:6px}.status-page .paper-support-grid{gap:0 32px}.status-page .paper-support-card{padding:16px 0 0;border:0;border-top:1px solid var(--border-color);border-radius:0;background:0 0}.status-page .paper-support-card:hover{border-color:var(--color)}@media (min-width:992px){.paper-table .cell-expires{white-space:nowrap}}.paper-settings .paper-settings-main .paper-settings-section{background:0 0;border:0;border-bottom:1px solid var(--border-color);border-radius:0;padding:0 0 28px;margin-bottom:28px}.paper-settings .paper-settings-main .paper-settings-section:last-of-type{border-bottom:0}@media (max-width:991px){.paper-settings .paper-settings-main .paper-settings-section{padding:0 0 20px;margin-bottom:20px}.share-link-row{flex-direction:column}.share-link-actions{flex-direction:column}.share-link-actions .btn{justify-content:center}.status-page .paper-support-grid{row-gap:20px}}.paper-page .q-range-btns{display:inline-flex;gap:2px;padding:2px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);overflow:visible}.paper-page .q-range-btns .btn,.paper-page .q-range-btns .btn+.btn{min-height:0;height:28px;padding:0 10px;border:0;border-radius:4px;background:0 0;color:var(--ink-muted);font:500 13px/1 var(--font-sans)}.paper-page .q-range-btns .btn:hover{background:var(--hover-bg-color);color:var(--color)}.paper-page .q-range-btns .btn.active{background:var(--accent-soft);color:var(--accent)}.q-header-actions>.btn{height:34px;min-height:34px}.paper-page .btn.btn-danger-soft{background:0 0;border-color:var(--status-error);color:var(--status-error)}.paper-page .btn.btn-danger-soft:hover{background:var(--status-error-soft-hover);border-color:var(--status-error);color:var(--status-error)}.q-auto-refresh,.q-section-label,.q-state-chip{font-family:var(--font-sans);letter-spacing:0;text-transform:none}.q-state-chip{font-size:12px;text-transform:capitalize}.overview-page .ov-ebar-fill{display:block}.paper-table .paper-table-row.repo-error{border-left:0;box-shadow:inset 2px 0 0 var(--status-error);padding-left:16px}.admin-page .q-table th,.admin-page .type-badge,.admin-page .user-detail-section h2,.admin-page section>h2,.admin-page th,.admin-page>h2,.errors-page .col-count,.errors-page .col-mod,.errors-page .col-msg,.errors-page .col-sev,.errors-page .col-status,.errors-page .col-when,.overview-page .ov-chart-legend,.overview-page .ov-ebar-label,.overview-page .ov-route-n,.overview-page .ov-route-name,.q-card-sub,.q-delay-hint,.q-legend-completed,.q-legend-exec,.q-legend-failed,.q-section-right,.q-stat-label,.user-detail-grid .detail-label{font-family:var(--font-sans);letter-spacing:0;text-transform:none}.admin-page th,.errors-page [class^=col-]{font-size:12px;font-weight:500;color:var(--ink-muted)}.q-card-sub,.q-stat-label,.user-detail-grid .detail-label{font-size:12px}.q-stat-value{font-family:var(--font-sans);font-size:20px;font-weight:600;font-variant-numeric:tabular-nums}.q-state-badge{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:capitalize}.admin-page .type-badge{font-size:12px;text-transform:none}.errors-page .col-sev span,.errors-page .sev-label{text-transform:capitalize}.dark-mode .overview-page .ov-daily-value,.overview-page .ov-daily-value{color:var(--color)}.dark-mode .overview-page .ov-spark-fill-alt,.dark-mode .overview-page .ov-spark-fill-new-user,.dark-mode .overview-page .ov-spark-fill-user,.overview-page .ov-spark-fill-alt,.overview-page .ov-spark-fill-new-user,.overview-page .ov-spark-fill-user{background:var(--accent)}.dark-mode .overview-page .ov-dot-new-user-fill,.dark-mode .overview-page .ov-dot-ok-fill,.dark-mode .overview-page .ov-dot-user-fill,.overview-page .ov-dot-new-user-fill,.overview-page .ov-dot-ok-fill,.overview-page .ov-dot-user-fill{background:var(--accent)}.errors-page .seg-tabs{display:inline-flex;gap:2px;padding:2px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card)}.errors-page .seg-tabs button,.errors-page .seg-tabs button+button{min-height:0;height:30px;padding:0 10px;border:0;border-radius:4px;background:0 0;color:var(--ink-muted);font:500 13px/1 var(--font-sans)}.errors-page .seg-tabs button:hover{background:var(--hover-bg-color);color:var(--color)}.errors-page .seg-tabs button.active{background:var(--accent-soft);color:var(--accent)}.user-detail-card .user-header .status-dot-wrap{display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:400;color:var(--ink-soft)}.user-detail-card .user-actions{display:flex;flex-wrap:wrap;gap:8px}.admin-page .paper-table .cell-anon .anon-sub{flex-wrap:wrap;row-gap:2px}.admin-page .paper-table .anon-sub>a,.admin-page .paper-table .anon-sub>span{flex:0 1 auto;max-width:100%;white-space:nowrap}.admin-page .paper-table .cell-views .mobile-meta-icon{margin-right:4px}.q-jobs{overflow-x:auto}.q-job-detail-label{font-family:var(--font-sans);font-size:12px;letter-spacing:0;text-transform:none}.admin-section-header h2{font-family:var(--font-sans);font-size:16px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--color)}.errors-page .sev-label{font-family:var(--font-sans);font-size:13px;letter-spacing:0}.email-prompt{width:min(560px,calc(100vw - 32px));padding:28px;border:1px solid var(--paper-border,#e5e1d9);border-radius:12px;background:var(--paper-bg,#faf9f6);color:inherit;box-shadow:0 16px 64px #0003}.email-prompt::backdrop{background:#0006}.email-prompt h2{margin:0 0 12px;font-size:24px}.email-prompt p{margin-bottom:20px}.email-prompt-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;margin-top:24px}body.dark-mode .email-prompt{background:#242320;color:#faf9f6}.file-error .viewer-error{width:100%;max-width:65ch;padding:2rem 1rem;overflow-wrap:anywhere;font-size:1rem;font-weight:400;text-align:left}.viewer-error .paper-empty-title{margin-bottom:1rem}.viewer-error p{line-height:1.6}.viewer-error .paper-error-actions{display:flex;flex-wrap:wrap;gap:.75rem}.markdown-body .markdown-alert{margin:1rem 0;padding:.5rem 1rem;border-left:4px solid var(--alert-color,#0969da)}.markdown-body .markdown-alert-note{--alert-color:#0969da}.markdown-body .markdown-alert-tip{--alert-color:#1a7f37}.markdown-body .markdown-alert-important{--alert-color:#8250df}.markdown-body .markdown-alert-warning{--alert-color:#9a6700}.markdown-body .markdown-alert-caution{--alert-color:#cf222e}.markdown-body .markdown-alert-title{color:var(--alert-color);font-weight:600;display:flex;align-items:center;gap:.5rem}.markdown-body .markdown-alert-title svg{fill:currentColor;flex-shrink:0}.markdown-body .markdown-alert>:last-child{margin-bottom:0}body.dark-mode .markdown-alert-note{--alert-color:#79c0ff}body.dark-mode .markdown-alert-tip{--alert-color:#7ee787}body.dark-mode .markdown-alert-important{--alert-color:#d2a8ff}body.dark-mode .markdown-alert-warning{--alert-color:#e3b341}body.dark-mode .markdown-alert-caution{--alert-color:#ff7b72}.markdown-body .emoji{display:inline;vertical-align:text-bottom}.markdown-body .footnotes{margin-top:2rem;border-top:1px solid var(--border-color,#d0d7de);padding-top:1rem;font-size:.875em}.markdown-body .katex-display{overflow-x:auto;overflow-y:hidden;padding:.25rem 0}.markdown-body .markdown-diagram{margin:1rem 0;border:1px solid var(--border-color,#d0d7de);padding:.75rem}.markdown-body .markdown-diagram svg{display:block;width:100%;max-height:360px}.markdown-body .markdown-diagram path{fill:#7cc7be;fill-opacity:.5;stroke:var(--link-color,#1f6a64);stroke-width:1;vector-effect:non-scaling-stroke}.markdown-body .diagram-controls{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem}.markdown-body .diagram-controls button{min-width:44px;min-height:44px;padding:.25rem .75rem;border:1px solid var(--border-color,#d0d7de);background:var(--main-bg-color,#fff);color:inherit;border-radius:4px}.markdown-body .diagram-controls button:hover{background:var(--hover-bg-color,#f6f8fa)}.markdown-body .diagram-controls button:focus-visible{outline:2px solid var(--link-color,#0969da);outline-offset:2px}.markdown-body .hljs-keyword,.markdown-body .hljs-selector-tag{color:#cf222e}.markdown-body .hljs-attr,.markdown-body .hljs-string{color:#0a3069}.markdown-body .hljs-literal,.markdown-body .hljs-number,.markdown-body .hljs-title{color:#8250df}.markdown-body .hljs-comment{color:#57606a}body.dark-mode .markdown-body .hljs-keyword{color:#ff7b72}body.dark-mode .markdown-body .hljs-attr,body.dark-mode .markdown-body .hljs-string{color:#a5d6ff}body.dark-mode .markdown-body .hljs-literal,body.dark-mode .markdown-body .hljs-number,body.dark-mode .markdown-body .hljs-title{color:#d2a8ff}body.dark-mode .markdown-body .hljs-comment{color:#b1bac4}body.dark-mode .markdown-body .gh-light-mode-only,body:not(.dark-mode) .markdown-body .gh-dark-mode-only{display:none}.billing-plan{padding:1.5rem 0;border-bottom:1px solid var(--paper-border,#d8d4cd)}.billing-plan h3{display:flex;flex-wrap:wrap;gap:.5rem 1.5rem;justify-content:space-between}.billing-price{font-variant-numeric:tabular-nums;font-weight:400}.billing-actions{display:flex;flex-wrap:wrap;gap:.75rem;margin-top:1rem}#settings-billing p{max-width:72ch}.repository-hosting{border-top:1px solid var(--border-color);margin-top:24px}.repository-hosting h2{font-size:1.5rem}.hosting-choices{border:0;padding:0;margin:20px 0;min-width:0}.hosting-choices legend{font-size:1rem;font-weight:600}.hosting-choice{display:flex;align-items:flex-start;gap:12px;padding:16px;border:1px solid var(--border-color);background:var(--paper-card);cursor:pointer}.hosting-choice+.hosting-choice{border-top:0}.hosting-choice input{margin-top:5px;accent-color:var(--accent)}.hosting-choice small,.hosting-choice strong{display:block}.hosting-choice small{margin-top:6px;color:var(--ink-muted)}.hosting-actions{display:flex;flex-wrap:wrap;align-items:center;gap:16px;margin:20px 0}.repository-pass{margin:20px 0}.repository-pass summary{cursor:pointer;color:var(--link-color)}.repository-pass p{margin-top:12px}.task-review-active.anonymize-page{height:auto!important;min-height:100%;overflow:visible}.task-review-active .anonymize-workspace{flex:0 0 auto;overflow:visible}.repository-task{width:100%;max-width:1020px;margin:0 auto;padding:32px 24px 42px;color:var(--color);font-size:15px;line-height:1.55}.repository-task .paper-crumbs{margin-bottom:24px}.repository-task h1{font:normal 40px/1.15 'Iowan Old Style',Georgia,serif;margin:8px 0 10px}.repository-task h2{font:normal 27px/1.25 'Iowan Old Style',Georgia,serif;margin:0 0 12px}.repository-task h3{font:600 17px/1.5 var(--font-sans);margin:0 0 10px}.repository-task p{margin:0 0 16px}.task-lede,.task-muted{color:var(--ink-muted)}.task-trail{list-style:none;display:flex;flex-wrap:wrap;gap:24px;margin:30px 0;padding:18px 0;border-bottom:1px solid var(--border-color);color:var(--ink-muted);font-size:13px}.task-trail [aria-current]{color:var(--color);font-weight:600}.task-trail button{color:inherit;padding:0;border:0;background:0 0;font:inherit;cursor:pointer}.task-columns{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(245px,1fr);gap:34px;align-items:start}.task-section-title{display:flex;align-items:baseline;justify-content:space-between;gap:16px}.task-text-button{border:0;padding:7px 0;background:0 0;color:var(--link-color);cursor:pointer;font:inherit;text-align:left}.task-text-button:focus-visible,.task-trail button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}.task-summary{background:var(--paper-bg-alt);padding:24px;border-radius:5px}.task-summary dt{margin-top:18px;font-size:12px;font-weight:400;color:var(--ink-muted)}.task-summary dd{margin:3px 0;overflow-wrap:anywhere}.task-redactions,.task-summary h3{overflow-wrap:anywhere}.task-summary dl{margin-bottom:0}.task-summary-foot{border-top:1px solid var(--border-color);margin-top:28px;padding-top:24px}.task-summary-foot p{color:var(--ink-muted);margin:0}.task-hosting{border-top:1px solid var(--border-color);margin-top:22px;padding-top:20px}.repository-task .hosting-choices{margin:18px 0}.repository-task .hosting-choice{padding:18px;gap:13px;font-weight:400;margin:0}.repository-task .hosting-choice:first-of-type{border-radius:6px 6px 0 0}.repository-task .hosting-choice:last-of-type{border-radius:0 0 6px 6px}.repository-task .hosting-choice small{margin-top:0;font-size:13px;line-height:1.55}.task-terms{font-size:13px;color:var(--ink-soft)}.task-actions{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:24px 0 16px}.repository-task .btn{padding:10px 16px;border-radius:5px;font-size:15px}.task-status{background:var(--accent-soft);padding:16px 20px;margin:20px 0}.task-status p:last-child{margin:0}@media(max-width:650px){.repository-task{padding:24px 18px}.repository-task h1{font-size:32px}.task-columns{display:block}.task-summary{margin-top:28px}.task-trail{gap:15px;font-size:12px}}.repository-task{scroll-margin-top:70px}.task-columns{align-items:stretch}.repository-task .btn-ink{background:var(--color);border-color:var(--color);color:var(--canvas-bg-color);box-shadow:none}.repository-task .btn-ink:hover{background:var(--ink-soft);border-color:var(--ink-soft)}.task-field{margin-bottom:24px}.task-field>label{display:block;font-weight:600;margin-bottom:8px}.task-field input,.task-field select,.task-field textarea{width:100%;padding:12px;border:1px solid var(--border-color);border-radius:4px;background:var(--paper-card);color:var(--color);font:inherit}.task-field textarea{resize:vertical}.task-field small{display:block;margin-top:8px;font-size:13px;color:var(--ink-muted)}.task-field :is(input,select,textarea):focus-visible{outline:3px solid var(--accent);outline-offset:3px}.task-paybox{background:var(--paper-card);border:1px solid var(--border-color);border-radius:5px;padding:22px;margin-top:20px}.task-ready{border-left:3px solid var(--success,#2f7a44);padding:16px 20px;background:var(--paper-bg-alt);margin-bottom:22px}.repository-task.status-page .paper-progress{margin-top:24px}.task-date-options summary{cursor:pointer;color:var(--link-color);font-size:13px}.task-date-options[open] summary{margin-bottom:12px}.task-result{padding:18px;background:var(--paper-bg-alt);overflow-wrap:anywhere}.task-result .detail-label{font-size:12px;color:var(--ink-muted);margin-bottom:6px}.task-configure-columns{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start}.task-live-preview{min-width:0;border:1px solid var(--border-color);border-radius:5px;background:var(--paper-card)}.task-preview-head{padding:20px 24px;border-bottom:1px solid var(--border-color)}.task-preview-head h2{font-size:23px;margin-bottom:6px}.task-preview-head p{margin:0;font-size:13px}.task-preview-content,.task-preview-message{padding:24px}.task-preview-content{overflow:auto;max-height:650px;overflow-wrap:anywhere}.repository-task .task-preview-content h1{font:600 28px/1.3 var(--font-sans)}.repository-task .task-preview-content h2{font:600 22px/1.3 var(--font-sans)}@media(max-width:650px){.task-live-preview{margin-top:28px}.task-preview-content{max-height:none}}.anonymize-page .anonymize-topbar{padding:14px 22px;background:var(--canvas-bg-color)}.anonymize-page .anonymize-topbar-inner{max-width:none}.anonymize-page .anonymize-topbar-head{justify-content:space-between}.anonymize-page .anonymize-topbar-title{font:600 19px/1.25 var(--font-sans);margin:0}.anonymize-page .anonymize-split{display:grid;grid-template-columns:minmax(320px,42%) minmax(0,1fr);overflow:hidden}.anonymize-page .anonymize-form-col{width:auto;min-height:0;overflow:hidden!important;background:var(--paper-card)}.anonymize-page .anonymize-form-col .paper-settings-main{display:flex;flex-direction:column;height:100%;min-height:0;max-width:none;padding:0}.anonymize-fields{flex:1;min-height:0;overflow:auto;position:relative;scrollbar-width:thin}.anonymize-page .paper-settings-main .paper-settings-section{border:0;border-top:1px solid var(--border-color);border-radius:0;padding:18px 20px 20px;margin:0;box-shadow:none}.anonymize-page .paper-settings-main .paper-settings-section:first-child{border-top:0;padding-top:18px}.anonymize-page .paper-section-eyebrow{font:600 15px/1.4 var(--font-sans);margin:0 0 16px;letter-spacing:0}.anonymize-page .paper-field-label{font:500 13px/1.5 var(--font-sans);margin-bottom:8px}.anonymize-page .form-group{margin-bottom:16px}.anonymize-page .form-group:last-child{margin-bottom:0}.anonymize-page .form-control{font-size:14px;min-height:40px;padding:9px 10px;border-radius:4px}.anonymize-page textarea.form-control{line-height:1.6}.anonymize-page .form-grid-2{gap:12px}.anonymize-page .form-check{padding-top:10px;padding-bottom:10px;font-size:14px}.anonymize-page .form-check-label{font-size:14px;line-height:1.5}.anonymize-page .form-text{font-size:12px;line-height:1.6}.anonymize-page .repo-access-details{margin:12px 0 16px}.anonymize-page .repo-access-note{font-size:11px}.anonymize-page .anonymize-submit-bar{flex:none;position:static;margin:0;padding:16px 20px;background:var(--paper-card);border-top:1px solid var(--border-color)}.anonymize-page .anonymize-submit-bar .btn{width:100%;font-size:14px;padding:10px 12px;min-height:43px}.checkout-return-note{font-size:11px;line-height:1.5;color:var(--ink-muted);margin:8px 0 0;text-align:center}.anonymize-fields>.alert,.anonymize-fields>.anonymize-review-note,.anonymize-fields>.draft-status{margin:12px 20px}.anonymize-page .anonymize-preview-col{min-height:0;padding:20px;position:relative;background:var(--canvas-bg-color)}.anonymize-page .anonymize-preview-head{border:0;padding:0;min-height:32px;margin-bottom:14px;display:flex;align-items:center;justify-content:space-between}.anonymize-preview-head button,.anonymize-preview-head strong{font-size:13px}.anonymize-page .anonymize-preview-body{padding:24px;border-radius:6px;min-height:480px;overflow-wrap:anywhere;font:14px/1.7 var(--font-sans)}.anonymize-page .anonymize-preview-body h1{font-size:27px;line-height:1.2;margin:0 0 20px;padding-bottom:14px}.anonymize-page .anonymize-preview-body h2{font-size:18px;margin:28px 0 12px}.anonymize-page .anonymize-preview-body pre{font-size:12px;padding:15px}.anonymize-page.preview-expanded .anonymize-split{grid-template-columns:1fr}.anonymize-page.preview-expanded .anonymize-form-col{display:none}.hosting-usage{font-size:12px;color:var(--ink-muted);margin:0 0 4px}.expiration-choices{border:0;padding:0;margin:0 0 16px;min-width:0}.expiration-choice{display:flex;align-items:flex-start;gap:10px;padding:12px 0;margin:0;cursor:pointer;font-size:14px;font-weight:500;line-height:1.5}.expiration-choice input{width:16px;height:16px;margin:4px 0 0;flex:none;accent-color:var(--accent)}.expiration-choice small{display:block;font-size:12px;font-weight:400;color:var(--ink-muted);margin-top:2px}.dated-options{padding:2px 0 14px 26px}.hosting-date,.hosting-property{display:grid;grid-template-columns:48px minmax(0,1fr);gap:8px;align-items:center;margin-bottom:10px}.hosting-property{margin-bottom:0}.hosting-date .paper-field-label,.hosting-property label{font-size:12px;font-weight:400;margin:0}.hosting-date .invalid-feedback{grid-column:1/-1}.anonymize-page .conference-field{border-top:1px solid var(--border-color);padding:16px 0;margin:0}.subscription-choice{padding:16px;background:var(--paper-bg-alt);border-radius:6px;margin-top:4px}.subscription-choice h3{font-size:13px;font-weight:600;margin:0 0 10px}.billing-cycle{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;border:0;padding:0;margin:0 0 12px;min-width:0}.cycle-option{display:flex;align-items:center;gap:7px;padding:10px 8px;border:1px solid var(--border-color);border-radius:4px;background:var(--paper-card);font-size:12px;cursor:pointer;margin:0}.cycle-option.selected{border-color:var(--accent);background:var(--accent-soft)}.cycle-option input{accent-color:var(--accent);margin:0;flex:none}.subscription-choice p{font-size:12px;line-height:1.5;margin:0 0 8px}.subscription-choice p:last-child{margin:0}.hosting-terms{font-size:12px;line-height:1.5;color:var(--ink-muted)}.capacity-reason{font-size:13px}.capacity-options,.repository-pass{margin-top:12px;font-size:12px}.capacity-options summary,.repository-pass summary{cursor:pointer}.capacity-options p,.repository-pass p{margin:8px 0}.capacity-options .task-text-button{display:block}@media(max-width:767px){.anonymize-page .anonymize-split{display:block;overflow:visible}.anonymize-page .anonymize-form-col .paper-settings-main{height:auto}.anonymize-fields{overflow:visible}.anonymize-page .paper-settings-main .paper-settings-section{padding:18px 16px 20px}.anonymize-page .anonymize-topbar{padding:14px 16px}.anonymize-page .anonymize-preview-col{padding:16px}.anonymize-page .anonymize-preview-body{padding:20px}.anonymize-page .anonymize-submit-bar{padding:16px}.anonymize-page .preview-expand{display:none}.anonymize-page .anonymize-mobile-tabs{padding:0;gap:0;background:var(--paper-card)}.anonymize-mobile-tabs button{border-radius:0;padding:12px;min-height:44px}.anonymize-mobile-tabs button[aria-selected=true]{background:0 0;box-shadow:inset 0 -2px var(--accent)}.cycle-option{min-height:44px}}.anonymize-page{--font-sans:system-ui,-apple-system,sans-serif;font-family:var(--font-sans)}body:not(.dark-mode) .anonymize-page{--paper-card:#fff;--canvas-bg-color:#faf9f6;--paper-bg-alt:#f5f4f0;--border-color:#dedbd4;--color:#242320;--ink-muted:#68655e}.anonymize-page .repo-access-controls .btn{font-size:12px;padding:8px 12px;min-height:36px}.anonymize-page .form-check-label{font-weight:400}.anonymize-page .form-check{padding-left:26px}.anonymize-page .form-check input{width:16px;height:16px;margin-left:-26px;accent-color:var(--accent)}.anonymize-page .form-check small{margin-top:6px}.anonymize-page .btn.btn-icon.refresh-branches{display:inline-flex;align-items:center;justify-content:center;width:40px;min-width:40px;height:100%;min-height:40px;padding:0}@media (max-width:767px){.anonymize-page #branch,.anonymize-page #commit{min-height:44px}.anonymize-page .btn.btn-icon.refresh-branches{width:44px;min-width:44px;min-height:44px}}.anonymize-page .redaction-syntax{font-size:12px;margin-top:8px}.redaction-syntax summary{cursor:pointer;color:var(--link-color);min-height:28px}.anonymize-page .anonymize-preview-body h2{border:0;padding:0;font-weight:700}.anonymize-page .anonymize-preview-body h1{font-weight:700}.anonymize-page .anonymize-preview-body pre{background:var(--paper-bg-alt);line-height:1.7}.anonymize-page .anonymize-preview-body pre code{font:12px/1.7 ui-monospace,monospace;background:0 0;padding:0}.anonymize-page .anonymize-preview-body{--main-bg-color:var(--paper-card);background:var(--paper-card)}.anonymize-page .anonymize-submit-bar .btn{font-weight:600}.anonymize-page .form-grid-2 .form-group{margin-bottom:0}.anonymize-page .form-check small{margin-left:-26px}.user-settings{max-width:1060px;padding:28px 26px 40px}.user-settings .settings-title{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:22px}.user-settings .settings-title h1{font:650 26px/1.3 var(--font-sans);letter-spacing:-.025em;margin:0}.settings-title>span{color:var(--ink-muted);font-size:12px}.settings-nav{display:flex;gap:28px;border-bottom:1px solid var(--border-color)}.settings-nav button{border:0;border-radius:0;background:0 0;color:var(--ink-muted);padding:12px 0;min-height:44px;font:13px/1.4 var(--font-sans)}.settings-nav button[aria-current=page]{color:var(--color);font-weight:600;box-shadow:inset 0 -2px var(--accent)}.settings-nav button:hover{color:var(--accent)}.user-settings .settings-scope{color:var(--ink-muted);font-size:12px;margin:20px 0 4px}.user-settings .settings-row{display:grid;grid-template-columns:210px minmax(0,1fr);gap:32px;padding:22px 0;border-bottom:1px solid var(--border-color)}.user-settings .settings-label,.user-settings .settings-row>h2{font-size:13px;font-weight:600;line-height:1.5;margin:8px 0 0;display:block}.user-settings .settings-help{font-size:12px;color:var(--ink-muted);margin:7px 0 0;line-height:1.6}.user-settings .form-control{font-size:14px;min-height:40px;padding:9px 10px;border-radius:4px;background:var(--paper-card)}.user-settings textarea.form-control{line-height:1.6}.settings-syntax{font-size:12px;margin-top:8px}.settings-syntax summary{cursor:pointer;color:var(--link-color);min-height:26px}.settings-syntax p{margin:8px 0}.settings-checks{display:grid;grid-template-columns:1fr 1fr;gap:14px 18px;padding:4px 0;margin-bottom:12px}.user-settings .settings-check{display:flex;gap:9px;align-items:center;font-size:13px;font-weight:400;margin:0}.user-settings .settings-check .form-check-input{position:static;width:16px;height:16px;margin:0;flex:none;accent-color:var(--accent)}.settings-save{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:18px 0 22px}.settings-save-status{color:var(--ink-muted);font-size:12px}.settings-save #save{margin-left:auto;min-width:125px}.settings-save-error{color:var(--status-error);flex-basis:100%}.user-settings .settings-inline{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}.user-settings .btn{font-size:13px;min-height:40px;padding:9px 13px}.settings-inline .btn{flex-shrink:0}.user-settings #settings-quota{margin:22px 0 16px}.user-settings .quota-row{gap:20px}.user-settings .quota-item{padding:0;background:0 0;border:0}.user-settings .quota-label{font-size:12px}.user-settings .quota-value{font-size:13px;margin:6px 0}.user-settings .quota-value strong{font-size:14px}.user-settings .quota-percent,.user-settings .quota-remaining{font-size:11px}.user-settings .settings-plan{background:var(--paper-card);border:1px solid var(--border-color);border-radius:6px;padding:20px;margin:20px 0 0}.user-settings .settings-plan h3{font-size:15px;margin:0 0 10px}.user-settings .settings-plan>p{font-size:13px}.settings-cycles{display:flex;flex-wrap:wrap;gap:10px;border:0;padding:0;margin:16px 0}.settings-cycles label{display:flex;align-items:center;gap:8px;border:1px solid var(--border-color);background:var(--paper-card);border-radius:5px;padding:12px;font-size:13px;margin:0;cursor:pointer;flex:1}.settings-cycles label.selected{border-color:var(--accent);background:var(--accent-soft)}.settings-cycles input{accent-color:var(--accent)}.settings-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}.settings-notice{background:var(--paper-bg-alt);border:1px solid var(--border-color);border-radius:5px;padding:14px 16px;margin:16px 0;font-size:13px}.settings-notice p{margin:6px 0 12px}.settings-text-button{border:0;background:0 0;color:var(--link-color);padding:8px 0;font-size:12px}.settings-pass{margin-top:18px}.settings-pass label{display:block;font-size:13px;margin-bottom:8px}.settings-pass .btn{margin-top:12px}.settings-purchase{padding:12px 0;border-bottom:1px solid var(--border-color);font-size:13px;overflow-wrap:anywhere}.settings-purchase:last-child{border:0}.settings-purchase>span{display:block;color:var(--ink-muted);margin-top:4px}.user-settings .paper-account-card{padding:24px 0;border:0;background:0 0}.user-settings .paper-danger-zone{margin-top:24px}@media (max-width:767px){.user-settings{padding:22px 18px 90px}.user-settings .settings-row{grid-template-columns:1fr;gap:12px;padding:20px 0}.user-settings .settings-label,.user-settings .settings-row>h2{margin-top:0}.settings-nav{gap:18px;justify-content:space-between}.settings-nav button{text-align:left;font-size:12px;max-width:124px}.user-settings .settings-check{font-size:12px;min-height:28px}.settings-save{gap:8px}.settings-save-status{font-size:11px}.settings-save #save{min-width:120px}.user-settings .quota-row{grid-template-columns:1fr}.settings-cycles label{flex-basis:100%}}.user-settings .settings-save .draft-status{margin:0;padding:0}.settings-delete-confirm{max-width:480px;margin-top:18px}.settings-delete-confirm label{display:block;font-size:13px;margin-bottom:8px}.user-settings .quota-usage{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.user-settings .quota-usage .quota-item+.quota-item{border:0;padding:0}.user-settings .quota-percent,.user-settings .quota-remaining{display:none}@media (max-width:767px){.user-settings .quota-usage{grid-template-columns:1fr}}.settings-header-actions{display:flex;align-items:center;gap:16px}.settings-header-actions>span{color:var(--ink-muted);font-size:12px}.user-settings .settings-upgrade{color:var(--accent);border-color:var(--accent);background:0 0}.user-settings .settings-comparison{width:100%;table-layout:fixed;margin:16px 0 20px;font-size:12px}.settings-comparison td,.settings-comparison th{padding:10px 8px;border-bottom:1px solid var(--border-color);text-align:left;overflow-wrap:anywhere;vertical-align:top}.settings-comparison th:first-child{width:42%;padding-left:0}.settings-comparison tbody th{font-weight:400}.settings-comparison thead th{font-weight:600}@media(max-width:580px){.settings-header-actions>span{display:none}.settings-header-actions{gap:8px}.user-settings .settings-comparison{font-size:11px}}.navbar .nav-link.nav-upgrade{border:1px solid var(--accent);border-radius:5px;color:var(--accent)!important;padding:7px 12px!important;margin-right:12px;font-size:12px;line-height:1.5}.navbar .nav-link.nav-upgrade:hover{background:var(--accent-soft)}@media(max-width:991px){.navbar .nav-link.nav-upgrade{display:inline-block;min-height:44px;padding:11px 14px!important;margin:8px 0}}@media (max-width:600px){.ov-cache .ov-performance-window{display:grid;grid-template-columns:auto minmax(0,1fr);width:100%}.ov-cache .ov-performance-window select{width:100%;min-width:0}}.viewer-loading{padding:24px clamp(16px,4vw,40px);min-height:280px;color:var(--color)}.viewer-loading-status{min-height:84px}.viewer-loading-status strong{display:block;font-size:15px;font-weight:600}.viewer-loading-name{display:block;margin-top:6px;font-family:monospace;font-size:13px;overflow-wrap:anywhere;opacity:.75}.viewer-loading-status p{margin:12px 0;font-size:14px}.viewer-loading-lines{display:grid;gap:14px;max-width:720px;padding-top:16px}.viewer-loading-lines span{height:9px;width:70%;background:var(--border-color);border-radius:3px}.viewer-loading-lines span:nth-child(3n+1){width:48%}.viewer-loading-lines span:nth-child(3n+2){width:88%}.ace_scrollbar-h{height:8px!important}.ace_scrollbar-h::-webkit-scrollbar{height:8px}.ace_scrollbar::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--ink-faint) 55%,transparent);border-radius:4px}.ace_scrollbar::-webkit-scrollbar-track{background:0 0}.explorer-content,.explorer-main{background:var(--paper-card)}.explorer-content .dir-card,.explorer-content .file-card{margin:0!important;border:0;border-radius:0;background:0 0;overflow:visible}.explorer-content .file-card{min-height:100%}.explorer-content .file-card.is-code{height:100%!important;overflow:clip}.explorer-content .file-card-head{position:sticky;top:0;z-index:3;min-height:52px;padding:8px 12px 8px 32px;border-radius:0!important;border-bottom:1px solid var(--border-soft);background:color-mix(in srgb,var(--paper-card) 90%,transparent);-webkit-backdrop-filter:saturate(1.2) blur(8px);backdrop-filter:saturate(1.2) blur(8px)}.dark-mode .explorer-content .file-card-head{background:color-mix(in srgb,var(--paper-card) 90%,transparent)}.explorer-content .file-card .file-content.markdown-body{max-width:880px;padding:36px 32px 96px;background:0 0!important}.explorer-content .file-card .ace_editor{border-radius:0}.explorer-content .ace_editor.ace-chrome .ace_gutter,.explorer-content .ace_editor.ace-nord-dark .ace_gutter{background:var(--paper-card)!important;border-right:0}.explorer-content .ace_editor .ace_gutter-cell{padding-left:20px}.explorer-content .dir-card{border-bottom:1px solid var(--border-soft)}.explorer-content .dir-card-head{padding:14px 32px;background:0 0!important;border-bottom:0}.explorer-content .dir-row{padding:0 32px}.explorer-content .dir-card+.file-card{margin-top:0!important}@media (max-width:767px){.explorer-content .file-card-head{padding:6px 8px 6px 16px;min-height:0}.explorer-content .file-card .file-content.markdown-body{padding:20px 16px 72px}.explorer-content .ace_editor .ace_gutter-cell{padding-left:8px}.explorer-content .dir-card-head,.explorer-content .dir-row{padding-left:16px;padding-right:16px}}.leftCol-brand{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:52px;padding:0 14px;border-bottom:1px solid var(--border-soft)}.leftCol-logo{font-family:var(--font-serif);font-size:19px;letter-spacing:-.01em;color:var(--color)!important;text-decoration:none!important;white-space:nowrap}.leftCol-logo em{font-style:italic}.leftCol-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 8px 2px 6px;border-radius:999px;background:var(--hover-bg-color);font-size:11.5px;font-weight:500;color:var(--ink-soft);white-space:nowrap}.leftCol-badge .ui-icon{width:13px;height:13px;color:var(--status-ready)}.leftCol-search{padding-top:12px}.leftCol-links{display:flex;align-items:center;gap:14px;margin:0 0 10px;font-size:13px}.leftCol-links a{color:var(--ink-soft);text-decoration:none}.leftCol-links a:hover{color:var(--color);text-decoration:underline;text-underline-offset:3px}.leftCol-theme{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:auto;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-soft);cursor:pointer}.leftCol-theme:hover{background:var(--hover-bg-color);color:var(--color)}.leftCol-theme .ui-icon{width:16px;height:16px}.leftCol-foot .leftCol-zip{display:flex}.card-files-btn{display:none!important}@media (max-width:991px){.card-files-btn{display:inline-flex!important}}.card-files-btn.is-needed{display:inline-flex!important}@media (max-width:991px){.leftCol-brand{padding-right:60px}.leftCol-head{position:absolute;top:0;right:0;z-index:2;padding:4px 6px!important;border:0!important;background:0 0!important}.leftCol-head .leftCol-eyebrow{display:none}}@media (max-width:991px){.leftCol-head .leftCol-close{width:40px;height:40px;border:0!important;background:0 0!important;border-radius:var(--radius-control);color:var(--ink-soft)}.leftCol-head .leftCol-close:hover{background:var(--hover-bg-color)!important;color:var(--color)}.leftCol-brand{min-height:52px}}.leftCol-links .leftCol-donate{display:inline-flex;align-items:center;gap:5px}.leftCol-links .leftCol-donate .ui-icon{width:14px;height:14px}.leftCol-links .leftCol-donate:hover{color:var(--status-error);text-decoration:none}.leftCol-links .leftCol-donate:hover .ui-icon{fill:currentColor}.files .file.active>a{border-radius:var(--radius-control)!important}.files .file.active>a::before{left:4px!important;top:7px!important;bottom:7px!important;width:3px!important;border-radius:2px!important}.leftCol-backdrop{-webkit-backdrop-filter:none!important;backdrop-filter:none!important;background:rgba(20,18,16,.45)!important}@media (max-width:991px){.leftCol-head{right:6px!important}}.symbols{position:relative}.file-card-head .btn.btn-symbols{gap:6px;padding:0 10px;color:var(--ink-soft);font-weight:500}.symbols-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:30;width:300px;max-height:420px;overflow:auto;padding:6px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);box-shadow:0 12px 32px rgba(26,24,21,.16)}.symbols-head{padding:6px 8px 8px;font-size:12px;font-weight:600;color:var(--ink-muted)}.symbols-item{display:flex;align-items:center;gap:10px;width:100%;height:32px;padding:0 8px;border:0;border-radius:var(--radius-control);background:0 0;font:inherit;font-size:13px;color:var(--color);text-align:left;cursor:pointer}.symbols-item:focus-visible,.symbols-item:hover{background:var(--hover-bg-color);outline:0}.symbols-kind{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;font:600 11px/1 var(--font-mono);flex-shrink:0}.symbols-kind.is-function{background:color-mix(in srgb,#6b45b8 14%,transparent);color:#6b45b8}.symbols-kind.is-class{background:color-mix(in srgb,#b4362a 14%,transparent);color:#b4362a}.dark-mode .symbols-kind.is-function{color:#c9a8f5}.dark-mode .symbols-kind.is-class{color:#f2897d}.symbols-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:12.5px}.symbols-line{font-size:12px;color:var(--ink-muted);font-variant-numeric:tabular-nums}@media (max-width:1180px){.file-card-head .btn-symbols .btn-find-label{display:none}.file-card-head .btn-symbols{width:32px;padding:0!important}}.ace_search{top:0!important;left:0!important;right:0!important;border:0!important;border-bottom:1px solid var(--border-soft)!important;border-radius:0!important;box-shadow:none!important;padding:8px 12px 8px 8px!important;background:var(--paper-card)!important}.ace_search_field{border-color:var(--border-color)!important}.ace_search .ace_button{border:1px solid transparent!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:color-mix(in srgb,var(--accent) 40%,transparent)!important}.ace_search_counter{color:var(--ink-soft)!important;font-size:13px!important}.ace_editor .ace_marker-layer .ace_selected-word{background:var(--accent-soft)!important}.ace_editor.ace-chrome .ace_marker-layer .ace_selection{background:color-mix(in srgb,var(--accent) 28%,transparent)!important;box-shadow:0 0 0 1px var(--accent)!important}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:color-mix(in srgb,var(--accent) 34%,transparent)!important;box-shadow:0 0 0 1px var(--accent)!important}.leftCol-foot{padding:10px 14px 12px}.leftCol-links{margin:0 0 6px;gap:12px}.leftCol-repo-actions{display:inline-flex}.leftCol-project-header .leftCol-hide{display:inline-flex}@media (max-width:991px){.leftCol-project-header button.leftCol-hide{display:none}}.ace-chrome .ace_string{color:#11704f!important}.ace-chrome .ace_entity.ace_name.ace_function,.ace-chrome .ace_support.ace_function{color:#5b3dc2!important}.ace-chrome .ace_keyword,.ace-chrome .ace_storage{color:#b02f22!important}.ace-chrome .ace_comment{color:#6f6a61!important}.ace-chrome .ace_keyword.ace_operator,.ace-chrome .ace_paren,.ace-chrome .ace_punctuation{color:#5c574f!important}.dark-mode .ace_editor.ace-nord-dark,.dark-mode .explorer-content,.dark-mode .explorer-content .ace_editor.ace-nord-dark .ace_gutter,.dark-mode .explorer-main{background:var(--canvas-bg-color)!important}.dark-mode .ace_search,.dark-mode .explorer-content .file-card-head{background:color-mix(in srgb,var(--canvas-bg-color) 92%,transparent)!important}.dark-mode .leftCol{background:var(--sidebar-bg-color)}.dark-mode .symbols-menu{background:#22201c}.paper-stats-grid{grid-template-columns:repeat(4,minmax(0,1fr))!important}.paper-stats-grid .paper-stat-card{padding-left:24px!important;padding-right:24px!important}.paper-stats-grid .paper-stat-card:first-child{padding-left:0!important}.paper-stat-value{font-size:40px!important}.paper-stats-grid .paper-stat-card{margin-right:0!important}.paper-stats-grid .paper-stat-card:last-child{border-right:0;padding-right:0!important}@media (max-width:767px){.paper-stats-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}.paper-stats-grid .paper-stat-card:nth-child(odd){padding-left:0!important}}.dark-mode .leftCol-foot,.leftCol-foot{background:0 0!important}.paper-faq-toc nav a.active,.paper-faq-toc nav a:hover{border-left-color:var(--accent)!important}.card-files-label{display:none}@media (max-width:991px){.file-card-head .card-files-btn{width:auto!important;padding:0 10px!important;gap:6px;border:1px solid var(--border-color)!important;color:var(--color)!important}.card-files-label{display:inline;font-size:13px;font-weight:500}}@media (max-width:767px){.file-card-head .btn.btn-icon-only{color:var(--color)!important}.file-card-head .file-card-meta .meta-extra{display:none!important}.file-card.is-code.no-wrap::after{width:64px!important}}.btn.btn-ghost.is-on{color:var(--accent)!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 35%,transparent)!important}.explorer-content .ace_editor.ace-chrome .ace_gutter,.explorer-content .ace_editor.ace-nord-dark .ace_gutter{border-right:1px solid var(--border-soft)!important}.ace_search .ace_button{width:28px!important;height:28px!important;border:1px solid var(--border-color)!important;color:var(--ink-soft)!important;font-size:12px!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:var(--accent)!important}.ace_search_counter{margin-left:2px!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(242,204,96,.38)!important;box-shadow:none!important}.ace_editor.ace-chrome .ace_marker-layer .ace_selection,.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:#f2cc60!important;box-shadow:0 0 0 1px #c69026!important}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:rgba(242,204,96,.55)!important}.explorer-content .file-card .file-content.markdown-body{margin:0 auto!important}.leftCol-project-header .leftCol-zipbtn{width:auto!important;gap:4px;padding:0 6px;font-size:11.5px;font-weight:600;letter-spacing:.02em}.leftCol-project-header .leftCol-zipbtn .ui-icon{width:13px;height:13px}.leftCol-brand{justify-content:space-between}.repo-panel{display:grid;gap:8px;padding:14px 14px 12px;border-bottom:1px solid var(--border-soft)}.repo-panel-title{display:flex;align-items:center;gap:8px;min-width:0;font-size:14px;font-weight:600;color:var(--color)}.repo-panel-title span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.repo-panel-title .ui-icon{width:16px;height:16px;flex-shrink:0;color:var(--ink-muted)}.repo-panel-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;font-size:12.5px;color:var(--ink-soft)}.repo-panel-meta .last-update{font:inherit!important;color:inherit!important;padding:0;border:0}.repo-panel-meta>span+span::before{content:"·";margin-right:10px;color:var(--ink-faint)}.repo-panel-meta>.repo-panel-badge+span::before{content:none}.repo-panel-badge{display:inline-flex;align-items:center;gap:4px;padding:1px 8px 1px 6px;border-radius:999px;background:color-mix(in srgb,var(--status-ready) 12%,transparent);color:var(--status-ready);font-size:11.5px;font-weight:600}.repo-panel-badge .ui-icon{width:12px;height:12px}.repo-panel-zip{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important}.repo-panel-zip:hover{background:var(--hover-bg-color);border-color:var(--ink-faint)}.repo-panel-zip .ui-icon{width:15px;height:15px}.dark-mode .repo-panel-zip{background:0 0}.leftCol-project-header{display:none!important}.leftCol-search{padding-top:12px;padding-bottom:4px}.tb-file{display:inline-flex;align-items:center;gap:4px;margin-left:6px;padding-left:10px;border-left:1px solid var(--border-color)}.file-card-head .btn.tb-btn{height:32px;gap:6px;padding:0 12px!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important;background:var(--paper-card)!important;color:var(--color)!important;font-size:13px;font-weight:500}.file-card-head .btn.tb-btn:hover{background:var(--hover-bg-color)!important;border-color:var(--ink-faint)!important}.file-card-head .btn.tb-btn .ui-icon{width:15px;height:15px}.file-card-head .btn.tb-btn-icon{width:32px;padding:0!important;justify-content:center}.dark-mode .file-card-head .btn.tb-btn{background:0 0!important}.file-card-actions>.tb-file:first-child{border-left:0;padding-left:0;margin-left:0}.tb-more{position:relative;display:none}.tb-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:40;width:220px;padding:6px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);box-shadow:0 12px 32px rgba(26,24,21,.18)}.tb-menu>a,.tb-menu>button{display:flex;align-items:center;gap:10px;width:100%;height:40px;padding:0 10px;border:0;border-radius:var(--radius-control);background:0 0;font:inherit;font-size:14px;color:var(--color)!important;text-align:left;text-decoration:none!important}.tb-menu>a:hover,.tb-menu>button:hover{background:var(--hover-bg-color)}.tb-menu .ui-icon{width:16px;height:16px;color:var(--ink-soft)}.tb-menu-check{margin-left:auto;color:var(--accent);font-weight:600}.tb-menu-sep{height:1px;margin:6px 4px;background:var(--border-soft)}@media (max-width:767px){.tb-more{display:inline-flex}.file-card-head .btn-find,.file-card-head .btn-goto,.file-card-head .btn-toggle,.file-card-head .symbols,.file-card-head .tb-file,.file-card-head .view-switch+.file-card-actions .tb-file{display:none!important}.file-card-head .tb-more-btn{width:40px!important;height:40px!important}.file-card-head .card-files-btn{height:40px!important}.file-card-path .paths .breadcrumb-item:last-child{font-size:15px}}.feat-mock{height:380px;overflow:hidden;background:var(--paper-card);font-family:var(--font-sans);color:var(--color)}.fm-form{padding:22px 26px;display:grid;gap:8px}.fm-label{font-size:12px;font-weight:600;color:var(--ink-soft);margin-top:6px}.fm-input{height:36px;display:flex;align-items:center;padding:0 12px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--input-bg);font-size:13.5px}.fm-input-sm{width:150px}.fm-chips{display:flex;flex-wrap:wrap;gap:6px}.fm-chips span{padding:4px 10px;border-radius:999px;background:color-mix(in srgb,var(--status-error) 9%,transparent);color:var(--color);font-size:12.5px;text-decoration:line-through;text-decoration-color:color-mix(in srgb,var(--status-error) 55%,transparent)}.fm-chips .fm-chip-add{background:0 0;border:1px dashed var(--border-color);color:var(--ink-muted);text-decoration:none}.fm-row{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:end}.fm-toggles{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}.fm-toggles span{padding:5px 10px;border:1px solid var(--border-color);border-radius:var(--radius-control);font-size:12.5px;color:var(--ink-muted)}.fm-toggles .is-on{border-color:color-mix(in srgb,var(--accent) 45%,transparent);background:var(--accent-soft);color:var(--accent)}.fm-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:14px;padding-top:14px;border-top:1px solid var(--border-soft)}.fm-share{font-family:var(--font-mono);font-size:12px;color:var(--ink-soft)}.fm-btn{padding:9px 14px;border-radius:var(--radius-button);background:var(--accent);color:#fff;font-size:13px;font-weight:600;white-space:nowrap}.dark-mode .fm-btn{background:#1f6a64}.fm-viewer{display:grid;grid-template-columns:150px 1fr;height:100%}.fm-tree{padding:14px 8px;background:var(--paper-bg-alt);border-right:1px solid var(--border-soft);font-size:12.5px}.fm-tree div{padding:5px 10px;border-radius:var(--radius-control);color:var(--ink-soft)}.fm-tree .is-folder::before{content:"▸ ";color:var(--ink-faint)}.fm-tree .is-active{background:rgba(128,128,128,.14);color:var(--color);font-weight:500;box-shadow:inset 3px 0 0 var(--accent)}.fm-code{min-width:0}.fm-code-bar{display:flex;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border-soft);font-size:12.5px;color:var(--ink-muted)}.fm-code-bar b{color:var(--color);font-weight:600}.fm-code pre{margin:0;padding:12px 16px;font-family:var(--font-mono);font-size:12.5px;line-height:1.75;color:var(--color);background:0 0;white-space:pre}.fm-code .ln{display:inline-block;width:22px;margin-right:14px;text-align:right;color:var(--ink-faint)}.fm-code .k{color:#b02f22}.fm-code .f{color:#5b3dc2}.fm-code .s{color:#11704f}.fm-code .c{color:var(--ink-muted);font-style:italic}.dark-mode .fm-code .k{color:#f2897d}.dark-mode .fm-code .f{color:#c9a8f5}.dark-mode .fm-code .s{color:#9dd3a8}.fm-code .redact{padding:0 4px;border-radius:3px;background:var(--accent-soft);color:var(--accent);font-style:normal}.fm-dash{padding:12px 18px;font-size:13px}.fm-dash-head,.fm-dash-row{display:grid;grid-template-columns:1.7fr .9fr .5fr .9fr;gap:12px;align-items:center}.fm-dash-head{padding:8px 0;border-bottom:1px solid var(--border-color);font-size:12px;font-weight:600;color:var(--ink-muted)}.fm-dash-row{padding:12px 0;border-bottom:1px solid var(--border-soft)}.fm-dash-row b{display:block;font-weight:600}.fm-dash-row i{display:block;font-style:normal;font-size:12px;color:var(--ink-muted);font-family:var(--font-mono)}.fm-dash-row.is-muted{color:var(--ink-muted)}.fm-dash .st{display:inline-flex;align-items:center;gap:6px}.fm-dash .st::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.fm-dash .st.is-ready{color:var(--status-ready)}.fm-dash .st.is-progress{color:var(--status-progress)}.fm-dash .st.is-expired{color:var(--ink-muted)}@media (max-width:767px){.feat-mock{height:auto;min-height:260px}.fm-viewer{grid-template-columns:1fr}.fm-tree{display:none}.fm-dash-head span:nth-child(4),.fm-dash-row span:nth-child(4){display:none}.fm-dash-head,.fm-dash-row{grid-template-columns:1.6fr 1fr .5fr}.fm-row{grid-template-columns:1fr}}.paper-stat-delta{margin-top:6px!important;font-family:var(--font-sans)!important;font-size:13px!important;color:var(--ink-muted)!important}.paper-stats-grid .paper-stat-card{padding-top:22px!important;padding-bottom:22px!important}.paper-eyebrow,.paper-faq-eyebrow,.paper-stats-meta{font-family:var(--font-sans)!important;font-size:12.5px!important;font-weight:600!important;letter-spacing:.06em!important;color:var(--ink-soft)!important}.paper-feature-tab .paper-eyebrow{font-size:12px!important}.paper-reassure li::before{color:var(--ink-soft)!important}.feat-mock{height:auto!important;min-height:300px}@media (max-width:767px){.file-card.is-code.no-wrap::after{display:none!important}.file-card.is-code.no-wrap .ace_scroller{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 48px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 48px),transparent)}.explorer-content .file-card-head{row-gap:6px;padding-top:8px;padding-bottom:8px}.file-card-path{flex:1 1 auto;gap:10px}.file-card-path .paths{flex:1 1 auto;min-width:0}.file-card-path .paths .breadcrumb-item:last-child{overflow:hidden;text-overflow:ellipsis}.file-card-head .card-files-btn{height:32px!important;border-radius:var(--radius-control)!important;padding:0 10px!important}.file-card-head .tb-more-btn{width:36px!important;height:32px!important}.file-card-head>.view-switch{order:4;margin-left:auto!important}.file-card-head .file-card-meta{order:3;flex-basis:auto!important;flex:1 1 auto;margin:0!important;align-self:center}}@media (min-width:768px){.explorer-content .file-card-head{padding-left:20px}.explorer-content .file-card .file-content.markdown-body{margin:0!important;padding-left:20px!important;padding-right:40px!important;max-width:900px}}.dark-mode .files .file.active>a{background:rgba(255,255,255,.13)!important}.dark-mode .btn.btn-ghost.is-on{background:rgba(124,199,190,.18)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.45)!important}.dark-mode .leftCol{background:#1e1b18!important}.ace_searchbtn_close{width:24px!important;height:24px!important;background-size:12px!important;opacity:.7}.ace_search_field:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.navbar .nav-link:not(.active):not(.btn-signin){color:var(--ink-soft)!important}.navbar .nav-support-inline{color:var(--ink-soft)!important}.paper-stat-delta{color:var(--ink-soft)!important}.paper-faq-toc-head,.paper-footer-head{font-family:var(--font-sans)!important;font-size:12px!important;font-weight:600!important;letter-spacing:.06em!important;text-transform:uppercase;color:var(--ink-soft)!important}.paper-feature:not(.is-on) .paper-feature-tab::after{border-color:var(--ink-soft)!important;border-width:0 2px 2px 0!important}.faq-section .panel-default .panel-body{max-width:720px!important}.repo-panel-meta .last-update{flex-basis:100%}.repo-panel-meta .last-update::before{content:none!important}@media (max-width:767px){.file-card-head .file-card-path{flex:1 1 calc(100% - 56px)!important}.file-card-head .file-card-actions{order:1}.file-card-head .file-card-meta{order:3}.file-card-head>.view-switch{order:4}}.repo-panel{display:none!important}.repo-id{display:flex;align-items:center;gap:10px;padding:14px 14px 6px}.repo-id-tile{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex-shrink:0;border-radius:9px;background:linear-gradient(140deg,color-mix(in srgb,var(--accent) 16%,var(--paper-card)),color-mix(in srgb,var(--accent) 6%,var(--paper-card)));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 22%,transparent);color:var(--accent)}.repo-id-tile .ui-icon{width:17px;height:17px}.repo-id-text{min-width:0}.repo-id-title{font-size:14px;font-weight:600;color:var(--color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.repo-id-sub{margin-top:1px;font-size:12.5px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.repo-id-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 14px 12px 58px;border-bottom:1px solid var(--border-soft);font-size:12.5px}.repo-id-count{color:var(--ink-muted);font-variant-numeric:tabular-nums}.repo-id-zip{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:var(--radius-control);color:var(--link-color)!important;font-weight:500;text-decoration:none!important}.repo-id-zip:hover{background:var(--accent-soft)}.repo-id-zip .ui-icon{width:14px;height:14px}.leftCol-foot .leftCol-links{display:none!important}.acct-bar{display:flex;align-items:center;justify-content:space-between;gap:8px}.acct{position:relative;min-width:0}.acct-chip{display:inline-flex;align-items:center;gap:8px;max-width:150px;height:34px;padding:0 8px 0 4px;border:0;border-radius:var(--radius-control);background:0 0;font:inherit;font-size:13px;font-weight:500;color:var(--color);cursor:pointer}.acct-chip:hover,.acct-chip[aria-expanded=true]{background:var(--hover-bg-color)}.acct-chip img,.acct-initial{width:24px;height:24px;border-radius:50%;flex-shrink:0}.acct-initial{display:inline-flex;align-items:center;justify-content:center;background:var(--accent-soft);color:var(--accent);font-size:12px;font-weight:700}.acct-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acct-caret{width:12px;height:12px;color:var(--ink-muted);transform:rotate(-90deg)}.acct-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:40;width:190px;padding:6px;background:var(--paper-card);border:1px solid var(--border-color);border-radius:var(--radius-surface);box-shadow:0 12px 32px rgba(26,24,21,.18)}.acct-menu a{display:flex;align-items:center;gap:10px;height:36px;padding:0 10px;border-radius:var(--radius-control);font-size:13.5px;color:var(--color)!important;text-decoration:none!important}.acct-menu a:hover{background:var(--hover-bg-color)}.acct-menu .ui-icon{width:16px;height:16px;color:var(--ink-soft)}.acct-signin{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 12px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important}.acct-signin:hover{background:var(--hover-bg-color);border-color:var(--ink-faint)}.dark-mode .acct-signin{background:0 0}.acct-icons{display:inline-flex;align-items:center;gap:2px}.acct-icons a,.acct-icons button{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-soft)!important;cursor:pointer}.acct-icons a:hover,.acct-icons button:hover{background:var(--hover-bg-color);color:var(--color)!important}.acct-icons .acct-donate:hover{color:var(--status-error)!important}.acct-icons .ui-icon{width:17px;height:17px}.leftCol-foot{padding:8px 10px 10px 14px!important}.leftCol-foot .last-update{display:none}.file-card-head .card-files-btn,.file-card-head .card-files-btn.is-needed{width:34px!important;height:34px!important;padding:0!important;border:0!important;background:0 0!important;color:var(--ink-soft)!important}.file-card-head .card-files-btn:hover{background:var(--hover-bg-color)!important;color:var(--color)!important}.file-card-head .card-files-btn .ui-icon{width:19px;height:19px}.card-files-label{display:none!important}.tb-copy-m{display:none!important}@media (max-width:767px){.tb-copy-m{display:inline-flex!important;width:36px!important;height:34px!important}.file-card-head .file-card-path{flex:1 1 calc(100% - 96px)!important;gap:6px!important}}.ace_search{flex-direction:row}.ace_searchbtn_close{order:99;margin-left:auto!important;width:28px!important;height:28px!important}.ace_search .ace_button,.ace_searchbtn{color:var(--ink-soft)!important}.file-card-head .btn-find:focus:not(:focus-visible){background:0 0!important}.dark-mode .explorer-content .markdown-body img{filter:brightness(.92)!important}@media (max-width:767px){.file-card.is-code.no-wrap .ace_scroller{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 80px),rgba(0,0,0,.15) calc(100% - 8px),transparent)!important;mask-image:linear-gradient(to right,#000 calc(100% - 80px),rgba(0,0,0,.15) calc(100% - 8px),transparent)!important}}.acct-menu-head{padding:6px 10px 8px;margin-bottom:4px;border-bottom:1px solid var(--border-soft);font-size:12px;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acct-menu-head b{color:var(--color);font-weight:600}@media (max-width:767px){.file-card.is-code.no-wrap{position:relative}.file-card.is-code.no-wrap::before{content:"Scroll \2194";position:absolute;right:12px;bottom:14px;z-index:25;padding:5px 10px;border-radius:999px;background:color-mix(in srgb,var(--color) 82%,transparent);color:var(--canvas-bg-color);font:600 12px/1 var(--font-sans);pointer-events:none;box-shadow:0 4px 14px rgba(0,0,0,.18)}.file-card-head>.view-switch{order:5!important;flex:1 1 100%;margin-left:0!important;display:grid!important;grid-template-columns:1fr 1fr}.file-card-head>.view-switch .btn{height:36px!important;min-height:36px!important;justify-content:center;font-size:14px}.file-card-head .file-card-meta{font-size:13px!important}}.acct-menu{width:240px}.acct-menu-head{white-space:normal;overflow-wrap:anywhere}.paper-feature .paper-feature-tab::after{border-width:0 1.5px 1.5px 0!important;border-color:var(--ink-soft)!important}.hero-mock-doc-bar span,.hero-mock-tree-title{color:var(--ink-soft)!important}.paper-frame-bar{color:var(--ink-soft)!important}.paper-frame-bar .dots i{background:color-mix(in srgb,var(--ink-muted) 55%,transparent)!important}.dark-mode .hero-mock-doc,.dark-mode .paper-frame{background:#24211d!important}.dark-mode .hero-mock-tree{background:#1c1a17!important}.dark-mode .paper-stats-grid .paper-stat-card{border-color:#3a3631!important}.dark-mode .paper-stats,.dark-mode .paper-stats-grid{border-color:#3a3631!important}.faq-section .panel-default .panel-body{padding-bottom:18px!important}.btn.btn-ghost.is-on{background:var(--hover-bg-color)!important;color:var(--accent)!important;box-shadow:none!important}.dark-mode .btn.btn-ghost.is-on{background:rgba(255,255,255,.08)!important;color:#7cc7be!important;box-shadow:none!important}.acct-chip .acct-caret,.acct-chip .acct-name{display:none}.acct-chip{padding:0 5px!important}.acct-chip img,.acct-initial{width:26px!important;height:26px!important}@media (min-width:768px){.explorer-content .file-card .file-content.markdown-body{margin:0 auto!important;padding-left:40px!important;padding-right:40px!important;max-width:920px}}.files .file.active>a::before{top:5px!important;bottom:5px!important}@media (max-width:767px){.explorer-content .ace_editor .ace_gutter-cell{padding-left:6px!important;padding-right:6px!important}}.repo-id,.repo-id-actions{display:none!important}.leftCol-search{display:flex;align-items:center;gap:6px;padding:12px 12px 8px!important}.leftCol-search .tree-search-box{flex:1 1 auto;min-width:0}.search-zip{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:1px solid var(--border-color);border-radius:8px;background:var(--canvas-bg-color);color:var(--ink-soft)!important}.search-zip:hover{background:var(--hover-bg-color);color:var(--color)!important;border-color:var(--ink-faint)}.search-zip .ui-icon{width:17px;height:17px}.dark-mode .search-zip{background:0 0}.support-cta{display:flex;align-items:center;gap:10px;margin:0 0 10px;padding:9px 12px;border-radius:var(--radius-surface);background:color-mix(in srgb,#e5484d 9%,var(--paper-card));box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 22%,transparent);color:var(--color)!important;text-decoration:none!important;transition:background .15s,box-shadow .15s}.support-cta:hover{background:color-mix(in srgb,#e5484d 14%,var(--paper-card));box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 40%,transparent)}.support-cta .ui-icon{width:18px;height:18px;flex-shrink:0;color:#e5484d;fill:#e5484d}.support-cta span{display:grid;min-width:0}.support-cta b{font-size:13px;font-weight:600}.support-cta small{font-size:12px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dark-mode .support-cta{background:color-mix(in srgb,#e5484d 12%,var(--sidebar-bg-color))}.leftCol-foot{padding:10px 12px 10px!important}.tb-file{display:inline-flex!important;align-items:stretch;gap:0!important;margin-left:8px;padding-left:0!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control);overflow:hidden}.tb-seg-btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 12px;border:0;background:var(--paper-card);font:inherit;font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important;cursor:pointer}.tb-seg-btn+.tb-seg-btn{border-left:1px solid var(--border-color)}.tb-seg-btn:hover{background:var(--hover-bg-color)}.tb-seg-icon{width:34px;padding:0;color:var(--ink-soft)!important}.tb-seg-icon:hover{color:var(--color)!important}.tb-seg-icon .ui-icon{width:15px;height:15px}.dark-mode .tb-seg-btn{background:0 0}.file-card-actions>.tb-file:first-child{margin-left:0}.file-card-head .btn-find,.file-card-head .btn-goto,.file-card-head .btn-toggle{width:32px!important;height:32px!important}.file-card.is-code.no-wrap::before{content:none!important;display:none!important}.acct-chip{width:34px;height:34px;padding:0!important;justify-content:center;border-radius:50%!important;background:0 0!important}.acct-chip:hover,.acct-chip[aria-expanded=true]{background:var(--hover-bg-color)!important}.ace_searchbtn_close{margin-left:4px!important}.dark-mode .explorer-content .markdown-body img{filter:brightness(.9)!important}.snapshot-line{display:flex;align-items:center;gap:6px;padding:0 14px 8px;font-size:12.5px;color:var(--ink-soft);white-space:nowrap;overflow:hidden}.snapshot-line span{overflow:hidden;text-overflow:ellipsis}.snapshot-line .ui-icon{width:14px;height:14px;flex-shrink:0;color:var(--ink-muted)}.leftCol-search{padding-bottom:6px!important}@media (max-width:767px){.explorer-content .file-card-head .file-card-meta{display:none!important}.explorer-content .file-card-head{flex-wrap:nowrap!important;min-height:52px!important}.file-card-head .card-files-btn,.file-card-head .tb-copy-m,.file-card-head .tb-more-btn{width:40px!important;height:40px!important}.file-card-head .card-files-btn{background:var(--hover-bg-color)!important;color:var(--color)!important;border-radius:var(--radius-control)!important}.file-card-head .file-card-path{flex:1 1 auto!important}}.acct-menu{box-shadow:0 16px 40px rgba(26,24,21,.22),0 0 0 1px var(--border-color)!important;border:0!important}.dark-mode .acct-menu{background:#26231f;box-shadow:0 16px 40px rgba(0,0,0,.55),0 0 0 1px #3a3631!important}.acct-chip{width:32px!important;height:32px!important}.acct-chip img,.acct-initial{width:30px!important;height:30px!important;font-size:13px!important}.ace_search{padding:6px 12px 6px 8px!important;min-height:40px}.ace_search_field{height:28px!important}.ace_search_options{gap:0!important;margin-left:4px!important}.ace_search_options .ace_button{border-radius:0!important;margin:0!important}.ace_search_options .ace_button+.ace_button{margin-left:-1px!important}.ace_search_options .ace_button:first-of-type{border-radius:var(--radius-control) 0 0 var(--radius-control)!important}.ace_search_options .ace_button:last-of-type{border-radius:0 var(--radius-control) var(--radius-control) 0!important}.ace_searchbtn_close{width:28px!important;height:28px!important;border-radius:var(--radius-control)!important;margin-left:8px!important;background-color:transparent!important;opacity:.65}.ace_searchbtn_close:hover{background-color:var(--hover-bg-color)!important;opacity:1}.ace_search_counter{color:var(--ink-soft)!important}.ace_editor .ace_marker-layer .ace_selected-word{background:color-mix(in srgb,var(--accent) 16%,transparent)!important}.ace_editor.ace-chrome .ace_marker-layer .ace_selection,.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:color-mix(in srgb,var(--accent) 34%,transparent)!important;box-shadow:0 0 0 1px var(--accent)!important}.dark-mode .btn.btn-ghost.is-on{background:rgba(124,199,190,.16)!important;color:#a8ddd6!important}.paper-features{grid-template-columns:1fr 1.4fr!important;gap:40px!important}.faq-section .panel-default .panel-title a:after{border-color:var(--ink-soft)!important}.paper-features .paper-feature.is-on .paper-feature-tab::after,.paper-features .paper-feature:not(.is-on) .paper-feature-tab::after{border-width:0 1.5px 1.5px 0!important;border-color:var(--ink-muted)!important}.markdown-body mark.redact-chip,.redact-chip{display:inline-block;padding:0 6px;margin:0 1px;border-radius:4px;background:var(--accent-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 25%,transparent);color:var(--accent);font-family:var(--font-mono);font-size:.82em;font-weight:500;line-height:1.5;vertical-align:.06em;cursor:help;transition:background .1s}.redact-chip:hover{background:color-mix(in srgb,var(--accent) 22%,transparent)}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip{font-size:.7em;vertical-align:.18em}.dark-mode .redact-chip{background:rgba(124,199,190,.14);color:#a8ddd6}.ace_editor .ace_marker-layer .ace_redacted{position:absolute;background:var(--accent-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent);border-radius:3px}.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.16)}.meta-redact{color:var(--accent);font-weight:500}.explorer-page{display:grid!important;grid-template-columns:var(--sidebar-w,280px) minmax(0,1fr);grid-template-rows:44px minmax(0,1fr)}.viewer-bar{grid-column:1/-1;grid-row:1;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px 0 14px;border-bottom:1px solid var(--border-color);background:var(--paper-bg-alt);z-index:20}.dark-mode .viewer-bar{background:var(--sidebar-bg-color)}.explorer-page>.leftCol{grid-row:2;grid-column:1;width:auto!important;flex:none!important;height:auto!important;min-height:0;border-top:0}.explorer-page>.explorer-main{grid-row:2;grid-column:2;min-height:0}.explorer-page.files-hidden{grid-template-columns:minmax(0,1fr)}.explorer-page.files-hidden>.explorer-main{grid-column:1/-1}.leftCol-resizer{top:44px!important}.card-files-btn.is-needed{display:none!important}.vb-left,.vb-right{display:flex;align-items:center;gap:4px;min-width:0}.vb-logo{font-family:var(--font-serif);font-size:18px;letter-spacing:-.01em;color:var(--color)!important;text-decoration:none!important;white-space:nowrap;margin-right:10px}.vb-logo em{font-style:italic}.vb-icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:0;border-radius:var(--radius-control);background:0 0;color:var(--ink-soft)!important;cursor:pointer;text-decoration:none!important}.vb-icon:hover{background:var(--hover-bg-color);color:var(--color)!important}.vb-icon .ui-icon{width:17px;height:17px}.vb-snapshot{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;margin-right:6px;border-radius:999px;background:var(--hover-bg-color);font-family:var(--font-mono);font-size:12px;color:var(--ink-soft);white-space:nowrap;cursor:help}.vb-snapshot .ui-icon{width:13px;height:13px;color:var(--ink-muted)}.vb-support{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 12px;margin-right:4px;border-radius:999px;background:color-mix(in srgb,#e5484d 10%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 28%,transparent);font-size:13px;font-weight:600;color:#c2353a!important;text-decoration:none!important}.vb-support:hover{background:color-mix(in srgb,#e5484d 16%,transparent)}.vb-support .ui-icon{width:15px;height:15px;fill:currentColor}.dark-mode .vb-support{color:#ff8a8e!important;background:rgba(229,72,77,.14)}.vb-signin{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 12px;margin-left:6px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font-size:13px;font-weight:500;color:var(--color)!important;text-decoration:none!important}.vb-signin:hover{background:var(--hover-bg-color)}.dark-mode .vb-signin{background:0 0}.viewer-bar .acct{margin-left:6px}.viewer-bar .acct-menu{top:calc(100% + 8px);bottom:auto;left:auto;right:0}.explorer-page>.leftCol .leftCol-search{padding-top:12px!important}@media (max-width:991px){.explorer-page{grid-template-columns:minmax(0,1fr)}.explorer-page>.explorer-main{grid-column:1/-1}.vb-sidebar{display:none}.explorer-page>.leftCol{grid-row:auto}}@media (max-width:575px){.vb-bug,.vb-snapshot{display:none}.vb-support span{display:none}.vb-support{width:32px;padding:0;justify-content:center}}@media (max-width:991px){.explorer-page>.leftCol .leftCol-head{position:static!important;display:flex!important;align-items:center;justify-content:space-between;min-height:52px;padding:6px 8px 6px 16px!important;border-bottom:1px solid var(--border-soft)!important;background:0 0!important}.explorer-page>.leftCol .leftCol-head .leftCol-eyebrow{display:inline!important;font:600 15px/1 var(--font-sans);letter-spacing:0;text-transform:none;color:var(--color)}.explorer-page>.leftCol .leftCol-close{width:44px!important;height:44px!important}.files a{min-height:38px!important;font-size:15px}}.vb-support{background:var(--paper-card)!important;box-shadow:inset 0 0 0 1px var(--border-color)!important;color:var(--color)!important;font-weight:500!important}.vb-support .ui-icon{color:#e5484d;fill:#e5484d}.vb-support:hover{background:var(--hover-bg-color)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,#e5484d 40%,var(--border-color))!important}.dark-mode .vb-support{background:0 0!important;color:var(--color)!important}.vb-snapshot{font-family:var(--font-sans)!important;font-variant-numeric:tabular-nums;font-size:12.5px!important}.file-card-path .breadcrumb-root a{color:var(--ink-soft)!important}.file-card-path .breadcrumb-root a:hover{color:var(--color)!important;text-decoration:underline}@media (max-width:767px){.viewer-bar{padding:0 6px 0 12px}.vb-icon,.vb-support{width:40px!important;height:40px!important}.vb-signin{height:36px}.file-card-head .card-files-btn{background:0 0!important;color:var(--ink-soft)!important}}.ace_search{background:var(--paper-bg-alt)!important;border-bottom:1px solid var(--border-color)!important;gap:6px!important}.dark-mode .ace_search{background:#1e1b18!important}.ace_search .ace_searchbtn{width:28px!important;height:28px!important;border-radius:var(--radius-control)!important;border:1px solid var(--border-color)!important;background:var(--paper-card)!important}.ace_searchbtn_close{order:99;margin-left:6px!important;border:1px solid transparent!important;background-position:center!important;opacity:.8!important}@media (max-width:767px){.file-card.is-code.no-wrap .ace_scroller{-webkit-mask-image:none!important;mask-image:none!important}.file-card.is-code.no-wrap .file-card-body{position:relative}.file-card.is-code.no-wrap .file-card-body::after{content:"";position:absolute;top:0;right:0;bottom:0;width:36px;z-index:8;pointer-events:none;background:linear-gradient(to right,color-mix(in srgb,var(--paper-card) 0,transparent),var(--paper-card) 85%)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 85%)}}.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.24)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.5)!important}.paper-reassure,.paper-stat-delta,.paper-stat-label{color:var(--ink-soft)!important}.paper-faq-title{font-size:clamp(2.2rem,4vw,3.1rem)!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--paper-card) 80%)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 80%)!important}}@media (max-width:575px){.vb-support{box-shadow:none!important;background:0 0!important;border-radius:var(--radius-control)!important}.vb-support:hover{background:var(--hover-bg-color)!important}}.meta-redact{display:inline-flex;align-items:center;height:20px;padding:0 7px;border-radius:999px;background:var(--accent-soft);color:var(--accent)!important;font-size:12px;font-weight:600;cursor:default}.dark-mode .meta-redact{background:rgba(124,199,190,.14);color:#a8ddd6!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.6em!important;padding:1px 6px!important;vertical-align:.25em!important}.ace_search .ace_button{color:var(--ink-soft)!important;font-size:12.5px!important;font-weight:600!important;background:var(--paper-card)!important}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;border-color:var(--accent)!important;position:relative;z-index:1}.ace_search{align-items:center!important}.ace_search_counter{align-self:center;line-height:28px}.hero-mock-row.is-active{background:rgba(128,128,128,.12)!important;box-shadow:inset 3px 0 0 var(--accent)}.mobile-sub{display:none}@media (max-width:767px){.file-card-head .file-card-path{display:grid!important;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;align-items:center}.file-card-head .file-card-path .card-files-btn{grid-row:1/span 2}.file-card-head .file-card-path .paths{grid-column:2}.mobile-sub{display:block;grid-column:2;margin-top:1px;font-size:12px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mobile-sub b{color:var(--accent);font-weight:600}.dark-mode .mobile-sub b{color:#a8ddd6}.vb-icon[href="/faq"]{display:none!important}}.ace_searchbtn_close{font-size:0!important;color:transparent!important;background-size:10px 10px!important;align-self:center}.acct-initial{background:var(--hover-bg-color)!important;color:var(--color)!important;box-shadow:inset 0 0 0 1px var(--border-color)}.acct-chip[aria-expanded=true] .acct-initial{background:var(--accent-soft)!important;color:var(--accent)!important}.viewer-bar .acct-menu{top:calc(100% + 12px)!important}.dark-mode .paper-frame{box-shadow:0 0 0 1px #3f3a34,0 24px 60px rgba(0,0,0,.5)!important}.faq-section .panel-default .panel-body{max-width:640px!important}.anonymize-landing{align-items:flex-start!important;padding-top:clamp(32px,9vh,88px)!important}.landing-url-row{display:flex;gap:8px;align-items:stretch}.landing-url-row .form-control{flex:1 1 auto;min-width:0}.landing-continue{flex-shrink:0;height:auto;min-height:46px;padding:0 20px;font-weight:600}@media (max-width:575px){.landing-url-row{flex-direction:column}.landing-continue{min-height:44px}.source-url-label-row{flex-direction:column;align-items:flex-start!important;gap:8px}}.paper-table.paper-table-dashboard{width:100%}.q-card-count,.q-stat-value{font-family:var(--font-sans)!important;font-variant-numeric:tabular-nums;font-weight:600}.paper-table .paper-table-head [role=columnheader],.paper-table-head>div,.q-stat-label{font-family:var(--font-sans)!important;font-size:12px!important;letter-spacing:.02em!important;color:var(--ink-soft)!important}.errors-page .kpi-card.kpi-info .kpi-value{color:var(--color)!important}.errors-page .col-msg,.errors-page .col-msg *{font-weight:400!important}.errors-page .col-msg .msg-code,.errors-page .msg-code{font-size:13px!important}.admin-page select,.anonymize-page select,.errors-page select,.paper-page select,.paper-table select{height:34px;padding:0 30px 0 10px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--input-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236F6A61' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center;-webkit-appearance:none;appearance:none;font:inherit;font-size:13px;color:var(--input-color)}input[type=checkbox]{accent-color:var(--accent);width:15px;height:15px}.anonymize-page .form-control,.paper-page .form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page textarea{border:1px solid color-mix(in srgb,var(--ink-faint) 55%,var(--border-color))!important;border-radius:var(--radius-control)!important}.anonymize-page code,.paper-page code,.paper-page kbd{font-family:var(--font-mono)!important}.filter-chip.is-active .count,.paper-chip.is-active .count,.status-chip.active .count{color:inherit!important}.user-header h2{display:flex;align-items:center;flex-wrap:wrap;gap:10px}.status-dot-wrap{display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:500;color:var(--ink-soft)}.user-actions{display:flex;flex-wrap:wrap;gap:8px}.user-detail-grid .detail-value.is-empty{color:var(--ink-muted);font-style:normal;font-family:var(--font-sans)!important}.admin-section-header h2{font-family:var(--font-sans)!important;font-size:15px!important;font-weight:600!important;letter-spacing:0!important;text-transform:none!important;color:var(--color)!important}.errors-page .kpi-sub.down,.errors-page .kpi-sub.up{color:var(--ink-soft)!important}.errors-page .kpi-card.kpi-error .kpi-value{color:var(--color)!important}.errors-page .kpi-card.kpi-error:has(.kpi-value:not(:empty)) .kpi-value{color:var(--color)!important}.errors-page .kpi-label{font-family:var(--font-sans)!important}.errors-page .col-msg{font-family:var(--font-mono)!important;font-size:13px!important}.admin-page .admin-head,.admin-page .paper-page-head,.admin-page>header{min-height:92px}.admin-page .paper-page-title{min-height:44px;display:flex;align-items:center;margin-bottom:8px!important}.admin-page .errors-header,.admin-page .ov-header,.admin-page .q-header{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;margin-bottom:8px!important}.admin-page .errors-header .paper-page-title,.admin-page .ov-header .paper-page-title,.admin-page .q-header .paper-page-title{margin-bottom:0!important}.admin-page .admin-nav{margin-top:0!important}.admin-page .paper-crumbs{min-height:20px;margin-bottom:4px!important}.dashboard-page,.paper-page{max-width:1112px}.landing-continue:disabled{background:var(--hover-bg-color)!important;border-color:var(--border-color)!important;color:var(--ink-muted)!important;opacity:1!important;box-shadow:none!important}.landing-continue:not(:disabled){background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.admin-summary .summary-pill{border-color:var(--border-color)!important;color:var(--color)!important;background:var(--paper-card)!important}.admin-summary .summary-pill::before{content:"";display:inline-block;width:7px;height:7px;margin-right:6px;border-radius:50%;background:var(--ink-faint);vertical-align:1px}.admin-summary .summary-pill.ok::before{background:var(--status-ready)}.admin-summary .summary-pill.progress::before,.admin-summary .summary-pill.warn::before{background:var(--status-progress)}.admin-summary .summary-pill.err::before,.admin-summary .summary-pill.error::before{background:var(--status-error)}.admin-summary .summary-pill .count{color:var(--ink-soft)!important;font-weight:600}.admin-summary .summary-pill.active{background:var(--accent-soft)!important;border-color:color-mix(in srgb,var(--accent) 45%,transparent)!important;color:var(--accent)!important}.admin-summary .summary-pill.active .count{color:var(--accent)!important}.admin-summary .summary-pill:has(.count:is(:empty)){opacity:.6}.admin-page .btn-dark,.admin-page .btn-primary{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.btn-outline-danger{border:1px solid color-mix(in srgb,var(--status-error) 45%,transparent)!important;color:var(--status-error)!important;background:0 0!important}.admin-page .paper-table-head>*,.admin-page [role=columnheader],.admin-page h3,.errors-page th,.q-card-title,.q-header-label,.q-panel-title{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12.5px!important;font-weight:600!important;color:var(--ink-soft)!important}.ov-flag{color:var(--status-error);font-weight:600}.overview-page .ov-daily-value{color:var(--color)!important}@media (max-width:767px){.paper-table.paper-table-dashboard{margin-left:0!important;margin-right:0!important;width:100%!important}.dashboard-meta{flex-wrap:wrap;row-gap:8px}.dashboard-active-filters{flex-basis:100%;flex-wrap:wrap}.anonymize-landing{padding-top:20px!important}.anonymize-landing .paper-page-lede{font-size:15px!important}}.anonymize-landing .source-privacy-note,.anonymize-landing .text-muted:has(.ui-icon){display:flex;gap:8px;align-items:flex-start;padding:10px 12px;margin-top:12px;border-radius:var(--radius-control);background:color-mix(in srgb,var(--status-progress) 9%,transparent);color:var(--color)!important;font-size:13px}.repo-access-note>span:has(.ui-icon){display:flex;gap:8px;align-items:flex-start;margin-top:10px;padding:9px 12px;border-radius:var(--radius-control);background:color-mix(in srgb,var(--status-progress) 10%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--status-progress) 25%,transparent);color:var(--color)!important;font-size:13px}.repo-access-note>span:has(.ui-icon) .ui-icon{color:var(--status-progress);flex-shrink:0;margin-top:2px}.admin-summary .summary-pill.active{background:var(--paper-card)!important;border-color:var(--border-color)!important;color:var(--color)!important}.admin-summary .summary-pill.active .count{color:var(--ink-soft)!important}.admin-summary .summary-pill:not(.active){background:0 0!important;border-style:dashed!important;color:var(--ink-muted)!important}.admin-summary .summary-pill:not(.active),.admin-summary .summary-pill:not(.active)>:not(.count){text-decoration:line-through;text-decoration-color:var(--ink-faint)}.admin-summary .summary-pill:not(.active) .count{text-decoration:none}.admin-summary .summary-pill:not(.active)::before{opacity:.45}.admin-page:has(.admin-users-row) .admin-summary .summary-pill:not(.active),.user-detail-card~* .admin-summary .summary-pill:not(.active){border-style:solid!important;text-decoration:none!important;color:var(--color)!important;background:var(--paper-card)!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill:not(.active)::before{opacity:1}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active{background:var(--accent-soft)!important;border-color:color-mix(in srgb,var(--accent) 45%,transparent)!important;color:var(--accent)!important}.copy-link-inline{display:inline-flex!important;align-items:center;gap:6px;height:30px;padding:0 10px!important;border:1px solid var(--border-color)!important;border-radius:var(--radius-control);background:var(--paper-card)!important;color:var(--color)!important;font-size:13px;font-weight:500}.copy-link-inline:hover{background:var(--hover-bg-color)!important}.copy-link-inline .project-type-svg{width:14px;height:14px}@media (max-width:767px){.copy-link-label{display:none}.copy-link-inline{width:36px;justify-content:center;padding:0!important}}.overview-page .ov-val-warn{color:var(--color)!important}.ov-flag{color:var(--status-progress)!important}@media (max-width:767px){.admin-page .admin-nav{flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none;-webkit-mask-image:linear-gradient(to right,#000 85%,transparent);mask-image:linear-gradient(to right,#000 85%,transparent)}.admin-page .admin-nav::-webkit-scrollbar{display:none}.admin-page .admin-nav a{flex-shrink:0}.admin-page .ov-header,.admin-page .paper-page-title{text-align:left!important;justify-content:flex-start!important}.overview-page .ov-kpi-row>:last-child:nth-child(odd){grid-column:1/-1}.admin-summary{flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none}.admin-summary>*{flex-shrink:0}}.user-detail-grid .detail-value:first-of-type{font-family:var(--font-mono);font-size:13px}.user-header .type-badge{font-family:var(--font-sans)!important;letter-spacing:0!important;text-transform:none!important;font-weight:600}.errors-page .count-badge,.errors-page .msg-count,.errors-page [class*=count]{background:var(--hover-bg-color)!important;color:var(--color)!important}.ov-flag.critical{color:var(--status-error)!important}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(280px,3fr) 180px 70px 150px 150px!important}.paper-table.paper-table-dashboard .paper-table-row>:last-child{display:flex;justify-content:flex-end;align-items:center;gap:4px}.copy-link-inline{white-space:nowrap}@media (max-width:767px){.admin-filter-row,.admin-page form[class*=filter]{display:grid!important;grid-template-columns:1fr;gap:8px}.admin-page form[class*=filter] input,.admin-page form[class*=filter] select{width:100%!important}.admin-page form[class*=filter]>*{width:100%}.admin-page form[class*=filter] label{font-size:12px!important}}@media (min-width:768px){.copy-link-inline{width:auto!important;min-width:0!important}}@media (max-width:767px){.copy-link-inline{width:44px!important;height:44px!important}}.errors-list-head,.errors-list-head>span,.q-section-label{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12.5px!important;font-weight:600!important;color:var(--ink-soft)!important}.errors-list-head>span{background:0 0!important}.q-state-chip{background:var(--paper-card)!important;color:var(--color)!important;border:1px solid var(--border-color)!important;text-transform:none!important;font-family:var(--font-sans)!important;letter-spacing:0!important}.q-state-chip::before{content:"";display:inline-block;width:7px;height:7px;margin-right:6px;border-radius:50%;background:var(--ink-faint)}.q-state-chip.q-state-active::before{background:var(--accent)}.q-state-chip.q-state-delayed::before,.q-state-chip.q-state-waiting::before{background:var(--status-progress)}.q-state-chip.q-state-failed::before{background:var(--status-error)}.q-state-chip.q-state-completed::before{background:var(--status-ready)}.role-pill{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;background:var(--hover-bg-color);font:600 12px/1 var(--font-sans);color:var(--color)}.user-header .type-badge{background:var(--hover-bg-color)!important;border:0!important}.admin-filter-inline label,.admin-page label{font-size:12px!important;color:var(--ink-soft)!important}.admin-page select,.admin-page select.form-control{padding-right:30px!important;background:var(--input-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236F6A61' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center!important;-webkit-appearance:none!important;appearance:none!important}.admin-page .paper-table-row .text-muted,.admin-page .paper-table-row [class*=meta]{color:var(--ink-soft)!important}.dashboard-page.container,.paper-page,.paper-page.container{max-width:1112px!important;padding-left:32px!important;padding-right:32px!important}@media (max-width:767px){.dashboard-page.container,.paper-page,.paper-page.container{padding-left:18px!important;padding-right:18px!important}}.page-head-row>a.btn{margin-top:6px}.paper-faq-toc nav,.paper-settings-toc nav{border-left:1px solid var(--border-color)}.paper-settings-toc nav a{margin-left:-1px;border-left:2px solid transparent}.paper-settings-toc nav a.active{border-left-color:var(--accent)!important;color:var(--color)}.paper-page textarea{resize:vertical}.paper-page input[type=date]{height:38px;padding:0 10px;color:var(--input-color);font-family:var(--font-sans)}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto!important;grid-template-areas:"name actions" "meta meta";row-gap:6px}.paper-table.paper-table-dashboard .paper-table-row>:first-child{grid-area:name}.paper-table.paper-table-dashboard .paper-table-row>:last-child{grid-area:actions;align-self:start}.paper-table.paper-table-dashboard .paper-table-row>:not(:first-child):not(:last-child){grid-column:1/-1}.copy-link-inline{width:36px!important;height:36px!important}.admin-filter-toolbar{display:grid!important;grid-template-columns:1fr 1fr;gap:8px}.admin-filter-toolbar>:first-child{grid-column:1/-1}.admin-filter-toolbar .admin-filter-inline{display:grid!important;gap:4px}.admin-filter-toolbar .admin-filter-inline label{margin:0}.admin-filter-toolbar>:last-child{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center}.overview-page .ov-header{align-items:flex-start!important;text-align:left!important}.overview-page .ov-header .paper-page-title{width:100%;justify-content:flex-start!important;text-align:left!important}}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{flex-wrap:wrap!important;align-items:flex-start!important;row-gap:6px}.paper-table.paper-table-dashboard .paper-table-row>.cell-anon{flex:1 1 0!important;min-width:0;order:1}.paper-table.paper-table-dashboard .paper-table-row>.cell-actions{flex:0 0 auto!important;order:2;width:auto!important;margin:0!important}.paper-table.paper-table-dashboard .paper-table-row>.dashboard-row-meta{flex:1 1 100%!important;order:3}.admin-filter-toolbar{display:block!important}.admin-filter-row{display:grid!important;grid-template-columns:1fr 1fr;gap:8px!important}.admin-filter-row>:first-child{grid-column:1/-1}.admin-filter-row .admin-filter-inline{display:grid!important;gap:4px;min-width:0}.admin-filter-row .admin-filter-inline input{width:100%!important}.admin-filter-row>:last-child{grid-column:1/-1;display:flex!important;justify-content:space-between;align-items:center;flex-direction:row!important}.admin-filter-row>:last-child .btn{width:auto!important}}.page-head-row .btn-primary,.paper-page .btn-primary{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.dark-mode .paper-page .btn-primary{background:#1f6a64!important;border-color:#1f6a64!important}.q-section-right,.q-section-right *,.q-stat-label{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12px!important;color:var(--ink-soft)!important}.q-card .q-card-sub,[class*=q-card] small{font-family:var(--font-sans)!important}.quota-ring-track{stroke:var(--border-color)!important;stroke-width:3!important;fill:none}.quota-ring-fill{stroke:var(--accent)!important;stroke-width:3!important;fill:none;stroke-linecap:round;transform:rotate(-90deg);transform-origin:12px 12px;min-width:0}.quota-summary-ring{width:18px!important;height:18px!important}.errors-list-head .col-count,.errors-list-head .col-msg{background:0 0!important;font-family:var(--font-sans)!important;font-weight:600!important}.errors-page .col-count>*{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;background:var(--hover-bg-color)!important;color:var(--color)!important;font:600 12px/1 var(--font-sans);box-shadow:none!important}.errors-page .col-count>*>*{background:0 0!important}.errors-page .col-when{display:flex;flex-direction:column;gap:0;line-height:1.3}.errors-page .errors-list>div:not(.errors-list-head){padding-top:10px!important;padding-bottom:10px!important;min-height:0!important}.errors-page .kpi-sub{font-style:normal!important;color:var(--ink-soft)!important}.q-legend-exec{display:inline-block;width:14px;height:0;border-top:2px dashed var(--ink-muted);vertical-align:middle;margin-right:4px}.admin-filter-row .admin-search,.admin-filter-row input[placeholder^=Search],.admin-filter-row input[type=search],.admin-filter-row>:first-child{flex:1 1 320px!important;min-width:280px}.repo-access-note>span:has(.ui-icon){background:var(--hover-bg-color)!important;box-shadow:none!important}.repo-access-note>span:has(.ui-icon) .ui-icon{color:var(--ink-soft)!important}.dark-mode .paper-table-row{background:0 0!important}.dark-mode .paper-table-row:hover{background:rgba(255,255,255,.04)!important}.dark-mode .btn-primary,.dark-mode .dashboard-page .btn-primary,.dark-mode .paper-page .btn-primary{background:#1f6a64!important;border-color:#1f6a64!important;color:#fff!important}.dark-mode .dashboard-type-filter.btn-primary{background:rgba(124,199,190,.16)!important;border-color:transparent!important;color:#a8ddd6!important}.paper-page .form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page textarea{border-color:var(--border-color)!important}.paper-page .form-control:focus,.paper-page textarea:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.errors-page .col-count{background:0 0!important;box-shadow:none!important;display:flex;align-items:center}.errors-page .col-count::after,.errors-page .col-count::before{content:none!important;display:none!important}.errors-page .col-count .count-pill{width:auto!important;min-width:0!important}.paper-page .dashboard-type-filter.btn-primary{background:var(--accent-soft)!important;border-color:transparent!important;color:var(--accent)!important}.paper-page>.row{margin-left:0!important;margin-right:0!important}.paper-page>.row>[class*=col],.paper-page>.row>[class*=w-100]{padding-left:0!important;padding-right:0!important}.q-table th{font-family:var(--font-sans)!important;text-transform:none!important;letter-spacing:0!important;font-size:12.5px!important;font-weight:600!important;color:var(--ink-soft)!important}.admin-page input:not([type=checkbox]),.claim-page input,.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=search],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea,.paper-page textarea.form-control{border:1px solid var(--border-color)!important}.dark-mode .admin-page input:not([type=checkbox]),.dark-mode .claim-page input,.dark-mode .paper-page input.form-control,.dark-mode .paper-page textarea{border-color:#5a554d!important}.landing-continue:disabled{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important;opacity:.45!important}.overview-page .ov-kpi-sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media (max-width:767px){.admin-page .admin-nav{flex-wrap:wrap!important;overflow:visible!important;-webkit-mask-image:none!important;mask-image:none!important;row-gap:4px}.admin-summary{flex-wrap:wrap!important;overflow:visible!important;row-gap:8px}.admin-page .paper-table-row .text-muted,.admin-page .paper-table-row [class*=meta]{white-space:normal!important}}.summary-total small{font-size:13px;font-weight:500;color:var(--ink-soft);margin-left:2px}@media (max-width:767px){.overview-page .ov-daily-row,.overview-page .ov-kpi-row,.overview-page [class*=ov-][class*="-row"]{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:10px!important}.overview-page .ov-kpi-card,.overview-page [class*=ov-daily-card]{min-width:0!important;width:auto!important}.overview-page .ov-kpi-row>:last-child:nth-child(odd){grid-column:1/-1}}.user-detail-grid .detail-value:nth-of-type(1){font-family:var(--font-mono)!important}.paper-table-empty .empty-title{display:block;font-size:15px;font-weight:600;color:var(--color);margin-bottom:4px}.paper-table-empty>span{max-width:440px;text-align:center}.paper-table-empty-actions{margin-top:12px;display:flex;gap:8px;justify-content:center}.q-action-sep{display:inline-block;width:1px;height:22px;margin:0 6px;background:var(--border-color);vertical-align:middle}.hidden-toggle{padding:0;border:0;background:0 0;color:var(--link-color);font:inherit;text-decoration:underline;text-underline-offset:3px;cursor:pointer}.ov-meter{height:4px;margin:6px 0 2px;border-radius:4px;background:var(--hover-bg-color);overflow:hidden}.ov-meter>span{display:block;height:100%;border-radius:4px;background:var(--ink-faint)}.ov-meter.warn>span{background:var(--status-progress)}.ov-meter.crit>span{background:var(--status-error)}.admin-filters-toggle{display:none}@media (max-width:767px){.admin-filters-toggle{display:inline-flex;margin:8px 0}.admin-filter-toolbar .admin-filter-row:not(.is-open)>:not(:first-child){display:none!important}.admin-filter-toolbar .admin-filter-row:not(.is-open){grid-template-columns:1fr!important}}.paper-settings-layout,.settings-layout{max-width:none!important}.paper-settings-layout>:last-child,.settings-layout>:last-child{max-width:none!important;flex:1 1 auto}.errors-list-head>span{font-weight:600!important}.errors-page .errors-list>div:not(.errors-list-head){min-height:56px!important}.reading-layout{display:grid;grid-template-columns:minmax(0,720px) 220px;justify-content:center;gap:48px;padding:0 32px}.reading-layout .file-content.markdown-body{max-width:none!important;margin:0!important;padding:40px 0 96px!important;font-size:16px!important;line-height:1.7!important}.reading-layout .markdown-body h1{font-size:34px!important;font-weight:600!important;letter-spacing:-.02em;border-bottom:0!important;margin-bottom:12px!important}.reading-layout .markdown-body h2{font-size:24px!important;font-weight:600!important;letter-spacing:-.01em;border-bottom:0!important;margin-top:44px!important;padding-bottom:0!important;scroll-margin-top:72px}.reading-layout .markdown-body h3{font-size:18px!important;margin-top:28px!important;scroll-margin-top:72px}.reading-layout .markdown-body>p:first-of-type{font-size:18px;color:var(--ink-soft)}.readme-toc{position:sticky;top:68px;align-self:start;margin-top:44px;max-height:calc(100% - 100px);overflow:auto;display:grid;gap:2px;font-size:13px}.readme-toc-head{font-size:12px;font-weight:600;color:var(--color);margin-bottom:8px}.readme-toc a{display:block;padding:4px 0 4px 12px;border-left:1px solid var(--border-color);color:var(--ink-soft)!important;text-decoration:none!important;line-height:1.35}.readme-toc a:hover{color:var(--color)!important;border-left-color:var(--ink-muted)}.readme-toc a.lvl-3{padding-left:24px;font-size:12.5px}@media (max-width:1199px){.reading-layout{grid-template-columns:minmax(0,760px)}.readme-toc{display:none}}@media (max-width:767px){.reading-layout{padding:0 16px}.reading-layout .file-content.markdown-body{padding:20px 0 72px!important}.reading-layout .markdown-body h1{font-size:26px!important}.reading-layout .markdown-body h2{font-size:20px!important}}.demo{margin-top:28px;display:grid;gap:14px}.demo-terms{display:grid;gap:6px}.demo-terms label{font-size:13px;font-weight:600;color:var(--color);margin:0}.demo-terms label span{font-weight:400;color:var(--ink-soft);margin-left:6px}.demo-terms input{height:42px;padding:0 14px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);font:14px var(--font-mono);color:var(--color)}.demo-terms input:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.demo-panes{display:grid;grid-template-columns:1fr 1fr;gap:14px}.demo .pane{border:1px solid var(--border-color);border-radius:var(--radius-surface);background:var(--paper-card);overflow:hidden;display:flex;flex-direction:column}.demo .pane-header{display:flex;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid var(--border-soft);font-size:12px;color:var(--ink-soft)}.demo .pane-label{font-weight:600;color:var(--color)}.demo .pane-url{font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.demo .pane-after{box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,var(--border-color))}.demo .pane-body{min-height:150px;padding:14px 16px;font:13px/1.75 var(--font-mono);color:var(--color);white-space:pre-wrap}.demo-input{border:0;resize:vertical;background:0 0;outline:0;width:100%}.demo-input:focus{background:color-mix(in srgb,var(--accent) 3%,transparent)}.demo-output .redact-chip{font-size:.92em}@media (max-width:767px){.demo-panes{grid-template-columns:1fr}}.paper-frame-bar .dots{display:none!important}.paper-frame-bar{justify-content:flex-start!important;height:32px!important;background:0 0!important}.paper-frame-bar .url{text-align:left!important;flex:none!important}.reading-layout .markdown-body>p:first-of-type{font-size:inherit!important;color:inherit!important}@media (max-width:767px){.mobile-sub b{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:999px;background:var(--accent-soft);font-size:11.5px}.explorer-content .file-card-head{min-height:52px!important;padding-top:6px!important;padding-bottom:6px!important}.file-card-head .file-card-path{gap:4px!important;column-gap:4px!important}.file-card.is-code.no-wrap .file-card-body::after{width:48px!important;background:linear-gradient(to right,transparent,var(--paper-card) 60%)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 60%)!important}}.ace_search{justify-content:flex-start}.ace_searchbtn_close{margin-left:auto!important;order:99}.fm-chips span,.paper-preview .term{background:var(--hover-bg-color)!important;color:var(--ink-soft)!important;text-decoration-color:var(--ink-muted)!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.06)!important}.dark-mode .ace-nord-dark .ace_marker-layer .ace_redacted,.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.32)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.65)!important}.paper-link-arrow{font-size:15px!important}.paper-reassure{font-size:14px!important}.acct-menu-head,.dark-mode .acct-menu-head{color:var(--ink-soft)!important}.reading-layout{grid-template-columns:minmax(0,720px) 200px!important;gap:40px!important;padding:0 24px!important}.vb-snapshot{color:var(--ink-soft)!important;background:var(--paper-card)!important;box-shadow:inset 0 0 0 1px var(--border-color)}.paper-faq-lede a{color:var(--color)!important;border-bottom:1px solid var(--ink-faint)!important;text-decoration:none!important}.paper-faq-lede a:hover{border-bottom-color:var(--color)!important}.paper-qa,.paper-section-head{margin-top:96px!important}.ace-chrome .ace_constant.ace_language,.ace-chrome .ace_constant.ace_numeric,.ace-chrome .ace_support.ace_constant{color:#0550ae!important}.ace-nord-dark .ace_constant.ace_language,.ace-nord-dark .ace_constant.ace_numeric,.ace-nord-dark .ace_support.ace_constant{color:#79c0ff!important}.copy-url{display:inline-flex;align-items:center;gap:8px;max-width:230px;height:30px;padding:0 8px 0 10px;border:0;border-radius:var(--radius-control);background:var(--hover-bg-color);font:500 12.5px/1 var(--font-mono);color:var(--ink-soft);cursor:pointer;transition:background .12s,color .12s}.copy-url:hover{background:var(--accent-soft);color:var(--accent)}.copy-url:disabled{cursor:progress}.copy-url-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.copy-url .project-type-svg{width:14px;height:14px;flex-shrink:0;opacity:.75}.copy-url:hover .project-type-svg{opacity:1}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(260px,3fr) 170px 64px 140px 280px!important}@media (max-width:767px){.copy-url{max-width:44px;padding:0;justify-content:center;width:40px;height:40px}.copy-url-text{display:none}}.dashboard-page .btn.dashboard-type-filter.btn-primary,.paper-page .btn.dashboard-type-filter.btn-primary{background:var(--accent-soft)!important;border-color:transparent!important;color:var(--accent)!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(242,204,96,.42)!important;box-shadow:none!important;border-radius:2px}.ace_editor.ace-chrome .ace_marker-layer .ace_selection{background:#f2cc60!important;box-shadow:0 0 0 1px #c69026!important}.ace_editor.ace-nord-dark .ace_marker-layer .ace_selection{background:rgba(242,204,96,.55)!important;box-shadow:0 0 0 1px #f2cc60!important}.ace_searchbtn_close{background-image:none!important;width:28px!important;height:28px!important;display:inline-flex!important;align-items:center;justify-content:center;border-radius:var(--radius-control)!important;opacity:1!important;color:var(--ink-soft)!important}.ace_searchbtn_close::before{content:"\00d7";font:400 20px/1 var(--font-sans);color:var(--ink-soft)}.ace_searchbtn_close:hover{background-color:var(--hover-bg-color)!important}.ace_search{padding:6px 12px!important}.readme-toc a.is-active{color:var(--color)!important;border-left:2px solid var(--accent)!important;margin-left:-.5px;font-weight:500}.demo .pane-after{box-shadow:none!important;border-color:var(--border-color)!important}.demo .pane-body{min-height:120px!important}.demo-input{resize:none!important}.vb-snapshot .vb-dot{margin:0 2px;color:var(--ink-faint)}.dark-mode .btn-hero,.dark-mode .btn-primary,.dark-mode .landing-continue:not(:disabled){background:#2b8a80!important;border-color:#2b8a80!important;color:#fff!important}.paper-reassure li::before{content:""!important;display:inline-block;width:14px;height:14px;margin-right:7px;vertical-align:-2px;background:var(--accent);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center/contain no-repeat}.nav-support-inline .nav-heart{width:14px;height:14px;color:#e5484d;fill:#e5484d}.fm-chips span{color:var(--color)!important}@media (max-width:767px){.vb-icon,.vb-support{width:36px!important;height:36px!important}.vb-signin{height:36px!important}.navbar .nav-signin-m,.navbar-toggler{height:36px!important;border-radius:var(--radius-control)!important}}.overview-page .ov-spark-fill,.overview-page .ov-spark-fill-alt,.overview-page .ov-spark-fill-new-user,.overview-page .ov-spark-fill-user{background:color-mix(in srgb,var(--accent) 70%,transparent)!important}.overview-page .ov-dot-legend{background:var(--accent)!important}.errors-page .bucket-info,.errors-page .col-sev .sev-info::before,.errors-page .errors-chart [class*=info]{background:var(--ink-faint)!important}.errors-header .btn-danger{background:0 0!important;color:var(--status-error)!important;border:1px solid color-mix(in srgb,var(--status-error) 45%,transparent)!important;margin-left:10px}.conferences-page .paper-table:has(.paper-table-empty) .paper-table-head{display:none!important}.copy-url{max-width:280px!important}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(240px,3fr) 150px 60px 130px 320px!important}.dark-mode .paper-table-dashboard .paper-table-row{background:0 0!important}.ace_editor .ace_marker-layer .ace_redacted{background:color-mix(in srgb,var(--accent) 18%,transparent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 60%,transparent)!important}.ace_searchbtn_close{margin-left:4px!important}.ace_search .ace_button{font-size:12px!important}.nav-support-inline .nav-heart,.support-cta .ui-icon,.vb-support .ui-icon{color:#c25b5e!important;fill:#c25b5e!important}.nav-support-inline:hover .nav-heart,.support-cta:hover .ui-icon,.vb-support:hover .ui-icon{color:#e5484d!important;fill:#e5484d!important}.vb-support{background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important}.dark-mode .nav-support-inline .nav-heart,.dark-mode .vb-support .ui-icon{color:#d9787b!important;fill:#d9787b!important}.readme-toc .toc-rail{border-left:1px solid var(--border-color)}.readme-toc a,.readme-toc a.is-active{border-left:0!important;margin-left:-1px!important;padding:5px 0 5px 13px!important;line-height:1.4}.readme-toc a.lvl-3{padding-left:25px!important}.readme-toc a:hover{box-shadow:inset 1px 0 0 var(--ink-muted)}.readme-toc a.is-active{box-shadow:inset 2px 0 0 var(--accent)!important}@media (min-width:992px){.demo-panes{grid-template-columns:1fr 1.4fr;gap:40px}}.fm-chips span{text-decoration:none!important}.dark-mode .btn-hero,.dark-mode .btn-primary,.dark-mode .landing-continue:not(:disabled){background:#237a71!important;border-color:#237a71!important}.dark-mode .paper-frame{box-shadow:0 0 0 1px #34302b,0 24px 60px rgba(0,0,0,.5)!important}.dark-mode .leftCol{background:#1d1a17!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.09)!important}.dark-mode .explorer-content .markdown-body img{filter:none!important}@media (max-width:767px){.viewer-bar{padding-left:12px!important;padding-right:12px!important}.vb-signin{height:30px!important;padding:0 10px!important}}.paper-table.paper-table-dashboard .paper-table-head,.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(240px,3fr) 190px 60px 120px 300px!important}.paper-table-dashboard .cell-status .status-sub-date{white-space:normal!important;overflow:visible!important;text-overflow:clip!important}.row-edit{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--radius-control);color:var(--ink-soft)!important}.row-edit:hover{background:var(--hover-bg-color);color:var(--color)!important}.row-edit .ui-icon{width:16px;height:16px}.cell-actions{display:flex;align-items:center;gap:4px;justify-content:flex-end}.dashboard-filter-controls .btn-group[aria-label=Type]{background:var(--hover-bg-color);padding:3px;border-radius:calc(var(--radius-control) + 3px);gap:2px}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn{border:0!important;background:0 0!important;color:var(--ink-soft)!important;border-radius:var(--radius-control)!important;height:32px;box-shadow:none!important}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn.btn-primary{background:var(--paper-card)!important;color:var(--color)!important;box-shadow:0 1px 2px rgba(0,0,0,.08),0 0 0 1px var(--border-color)!important}.admin-filter-row .form-control,.dashboard-page .dashboard-filter-row .form-control,.paper-page .search-wrap .form-control{border:1px solid #a39d92!important}.dark-mode .admin-filter-row .form-control,.dark-mode .dashboard-page .dashboard-filter-row .form-control,.dark-mode .paper-page .search-wrap .form-control{border-color:#4a443d!important}.ov-health{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;margin:0 0 20px;border-radius:var(--radius-control);background:color-mix(in srgb,var(--status-error,#b42318) 8%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--status-error,#b42318) 30%,transparent);font-size:14px;color:var(--color)}.ov-health .ui-icon{width:18px;height:18px;flex:none;margin-top:1px;color:var(--status-error,#b42318)}.dark-mode .ov-health{background:rgba(240,138,130,.1);box-shadow:inset 0 0 0 1px rgba(240,138,130,.35)}.dark-mode .ov-health .ui-icon{color:#f08a82}.cell-anon .record-type-icon{display:none!important}.cell-anon{padding-left:0!important}.errors-page .errors-row-main{padding-top:10px!important;padding-bottom:10px!important;min-height:46px;align-items:center!important}.errors-page .col-when{display:flex;gap:8px;align-items:baseline}.errors-page .when-abs{font-size:12px}.errors-page .msg-code{font-family:var(--font-sans)!important;font-weight:500!important;font-size:13.5px!important}@media (max-width:767px){.viewer-bar .vb-icon,.viewer-bar .vb-signin,.viewer-bar .vb-support{height:36px!important;border-radius:var(--radius-control)!important;background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important;border:0!important}.viewer-bar .vb-icon,.viewer-bar .vb-support{width:36px!important}.viewer-bar .vb-signin{padding:0 12px!important}.file-card-head .card-files-btn,.file-card-head .tb-copy-m,.file-card-head .tb-more-btn{width:36px!important;height:36px!important;border-radius:var(--radius-control)!important;background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important;color:var(--color)!important}.file-card.is-code.no-wrap .file-card-body::after{width:64px!important;background:linear-gradient(to right,transparent,var(--paper-card) 70%)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,transparent,var(--canvas-bg-color) 75%)!important}}.btn-toggle.is-on,.btn-toggle[aria-pressed=true]{color:var(--accent)!important;background:var(--accent-soft)!important}.dark-mode .btn-toggle.is-on{background:rgba(124,199,190,.14)!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.78em!important;padding:0 6px!important;vertical-align:.08em!important;font-weight:600!important}@media (min-width:1200px){.readme-toc{padding-top:10px}}.fm-toggles span{color:var(--color)!important}.dark-mode .paper-frame{border:0!important;box-shadow:0 0 0 1px #34302b,0 24px 60px rgba(0,0,0,.45)!important}.q-section-right .q-legend-completed{color:#2f7d4f!important}.q-section-right .q-legend-failed{color:#b42318!important}.dark-mode .q-section-right .q-legend-completed{color:#6fbf8a!important}.dark-mode .q-section-right .q-legend-failed{color:#f08a82!important}.q-chart-wrap canvas{max-height:180px}.avatar-fallback{width:28px;height:28px;flex:none;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--hover-bg-color);color:var(--ink-soft);font-size:12px;font-weight:600}.mono-id{font-family:var(--font-mono)!important;font-size:13px!important}.landing-continue:disabled{background:var(--hover-bg-color)!important;border-color:var(--border-color)!important;color:var(--ink-soft)!important;opacity:1!important;cursor:not-allowed}.paper-table .anon-sub,.paper-table .anon-sub *{font-size:12px}.dashboard-heading-actions{display:flex;align-items:center;gap:10px}.dashboard-heading-actions .dashboard-quota{position:relative;margin:0!important}.dashboard-heading-actions .quota-panel{right:0;left:auto!important}@media (max-width:767px){.admin-nav{display:flex!important;flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none;gap:4px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px}.admin-nav::-webkit-scrollbar,.admin-summary::-webkit-scrollbar{display:none}.admin-nav a{flex:none;white-space:nowrap}.admin-summary{display:flex!important;flex-wrap:nowrap!important;overflow-x:auto;scrollbar-width:none;gap:6px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px}.admin-summary>*{flex:none;white-space:nowrap}.paper-table .anon-sub{flex-wrap:wrap!important;white-space:normal!important;row-gap:2px}.paper-table .anon-sub>a,.paper-table .anon-sub>span{overflow:visible!important;text-overflow:clip!important;white-space:nowrap}.paper-table .cell-anon .repo-name{white-space:normal!important;overflow-wrap:anywhere;overflow:visible!important;text-overflow:clip!important}.overview-page .ov-kpi-sub{white-space:normal!important;overflow:visible!important;text-overflow:clip!important}.overview-page .ov-kpi-row{grid-template-columns:1fr 1fr!important}}.dashboard-page .dashboard-heading-actions{flex-direction:row!important;align-items:center!important;gap:10px!important}@media (max-width:767px){.admin-page .admin-nav{flex-wrap:nowrap!important;overflow-x:auto!important;margin-left:-16px;margin-right:-16px;padding:0 16px 4px}header.viewer-bar .vb-icon,header.viewer-bar .vb-support{box-shadow:inset 0 0 0 1px var(--border-color)!important;background:0 0!important}}@media (max-width:767px){.dashboard-page .dashboard-heading{flex-direction:column!important;align-items:stretch!important;gap:12px}.dashboard-page .dashboard-heading-actions{justify-content:space-between!important;width:100%}.row-edit{display:none!important}}@media (max-width:767px){.paper-table-dashboard .copy-url{background:0 0!important;box-shadow:inset 0 0 0 1px var(--border-color)!important}}@media (max-width:767px){.mobile-sub{display:flex!important;align-items:center;gap:6px;margin-top:3px!important}.mobile-sub>*+::before{content:"·";margin-right:6px;color:var(--ink-muted);font-weight:400}.mobile-sub>b::before{display:none}header.viewer-bar .vb-icon,header.viewer-bar .vb-support{box-shadow:none!important;background:0 0!important}.file-card-head .card-files-btn,.file-card-head .tb-copy-m,.file-card-head .tb-more-btn{box-shadow:none!important;background:0 0!important}.file-card-head .card-files-btn:active,.file-card-head .tb-copy-m:active,.file-card-head .tb-more-btn:active{background:var(--hover-bg-color)!important}.file-card-head{padding-top:6px!important;padding-bottom:6px!important}}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{box-shadow:none!important;border:0!important;padding:0 5px!important;font-weight:500!important}@media (min-width:1200px){.reading-layout{gap:40px!important}}.ace_search .ace_search_field:focus,.ace_search_field:focus{box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 22%,transparent)!important;border-color:color-mix(in srgb,var(--accent) 70%,transparent)!important}.demo-terms label span,.paper-eyebrow,.paper-footer,.paper-footer a{color:var(--ink-soft)!important}.dark-mode .anonymize-page .btn-ink,.dark-mode .btn-hero,.dark-mode .btn-primary,.dark-mode .landing-continue,.dark-mode .page-head-row .btn-primary,.dark-mode .paper-page .btn-ink,.dark-mode .paper-page .btn-primary{background:#237a71!important;border-color:#237a71!important;color:#fff!important}.dark-mode .anonymize-page .btn-ink:hover,.dark-mode .btn-primary:hover,.dark-mode .paper-page .btn-ink:hover{background:#1e6b63!important;color:#fff!important;filter:none!important}.landing-continue,.landing-continue:disabled{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important;opacity:1!important;cursor:pointer}.errors-page .seg.seg-info{background:var(--ink-faint)!important;opacity:1!important}.errors-page .dot-info,.errors-page .sev-dot.sev-info{background:var(--ink-muted)!important}@media (max-width:767px){.admin-page .admin-nav,.admin-summary{-webkit-mask-image:linear-gradient(to right,#000 82%,transparent)!important;mask-image:linear-gradient(to right,#000 82%,transparent)!important;scroll-snap-type:x proximity}.admin-page .admin-nav a,.admin-summary>*{scroll-snap-align:start}.admin-page .admin-filters-toggle,.admin-page .dashboard-filters-toggle{width:auto!important}}.admin-filter-inline{display:inline-flex;align-items:center;gap:6px;margin-left:14px}.admin-filter-inline label{margin:0!important;font-size:12.5px;color:var(--ink-soft)}.admin-filter-row .form-control,.dashboard-page .dashboard-filter-row .form-control,.paper-page .search-wrap .form-control{border-color:var(--border-color)!important;height:36px!important}.admin-filter-row .form-control:focus,.dashboard-page .dashboard-filter-row .form-control:focus{border-color:var(--accent)!important}.dashboard-page .dashboard-filter-controls .btn,.dashboard-page .dashboard-filter-controls .dropdown-toggle{height:36px}.admin-page .cell-repos a,.paper-table [role=cell].num a{color:var(--color)!important;font-family:var(--font-sans)!important}.errors-page .col-count{font-weight:inherit!important;color:inherit!important}.errors-page .errors-row-main{min-height:52px!important;padding-top:8px!important;padding-bottom:8px!important}.connections-grid{display:grid!important;grid-template-columns:1fr 1fr!important;align-items:stretch}@media (max-width:767px){.connections-grid{grid-template-columns:1fr!important}}.admin-page .admin-filter-inline .form-control,.admin-page .admin-filter-row .form-control,.admin-page .search-wrap .form-control{height:36px!important;min-height:36px!important;border:1px solid var(--border-color)!important;font-size:13.5px!important;padding-top:0!important;padding-bottom:0!important}.admin-page .admin-filter-inline .form-control:focus,.admin-page .admin-filter-row .form-control:focus{border-color:var(--accent)!important}.admin-page select.form-control{height:36px!important;border:1px solid var(--border-color)!important}.admin-page .paper-table .anon-sub{gap:0!important;column-gap:0!important}.admin-page .paper-table .anon-sub>a{border-bottom:0!important;text-decoration:none}.tb-menu-note{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13px;color:var(--ink-soft)}.tb-menu-note .ui-icon{width:16px;height:16px}.readme-toc a{color:var(--ink-soft)!important;font-size:13.5px!important}.readme-toc a.lvl-3{font-size:13px!important}.readme-toc a.is-active{color:var(--accent)!important;font-weight:500!important}.dark-mode .readme-toc a.is-active{color:#a8ddd6!important}.ace_search_form .ace_searchbtn{border-radius:0!important;margin:0!important}.ace_search_form .ace_searchbtn.prev{border-radius:var(--radius-control) 0 0 var(--radius-control)!important;margin-left:8px!important}.ace_search_form .ace_searchbtn.next{border-radius:0 var(--radius-control) var(--radius-control) 0!important;margin-left:-1px!important}.ace_search .ace_searchbtn.next,.ace_search .ace_searchbtn.prev{box-shadow:none!important}.ace_editor .ace_marker-layer .ace_selected-word,.ace_editor .ace_marker-layer .ace_selection{box-shadow:none!important;border:0!important}@media (max-width:767px){.file-card-head{padding-left:12px!important;padding-right:12px!important}.file-card-head .file-card-path{column-gap:8px!important}}.dark-mode .files .file.active>a{background:rgba(255,255,255,.07)!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,700px) 190px!important;gap:40px!important;padding:0 24px!important}}.ace-chrome .ace_string{color:#0a3069!important}.ace-nord-dark .ace_string,.dark-mode .ace_editor .ace_string{color:#a5c8ff!important}.ace_editor .ace_marker-layer .ace_redacted{background:color-mix(in srgb,var(--accent) 20%,transparent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 55%,transparent)!important;border-radius:3px}.ace_search .ace_button{height:28px!important;min-width:30px!important;font-size:12px!important;color:var(--ink-soft)!important;display:inline-flex!important;align-items:center;justify-content:center}.ace_search .ace_button.checked{background:var(--accent-soft)!important;color:var(--accent)!important;box-shadow:inset 0 0 0 1px var(--accent)!important}.ace_search_counter{font-size:12.5px!important;color:var(--ink-soft)!important}.ace_searchbtn_close::before{font-size:18px!important;font-weight:300!important}.btn-toggle.is-on{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 45%,transparent)!important}.vb-snapshot{background:0 0!important;box-shadow:none!important;color:var(--ink-soft)!important;padding-left:4px!important;padding-right:4px!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.1)!important;color:#f2eee8!important}.paper-faq-title,.paper-hero h1,.paper-section-head h2{text-wrap:balance}@media (min-width:992px){.demo-panes{grid-template-columns:1fr 1fr!important;gap:20px!important}}.paper-faq details a:not(.btn),.paper-faq-a a{text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;text-decoration-color:color-mix(in srgb,currentColor 40%,transparent)}@media (max-width:767px){.file-card.is-code .ace_gutter-cell{padding-left:8px!important;padding-right:8px!important}}.auto-refresh-label{font-family:var(--font-sans)!important}.ov-spark-fill{opacity:1!important;background:color-mix(in srgb,var(--accent) 70%,transparent)!important}.connections-grid .connection-link:not(.connection-danger){display:inline-flex;align-items:center;height:34px;padding:0 12px;border:1px solid var(--border-color);border-radius:var(--radius-control);color:var(--color)!important;text-decoration:none!important}.reading-layout .file-content.markdown-body,.reading-layout .markdown-body{padding-left:0!important;padding-right:0!important}.explorer-content .file-card .reading-layout .file-content.markdown-body{padding-left:0!important;padding-right:0!important}.admin-page h1.paper-page-title{margin:0!important;line-height:44px!important;min-height:44px}.admin-page .errors-header,.admin-page .ov-header,.admin-page .q-header{min-height:44px;margin:0 0 12px!important;display:flex;align-items:center;justify-content:space-between}.admin-page>h1.paper-page-title{margin-bottom:12px!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active{background:var(--accent)!important;border-color:var(--accent)!important;color:#fff!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active .count{color:rgba(255,255,255,.8)!important}.admin-page:has(.admin-users-row) .admin-summary .summary-pill.active::before{display:none!important}.admin-page input[type=checkbox]{accent-color:var(--accent);width:16px;height:16px}.ov-health{background:var(--paper-card)!important;box-shadow:inset 3px 0 0 var(--status-error,#b42318),inset 0 0 0 1px var(--border-color)!important}.dark-mode .ov-health{background:var(--paper-card)!important;box-shadow:inset 3px 0 0 #f08a82,inset 0 0 0 1px var(--border-color)!important}.dashboard-quota .quota-summary-ring{display:none!important}.errors-page .errors-row-main{min-height:44px!important}.errors-page .when-abs{display:none!important}@media (max-width:767px){.admin-summary{flex-wrap:wrap!important;overflow:visible!important;-webkit-mask-image:none!important;mask-image:none!important;margin-left:0!important;margin-right:0!important;padding:0!important}.admin-summary .summary-pill{height:30px!important;padding:0 10px!important;font-size:13px!important}.admin-page .admin-nav{flex-wrap:wrap!important;overflow:visible!important;-webkit-mask-image:none!important;mask-image:none!important;margin-left:0!important;margin-right:0!important;padding:0!important;gap:2px!important}.admin-page .admin-nav a{min-height:34px!important;padding:0 10px!important;font-size:13.5px!important}.admin-page .admin-nav a .ui-icon{display:none!important}}.fm-toggles span{display:inline-flex;align-items:center;gap:6px}.fm-toggles span::before{content:"";width:12px;height:12px;flex:none}.fm-toggles .is-on::before{background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E") center/contain no-repeat}.faq-toc a,.paper-faq nav a,.paper-faq-toc a{text-decoration:none!important}@media (max-width:991px){.navbar .navbar-toggler{border-color:transparent!important;background:0 0!important;box-shadow:none!important}}.ace-chrome .ace_gutter-cell{color:#8a847a!important}.dark-mode .ace_editor .ace_gutter-cell{color:#7a746b!important}.ace-chrome .ace_keyword,.ace-chrome .ace_keyword.ace_operator.ace_new,.ace-chrome .ace_storage{color:#7a3e6e!important}.ace-chrome .ace_string{color:#6b5a1e!important}.ace-chrome .ace_entity.ace_name.ace_function,.ace-chrome .ace_support.ace_function{color:#2b2824!important;font-weight:500}.ace-chrome .ace_constant,.ace-chrome .ace_constant.ace_language,.ace-chrome .ace_constant.ace_numeric{color:#8a4b12!important}.ace-chrome .ace_comment{color:#8a847a!important;font-style:italic}.ace-chrome .ace_identifier,.ace-chrome .ace_variable{color:#2b2824!important}.ace-chrome .ace_keyword.ace_operator,.ace-chrome .ace_paren,.ace-chrome .ace_punctuation{color:#6e6860!important}.ace-chrome .ace_entity.ace_name.ace_tag,.ace-chrome .ace_support.ace_class,.ace-chrome .ace_support.ace_type{color:#7a3e6e!important}.ace-chrome .ace_entity.ace_other.ace_attribute-name{color:#8a4b12!important}.dark-mode .ace_editor .ace_entity.ace_name.ace_tag,.dark-mode .ace_editor .ace_keyword,.dark-mode .ace_editor .ace_storage,.dark-mode .ace_editor .ace_support.ace_type{color:#d7a6cb!important}.dark-mode .ace_editor .ace_string{color:#d9c08a!important}.dark-mode .ace_editor .ace_entity.ace_name.ace_function,.dark-mode .ace_editor .ace_identifier,.dark-mode .ace_editor .ace_support.ace_function,.dark-mode .ace_editor .ace_variable{color:#ece7df!important}.dark-mode .ace_editor .ace_constant,.dark-mode .ace_editor .ace_constant.ace_numeric,.dark-mode .ace_editor .ace_entity.ace_other.ace_attribute-name{color:#e0b070!important}.dark-mode .ace_editor .ace_comment{color:#8a847a!important;font-style:italic}.dark-mode .ace_editor .ace_keyword.ace_operator,.dark-mode .ace_editor .ace_paren,.dark-mode .ace_editor .ace_punctuation{color:#a39d92!important}.ace_editor .ace_marker-layer .ace_redacted{background:rgba(31,106,100,.22)!important;box-shadow:inset 0 0 0 1px rgba(31,106,100,.55)!important;border-radius:3px;mix-blend-mode:multiply}.dark-mode .ace_editor .ace_marker-layer .ace_redacted{background:rgba(124,199,190,.26)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.6)!important;mix-blend-mode:screen}.ace-chrome .ace_gutter-cell{color:#77716a!important}.dark-mode .ace_editor .ace_gutter-cell{color:#8a847a!important}@media (min-width:1200px){.reading-layout{justify-content:center!important}}.fm-toggles span:not(.is-on)::before{display:none!important}.demo-panes .pane-header{display:flex;gap:10px;min-width:0}.demo-panes .pane-url{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (max-width:767px){.mobile-sub{font-size:12.5px!important;line-height:1.4}}.ov-live{font-size:12.5px;color:var(--ink-soft)}.admin-summary .summary-pill:not(.active) .count{text-decoration:none!important}.paper-table:not(.paper-table-dashboard) .paper-table-head,.paper-table:not(.paper-table-dashboard) .paper-table-row{column-gap:24px!important}.connection-label{font-size:12.5px;color:var(--ink-soft);margin-right:4px}.paper-table-dashboard .copy-url:hover{background:var(--hover-bg-color)!important;box-shadow:inset 0 0 0 1px var(--border-color)!important}.dashboard-page .dashboard-count button,.dashboard-page .show-hidden{font-size:13px!important}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{display:grid!important;grid-template-columns:minmax(0,1fr) auto!important;grid-template-areas:"name more" "meta meta" "link link"!important;row-gap:8px!important}.paper-table.paper-table-dashboard .paper-table-row>.cell-anon{grid-area:name!important;grid-column:auto!important;min-width:0}.paper-table.paper-table-dashboard .paper-table-row>.dashboard-row-meta{grid-area:meta!important;grid-column:auto!important}.paper-table.paper-table-dashboard .paper-table-row>.cell-actions{display:contents!important}.paper-table-dashboard .cell-actions>.copy-url{grid-area:link;width:100%!important;height:36px!important;padding:0 12px!important;justify-content:space-between!important;background:var(--hover-bg-color)!important;box-shadow:none!important}.paper-table-dashboard .cell-actions>.copy-url .copy-url-text{display:inline!important;min-width:0;overflow:hidden;text-overflow:ellipsis}.paper-table-dashboard .cell-actions>.dropdown{grid-area:more;align-self:start}.paper-table-dashboard .cell-actions>.project-details-button,.paper-table-dashboard .cell-actions>.row-edit{display:none!important}}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{padding-right:12px!important}}.vb-support{background:0 0!important;box-shadow:none!important;border:0!important}.vb-support:hover{background:var(--hover-bg-color)!important}.vb-snapshot{margin-right:8px!important}.paper-eyebrow,.paper-stat small,.paper-stat-sub,.paper-stats .sub{color:#5e5950!important}.dark-mode .paper-eyebrow,.dark-mode .paper-stat-sub{color:#b5aea3!important}.paper-qa,.paper-section-head{margin-top:80px!important}.demo{margin-bottom:0!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(242,204,96,.35)!important}.ace_editor .ace_marker-layer .ace_selection{background:rgba(232,184,58,.55)!important}.paper-faq details>:not(summary),.paper-faq-a{max-width:720px!important}.q-state-chip.q-state-active::before{background:var(--color)!important}.q-state-chip.q-state-delayed::before{background:var(--ink-muted)!important}.repo-access-note>span:last-child{display:flex;gap:8px;align-items:flex-start;padding:10px 12px;border-radius:var(--radius-control);background:color-mix(in srgb,#b7791f 8%,transparent);box-shadow:inset 3px 0 0 #b7791f;color:var(--color)!important}.repo-access-note>span:last-child .ui-icon{color:#9a6416;flex:none;margin-top:2px}.dark-mode .repo-access-note>span:last-child{background:rgba(224,176,112,.1);box-shadow:inset 3px 0 0 #e0b070}.repo-access-note>span:has(.ui-icon){background:color-mix(in srgb,#b7791f 8%,transparent)!important;box-shadow:inset 3px 0 0 #b7791f!important;color:var(--color)!important}.repo-access-note>span:has(.ui-icon) .ui-icon{color:#9a6416!important}.dark-mode .repo-access-note>span:has(.ui-icon){background:rgba(224,176,112,.1)!important;box-shadow:inset 3px 0 0 #e0b070!important}.dark-mode .repo-access-note>span:has(.ui-icon) .ui-icon{color:#e0b070!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,680px) 190px!important;padding:0 40px!important}}.ace-chrome .ace_entity.ace_name.ace_tag,.ace-chrome .ace_keyword,.ace-chrome .ace_storage,.ace-chrome .ace_support.ace_type{color:#93306a!important}.ace-chrome .ace_string{color:#7a5300!important}.ace-chrome .ace_constant,.ace-chrome .ace_constant.ace_numeric{color:#a14a0f!important}.dark-mode .ace_editor .ace_string{color:#cdb98e!important}.ace_editor .ace_marker-layer .ace_redacted{background:rgba(31,106,100,.28)!important;box-shadow:inset 0 0 0 1px rgba(31,106,100,.65)!important}.ace_search{background:var(--paper-card)!important;box-shadow:inset 0 1px 0 var(--border-color),inset 0 -1px 0 var(--border-color)!important}.ace_search .ace_search_field{height:32px!important;border-radius:var(--radius-control)!important}.ace_search .ace_searchbtn{height:32px!important}.paper-stats-dot{background:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.paper-stats-meta-right{color:var(--ink-soft)!important;font-weight:400!important}.paper-frame{box-shadow:0 0 0 1px var(--border-color),0 12px 32px rgba(40,30,20,.08)!important;border:0!important}.dark-mode .paper-frame{box-shadow:0 0 0 1px #34302b,0 12px 32px rgba(0,0,0,.4)!important}.paper-faq details>summary,.paper-faq summary{font-weight:600!important}@media (max-width:767px){.file-card-head .file-card-path{column-gap:12px!important}.file-card.is-code.no-wrap .file-card-body::after{width:72px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 65%)!important}}.dashboard-page .hidden-toggle{font-size:13px!important;color:var(--color)!important}.paper-table-dashboard .btn-icon-dots{width:32px!important;height:32px!important}.access-q{display:block;font-size:13px;color:var(--ink-soft);margin:4px 0 6px}.repo-access-note{max-width:none!important;width:100%}.repo-access-note>span:has(.ui-icon){width:100%}@media (max-width:767px){.anonymize-page .repo-access-switch,.anonymize-page [role=group][aria-label*=onnection]{width:100%!important;display:flex!important}.anonymize-page .repo-access-switch button{flex:1 1 0}}.admin-empty,.paper-table-empty{border:1px solid var(--border-color)!important;border-radius:var(--radius-surface,12px)!important;background:var(--paper-card)!important;padding:40px 24px!important;text-align:center}.dark-mode .paper-table-head{background:#26221e!important}.dark-mode .paper-table-row{background:#1e1b18}.paper-table:has(.paper-table-empty),:has(>.paper-table-empty:only-child){border-top:0!important;border-bottom:0!important;box-shadow:none!important}.ace_editor .ace_text-layer .ace_redacted{color:var(--accent)!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent);border-radius:3px;font-weight:500}.dark-mode .ace_editor .ace_text-layer .ace_redacted{color:#a8ddd6!important;background:rgba(124,199,190,.16)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.35)}.ace_editor .ace_marker-layer .ace_redacted{display:none!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.82em!important;padding:0 .2em!important;background:color-mix(in srgb,var(--accent) 9%,transparent)!important;box-shadow:none!important;border:0!important;line-height:1.1!important;vertical-align:baseline!important;font-weight:500!important}.vb-snapshot{color:var(--color)!important;opacity:.78}.ace_search{box-shadow:inset 0 -1px 0 var(--border-color)!important}.btn-find.is-on,.btn-toggle.is-on{color:var(--accent)!important;background:var(--accent-soft)!important}.readme-toc a{text-indent:0;padding-left:13px!important}.readme-toc a.lvl-3{padding-left:25px!important}@media (max-width:767px){.anonymize-page .repo-access,.repo-access{flex-wrap:wrap;width:100%}.repo-access-switch{display:flex!important;width:100%!important}.repo-access-switch button{flex:1 1 0!important;white-space:nowrap!important;justify-content:center}}.q-card.selected{box-shadow:none!important;border-color:color-mix(in srgb,var(--accent) 60%,transparent)!important}.dark-mode .paper-table-head{background:#2e2a25!important}.dark-mode .paper-table-row{background:#1c1a17!important}.ov-chart-legend{font-family:var(--font-sans)!important;font-size:12px!important;color:var(--ink-soft)!important}.readme-toc a.lvl-2{font-weight:500!important;color:var(--color)!important}.readme-toc a.lvl-3{font-size:12.5px!important;color:var(--ink-soft)!important;font-weight:400!important}.readme-toc a.is-active{color:var(--accent)!important}.readme-toc a.lvl-2+a.lvl-2,.readme-toc a.lvl-3+a.lvl-2{margin-top:4px}.dark-mode .ace_editor .ace_text-layer .ace_redacted,.dark-mode .redact-chip{color:#9fe0d7!important;background:rgba(124,199,190,.2)!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.38)!important}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true]{color:var(--accent)!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 45%,transparent)!important}.dark-mode .file-card-head .btn.btn-toggle.is-on{color:#a8ddd6!important;background:rgba(124,199,190,.14)!important}@media (max-width:767px){.file-card-head .file-card-path .card-files-btn{align-self:start!important}}@media (max-width:767px){.paper-hero-shot{max-height:300px;overflow:hidden;-webkit-mask-image:linear-gradient(#000 70%,transparent);mask-image:linear-gradient(#000 70%,transparent)}}.dark-mode .hero-mock-doc,.dark-mode .paper-frame{background:#24211d!important}.paper-stats-meta-right{color:#5e5950!important}.dark-mode .paper-stats-meta-right{color:#b5aea3!important}.errors-page .status-pill.status-neutral{background:var(--hover-bg-color)!important;color:var(--ink-soft)!important;border-color:var(--border-color)!important}.connections-grid .btn,.connections-grid .connection-link:not(.connection-danger){height:36px!important;min-height:36px!important;padding-top:0!important;padding-bottom:0!important;display:inline-flex!important;align-items:center}.claim-page .form-control,.claim-page input{border:1px solid var(--border-color)!important}@media (max-width:767px){.overview-page .ov-kpi-sub{white-space:nowrap!important;font-size:12px!important;overflow:visible!important}.overview-page .ov-kpi-row>:last-child:nth-child(odd){grid-column:1/-1}.admin-summary .summary-pill{height:28px!important}.admin-summary .summary-total{flex-basis:100%}.admin-page .paper-table:not(.paper-table-dashboard) .paper-table-row{position:relative;padding-right:44px!important}.admin-page .paper-table:not(.paper-table-dashboard) .paper-table-row>:last-child{position:absolute!important;top:10px;right:4px}.admin-page .paper-table .anon-sub>span{white-space:nowrap}}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true]{background:0 0!important;box-shadow:none!important;color:var(--accent)!important}.file-card-head .btn.btn-toggle.is-on:hover{background:var(--hover-bg-color)!important}.dark-mode .file-card-head .btn.btn-toggle.is-on{background:0 0!important;color:#a8ddd6!important}.ace-chrome .ace_string{color:#3d5a73!important}.dark-mode .ace_editor .ace_string{color:#afc4d6!important}.ace-chrome .ace_gutter-cell{color:#6b655d!important}.readme-toc a.lvl-2{color:var(--ink-soft)!important;font-weight:500!important}.readme-toc a.lvl-3{color:var(--ink-soft)!important;opacity:.9}.readme-toc a.is-active{color:var(--accent)!important}.readme-toc-head{font-size:12.5px!important}.vb-snapshot{opacity:1!important;color:var(--ink-soft)!important}@media (max-width:767px){.viewer-bar .vb-signin{height:32px!important}}.paper-footer-meta{font-size:15px!important;color:var(--ink-soft)!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.07)!important}@media (min-width:992px){.anonymize-landing{justify-content:flex-start!important;align-items:flex-start!important}.anonymize-landing-inner{margin-left:max(4px,calc((100vw - 1048px)/ 2 - 20px))!important;margin-right:auto!important;max-width:640px!important}}.paper-settings-body>:not(aside){max-width:none!important}.paper-settings-body .paper-card,.paper-settings-body section{max-width:none!important}.dark-mode .paper-table-row{background:0 0!important}.dark-mode .paper-table-head{background:#26231f!important}@media (max-width:767px){.overview-page .ov-kpi-sub{white-space:normal!important;font-size:12px!important;padding-right:4px}.dashboard-count{position:static!important;width:auto!important;height:auto!important;margin:0 0 0 auto!important;overflow:visible!important;clip:auto!important;font-size:12.5px!important}}.errors-page input[type=search]{font-family:var(--font-sans)!important}.paper-settings-body .form-text,.paper-settings-body .paper-field-help,.paper-settings-body small{max-width:none!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip{font-size:.7em!important;vertical-align:.12em!important}.ace_search{border-bottom:1px solid var(--border-color)!important;box-shadow:none!important;border-top:0!important}.ace_search .ace_button,.ace_search .ace_searchbtn{height:32px!important}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true]{background:var(--accent-soft)!important;color:var(--accent)!important;box-shadow:none!important}.dark-mode .file-card-head .btn.btn-toggle.is-on{background:rgba(124,199,190,.14)!important}@media (max-width:767px){.file-card-head .card-files-btn{width:32px!important;height:32px!important}.file-card-head .file-card-path{column-gap:8px!important}}.dark-mode .leftCol{background:#1c1a17!important}.dark-mode .explorer-content,.dark-mode .file-card{background:#141311!important}.nav-support-inline{color:var(--color)!important;font-weight:500!important}@media (min-width:992px){.anonymize-landing{padding-top:24px!important}.anonymize-landing-inner{padding-top:0!important;margin-top:0!important}}.anonymize-page .access-q+.btn-outline-ink{height:30px!important;padding:0 10px!important;font-size:13px!important}.admin-summary .summary-pill:not(.active){color:var(--ink-soft)!important}.volume-legend>span{display:inline-flex;align-items:center;gap:5px;margin-left:12px}.dark-mode .leftCol{background:#221f1b!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{box-shadow:inset -1px 0 0 var(--border-color),inset -10px 0 10px -10px rgba(0,0,0,.18)!important}}.readme-toc a.is-active{font-weight:600!important}.paper-stats-meta-right{color:var(--ink-soft)!important;font-weight:500!important;letter-spacing:inherit}.paper-faq details a:not(.btn),.paper-faq-a a,.paper-section a{text-decoration-color:color-mix(in srgb,currentColor 35%,transparent)!important;text-decoration-thickness:1px!important}.volume-axis{display:flex;justify-content:space-between;font-size:11.5px;color:var(--ink-soft);margin-top:6px}.errors-page .seg.seg-info{background:color-mix(in srgb,var(--ink-muted) 55%,transparent)!important}.paper-crumbs a{color:var(--ink-soft)!important}.paper-crumbs .here{color:var(--color)!important}.paper-table .mobile-meta-icon,.paper-table [role=cell]>.ui-icon{opacity:.45}@media (min-width:768px){.paper-table-dashboard .cell-expires{padding-left:24px!important}}.dark-mode .paper-table-dashboard .paper-table-head,.dark-mode .paper-table-head{background:0 0!important;box-shadow:inset 0 -1px 0 var(--border-color)!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.copy-url{max-width:none!important;justify-self:stretch}.paper-table.paper-table-dashboard .paper-table-row{padding-right:16px!important}.dashboard-filters-toggle{border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important;height:44px;padding:0 12px!important}.overview-page .ov-kpi-card{padding-left:14px!important;padding-right:14px!important}.overview-page .ov-kpi-sub{white-space:nowrap!important;font-size:11.5px!important}.admin-filter-row kbd,.search-wrap .kbd,.search-wrap kbd{display:none!important}}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{background:0 0!important;padding:0!important;font-size:.78em!important;color:var(--accent)!important;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:6px;text-decoration-color:color-mix(in srgb,var(--accent) 30%,transparent)}.dark-mode .markdown-body h1 .redact-chip,.dark-mode .markdown-body h2 .redact-chip{color:#9fe0d7!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:56px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 55%)!important}}.readme-toc a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media (min-width:768px){.paper-table-dashboard .cell-expires,.paper-table-dashboard .paper-table-head>:nth-child(4){text-align:left!important;justify-content:flex-start!important;padding-left:24px!important}}.anonymize-page #sourceUrl-landing,.anonymize-page .form-control-lg{border:1px solid var(--border-color)!important}.anonymize-page .form-control-lg:focus{border-color:var(--accent)!important}.repo-access-note>span:has(.ui-icon){background:var(--accent-soft)!important;box-shadow:inset 3px 0 0 var(--accent)!important;color:var(--color)!important;font-size:13.5px}.repo-access-note>span:has(.ui-icon) .ui-icon{color:var(--accent)!important}.dark-mode .repo-access-note>span:has(.ui-icon){background:rgba(124,199,190,.1)!important;box-shadow:inset 3px 0 0 #7cc7be!important}.dark-mode .repo-access-note>span:has(.ui-icon) .ui-icon{color:#7cc7be!important}.readme-toc a{white-space:normal!important;overflow:visible!important;text-overflow:clip!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::before{content:"Scroll \2192";position:absolute;right:10px;bottom:14px;z-index:9;font:500 12px/1 var(--font-sans);color:var(--ink-soft);background:var(--paper-card);box-shadow:0 0 0 1px var(--border-color),0 2px 6px rgba(0,0,0,.06);border-radius:999px;padding:6px 10px;pointer-events:none}}.ace-chrome .ace_string{color:#2d5f86!important}.ace-chrome .ace_keyword,.ace-chrome .ace_storage{color:#9a2b6b!important}.dark-mode .ace_editor .ace_string{color:#9cc3e6!important}.dark-mode .ace_editor .ace_keyword,.dark-mode .ace_editor .ace_storage{color:#e3a6d2!important}.reading-layout .markdown-body h1{font-size:34px!important;letter-spacing:-.015em}.errors-page .btn-group>.btn,.errors-page .btn-icon,.errors-page .form-control,.errors-page select{height:36px!important;min-height:36px!important}.errors-page .col-when .when-rel{font-size:13.5px!important}.q-danger-group{display:inline-flex;gap:6px;margin-left:auto;padding-left:12px;border-left:1px solid var(--border-color)}.q-stats-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.repo-access-note>span:has(.ui-icon){background:color-mix(in srgb,#b7791f 8%,transparent)!important;box-shadow:inset 3px 0 0 #b7791f!important}.repo-access-note>span:has(.ui-icon) .ui-icon{color:#9a6416!important}.dark-mode .repo-access-note>span:has(.ui-icon){background:rgba(224,176,112,.1)!important;box-shadow:inset 3px 0 0 #e0b070!important}.dark-mode .repo-access-note>span:has(.ui-icon) .ui-icon{color:#e0b070!important}@media (max-width:767px){.admin-page .paper-table [role=cell]:has(>input[type=checkbox]),.admin-page .paper-table input[type=checkbox],.admin-page .paper-table-head [role=columnheader]:has(>input[type=checkbox]){display:none!important}.admin-page .admin-filters-toggle,.admin-page .dashboard-filters-toggle{width:auto!important}}.q-danger-group{border-left:0!important;padding-left:0!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::before{animation:scrollHintOut .4s ease 2.6s forwards}}@keyframes scrollHintOut{to{opacity:0;visibility:hidden}}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{text-decoration:none!important;background:color-mix(in srgb,var(--accent) 8%,transparent)!important;padding:0 .15em!important;border-radius:4px}.readme-toc a.lvl-3{padding-left:29px!important}.ace_search .ace_button{color:var(--color)!important;opacity:.8}.ace_search .ace_button.checked{opacity:1}.dark-mode .files .file>a,.dark-mode .files .folder>a{color:#d8d2c8!important}.ov-live{display:inline-flex;align-items:center;gap:6px}.ov-live-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.anon-sub .meta-sep{display:inline-flex;align-items:center;white-space:nowrap}@media (max-width:767px){.anon-sub{column-gap:0!important}}.connection-status-legacy{color:var(--ink-soft)!important}.connection-status-legacy::before{background:var(--ink-muted)!important}.q-detail{align-items:start!important}.q-chart-wrap canvas{max-height:200px}.admin-page .paper-table .cell-anon .anon-text:has(.meta-sep){overflow:hidden}.admin-page .paper-table .anon-sub:has(.meta-sep){display:flex!important;flex-wrap:wrap!important;margin-left:-14px!important;gap:0!important}.admin-page .paper-table .anon-sub:has(.meta-sep)>*{white-space:nowrap}.admin-page .paper-table .anon-sub:has(.meta-sep)>::before{content:"\00b7";display:inline-block;width:14px;text-align:center;color:var(--ink-muted)}.ace-chrome .ace_gutter-cell,.dark-mode .ace_editor .ace_gutter-cell{font-size:12px!important}.q-stats-actions{display:grid!important;grid-template-columns:auto auto 1fr;gap:8px;align-items:center}.q-danger-group{grid-column:1/-1;justify-self:start;margin:4px 0 0!important;padding:10px 0 0!important;border-top:1px solid var(--border-color)!important;width:100%;display:flex!important;gap:8px}@media (max-width:767px){.overview-page .ov-header{flex-wrap:nowrap!important;align-items:center!important}.ov-live{font-size:12px}.ov-live .ov-live-text-extra{display:none}.overview-page .ov-daily-card,.overview-page .ov-kpi-card{padding-left:14px!important;padding-right:14px!important}}.admin-page input[type=checkbox]{width:14px!important;height:14px!important}.demo-input-wrap{position:relative}.demo-input-wrap .demo-backdrop{position:absolute;inset:0;margin:0;overflow:hidden;color:var(--color);pointer-events:none;overflow-wrap:break-word}.demo-input-wrap .demo-input{position:relative;color:transparent!important;caret-color:var(--color);background:0 0!important;overflow-wrap:break-word}.demo-input-wrap .demo-input::selection{color:var(--color);background:color-mix(in srgb,var(--accent) 25%,transparent)}.demo-match{background:0 0;color:inherit;padding:0;border-radius:2px;box-shadow:inset 0 -2px 0 color-mix(in srgb,var(--accent) 55%,transparent);background-color:color-mix(in srgb,var(--accent) 10%,transparent)}.demo-output .redact-chip{font-size:12.5px!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::before{display:none!important}.file-card.is-code.no-wrap .file-card-body::after{width:36px!important;background:linear-gradient(to right,rgba(0,0,0,0),rgba(60,45,30,.1))!important;box-shadow:inset -1px 0 0 rgba(60,45,30,.18)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(0,0,0,0),rgba(0,0,0,.45))!important;box-shadow:inset -1px 0 0 rgba(255,255,255,.12)!important}}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:44px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 70%)!important;box-shadow:inset -8px 0 10px -6px rgba(40,30,20,.28),inset -1px 0 0 rgba(40,30,20,.16)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(0,0,0,0),var(--canvas-bg-color) 70%)!important;box-shadow:inset -8px 0 10px -6px rgba(0,0,0,.6),inset -1px 0 0 rgba(255,255,255,.1)!important}}.q-chart-wrap{position:relative}.q-chart-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;background:var(--paper-card);border-bottom:1px solid var(--border-color);text-align:center}.q-chart-empty strong{font-size:14px;font-weight:500;color:var(--color)}.q-chart-empty span{font-size:12.5px;color:var(--ink-soft)}.q-danger-group .btn{margin-left:0!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{background:0 0!important;padding:0!important;border-radius:0!important}.paper-settings-body .form-control,.paper-settings-body input[type=date],.paper-settings-body input[type=text],.paper-settings-body input[type=url],.paper-settings-body textarea{border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important}.admin-page .paper-table .cell-views .ui-icon{display:none!important}.admin-page:has(.admin-users-row) .paper-table-head,.admin-page:has(.admin-users-row) .paper-table-row{column-gap:40px!important}@media (max-width:767px){.overview-page .ov-header{justify-content:space-between!important}.ov-live{margin-left:0!important}}.dark-mode .paper-table-dashboard .paper-table-row{background:0 0!important}.mono-hash{font-family:var(--font-mono)!important;font-size:11.5px}.nav-heart-m{display:none}@media (max-width:991px){.nav-heart-m{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;margin-left:auto;color:#c25b5e}.nav-heart-m .ui-icon{width:18px;height:18px;fill:currentColor}.nav-heart-m+.nav-signin-m{margin-left:4px!important}}.file-card:has(.ace_search:not([style*=none])) .btn-find{color:var(--accent)!important;background:var(--accent-soft)!important}.paper-link-arrow{color:var(--color)!important;text-decoration-color:color-mix(in srgb,var(--accent) 55%,transparent)!important}.paper-faq details p,.paper-faq details>:not(summary),.paper-faq-a{max-width:none!important}.paper-stat-value{font-family:var(--font-display,var(--font-serif))!important;font-weight:400!important;letter-spacing:-.01em}@media (max-width:767px){.ov-live-extra{display:none}.paper-table-dashboard .cell-views.num::after{content:" views";color:var(--ink-soft)}.paper-table-dashboard .cell-expires>span:not(.expires-never):not(.expires-past)::before{content:"Expires ";color:var(--ink-soft)}.admin-page .admin-filters-toggle{width:auto!important;height:36px!important;padding:0 12px!important}}.dashboard-quota summary{border:0!important;box-shadow:none!important;background:0 0!important;padding-left:0!important}.q-card.selected{background:var(--accent-soft)!important}.dark-mode .q-card.selected{background:rgba(124,199,190,.08)!important}.dark-mode .ov-meter .fill.critical,.dark-mode .ov-meter-fill.critical{background:#f08a82!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-family:inherit!important;font-weight:inherit!important;font-size:.9em!important;background:var(--accent-soft)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent)!important;padding:0 .25em!important;border-radius:6px!important;color:var(--accent)!important;text-decoration:none!important}.dark-mode .markdown-body h1 .redact-chip,.dark-mode .markdown-body h2 .redact-chip,.dark-mode .markdown-body h3 .redact-chip{background:rgba(124,199,190,.16)!important;color:#9fe0d7!important}.file-card-head .btn.btn-toggle.is-on,.file-card-head .btn.btn-toggle[aria-pressed=true],.file-card:has(.ace_search:not([style*=none])) .btn-find{background:0 0!important;color:var(--accent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 50%,transparent)!important}.dark-mode .file-card-head .btn.btn-toggle.is-on,.dark-mode .file-card:has(.ace_search:not([style*=none])) .btn-find{background:0 0!important;color:#a8ddd6!important;box-shadow:inset 0 0 0 1px rgba(124,199,190,.5)!important}@media (min-width:768px){.file-card-head{padding-left:46px!important}}.breadcrumb-root{display:none!important}.breadcrumb-root+.breadcrumb-item::before{display:none!important}.ace_editor .ace_marker-layer .ace_selection{background:rgba(232,170,40,.65)!important;box-shadow:0 0 0 1px rgba(160,100,10,.6)!important}.ace_search_counter{color:var(--color)!important;opacity:.75}.dark-mode .files .file.active>a{background:rgba(255,255,255,.12)!important}.dark-mode .leftCol{box-shadow:inset -1px 0 0 rgba(255,255,255,.08)!important}.hero-mock-doc-bar span,.paper-stat-delta,.paper-stat-sub,.paper-stats .sub{color:#5e5950!important}.dark-mode .paper-stat-delta,.dark-mode .paper-stat-sub{color:#b5aea3!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{font-size:.72em!important;font-weight:600!important;vertical-align:.1em;padding:.05em .3em!important}.is-unsynced{color:var(--ink-muted)!important;font-size:12.5px;font-family:var(--font-sans)!important}.q-dot.q-dot-idle{background:var(--ink-muted)!important}.dark-mode .paper-table,.dark-mode .paper-table-dashboard{background:0 0!important}.dark-mode .paper-table .paper-table-head{background:#24211d!important}.dark-mode .paper-table .paper-table-row{background:#141311!important}@media (min-width:768px){.file-card.is-code .file-card-head{padding-left:16px!important}.readme-toc{margin-left:16px}.reading-layout{grid-template-columns:minmax(0,660px) 190px!important;gap:56px!important}}.dark-mode .leftCol{box-shadow:none!important;border-right:1px solid var(--border-color)!important}.dark-mode .leftCol-resizer,.dark-mode .sidebar-resizer{background:0 0!important}.markdown-body h1 .redact-chip,.markdown-body h2 .redact-chip,.markdown-body h3 .redact-chip{box-shadow:none!important}.files .file.active>a::before{left:4px!important;top:6px!important;bottom:6px!important;width:3px!important;border-radius:2px!important}.hero-mock-row.is-active{box-shadow:inset 3px 0 0 var(--accent)!important;border-radius:6px}.fm-label,.paper-eyebrow,.paper-stats-meta-left,.paper-stats-meta-right{color:#57524a!important}.dark-mode .paper-eyebrow,.dark-mode .paper-stats-meta-left,.dark-mode .paper-stats-meta-right{color:#bdb6ab!important}.demo-match{box-shadow:inset 0 -1px 0 color-mix(in srgb,var(--accent) 50%,transparent)!important}.redact-glue{white-space:nowrap}@media (min-width:768px){.file-card-head,.file-card.is-code .file-card-head{padding-left:20px!important}}.dark-mode .files .file.active>a{background:rgba(255,255,255,.08)!important}.files .file.active>a{position:relative}.files .file.active>a::before{left:0!important;top:4px!important;bottom:4px!important;width:3px!important;border-radius:0 2px 2px 0!important}.admin-page .paper-table [role=cell]>svg.ui-icon[aria-hidden],.paper-table .cell-expires .mobile-meta-icon{display:none!important}@media (max-width:767px){.admin-page .paper-table .cell-views.num::after{content:" views";color:var(--ink-soft)}}.dark-mode .paper-table .paper-table-row{background:#1e1b18!important}.dark-mode .paper-table .paper-table-head{background:#26231f!important}.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=email],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea,.paper-page textarea.form-control{border:1px solid var(--border-color)!important;border-radius:var(--radius-control)!important}.paper-page input:focus,.paper-page select:focus,.paper-page textarea:focus{border-color:var(--accent)!important}.q-detail{align-items:stretch!important}.q-throughput{display:flex;flex-direction:column}.q-throughput .q-chart-wrap{flex:1;min-height:180px}.admin-page .search-wrap{flex:0 1 420px!important;max-width:420px!important}@media (max-width:767px){.overview-page .ov-header{display:flex!important;flex-direction:row!important;flex-wrap:nowrap!important;justify-content:space-between!important;align-items:center!important}.overview-page .ov-header .ov-live{margin:0 0 0 auto!important;align-self:center}}.hero-mock-count{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent)!important;font-weight:600;font-size:11.5px}.dark-mode .hero-mock-count{background:rgba(124,199,190,.16);color:#a8ddd6!important}.vb-support{color:var(--ink-soft)!important;font-weight:400!important}.vb-support:hover{color:var(--color)!important}@media (max-width:767px){.file-card.is-code .ace_gutter-cell{padding-left:4px!important;padding-right:6px!important}.file-card.is-code .ace_scroller{left:30px!important}.file-card.is-code .ace_gutter{width:30px!important}}.md-find-bar{position:sticky;top:52px;z-index:5;display:flex;align-items:center;gap:10px;padding:8px 20px;background:var(--paper-card);border-bottom:1px solid var(--border-color)}.md-find-input{width:260px;height:32px;padding:0 10px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);color:var(--color);font:14px var(--font-sans)}.md-find-input:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.md-find-count{font-size:12.5px;color:var(--ink-soft);min-width:64px;font-variant-numeric:tabular-nums}.md-find-nav{display:inline-flex}.md-find-close,.md-find-nav button{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border-color);background:var(--paper-card);color:var(--color)}.md-find-nav button:first-child{border-radius:var(--radius-control) 0 0 var(--radius-control)}.md-find-nav button:last-child{border-radius:0 var(--radius-control) var(--radius-control) 0;margin-left:-1px}.md-find-nav button:disabled{opacity:.45}.md-find-close{border:0;background:0 0;font-size:20px;border-radius:var(--radius-control)}.md-find-close:hover{background:var(--hover-bg-color)}mark.md-find{background:rgba(242,204,96,.45);color:inherit;border-radius:2px;padding:0}mark.md-find.is-current{background:rgba(232,170,40,.8);box-shadow:0 0 0 1px rgba(160,100,10,.6)}@media (min-width:1280px){.viewer-bar a.vb-icon[href="/faq"]{width:auto!important;padding:0 10px!important;gap:6px}.viewer-bar a.vb-icon[href="/faq"]::after{content:"FAQ";font:500 13px var(--font-sans);color:var(--ink-soft)}}.file-card-head .btn-md-find.is-on{background:0 0!important;color:var(--accent)!important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 50%,transparent)!important}.md-find-input::-webkit-search-cancel-button{-webkit-appearance:none;display:none}@media (min-width:768px){.md-find-bar{padding-left:63px!important}}.md-find-input{width:240px!important}.file-card:has(.md-find-bar) .readme-toc{top:120px!important}.claim-page input,.claim-page textarea{border-color:var(--border-color)!important}.connection-status{font-size:12.5px!important}@media (max-width:767px){.admin-page .admin-filters-toggle::before{content:"";width:14px;height:14px;margin-right:6px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 6h16M7 12h10M10 18h4'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 6h16M7 12h10M10 18h4'/%3E%3C/svg%3E") center/contain no-repeat}.admin-page .admin-filters-toggle{display:inline-flex!important;align-items:center}}.paper-faq-title{text-wrap:balance}.file-card-head,.md-find-bar{background:var(--paper-card)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important;opacity:1!important}.dark-mode .file-card-head,.dark-mode .md-find-bar{background:#141311!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,640px) 190px!important;gap:56px!important;justify-content:center!important;padding:0 24px!important}}.reading-layout .file-content.markdown-body{padding-top:32px!important}@media (min-width:1200px){.md-find-bar{padding-left:calc((100% - 886px)/ 2)!important}}@media (min-width:1280px){.viewer-bar a.vb-bug{width:auto!important;padding:0 10px!important;gap:6px}.viewer-bar a.vb-bug::after{content:"Report a bug";font:500 13px var(--font-sans);color:var(--ink-soft)}}.vb-snapshot{font-size:13px!important;color:var(--ink-soft)!important}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn,.dashboard-page .dashboard-filter-controls .dropdown-toggle,.dashboard-page .dashboard-filter-row .form-control{height:36px!important;min-height:36px!important}.dashboard-filter-controls .btn-group[aria-label=Type]{padding:0!important;height:36px;align-items:center}.dashboard-filter-controls .btn-group[aria-label=Type]>.btn{height:30px!important;min-height:30px!important;margin:3px 0}.errors-page .errors-toolbar{flex-wrap:nowrap!important}.errors-page .errors-toolbar input[type=search]{min-width:0;flex:1 1 260px}.paper-table .paper-table-head{background:color-mix(in srgb,var(--hover-bg-color) 60%,transparent)!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,700px) 190px!important;gap:48px!important}}.md-find-bar{padding-top:4px!important;padding-bottom:4px!important}.md-find-input{height:30px!important}.md-find-input:focus{box-shadow:0 0 0 1px var(--accent)!important}.ace_editor .ace_marker-layer .ace_selection,mark.md-find.is-current{background:rgba(240,190,80,.6)!important;box-shadow:0 0 0 1px rgba(170,110,20,.5)!important}.viewer-bar .vb-right{gap:12px!important}.viewer-bar .vb-right>*{margin:0!important}.auto-refresh-label{white-space:nowrap!important}.errors-page .errors-toolbar{flex-wrap:wrap!important}.errors-page .errors-row-head,.errors-page .errors-thead,.paper-table .paper-table-head,.q-table thead th{background:#f3f1ec!important}.dark-mode .errors-page .errors-thead,.dark-mode .paper-table .paper-table-head,.dark-mode .q-table thead th{background:#24211d!important}.summary-show-label{font-size:12.5px;color:var(--ink-soft);margin-right:2px;align-self:center}.anon-sub a.mono-hash,.mono-hash{text-decoration:none!important;border-bottom:0!important}.errors-page .errors-list-head{background:#f3f1ec!important}.dark-mode .errors-page .errors-list-head{background:#24211d!important}.errors-page .errors-toolbar .search-wrap,.errors-page .errors-toolbar input[type=search]{flex:1 1 220px!important;max-width:340px!important;min-width:0!important}.errors-page .errors-toolbar{flex-wrap:nowrap!important;gap:8px!important}.faq-section .panel-default .panel-title a{padding:16px 32px 16px 0!important;position:relative;display:block}.faq-section .panel-default .panel-body{padding-left:0!important}.faq-section .panel-default .panel-title a:after{left:auto!important;right:6px!important}.md-find-bar{padding-left:20px!important}.vb-logo{font-size:19px!important}@media (max-width:767px){.vb-logo{font-size:18px!important}.admin-summary .summary-show-label{flex-basis:100%}}.btn-ghost-danger{background:0 0!important;border:1px solid transparent!important;color:#b42318!important;display:inline-flex;align-items:center;gap:6px}.btn-ghost-danger:hover{background:color-mix(in srgb,#b42318 8%,transparent)!important}.dark-mode .btn-ghost-danger{color:#f08a82!important}mark.md-find{background:rgba(242,204,96,.32)!important;box-shadow:none!important}mark.md-find.is-current{background:rgba(232,160,30,.85)!important;box-shadow:0 0 0 1px rgba(150,90,10,.7)!important;color:#2b2824!important}.md-find-opt{height:30px;min-width:32px;padding:0 8px;border:1px solid var(--border-color);border-radius:var(--radius-control);background:var(--paper-card);color:var(--ink-soft);font:600 12px var(--font-sans)}.md-find-opt.is-on{color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.md-find-nav button{color:var(--ink-soft)!important}.md-find-close{font-size:18px!important;font-weight:300!important;color:var(--ink-soft)!important}@media (min-width:768px){.file-card .ace_search{left:0!important;right:0!important;padding-left:20px!important;margin:0!important;max-width:none!important}}@media (min-width:768px){.md-find-bar{padding-left:71px!important}}@media (min-width:768px){.md-find-bar{padding-left:71px!important}.file-card .ace_search{overflow:visible!important}}@media (min-width:1200px){.file-card:has(.md-find-bar) .readme-toc,.readme-toc{top:120px!important}}.dark-mode .dashboard-filter-controls .btn-group[aria-label=Type]>.btn.btn-primary{background:#3a3631!important;color:#f2eee8!important;box-shadow:0 0 0 1px rgba(255,255,255,.14)!important}.admin-page .paper-table .user-name,.admin-page .paper-table .username,.admin-page:has(.admin-users-row) .cell-anon .repo-name,.admin-users-row .repo-name{font-family:var(--font-sans)!important;font-weight:600}.paper-page input[type=checkbox]{-webkit-appearance:none;appearance:none;width:16px!important;height:16px!important;margin:0;border:1.5px solid #a39d92;border-radius:4px;background:var(--paper-card);display:inline-grid;place-content:center;cursor:pointer;vertical-align:middle}.paper-page input[type=checkbox]::before{content:"";width:9px;height:9px;transform:scale(0);background:#fff;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat}.paper-page input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}.paper-page input[type=checkbox]:checked::before{transform:scale(1)}.paper-page input[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.dark-mode .paper-page input[type=checkbox]{border-color:#6e6860;background:0 0}.admin-page .detail-value:is(:empty),.detail-value.is-empty{color:var(--ink-muted)!important;font-style:italic}.paper-table-dashboard .btn-icon-dots,.row-edit{width:32px!important;height:32px!important;border-radius:var(--radius-control)}.paper-table-dashboard .btn-icon-dots:hover,.row-edit:hover{background:var(--hover-bg-color)!important}.q-state-toggle input[type=checkbox]{position:absolute;opacity:0;width:1px!important;height:1px!important;pointer-events:none}.q-state-toggle{position:relative;cursor:pointer}.q-state-toggle input:not(:checked)+.q-state-chip{opacity:.45;text-decoration:line-through}.q-state-toggle input:focus-visible+.q-state-chip{outline:2px solid var(--accent);outline-offset:2px}@media (min-width:768px){.md-find-bar{padding-left:31px!important}}.ace_search .ace_search_field,.anonymize-page .form-control,.md-find-input,.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea{border-radius:8px!important}.page-head-row .btn-primary,.paper-page .btn-ink,.paper-page .btn-primary{min-height:40px!important}.errors-page .errors-list-head>*,.paper-table .paper-table-head [role=columnheader],.paper-table .paper-table-head>div{font-weight:500!important;font-size:12.5px!important;letter-spacing:0!important;text-transform:none!important;color:var(--ink-soft)!important}.admin-page .admin-section-header h2{font:600 18px/1.3 var(--font-sans)!important;display:inline-flex;align-items:center;gap:8px}.admin-page .admin-section-header h2 .ui-icon{width:16px;height:16px;color:var(--ink-soft)}.files .file.active>a{background:#e2ded7!important}.files .file.active>a::before{left:3px!important}.dark-mode .files .file.active>a{background:rgba(255,255,255,.09)!important}.admin-page:has(.admin-users-row) .paper-table-head,.admin-page:has(.admin-users-row) .paper-table-row{grid-template-columns:28px minmax(220px,320px) 120px 140px 1fr 40px!important}@media (max-width:767px){.mobile-sub{margin-top:3px!important}}.paper-faq-lede,.paper-page-lede{text-wrap:pretty;max-width:640px}.errors-page .status-pill.status-neutral:not([title=""])[title]{background:0 0!important;border-color:transparent!important;color:var(--ink-muted)!important}.md-find-bar{min-height:47px!important;padding-top:7px!important;padding-bottom:7px!important}.ace_search .ace_search_field:focus,.md-find-input:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}mark.md-find{background:rgba(246,224,138,.7)!important}.ace_editor .ace_marker-layer .ace_selected-word{background:rgba(246,224,138,.7)!important}.files .file.active>a{background:#e6e3dc!important}@media (min-width:1200px){.readme-toc{margin-top:32px!important;padding-top:0!important}}.errors-page .kpi-card.kpi-info .kpi-value{color:var(--ink-soft)!important;font-weight:500!important;font-size:26px!important}.errors-page .kpi-card.kpi-info{background:color-mix(in srgb,var(--paper-card) 70%,transparent)!important}.ov-meter span{background:var(--accent)}.ov-meter.warn span{background:#b7791f!important}.ov-meter.crit span{background:#b42318!important}@media (max-width:767px){.admin-summary .summary-show-label{flex-basis:auto!important}}@media (max-width:767px){.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto auto!important;grid-template-areas:"name edit more" "meta meta meta" "link link link"!important;column-gap:2px!important}.paper-table-dashboard .cell-actions>.row-edit{display:inline-flex!important;grid-area:edit;align-self:start;align-items:center;justify-content:center}.dashboard-page .paper-table-dashboard .dashboard-row-meta{display:flex!important;flex-wrap:nowrap!important;align-items:center!important;gap:0!important;padding-left:26px;font-size:12.5px;color:var(--ink-soft)}.dashboard-page .dashboard-row-meta .cell-status{flex:0 0 auto!important}.dashboard-page .dashboard-row-meta .cell-status .status-line{gap:6px}.dashboard-page .dashboard-row-meta .cell-expires,.dashboard-page .dashboard-row-meta .cell-views{display:inline-flex!important;align-items:center;font-family:var(--font-sans,Inter,sans-serif)!important;font-size:12.5px!important;color:var(--ink-soft)!important;white-space:nowrap;margin:0!important;padding:0!important;text-align:left!important;justify-content:flex-start!important}.dashboard-page .dashboard-row-meta .cell-expires::before,.dashboard-page .dashboard-row-meta .cell-views::before{content:"·";margin:0 8px;color:var(--ink-muted)}.dashboard-page .dashboard-row-meta .cell-views .ui-icon,.dashboard-page .dashboard-row-meta .cell-views svg{display:none!important}.dashboard-page .dashboard-row-meta .cell-expires .cell-label,.dashboard-page .dashboard-row-meta .cell-expires small{font-size:inherit!important;color:inherit!important;margin-right:4px}}.dashboard-page .dashboard-quota>summary{border:1px solid var(--border-color)!important;border-radius:999px!important;padding:0 12px 0 10px!important;height:36px;min-height:36px!important;display:inline-flex!important;align-items:center;gap:8px;background:var(--paper-card)!important}.dashboard-page .dashboard-quota>summary:hover{background:var(--hover-bg-color)!important}.dashboard-page .dashboard-quota .quota-summary-ring{display:inline-block!important;width:16px;height:16px}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,660px) 176px!important;gap:56px!important;justify-content:center!important}}@media (max-width:767px){.admin-summary .summary-show-label{flex-basis:100%!important;margin:4px 0 -2px!important}.admin-summary .summary-meta{flex:1 1 auto}}.q-throughput:has(.q-chart-empty) .q-chart-wrap{flex:0 0 auto!important;min-height:120px!important;height:120px}.q-throughput:has(.q-chart-empty) .q-chart-wrap canvas{visibility:hidden}.q-chart-empty{border-bottom:0!important;border-radius:8px}@media (min-width:768px){.md-find-bar{padding-left:20px!important}.file-card .ace_search{padding-left:20px!important}.file-card .ace_search .ace_search_form{margin-left:0!important}}@media (min-width:768px){.file-card .ace_search{left:0!important;padding-left:0!important}}@media (min-width:1200px){.md-find-bar{padding-left:max(20px,calc((100% - 892px)/ 2))!important}}.q-throughput:has(.q-chart-empty) .q-chart-wrap{flex:1 1 auto!important;height:auto!important;min-height:160px!important}.q-danger-group{align-items:center;flex-wrap:wrap}.q-danger-group::before{content:"Clear jobs";flex-basis:100%;font-size:12px;color:var(--ink-soft);margin-bottom:2px}.q-legend .q-legend-item,.q-legend button,.q-legend span{font-size:12px!important}.q-state-filters .q-state-toggle,.q-state-filters .q-state-toggle *{font-size:12px!important}.admin-nav a.active,.admin-nav a.router-link-active{background:0 0!important;color:var(--accent)!important;box-shadow:inset 0 -2px 0 var(--accent)!important;border-radius:0!important}.admin-nav a:not(.active):hover{background:0 0!important;color:var(--color)!important;box-shadow:inset 0 -2px 0 var(--border-color)!important}.dark-mode .q-card.selected,.q-card.selected{background:var(--paper-card)!important;border-color:var(--accent)!important;box-shadow:inset 0 0 0 1px var(--accent)!important}.errors-page .kpi-card .kpi-value,.errors-page .kpi-card.kpi-info .kpi-value,.errors-page .kpi-card.kpi-warn .kpi-value{color:var(--color)!important;font-weight:600!important;font-size:28px!important}.errors-page .kpi-card.kpi-info{background:var(--paper-card)!important}.paper-table-dashboard .btn-icon-dots,.paper-table-dashboard .row-edit{color:var(--color)!important;opacity:.72}.paper-table-dashboard .btn-icon-dots:hover,.paper-table-dashboard .row-edit:hover{opacity:1}@media (max-width:767px){.paper-table-dashboard .cell-actions>.dropdown,.paper-table-dashboard .cell-actions>.row-edit{margin-top:-6px}}@media (min-width:1280px){.viewer-bar a.vb-bug,.viewer-bar a.vb-icon[href="/faq"]{width:32px!important;padding:0!important}.viewer-bar a.vb-bug::after,.viewer-bar a.vb-icon[href="/faq"]::after{content:none!important}}.markdown-body h2{margin-bottom:12px!important}.markdown-body h3{margin-top:32px!important;margin-bottom:8px!important}.markdown-body h4{margin-top:24px!important;margin-bottom:6px!important;color:var(--ink-soft)!important}@media (min-width:768px){.file-card .ace_search{padding-left:16px!important}}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:48px!important;background:linear-gradient(to right,rgba(255,255,255,0),var(--paper-card) 55%)!important;box-shadow:inset -1px 0 0 var(--border-color)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(20,19,17,0),#141311 55%)!important}}.connections-page .connection-actions{flex-wrap:nowrap!important;align-items:center;gap:12px!important}.connections-page .connection-actions .connection-link{white-space:nowrap}.paper-table-dashboard .paper-table-head .col-link{font-size:inherit;color:inherit}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:40px!important;background:linear-gradient(to right,rgba(40,32,24,0),rgba(40,32,24,.07)),linear-gradient(to right,rgba(255,255,255,0),#fff 70%)!important;border-right:2px solid rgba(40,32,24,.12)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(0,0,0,0),rgba(0,0,0,.35)),linear-gradient(to right,rgba(20,19,17,0),#141311 70%)!important;border-right-color:rgba(255,255,255,.08)}}.connections-page .connection-actions .btn{white-space:nowrap}.connections-page .connection-refresh{width:36px;height:36px;justify-content:center;padding:0!important;border-radius:var(--radius-control,8px);margin-left:auto}.faq-section .faq-cat,.faq-section .panel-group{max-width:720px}.ace-chrome .ace_gutter-cell{color:#5f5a52!important}.connections-grid{align-items:start!important}.connection-installations{border-bottom:0!important;margin-bottom:0!important;padding-bottom:0!important}.anonymize-page .form-control,.paper-page input.form-control,.paper-page input[type=date],.paper-page input[type=text],.paper-page input[type=url],.paper-page select.form-control,.paper-page textarea,.paper-page textarea.form-control{border-color:#c2baac!important}.dark-mode .anonymize-page .form-control,.dark-mode .paper-page input.form-control,.dark-mode .paper-page input[type=date],.dark-mode .paper-page input[type=text],.dark-mode .paper-page input[type=url],.dark-mode .paper-page select.form-control,.dark-mode .paper-page textarea{border-color:#4a453e!important}.paper-page input::placeholder,.paper-page textarea::placeholder{color:#a39b8e!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.dropdown,.paper-table-dashboard .cell-actions>.dropdown>.btn-icon-dots,.paper-table-dashboard .cell-actions>.row-edit{width:36px!important;height:36px!important;margin-top:-4px!important;align-self:start!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}}.anonymize-page .repo-access-switch{border:0!important;background:0 0!important;padding:0!important;box-shadow:none!important;gap:0!important;display:inline-flex!important;align-items:center;width:auto!important;font-size:12.5px;color:var(--ink-soft)}.anonymize-page .repo-access-switch::before{content:"Access:";margin-right:6px;color:var(--ink-soft)}.anonymize-page .repo-access-switch button{background:0 0!important;border:0!important;box-shadow:none!important;padding:2px 4px!important;height:auto!important;min-height:0!important;font-size:12.5px!important;font-weight:500!important;color:var(--ink-soft)!important;flex:0 0 auto!important;border-radius:4px!important}.anonymize-page .repo-access-switch button+button::before{content:"·";margin-right:8px;color:var(--ink-muted)}.anonymize-page .repo-access-switch button[aria-pressed=true]{color:var(--color)!important;font-weight:600!important;opacity:1!important}.anonymize-page .repo-access-switch button:not([aria-pressed=true]):hover{color:var(--accent)!important;text-decoration:underline}.anonymize-page .repo-access-switch button .ui-icon{display:none!important}.ace_editor .ace_marker-layer .ace_selection,mark.md-find.is-current{background:rgba(240,206,90,.95)!important;box-shadow:0 0 0 1.5px #5c4a12!important;color:inherit!important}.dark-mode .ace_editor .ace_marker-layer .ace_selection,.dark-mode mark.md-find.is-current{background:rgba(214,178,60,.55)!important;box-shadow:0 0 0 1.5px #f2d77a!important}.ace_search .ace_search_field,.md-find-input,.paper-page input:not([type=checkbox]):not([type=radio]):not([type=range]),.paper-page select,.paper-page textarea{border-color:#cfc7b9!important}.dark-mode .md-find-input,.dark-mode .paper-page input:not([type=checkbox]):not([type=radio]):not([type=range]),.dark-mode .paper-page select,.dark-mode .paper-page textarea{border-color:#48433c!important}.ace_search .ace_search_field:focus,.md-find-input:focus,.paper-page input:focus,.paper-page select:focus,.paper-page textarea:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.q-dot.q-dot-idle{background:#2f7d4f!important}.dark-mode .q-dot.q-dot-idle{background:#6fbf8a!important}@media (min-width:1200px){.readme-toc{margin-top:0!important;padding-top:0!important}}@media (min-width:1200px){.readme-toc{margin-top:-30px!important}}.anonymize-workspace .anonymize-topbar{background:var(--paper-bg,var(--background))!important;border-bottom:1px solid var(--border-color)!important}.anonymize-workspace .type-badge{font-family:var(--font-sans)!important;letter-spacing:0!important;text-transform:none!important;font-size:12px!important;font-weight:600!important;padding:2px 8px!important;border-radius:999px!important;background:var(--hover-bg-color)!important;color:var(--ink-soft)!important;border:0!important;vertical-align:middle}.anonymize-page input[type=checkbox]{-webkit-appearance:none;appearance:none;width:16px!important;height:16px!important;margin:2px 0 0;border:1.5px solid #a39d92;border-radius:4px;background:var(--paper-card);display:inline-grid;place-content:center;cursor:pointer;flex:0 0 16px}.anonymize-page input[type=checkbox]::before{content:"";width:9px;height:9px;transform:scale(0);background:#fff;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5'/%3E%3C/svg%3E") center/contain no-repeat}.anonymize-page input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}.anonymize-page input[type=checkbox]:checked::before{transform:scale(1)}.anonymize-page input[type=checkbox]:disabled{opacity:.45;cursor:not-allowed}.anonymize-page input[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.dark-mode .anonymize-page input[type=checkbox]{border-color:#6e6860;background:0 0}.anonymize-page #commit{font-family:var(--font-mono, "JetBrains Mono", monospace)!important;font-size:13px!important}.anonymize-page .form-control,.anonymize-page select,.anonymize-page textarea{border-color:#cfc7b9!important}.dark-mode .anonymize-page .form-control,.dark-mode .anonymize-page select,.dark-mode .anonymize-page textarea{border-color:#48433c!important}.anonymize-page .form-control:focus,.anonymize-page select:focus,.anonymize-page textarea:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.anonymize-review-note{font-size:12.5px!important;color:var(--ink-soft)!important;margin:0 0 8px!important}.anonymize-page .form-check-input[type=checkbox]{position:absolute!important;margin-left:-28px!important;margin-top:3px!important}.anonymize-page .repo-access-controls:not(:has(button,a)),.anonymize-page .repo-access-main:not(:has(button,a)){display:none!important}.anonymize-page .repo-access-note:not(:has(span+span)){margin-top:0!important}.anonymize-form-col .anonymize-submit-bar{position:sticky;bottom:0;z-index:3;background:var(--paper-bg-alt)!important;border-top:1px solid var(--border-color);padding:12px 0 16px!important;margin-top:8px}.anonymize-form-col .anonymize-submit-bar::before{content:"";position:absolute;left:0;right:0;top:-25px;height:24px;background:linear-gradient(to bottom,transparent,var(--paper-bg-alt));pointer-events:none}.anonymize-form-col .anonymize-review-note,.anonymize-form-col .draft-status{font-size:12.5px!important;color:var(--ink-soft)!important;margin:0 0 6px!important;padding:0 2px}.anonymize-form-col .anonymize-review-note::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--status-progress,#b7791f);margin-right:8px;vertical-align:1px}.anonymize-page .repo-access-switch button[aria-pressed=true]{color:var(--accent)!important;box-shadow:inset 0 -2px 0 var(--accent)!important;border-radius:0!important}.anonymize-page .input-group-append .btn-icon{height:40px!important;width:40px!important;min-height:40px!important;display:inline-flex!important;align-items:center;justify-content:center;border:1px solid #cfc7b9!important;border-radius:8px!important;margin-left:8px!important;background:var(--paper-card)!important}.dark-mode .anonymize-page .input-group-append .btn-icon{border-color:#48433c!important}.custom-select,input.form-control,select.form-control,textarea.form-control{border-color:#cfc7b9!important}.dark-mode .custom-select,.dark-mode input.form-control,.dark-mode select.form-control,.dark-mode textarea.form-control{border-color:#48433c!important}input.form-control:focus,select.form-control:focus,textarea.form-control:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}@media (max-width:767px){.paper-table-dashboard .cell-views.num::after{content:"views"!important;margin-left:4px}}@media (min-width:768px){.paper-table-dashboard .cell-views.num::after{content:none!important}}@media (max-width:767px){.admin-page .paper-table:not(.users-table) .cell-views.num::after{margin-left:4px}}.markdown-body li .redact-chip,.markdown-body p .redact-chip{line-height:1!important;padding-top:1px!important;padding-bottom:1px!important;vertical-align:baseline}.dark-mode .explorer-sidebar,.dark-mode .leftCol{background:#1e1c19!important}.errors-page .seg-tabs,.q-range-btns{display:inline-flex!important;gap:2px!important;padding:3px!important;background:var(--hover-bg-color)!important;border:0!important;border-radius:9px!important}.errors-page .seg-tabs button,.q-range-btns .btn{border:0!important;background:0 0!important;color:var(--ink-soft)!important;border-radius:7px!important;height:28px!important;min-height:28px!important;padding:0 10px!important;font-size:13px!important;font-weight:500!important;box-shadow:none!important}.errors-page .seg-tabs button.active,.q-range-btns .btn.active{background:var(--paper-card)!important;color:var(--color)!important;box-shadow:0 1px 2px rgba(40,32,24,.12),0 0 0 1px rgba(40,32,24,.06)!important}.dark-mode .errors-page .seg-tabs button.active,.dark-mode .q-range-btns .btn.active{background:#2c2925!important;box-shadow:0 0 0 1px rgba(255,255,255,.06)!important}.ace_search .ace_search_field,.md-find-input{border-width:1px!important}.ace_search .ace_search_field:focus,.md-find-input:focus{outline:0!important;border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.anonymize-form-col .anonymize-review-note{color:#8a5a12!important;font-weight:500}.dark-mode .anonymize-form-col .anonymize-review-note{color:#e3b062!important}@media (max-width:767px){.anonymize-form-col .anonymize-submit-bar{position:sticky!important;bottom:0!important;padding:12px 0 calc(12px + env(safe-area-inset-bottom))!important}}@media (max-width:767px){.anonymize-form-col{overflow:visible!important}.anonymize-page .form-group+.form-group,.anonymize-page .form-row>[class*=col]+[class*=col]{margin-top:0!important}.anonymize-page .input-group-append .btn-icon{height:44px!important;width:44px!important}}#sourceUrl,#sourceUrl-landing{border-color:#cfc7b9!important}.dark-mode #sourceUrl,.dark-mode #sourceUrl-landing{border-color:#48433c!important}#sourceUrl-landing:focus,#sourceUrl:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-soft)!important}.anonymize-form-col .anonymize-review-note{color:#6e470c!important}.dark-mode .anonymize-preview-col .markdown-body h1,.dark-mode .anonymize-preview-col .markdown-body h2,.dark-mode .anonymize-preview-col .markdown-body hr{border-color:var(--border-color)!important}.dark-mode .q-dot.q-dot-idle,.q-dot.q-dot-idle{background:var(--ink-muted)!important}.ace_search .ace_search_counter,.md-find-count{color:var(--color)!important;font-weight:500!important;font-variant-numeric:tabular-nums;font-size:12.5px!important}.readme-toc a:not(.is-active){color:#57524a!important}.dark-mode .readme-toc a:not(.is-active){color:#b8b1a6!important}.viewer-bar .vb-support{margin-right:8px!important}:root{--redact-bar:#1F1C19;--redact-ink:#5A544C;--redact-reveal:#F4EFE6}.dark-mode{--redact-bar:#000000;--redact-ink:#3E3A35;--redact-reveal:#F4EFE6}.demo-output .redact-chip,.hero .redact,.hero-mock .redact,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip,.markdown-body h4 mark.redact-chip,.markdown-body mark.redact-chip,span.redact{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:none!important;border:0!important;border-radius:2px!important;padding:0 .28em!important;text-decoration:none!important;font-weight:inherit;letter-spacing:.02em;-webkit-box-decoration-break:clone;box-decoration-break:clone;transition:color .12s ease;cursor:help}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact{box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.demo-output .redact-chip:hover,.markdown-body mark.redact-chip:hover,span.redact:hover{color:var(--redact-reveal)!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip{font-size:inherit!important;vertical-align:baseline!important;padding:0 .2em!important}.ace_text-layer .ace_redacted{background:var(--redact-bar)!important;color:var(--redact-ink)!important;border-radius:2px;box-shadow:none!important;text-decoration:none!important}.dark-mode .ace_text-layer .ace_redacted{box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.meta-redact{background:0 0!important;color:var(--ink-soft)!important;box-shadow:none!important;padding:0!important;display:inline-flex!important;align-items:center;gap:6px;font-weight:500!important}.meta-redact::before{content:"";width:14px;height:8px;border-radius:1.5px;background:var(--redact-bar);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted{background:var(--redact-bar)!important;color:var(--redact-ink)!important;border-radius:2px!important}.ace_editor .ace_text-layer .ace_redacted{box-shadow:none!important}.dark-mode .ace_editor .ace_text-layer .ace_redacted{box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)!important}.dark-mode .demo-output .redact-chip:hover,.dark-mode .markdown-body mark.redact-chip:hover{color:var(--redact-reveal)!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{color:#3e3933!important}.dark-mode .meta-redact{color:var(--ink-soft)!important}.dark-mode .hero-mock-count,.hero-mock-count{background:0 0!important;color:var(--ink-soft)!important;padding:0!important;gap:6px;font-weight:500!important}.hero-mock-count::before{content:"";width:12px;height:7px;border-radius:1.5px;background:var(--redact-bar);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}@media (max-width:767px){.dark-mode .mobile-sub b,.mobile-sub b{background:0 0!important;color:var(--ink-soft)!important;padding:0!important;gap:5px;font-weight:500!important}.mobile-sub b::before{content:"";width:12px;height:7px;border-radius:1.5px;background:var(--redact-bar);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}}:root{--redact-bar:#E4DED3;--redact-ink:#8A8378;--redact-reveal:#2B2824}.dark-mode{--redact-bar:#34302A;--redact-ink:#8F887D;--redact-reveal:#F1ECE3}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted,.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact,.demo-output .redact-chip,.markdown-body mark.redact-chip,span.redact{box-shadow:none!important;border-radius:3px!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{color:var(--redact-ink)!important}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip{box-shadow:none!important}.hero-mock-count::before,.meta-redact::before,.mobile-sub b::before{box-shadow:none!important}:root{--redact-bar:#EEEAE2;--redact-ink:#8C8478;--redact-reveal:#2B2824;--redact-deboss:inset 0 1px 2px rgba(60,48,32,.18),inset 0 0 0 1px rgba(60,48,32,.06)}.dark-mode{--redact-bar:#0E0D0B;--redact-ink:#7A746A;--redact-reveal:#F1ECE3;--redact-deboss:inset 0 1px 2px rgba(0,0,0,.7),inset 0 0 0 1px rgba(255,255,255,.05)}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact,.demo-output .redact-chip,.markdown-body mark.redact-chip,span.redact{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:var(--redact-deboss)!important;border-radius:4px!important;padding:.05em .45em!important;font-family:var(--font-mono, "JetBrains Mono", monospace)!important;font-size:.78em!important;font-weight:500!important;letter-spacing:.02em!important;line-height:1.4!important;vertical-align:.08em!important;white-space:nowrap}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip{font-size:.55em!important;vertical-align:.3em!important;color:var(--redact-ink)!important;padding:.15em .5em!important}.demo-output .redact-chip:hover,.markdown-body mark.redact-chip:hover,span.redact:hover{color:var(--redact-reveal)!important}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted{background:var(--redact-bar)!important;color:var(--redact-ink)!important;box-shadow:var(--redact-deboss)!important;border-radius:3px!important}.hero-mock-count::before,.meta-redact::before,.mobile-sub b::before{background:var(--redact-bar)!important;box-shadow:var(--redact-deboss)!important;border-radius:2px!important}:root{--slot-bg:#E3DCD0;--slot-bg-sel:#D3CBBD;--slot-num:#1F5F5A}.dark-mode{--slot-bg:#3A352F;--slot-bg-sel:#4A443C;--slot-num:#7CC7BE}.dark-mode .demo-output .redact-chip,.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.dark-mode .markdown-body h3 mark.redact-chip,.dark-mode .markdown-body mark.redact-chip,.dark-mode span.redact,.demo-output .redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip,.markdown-body h3 mark.redact-chip,.markdown-body h4 mark.redact-chip,.markdown-body mark.redact-chip,.redact-slot,span.redact{display:inline-flex!important;align-items:center!important;justify-content:flex-end!important;min-width:2.5em!important;height:1.12em!important;padding:0 .32em!important;margin:0 .06em!important;background:var(--slot-bg)!important;color:var(--slot-num)!important;box-shadow:none!important;border:0!important;border-radius:3px!important;vertical-align:-.2em!important;line-height:1!important;font-size:inherit!important;letter-spacing:0!important;text-decoration:none!important;cursor:help;white-space:nowrap}.markdown-body mark.redact-chip i,.redact-slot i,span.redact i{font:700 .66em/1 var(--font-sans,Inter,sans-serif)!important;font-style:normal!important;color:var(--slot-num)!important}.demo-output .redact-chip{font:700 .66em/1 var(--font-sans,Inter,sans-serif)!important;min-width:3.8em!important;height:1.7em!important;vertical-align:-.35em!important}.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{height:.95em!important;vertical-align:-.1em!important;border-radius:4px!important}.files .redact-slot{min-width:2.3em!important;height:1.08em!important;vertical-align:-.18em!important}.files .file.active>a .redact-slot,.files a[aria-current] .redact-slot{background:var(--slot-bg-sel)!important}.breadcrumb .redact-slot{vertical-align:-.22em!important}.ace_editor .ace_text-layer .ace_redacted,.dark-mode .ace_editor .ace_text-layer .ace_redacted{position:relative;color:transparent!important;background:var(--slot-bg)!important;box-shadow:none!important;border-radius:3px!important}.ace_editor .ace_text-layer .ace_redacted::after{position:absolute;right:.35em;top:50%;transform:translateY(-50%);font:700 9.5px/1 var(--font-sans,Inter,sans-serif);color:var(--slot-num)}.hero-mock-count::before,.meta-redact::before,.mobile-sub b::before{background:var(--slot-bg)!important;box-shadow:none!important;border-radius:2px!important}.files .tree-name{display:inline-flex;align-items:center;gap:0;min-width:0}.files .tree-name .redact-slot{vertical-align:middle!important;margin:0 .08em!important}.markdown-body li mark.redact-chip,.markdown-body p mark.redact-chip{vertical-align:-.16em!important}.ace_editor .ace_marker-layer .ace_selection,mark.md-find.is-current{background:rgba(240,196,70,.95)!important;box-shadow:0 0 0 2px rgba(196,140,20,.55)!important}.dark-mode .ace_editor .ace_marker-layer .ace_selection,.dark-mode mark.md-find.is-current{background:rgba(214,170,50,.6)!important;box-shadow:0 0 0 2px rgba(240,200,90,.55)!important}.demo-output .redact-chip,span.redact i{font-size:10px!important}.dark-mode{--slot-num:#8FD6CC}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{min-width:1.9em!important;height:.78em!important;vertical-align:0!important;padding:0 .22em!important}.viewer-bar .vb-right{gap:4px!important}.viewer-bar .vb-right .vb-icon{width:34px!important;height:34px!important;margin:0!important}.viewer-bar .vb-support{margin-right:10px!important}.viewer-bar .vb-snapshot{margin-right:14px!important}@media (max-width:767px){.mobile-sub{font-size:13px!important;color:#5f5a52!important}.dark-mode .mobile-sub{color:#b3aca1!important}}.file-card-meta{overflow:visible!important}:root{--control-border:#CFC7B9}.dark-mode{--control-border:#48433C}.admin-page .btn:not(.btn-ink):not(.btn-primary):not(.btn-outline-danger):not(.summary-pill),.admin-page select,.anonymize-page #branch,.anonymize-page #sourceUrl,.anonymize-page #sourceUrl-landing,.anonymize-page .form-control-lg,.anonymize-page select.form-control,.dashboard-page .dashboard-quota>summary,.dashboard-page .dropdown-toggle,.dashboard-page .filter-btn,.errors-page select,.q-range-btns+.btn{border-color:var(--control-border)!important}.anonymize-page #sourceUrl-landing:focus,.anonymize-page #sourceUrl:focus{border-color:var(--accent)!important}.errors-page .seg-tabs{height:36px!important;align-items:center}.errors-page .seg-tabs button{height:30px!important}.admin-page .admin-toolbar .btn,.admin-page .btn-export,.admin-page a.btn[href*=export],.admin-page button[title*=xport]{height:36px!important;min-height:36px!important}@media (max-width:767px){.paper-table .cell-views{gap:0!important}}@media (min-width:1200px){.anonymize-workspace .anonymize-split,.anonymize-workspace .anonymize-topbar-inner{max-width:1048px;margin-left:auto!important;margin-right:auto!important}.anonymize-workspace .anonymize-topbar{padding-left:0!important;padding-right:0!important}.anonymize-workspace .anonymize-form-col{padding-left:0!important}}@media (max-width:767px){.anonymize-form-col .anonymize-submit-bar{margin:8px 0 0!important;padding:10px 0 calc(10px + env(safe-area-inset-bottom))!important}.anonymize-form-col .anonymize-submit-bar .btn{border-radius:10px!important}}.img-unavailable{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px dashed var(--control-border);border-radius:8px;font-size:12.5px;color:var(--ink-soft);background:var(--hover-bg-color)}.anonymize-form-col .draft-status{color:var(--ink-soft)!important}.errors-page .errors-toolbar .form-control,.errors-page .errors-toolbar select,.errors-page form select{border-color:var(--control-border)!important}@media (min-width:1200px){.anonymize-workspace .anonymize-form-col,.anonymize-workspace .anonymize-form-col .paper-section,.anonymize-workspace .anonymize-form-col form{padding-left:0!important}.anonymize-workspace .anonymize-form-col{background:0 0!important;padding-right:24px!important}.anonymize-workspace .anonymize-form-col .anonymize-submit-bar,.anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:var(--paper-bg,var(--background))!important}.anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:linear-gradient(to bottom,transparent,var(--paper-bg,#faf9f6))!important}}@media (max-width:767px){.anonymize-page .form-row>.form-group,.anonymize-page .form-row>[class*=col] .form-group{margin-bottom:14px!important}.anonymize-page .form-row>[class*=col]{margin-bottom:0!important;padding-bottom:0!important}}@media (min-width:1200px){.anonymize-workspace .anonymize-form-col .anonymize-submit-bar{background:#faf9f6!important}.anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:linear-gradient(to bottom,rgba(250,249,246,0),#faf9f6)!important}.dark-mode .anonymize-workspace .anonymize-form-col .anonymize-submit-bar{background:#161412!important}.dark-mode .anonymize-workspace .anonymize-form-col .anonymize-submit-bar::before{background:linear-gradient(to bottom,rgba(22,20,18,0),#161412)!important}}:root{--slot-num:#0F4F4A}.dark-mode{--slot-num:#A8E6DD;--slot-bg:#3F3A33}.markdown-body li mark.redact-chip,.markdown-body p mark.redact-chip,.markdown-body td mark.redact-chip,span.redact{display:inline-block!important;height:1.08em!important;line-height:1.08em!important;vertical-align:-.17em!important;min-width:2.5em!important;text-align:right!important;padding:0 .34em!important}.breadcrumb .redact-slot i,.files .redact-slot i,.markdown-body li mark.redact-chip i,.markdown-body p mark.redact-chip i,.markdown-body td mark.redact-chip i,span.redact i{font-size:max(10.5px,.72em)!important;line-height:inherit!important;vertical-align:top!important;font-weight:700!important}.markdown-body h1 mark.redact-chip i,.markdown-body h2 mark.redact-chip i,.markdown-body h3 mark.redact-chip i{font-size:.62em!important}.ace_editor .ace_text-layer .ace_redacted::after{font-size:10.5px!important}.demo-output .redact-chip{font-size:11px!important}.errors-page .bar-info,.errors-page .vol-info,.errors-page [class*=seg-info]{opacity:.45}.connections-grid{align-items:stretch!important}.connections-grid>section{display:flex;flex-direction:column}.connections-grid>section .connection-installations{margin-top:auto!important;border-top:1px solid var(--border-color);border-bottom:0!important}.navbar .nav-link.active,.site-nav .nav-link.active,header .nav-link.active{color:var(--color)!important;box-shadow:inset 0 -2px 0 var(--accent)!important;border-bottom-color:transparent!important}@media (max-width:767px){.anonymize-topbar .paper-crumbs{display:block!important;margin-bottom:4px;font-size:13px}}.anonymize-form-col .draft-discard{background:0 0;border:0;padding:0 0 0 6px;font-size:12.5px;font-weight:500;color:var(--color)!important;text-decoration:underline;text-underline-offset:3px}.anonymize-form-col .draft-discard:hover{color:#b42318!important}@media (max-width:767px){.mobile-sub b{display:inline-flex!important;align-items:center;gap:5px;font-weight:400!important;color:inherit!important}.mobile-sub b::before{content:""!important;display:inline-block!important;width:12px;height:7px;border-radius:2px;background:var(--slot-bg)!important}.mobile-sub span:has(+b)::after{content:"·";margin:0 6px;opacity:.7}}.demo-output .redact-chip,span.redact{display:inline-block!important;height:1.15em!important;line-height:1.15em!important;vertical-align:-.2em!important;text-align:right!important}.demo-output .redact-chip{font-size:11px!important;min-width:3.4em!important}.ace-chrome .ace_gutter-cell{color:#5a554d!important}.dark-mode .ace_editor .ace_gutter-cell{color:#a39c91!important}.files .file.active>a{background:#e2ded6!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,700px) 176px!important;gap:44px!important}}@media (hover:none){.anonymize-page .repo-access-switch button:not([aria-pressed=true]):hover{color:var(--ink-soft)!important;text-decoration:none!important}}.connections-grid>section .connection-installations{margin-top:16px!important}.dashboard-page .dashboard-quota .quota-summary-ring{display:none!important}.dashboard-page .dashboard-quota>summary::before{content:"";width:28px;height:4px;border-radius:2px;background:linear-gradient(to right,var(--accent) var(--quota-pct,5%),var(--border-color) 0);flex:0 0 28px}.paper-table-dashboard .row-open{margin-right:2px}@media (max-width:767px){.paper-table-dashboard .cell-actions>.row-open{display:none!important}}.dashboard-page .dashboard-quota>summary::before{width:32px;height:5px;flex-basis:32px;background:linear-gradient(to right,var(--accent) 0 max(3px,var(--quota-pct,5%)),#d9d3c8 0)!important}.dark-mode .dashboard-page .dashboard-quota>summary::before{background:linear-gradient(to right,#7cc7be 0 max(3px,var(--quota-pct,5%)),#3a352f 0)!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,680px) 176px!important;gap:44px!important}.md-find-bar{padding-left:max(20px,calc((100% - 900px)/ 2))!important}}.breadcrumb .redact-slot i,.files .redact-slot i,.markdown-body li mark.redact-chip i,.markdown-body p mark.redact-chip i,.markdown-body td mark.redact-chip i,span.redact i{font-size:max(11px,.8em)!important;letter-spacing:.01em}.files .file.active>a{background:#ebe7e0!important}.files .file.active>a .redact-slot{background:#d6cec0!important}.demo-output .redact-chip{font-size:12px!important;height:1.3em!important;line-height:1.3em!important;min-width:3em!important;vertical-align:-.28em!important;padding:0 .4em!important}.paper-table-dashboard .cell-actions{gap:4px!important}.paper-table-dashboard .cell-actions>.copy-url{margin-right:6px!important}.paper-table-dashboard .btn-icon-dots,.paper-table-dashboard .row-edit{width:32px!important;height:32px!important;display:inline-flex!important;align-items:center;justify-content:center;border-radius:8px}.paper-table-dashboard .btn-icon-dots:hover,.paper-table-dashboard .row-edit:hover{background:var(--hover-bg-color)!important}.anonymize-page #commit{text-overflow:ellipsis}@media (max-width:767px){.anonymize-page .form-grid-2{row-gap:0!important;gap:0!important}.anonymize-page .form-grid-2>.form-group{margin-bottom:16px!important}}.connections-grid>section .connection-installations{border-top:0!important;padding-top:0!important}.vb-help{position:relative;display:inline-flex}.vb-help-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:0;border-radius:8px;background:0 0;color:var(--ink-soft);font:500 13px var(--font-sans,Inter,sans-serif);cursor:pointer}.vb-help-btn:hover,.vb-help-btn[aria-expanded=true]{background:var(--hover-bg-color);color:var(--color)}.vb-help-menu{position:absolute;top:calc(100% + 8px)!important;right:0;left:auto;min-width:250px;z-index:50}.vb-help-menu a{display:flex;gap:10px;align-items:flex-start}.vb-help-menu small{display:block;font-size:12px;color:var(--ink-soft);font-weight:400;margin-top:1px}@media (max-width:767px){.viewer-bar .vb-help{display:none!important}}.paper-table-dashboard .paper-table-head .col-link{padding-left:0!important;margin-left:-16px}@media (min-width:768px){.anonymize-workspace .anonymize-topbar{padding-top:12px!important;padding-bottom:12px!important}.anonymize-workspace .anonymize-topbar .paper-crumbs{margin-bottom:2px!important}.anonymize-workspace .anonymize-topbar .paper-page-title,.anonymize-workspace .anonymize-topbar h1{font-size:26px!important;margin:0!important}}#sourceUrl-landing::placeholder,#terms::placeholder,.form-control::placeholder,input::placeholder,textarea::placeholder{color:#a39b8e!important;opacity:1!important}.dark-mode .form-control::placeholder,.dark-mode input::placeholder,.dark-mode textarea::placeholder{color:#6f695f!important}.q-dot-active,.q-state-filters .q-legend-active,.q-state-filters [class*=active]>.q-dot,.q-state-toggle .q-dot-active{background:var(--accent)!important}@media (max-width:767px){.mobile-sub b{font-size:inherit!important;gap:6px!important;margin-left:0!important;padding-left:0!important}.mobile-sub span:has(+b)::after{margin:0 6px 0 6px!important}.mobile-sub b::before{width:14px!important;height:8px!important;background:#d6cec0!important}.file-card-head{min-height:56px!important}}.markdown-body h1 mark.redact-chip i,.markdown-body h2 mark.redact-chip i{font-weight:600!important;font-size:.58em!important}.dark-mode .markdown-body h1 mark.redact-chip,.dark-mode .markdown-body h2 mark.redact-chip,.markdown-body h1 mark.redact-chip,.markdown-body h2 mark.redact-chip{min-width:1.6em!important;padding:0 .18em!important}.readme-toc a.lvl-3{font-size:12.5px!important;color:#6e685f!important}.dark-mode .readme-toc a.lvl-3{color:#9c958a!important}.file-card-head .file-card-tools,.file-card-head .view-tools{margin-right:10px!important;padding-right:10px!important;border-right:1px solid var(--border-color)!important}.files .file.active>a{background:#e5e0d8!important}.files .file.active>a .redact-slot{background:#cec5b5!important}.markdown-body li mark.redact-chip,.markdown-body p mark.redact-chip,.markdown-body td mark.redact-chip{vertical-align:-.1em!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:24px!important;transition:opacity .15s}.file-card.at-right-edge .file-card-body::after{opacity:0!important}}.hero-mock .is-active,.hero-mock [class*=active]{position:relative}.hero-mock .is-active::before{left:0!important}.anonymize-form-col form{padding-bottom:36px!important}.connections-grid>section p{font-size:13.5px!important;line-height:1.55!important}.connections-grid>section>.btn,.connections-grid>section>a.btn{width:auto!important;align-self:flex-start}@media (max-width:767px){.paper-table-dashboard .cell-actions>.dropdown,.paper-table-dashboard .cell-actions>.dropdown>.btn-icon-dots,.paper-table-dashboard .cell-actions>.row-edit{width:40px!important;height:40px!important}.dashboard-page .dashboard-row-meta .cell-expires,.dashboard-page .dashboard-row-meta .cell-status,.dashboard-page .dashboard-row-meta .cell-views{padding:0!important;margin:0!important}.dashboard-page .dashboard-row-meta .cell-expires::before,.dashboard-page .dashboard-row-meta .cell-views::before{margin:0 7px!important;width:auto!important}.dashboard-page .dashboard-row-meta .status-line{padding:0!important;margin:0!important}.dashboard-page .dashboard-row-meta .cell-expires .cell-label{margin-right:4px!important}}.connection-installations,.connection-installations li{border:0!important;padding-bottom:0!important;margin-bottom:0!important}.connection-installations li{padding-top:0!important}.connections-page .connection-refresh{margin-left:0!important}.anonymize-form-col .anonymize-submit-bar::before{display:none!important}.anonymize-form-col .anonymize-submit-bar{border-top:1px solid var(--border-color)!important;box-shadow:0 -6px 12px -10px rgba(40,32,24,.18)}.anonymize-form-col form{padding-bottom:24px!important}.img-unavailable{display:flex!important;align-items:center;justify-content:center;gap:8px;width:100%;min-height:72px;border:1px solid var(--border-color)!important;border-style:solid!important;background:var(--hover-bg-color);font:500 12.5px var(--font-sans,Inter,sans-serif)!important;color:var(--ink-soft)}.img-unavailable::before{content:"";width:16px;height:13px;border:1.5px solid currentColor;border-radius:2px;opacity:.7;background:linear-gradient(135deg,transparent 55%,currentColor 55% 62%,transparent 62%)}.admin-summary .summary-pill:not(.active) .count{text-decoration:none!important;opacity:1!important;color:var(--ink-soft)!important}.admin-summary .summary-pill:not(.active)::before{background:#8a8680!important}.dashboard-page .dashboard-quota>summary::before{width:44px!important;flex-basis:44px!important}@media (min-width:1200px){.reading-layout{grid-template-columns:minmax(0,664px) 176px!important;gap:40px!important}.md-find-bar{padding-left:max(20px,calc((100% - 880px)/ 2))!important}}.file-card-head .tb-file{margin-left:8px!important;position:relative}.file-card-head .tb-file::before{content:"";position:absolute;left:-9px;top:6px;bottom:6px;width:1px;background:var(--border-color)}.tree-search-input::placeholder{color:#8a8378!important}.tree-search-kbd{color:#6e685f!important;border:1px solid var(--border-color)!important;background:var(--paper-card)!important}@media (max-width:767px){.mobile-sub{gap:0!important;column-gap:0!important}.mobile-sub span:has(+b)::after,.mobile-sub>span+span::before{margin:0 6px!important}.mobile-sub b::before{margin:0!important}.file-card-head{min-height:52px!important;padding-top:6px!important;padding-bottom:6px!important}}.errors-page .dot-info,.errors-page .seg-info{background:#cfc9bf!important;opacity:1!important}.dark-mode .errors-page .dot-info,.dark-mode .errors-page .seg-info{background:#4a453e!important}.errors-page .dot-error,.errors-page .seg-error{background:#b42318!important}.errors-page .dot-warn,.errors-page .seg-warn{background:#b7791f!important}.dark-mode .leftCol,.dark-mode .viewer-bar{background:#221f1b!important}.paper-table-dashboard .copy-url{box-shadow:inset 0 0 0 1px var(--control-border,#cfc7b9)!important;background:var(--paper-card)!important}.paper-table-dashboard .copy-url .ui-icon,.paper-table-dashboard .copy-url svg{width:15px!important;height:15px!important;color:var(--color)!important;opacity:.75}.paper-table-dashboard .copy-url:hover{background:var(--hover-bg-color)!important}@media (min-width:1200px){.anonymize-workspace .anonymize-preview-col{padding-right:0!important}}.anonymize-submit-bar .draft-status{display:block;margin:0 0 8px!important;font-size:12.5px!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.row-open{display:inline-flex!important;grid-area:auto}.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto auto auto!important;grid-template-areas:"name open edit more" "meta meta meta meta" "link link link link"!important}.paper-table-dashboard .cell-actions>.row-open{grid-area:open;width:40px!important;height:40px!important;align-self:start;margin-top:-4px}}.overview-page :is(.ov-kpi,.ov-card,.kpi-card):has(.ov-meter.crit){border-color:rgba(180,35,24,.45)!important;box-shadow:inset 3px 0 0 #b42318!important}.overview-page :is(.ov-kpi,.ov-card,.kpi-card):has(.ov-meter.warn):not(:has(.ov-meter.crit)){border-color:rgba(183,121,31,.45)!important;box-shadow:inset 3px 0 0 #b7791f!important}.dark-mode{--slot-bg:#46403A}.faq-section .panel-default .panel-title a:after{transform:rotate(45deg)!important}.faq-section .panel-default .panel-title a[aria-expanded=true]:after{transform:rotate(-135deg)!important}.overview-page .ov-kpi-card:has(.ov-meter.crit){border-color:rgba(180,35,24,.45)!important;box-shadow:inset 3px 0 0 #b42318!important}.overview-page .ov-kpi-card:has(.ov-meter.warn):not(:has(.ov-meter.crit)){border-color:rgba(183,121,31,.45)!important;box-shadow:inset 3px 0 0 #b7791f!important}@media (max-width:767px){.paper-table-dashboard .cell-anon,.paper-table-dashboard .cell-anon a,.paper-table-dashboard .cell-anon strong,.paper-table-dashboard .cell-anon>*{min-width:0}.paper-table-dashboard .cell-anon .anon-name,.paper-table-dashboard .cell-anon .anon-title,.paper-table-dashboard .cell-anon .anon-title>a{white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important;word-break:normal!important;overflow-wrap:normal!important;display:block}}.admin-page .summary-pill.ok::before,.users-page .summary-pill.ok::before{background:var(--status-ready,#2f7d4f)!important}.admin-page .summary-pill.error::before{background:var(--status-error,#b42318)!important}@media (max-width:767px){.paper-table-dashboard .cell-actions>.row-open{display:none!important}.paper-table.paper-table-dashboard .paper-table-row{grid-template-columns:minmax(0,1fr) auto auto!important;grid-template-areas:"name edit more" "meta meta meta" "link link link"!important}.paper-table-dashboard .cell-anon .anon-title,.paper-table-dashboard .cell-anon .anon-title>a{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;overflow-wrap:anywhere!important}}.ace-chrome .ace_keyword,.ace-chrome .ace_storage,.ace-chrome .ace_storage.ace_type{color:#7a3e6b!important}.dark-mode .ace_editor .ace_keyword,.dark-mode .ace_editor .ace_storage{color:#d3a3c6!important}.dark-mode{--slot-bg:#4A443D;--slot-num:#9FE0D6}.faq-hero h1,.faq-page h1,.paper-page-title{text-wrap:balance}@media (min-width:1200px){.readme-toc{margin-top:-18px!important}}@media (min-width:768px){.leftCol-search{height:52px!important;min-height:52px!important;box-sizing:border-box;display:flex!important;align-items:center;padding-top:0!important;padding-bottom:0!important;border-bottom:1px solid var(--border-color)!important;margin-bottom:6px!important}}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:32px!important;background:linear-gradient(to right,rgba(255,255,255,0),#fff 75%)!important;border-right:2px solid rgba(40,32,24,.14)}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(20,19,17,0),#141311 75%)!important;border-right-color:rgba(255,255,255,.1)}}.anonymize-form-col form{padding-bottom:0!important}.anonymize-form-col .anonymize-submit-bar{margin-bottom:0!important}.errors-page .kpi-filter{cursor:pointer;transition:border-color .12s,box-shadow .12s}.errors-page .kpi-filter:hover{border-color:var(--control-border,#cfc7b9)!important}.errors-page .kpi-filter.is-selected{border-color:var(--accent)!important;box-shadow:inset 0 0 0 1px var(--accent)!important}.errors-page .kpi-filter:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.readme-toc a:not(.is-active){color:#4f4a43!important}.readme-toc a.lvl-3:not(.is-active){color:#625c53!important}.ace_search .ace_search_field:focus,.md-find-input:focus{box-shadow:0 0 0 2px var(--accent-soft)!important;border-color:var(--accent)!important}@media (max-width:767px){.file-card-head{align-items:flex-start!important}.file-card-head .btn-sidebar-toggle,.file-card-head .mobile-files-toggle,.file-card-head>button:first-child{margin-top:2px!important}}@media (max-width:767px){.dashboard-page #search,.dashboard-page .btn-filters,.dashboard-page .dashboard-heading-actions .btn,.dashboard-page .dashboard-quota>summary,.dashboard-page .filters-toggle{height:40px!important;min-height:40px!important}}.profile-page .quota-tile:has(.quota-unlimited)::after,.quota-panel .quota-item:has(.quota-unlimited)::after{content:"No limit on file count";display:block;font-size:12.5px;color:var(--ink-soft);margin-top:6px}.admin-page .detail-value.is-empty,.admin-page .is-unsynced{color:#9a9389!important;font-style:normal!important}@media (min-width:768px){.anonymize-form-col .anonymize-submit-bar{display:flex!important;align-items:center;gap:16px}.anonymize-submit-bar .draft-status{margin:0!important;flex:0 1 auto;max-width:46%;line-height:1.35}.anonymize-submit-bar .btn-ink{flex:1 1 auto}}.anonymize-landing .repo-access-note>span:has(.ui-icon){display:none!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{box-shadow:inset -8px 0 8px -8px rgba(40,32,24,.35)!important;border-right:0!important}}.dark-mode .leftCol{background:#1c1a17!important}.dark-mode .viewer-bar{background:#181614!important}@media (max-width:767px){.file-card.is-code.no-wrap .file-card-body::after{width:44px!important;background:linear-gradient(to right,rgba(250,249,246,0) 0,rgba(250,249,246,.92) 55%,rgba(226,220,210,.95) 100%)!important;box-shadow:inset -1px 0 0 rgba(40,32,24,.18)!important}.dark-mode .file-card.is-code.no-wrap .file-card-body::after{background:linear-gradient(to right,rgba(20,19,17,0) 0,rgba(20,19,17,.92) 55%,rgba(44,40,35,.95) 100%)!important;box-shadow:inset -1px 0 0 rgba(255,255,255,.14)!important}}@media (max-width:767px){.file-card-head{min-height:48px!important;align-items:center!important;padding-top:4px!important;padding-bottom:4px!important}.file-card-head .file-card-path{display:flex!important;flex-direction:row!important;align-items:center!important;flex-wrap:nowrap!important;gap:8px!important;min-width:0}.file-card-head .file-card-path .breadcrumb{min-width:0;flex:0 1 auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.mobile-sub{margin:0!important;flex:0 0 auto;white-space:nowrap}.mobile-sub>span{display:none!important}.mobile-sub span:has(+b)::after{content:none!important}.mobile-sub b{font-size:12.5px!important}}.paper-features,.paper-qa,.paper-section-head,.paper-stats{margin-top:88px!important}.paper-hero+.paper-stats{margin-top:40px!important}@media (min-width:1100px){.paper-faq-body{grid-template-columns:180px minmax(0,1fr) 220px!important;gap:48px!important}.paper-faq-help{display:block}}.paper-faq-help{display:none}.paper-faq-help-card{position:sticky;top:96px;border:1px solid var(--border-color);border-radius:12px;background:var(--paper-card);padding:18px}.paper-faq-help-card strong{display:block;font-size:14px;margin-bottom:6px}.paper-faq-help-card p{font-size:13px;line-height:1.5;color:var(--ink-soft);margin:0 0 12px}.anonymize-submit-bar .draft-status{white-space:nowrap;flex:0 0 auto!important;max-width:none!important}.q-throughput .q-range-btns{float:right;margin:-4px 0 0 12px}.admin-page .paper-table{border:1px solid var(--border-color)!important;border-radius:12px!important;overflow:hidden;background:var(--paper-card)}.dark-mode{--ink-soft:#ADA699;--ink-muted:#8C8579}.hero-mock-count::before{background:var(--slot-bg)!important}@media (min-width:1100px){.paper-faq-help{display:block!important}}.q-throughput{position:relative}.q-throughput .q-range-btns{position:absolute!important;top:12px;right:16px;float:none!important;margin:0!important;width:auto!important;display:inline-flex!important;z-index:2}.anonymize-submit-bar .draft-status{order:-1} \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index ff41fe3..6965fe2 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,3 +1,5 @@ +/* Type scale: 10 11 12 13 14 15 16 18 20 22 24 28 32 40px (rem values use the same + steps so they still follow the reader's browser font size). */ /* ===== Paper design tokens ===== Editorial minimal — warm neutrals, single indigo accent. Serif display (Instrument Serif), Inter body, JetBrains Mono. */ @@ -430,8 +432,6 @@ a:hover { padding: 0; } .navbar .nav-support-label { display: none; } - [class*="floatingchat-container-wrap"], - iframe.floatingchat-container { display: none !important; } } .navbar .user-chip { @@ -443,7 +443,7 @@ a:hover { border: 1px solid var(--border-color); border-radius: 999px; background: var(--sidebar-bg-color); - font-size: 12.5px; + font-size: 13px; color: var(--color) !important; } @@ -537,7 +537,7 @@ a:hover { .hero-subtitle { margin-top: 18px; - font-size: 17px; + font-size: 18px; line-height: 1.55; color: var(--ink-soft); max-width: 30em; @@ -983,7 +983,7 @@ a:hover { .paper-preview .pane-body { padding: 16px 18px; font-family: var(--font-mono); - font-size: 12.5px; + font-size: 13px; line-height: 1.9; color: var(--ink-soft); } @@ -1118,7 +1118,7 @@ a:hover { } .paper-feature-more p { - font-size: 14.5px; + font-size: 15px; color: var(--ink-soft); line-height: 1.55; margin: 10px 0 12px; @@ -1276,7 +1276,7 @@ a:hover { margin-right: 16px; } .paper-stat-value { - font-size: 36px; + font-size: 40px; } .paper-section-head, .paper-qa { @@ -1286,7 +1286,7 @@ a:hover { grid-template-columns: 1fr; } .paper-preview .pane-body { - font-size: 11.5px; + font-size: 12px; line-height: 1.8; } /* Tabs become a strip above the screenshot; the selected description @@ -1319,7 +1319,7 @@ a:hover { } .paper-feature-tab .paper-eyebrow { margin: 0; - font-size: 10.5px; + font-size: 11px; letter-spacing: 0.08em; } .paper-feature-tab .paper-feature-title { @@ -1346,7 +1346,7 @@ a:hover { } .paper-feature-more .paper-feature-title-m { display: block; - font-size: 26px; + font-size: 28px; line-height: 1.1; } .paper-feature-more p { @@ -1394,7 +1394,7 @@ a:hover { .dropdown-menu .dropdown-header { color: var(--ink-muted); font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; } @@ -2160,7 +2160,7 @@ a:hover { .tree-search-empty { padding: 16px 12px; color: var(--ink-muted); - font-size: 12.5px; + font-size: 13px; font-family: var(--font-sans); text-align: center; } @@ -2286,7 +2286,7 @@ a:hover { .last-update { font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; color: var(--ink-muted); padding: 8px 10px; border-top: 1px solid var(--border-color); @@ -2328,7 +2328,7 @@ a:hover { color: var(--color); border-radius: 6px; padding: 4px 10px; - font-size: 12.5px; + font-size: 13px; min-height: 30px; } .status-bar-actions .btn:hover { @@ -2560,7 +2560,7 @@ notebook { .paper-faq-title em { font-style: italic; color: var(--color); } .paper-faq-lede { color: var(--ink-soft); - font-size: 17px; + font-size: 18px; line-height: 1.55; max-width: 38em; margin: 0; @@ -2577,11 +2577,11 @@ notebook { .paper-faq-toc { position: sticky; top: 32px; - font-size: 13.5px; + font-size: 14px; } .paper-faq-toc-head { font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; color: var(--ink-muted); letter-spacing: 0.14em; text-transform: uppercase; @@ -2808,7 +2808,7 @@ code { /* ===== Dashboard & List Styles ===== */ .dashboard-title { - font-size: 1.4rem; + font-size: 1.375rem; font-weight: 600; letter-spacing: -0.01em; } @@ -2842,7 +2842,7 @@ code { } .quota-value { - font-size: 0.8rem; + font-size: 0.8125rem; font-weight: 500; font-variant-numeric: tabular-nums; } @@ -2863,7 +2863,7 @@ code { align-items: center; gap: 4px; padding: 3px 10px; - font-size: 0.8rem; + font-size: 0.8125rem; font-weight: 500; background: var(--primary-bg); color: var(--primary-color); @@ -2944,7 +2944,7 @@ code { } .repo-source { - font-size: 0.85rem; + font-size: 0.875rem; opacity: 0.8; display: flex; flex-wrap: wrap; @@ -2961,7 +2961,7 @@ code { flex-wrap: wrap; gap: 12px; margin-top: 8px; - font-size: 0.8rem; + font-size: 0.8125rem; opacity: 0.7; } @@ -2985,14 +2985,14 @@ code { gap: 8px; padding: 48px 20px; opacity: 0.5; - font-size: 0.95rem; + font-size: 0.9375rem; list-style: none; border-bottom: 1px solid var(--border-color); border-top: 1px solid var(--border-color); } .repo-list-empty i { - font-size: 1.8rem; + font-size: 1.75rem; } /* Type badges (Repo / PR) */ @@ -3000,7 +3000,7 @@ code { display: inline-flex; align-items: center; gap: 3px; - font-size: 0.7rem; + font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; @@ -3221,7 +3221,7 @@ code { .paper-settings-copy { color: var(--ink-soft); font-size: 13px; margin: 0 0 14px; } .anonymize-section-title { - font-size: 0.85rem; + font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; @@ -3283,7 +3283,7 @@ code { @media (max-width: 767px) { .anonymize-workspace { flex: 0 0 auto; min-height: 0; } .anonymize-topbar { padding: 14px 16px; } - .anonymize-topbar-title { font-size: 1.6rem; margin-top: 2px; } + .anonymize-topbar-title { font-size: 1.5rem; margin-top: 2px; } .anonymize-split { display: flex; flex-direction: column; @@ -3374,9 +3374,9 @@ code { max-width: 100%; } - h1 { font-size: 1.6rem; } - h2 { font-size: 1.35rem; } - h3 { font-size: 1.15rem; } + h1 { font-size: 1.5rem; } + h2 { font-size: 1.375rem; } + h3 { font-size: 1.125rem; } /* Dashboard list items */ .repo-list-item { @@ -3421,7 +3421,7 @@ code { min-height: 32px; } .status-bar .paths { - font-size: 11.5px; + font-size: 12px; overflow-x: auto; flex-wrap: nowrap; white-space: nowrap; @@ -3435,7 +3435,7 @@ code { /* Home hero section */ #home .display-4 { - font-size: 1.8rem; + font-size: 1.75rem; } #home h4.subtext-header { @@ -3465,12 +3465,6 @@ code { margin-bottom: 2px; } - /* Ko-fi widget hidden on mobile */ - .floatingchat-container-wrap { - display: none !important; - } - - /* ---- Anonymize page mobile ---- */ .anonymize-landing { @@ -3496,7 +3490,7 @@ code { } .anonymize-section-title { - font-size: 0.8rem; + font-size: 0.8125rem; margin: 12px 0 8px 0; } @@ -3525,7 +3519,7 @@ code { } .anonymize-page .form-check-label { - font-size: 0.95rem; + font-size: 0.9375rem; } .anonymize-page .card-body { @@ -3534,7 +3528,7 @@ code { /* PR preview on mobile */ .pr-title { - font-size: 1.15rem; + font-size: 1.125rem; } .pr-diff pre { @@ -3547,7 +3541,7 @@ code { .nav-tabs .nav-link { padding: 8px 12px; - font-size: 0.9rem; + font-size: 0.875rem; } } @@ -3626,7 +3620,7 @@ code { border: 1px solid rgba(138, 107, 30, 0.35); border-radius: 8px; color: var(--color); - font-size: 12.5px; + font-size: 13px; line-height: 1.4; } .paper-inline-warning i { color: var(--status-progress); margin-top: 2px; flex-shrink: 0; } @@ -3750,7 +3744,7 @@ code { color: var(--ink-muted); text-decoration: none; font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; @@ -3800,7 +3794,7 @@ code { .admin-stat-card .stat-value { font-family: var(--font-serif); - font-size: 1.4rem; + font-size: 1.375rem; font-weight: 400; line-height: 1.1; color: var(--color); @@ -3808,7 +3802,7 @@ code { .admin-stat-card .stat-label { font-family: var(--font-mono); - font-size: 9.5px; + font-size: 10px; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.08em; @@ -3890,7 +3884,7 @@ code { } .admin-list-item .item-meta { - font-size: 0.85rem; + font-size: 0.875rem; opacity: 0.8; } @@ -3932,7 +3926,7 @@ code { letter-spacing: 0.02em; border: 1px solid transparent; text-transform: none; - font-size: 11.5px; + font-size: 12px; padding: 1px 9px 2px; border-radius: 999px; } @@ -3990,7 +3984,7 @@ code { .queue-job-card .job-id { font-family: var(--font-mono); - font-size: 0.95rem; + font-size: 0.9375rem; color: var(--color); word-break: break-all; display: flex; @@ -4000,7 +3994,7 @@ code { .queue-job-card .job-timestamps { font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; color: var(--ink-muted); display: flex; flex-wrap: wrap; @@ -4084,7 +4078,7 @@ code { margin: 0; font-family: var(--font-serif); font-weight: 400; - font-size: 1.6rem; + font-size: 1.5rem; display: flex; align-items: center; gap: 10px; @@ -4095,12 +4089,12 @@ code { display: grid; grid-template-columns: 160px 1fr; gap: 10px 20px; - font-size: 0.92rem; + font-size: 0.9375rem; } .user-detail-grid .detail-label { font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); @@ -4155,7 +4149,7 @@ code { font-style: italic; } .paper-empty-lede { - font-size: 17px; + font-size: 18px; line-height: 1.55; color: var(--ink-soft); text-wrap: pretty; @@ -4315,7 +4309,7 @@ code { } .navbar .user-chip-wrap .dropdown-menu .dropdown-item { padding: 10px 4px 10px 42px !important; - font-size: 13.5px; + font-size: 14px; color: var(--ink-muted) !important; background: transparent !important; border-bottom: 1px solid var(--border-color); @@ -4342,7 +4336,7 @@ code { border: 1px solid var(--border-color); color: var(--ink-soft); font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; } @@ -4379,7 +4373,7 @@ code { align-items: center; margin-top: 10px; font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted); @@ -4454,7 +4448,7 @@ code { } .paper-error-title { font-family: var(--font-serif); - font-size: 1.4rem; + font-size: 1.375rem; line-height: 1.2; margin-top: 2px; color: var(--color); @@ -4472,7 +4466,7 @@ code { margin: 14px 0 0; padding-left: 18px; color: var(--ink-soft); - font-size: 13.5px; + font-size: 14px; line-height: 1.6; } .paper-error-actions { @@ -4488,11 +4482,11 @@ code { grid-template-columns: 160px 1fr; gap: 10px 20px; margin-top: 18px; - font-size: 0.92rem; + font-size: 0.9375rem; } .paper-detail-grid .detail-label { font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); @@ -4560,26 +4554,6 @@ code { .paper-support-card:hover .paper-support-cta i { transform: translateX(2px); } .paper-support-card .paper-support-cta i { transition: transform 0.15s ease; } -/* Ko-fi embed wrapper */ -.paper-kofi-wrap { - border: 1px solid var(--border-color); - border-radius: 10px; - overflow: hidden; - background: var(--paper-card); -} -.paper-kofi-wrap iframe { - border: 0; - width: 100%; - height: 650px; - display: block; -} -.dark-mode .paper-kofi-wrap iframe { - filter: invert(0.88) hue-rotate(180deg) sepia(0.15) saturate(1.2) brightness(1.1) contrast(0.85); -} -@media (max-width: 700px) { - .paper-kofi-wrap iframe { height: 720px; } -} - /* ===== Pull request page (paper) ===== */ .pr-page { min-height: 100%; @@ -4638,7 +4612,7 @@ code { border: 0; border-bottom: 2px solid transparent; font-family: var(--font-sans); - font-size: 13.5px; + font-size: 14px; font-weight: 500; color: var(--ink-muted); cursor: pointer; @@ -4684,7 +4658,7 @@ code { background: var(--paper-bg-alt); border-bottom: 1px solid var(--border-color); font-family: var(--font-mono); - font-size: 12.5px; + font-size: 13px; color: var(--color); } .diff-file-icon { color: var(--ink-muted); flex-shrink: 0; } @@ -4718,7 +4692,7 @@ code { width: 100%; border-collapse: collapse; font-family: var(--font-mono); - font-size: 12.5px; + font-size: 13px; line-height: 1.55; table-layout: fixed; } @@ -4793,7 +4767,7 @@ code { .dark-mode .diff-row-remove .diff-gutter { background: rgba(255,139,123,0.06); color: var(--status-error); } @media (max-width: 700px) { - .diff-file-table { font-size: 11.5px; } + .diff-file-table { font-size: 12px; } .diff-file-table .diff-gutter { width: 36px; padding: 0 6px; } .diff-file-table .diff-sign { width: 14px; padding: 0 4px; } .diff-file-table .diff-code { padding: 1px 8px; } @@ -4835,7 +4809,7 @@ code { .pr-comment-author i { color: var(--ink-muted); font-size: 12px; } .pr-comment-date { font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; color: var(--ink-muted); letter-spacing: 0.02em; } @@ -4847,7 +4821,7 @@ code { .pr-body-card { padding: 14px 14px; border-radius: 8px; } .pr-comment { padding: 12px 14px; } .paper-tabs .paper-tab { padding: 10px 12px; font-size: 13px; } - .pr-diff pre { font-size: 11.5px; padding: 12px; } + .pr-diff pre { font-size: 12px; padding: 12px; } } /* Toasts — paper style, dark-mode aware */ @@ -4877,7 +4851,7 @@ code { color: var(--ink-muted) !important; text-shadow: none; opacity: 0.9; - font-size: 1.2rem; + font-size: 1.25rem; } .toast .toast-header .close:hover { color: var(--color) !important; opacity: 1; } .toast .toast-body { @@ -4925,7 +4899,7 @@ code { border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; color: var(--ink-muted); letter-spacing: 0.02em; } @@ -4961,7 +4935,7 @@ code { .paper-kbd { display: inline-block; font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; padding: 1px 6px; border: 1px solid var(--border-color); border-bottom-width: 2px; @@ -4976,7 +4950,7 @@ code { align-items: center; gap: 6px; font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; padding: 2px 8px; border: 1px solid var(--border-color); border-radius: 999px; @@ -5029,7 +5003,7 @@ code { .paper-form-step:first-child { border-top: 1px solid var(--border-color); } .paper-form-step .step-num { font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; color: var(--ink-muted); padding-top: 4px; letter-spacing: 0.02em; @@ -5043,7 +5017,7 @@ code { } .paper-form-step .step-sub { color: var(--ink-soft); - font-size: 13.5px; + font-size: 14px; margin-bottom: 14px; text-wrap: pretty; } @@ -5072,7 +5046,7 @@ code { .paper-switch-row .sw-label { font-weight: 500; } .paper-switch-row .sw-sub { display: block; - font-size: 12.5px; + font-size: 13px; color: var(--ink-muted); margin-top: 2px; } @@ -5760,7 +5734,7 @@ textarea::selection { text-align: center; border-top: 1px solid var(--border-soft, var(--border-color)); } -.paper-table .paper-table-empty > i { font-size: 1.6rem; color: var(--ink-faint); } +.paper-table .paper-table-empty > i { font-size: 1.5rem; color: var(--ink-faint); } .paper-table .paper-table-empty-actions { display: flex; gap: 8px; margin-top: 8px; } .bulk-bar { @@ -5775,7 +5749,7 @@ textarea::selection { } /* Tighter admin page header — admin is power-user UI, no need for huge title + lede */ -.paper-page.admin-page .paper-page-title { font-size: 1.6rem; margin: 4px 0; } +.paper-page.admin-page .paper-page-title { font-size: 1.5rem; margin: 4px 0; } .paper-page.admin-page .paper-page-lede { display: none; } .paper-page.admin-page .paper-crumbs { margin-bottom: 4px; } .paper-page.admin-page .admin-nav { margin: 6px 0 12px; } @@ -5913,7 +5887,7 @@ textarea::selection { } .admin-summary .summary-total { font-family: var(--font-serif); - font-size: 1.05rem; + font-size: 1rem; color: var(--color); margin-right: 8px; } @@ -5979,7 +5953,7 @@ textarea::selection { gap: 8px; border-top: 1px solid var(--border-soft, var(--border-color)); } -.paper-table-empty > i { font-size: 1.6rem; } +.paper-table-empty > i { font-size: 1.5rem; } /* ===== Queue dashboard (redesigned) ===== */ .q-header { @@ -6016,18 +5990,6 @@ textarea::selection { background: var(--color); color: var(--paper-card); } -.btn-dark { - background: var(--color) !important; - color: var(--paper-card) !important; - border-color: var(--color) !important; - border-radius: 999px; - padding: 5px 18px; - font-family: var(--font-mono); - font-size: 11px; - font-weight: 500; - letter-spacing: 0.04em; -} - /* Queue overview cards */ .q-cards { display: flex; @@ -6068,7 +6030,7 @@ textarea::selection { .dark-mode .q-dot-red { background: #F08A82; } .q-card-name { font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; font-weight: 500; color: var(--ink-muted); letter-spacing: 0.04em; @@ -6083,7 +6045,7 @@ textarea::selection { } .q-card-sub { font-family: var(--font-mono); - font-size: 10.5px; + font-size: 11px; color: var(--ink-muted); letter-spacing: 0.04em; } @@ -6193,7 +6155,7 @@ textarea::selection { } .q-stat-label { font-family: var(--font-mono); - font-size: 9.5px; + font-size: 10px; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.08em; @@ -6201,7 +6163,7 @@ textarea::selection { } .q-stat-value { font-family: var(--font-serif); - font-size: 1.3rem; + font-size: 1.25rem; font-weight: 400; color: var(--color); line-height: 1.2; @@ -6328,7 +6290,7 @@ textarea::selection { } .q-table thead th { font-family: var(--font-mono); - font-size: 9.5px; + font-size: 10px; font-weight: 500; color: var(--ink-muted); text-transform: uppercase; @@ -6346,7 +6308,7 @@ textarea::selection { .q-table tbody tr:last-child { border-bottom: none; } .q-table tbody td { padding: 10px 10px; - font-size: 0.88rem; + font-size: 0.875rem; vertical-align: middle; } .q-cell-id { @@ -6360,7 +6322,7 @@ textarea::selection { gap: 8px; } .q-chevron { - font-size: 9px; + font-size: 10px; color: var(--ink-muted); transition: transform 0.15s; flex-shrink: 0; @@ -6430,7 +6392,7 @@ textarea::selection { } .q-error-reason { color: #B42318; - font-size: 0.82rem; + font-size: 0.8125rem; margin-bottom: 4px; } .dark-mode .q-error-reason { color: #F08A82; } @@ -6456,7 +6418,7 @@ textarea::selection { } .q-error-stack { - font-size: 0.78rem; + font-size: 0.75rem; max-height: 120px; overflow: auto; margin: 4px 0 0; @@ -6491,7 +6453,7 @@ textarea::selection { } .q-job-detail-label { font-family: var(--font-mono); - font-size: 9px; + font-size: 10px; font-weight: 500; color: var(--ink-muted); text-transform: uppercase; @@ -6853,7 +6815,7 @@ textarea::selection { .paper-page .accordion .form-check-label { color: var(--color); font-weight: 500; - font-size: 13.5px; + font-size: 14px; } .paper-page .accordion .form-text { color: var(--ink-muted); @@ -6910,7 +6872,7 @@ textarea::selection { .paper-footer-tag { margin-top: 10px; color: var(--ink-soft); - font-size: 13.5px; + font-size: 14px; line-height: 1.55; max-width: 340px; } @@ -6925,7 +6887,7 @@ textarea::selection { .paper-footer-col a { display: block; color: var(--color); - font-size: 13.5px; + font-size: 14px; padding: 4px 0; text-decoration: none; } @@ -6950,7 +6912,7 @@ textarea::selection { .paper-footer-foot .paper-footer-meta { font-family: var(--font-serif); font-style: italic; - font-size: 13.5px; + font-size: 14px; letter-spacing: 0; color: var(--ink-soft); } @@ -7032,7 +6994,7 @@ body { .errors-page .kpi-card:hover { box-shadow: var(--card-shadow-hover); } .errors-page .kpi-label { font-family: var(--font-mono); - font-size: 0.7rem; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); @@ -7048,7 +7010,7 @@ body { .errors-page .kpi-card.kpi-error .kpi-value { color: var(--bucket-error-fg); } .errors-page .kpi-card.kpi-warn .kpi-value { color: var(--bucket-warn-fg); } .errors-page .kpi-card.kpi-info .kpi-value { color: var(--bucket-info-fg); } -.errors-page .kpi-sub { font-size: 0.78rem; color: var(--ink-muted); font-style: italic; } +.errors-page .kpi-sub { font-size: 0.75rem; color: var(--ink-muted); font-style: italic; } .errors-page .kpi-sub.up { color: var(--bucket-error-fg); font-style: normal; } .errors-page .kpi-sub.down { color: var(--bucket-info-fg); font-style: normal; } .errors-page .dropped-warn { color: var(--bucket-error-fg); font-weight: 600; font-style: normal; } @@ -7064,7 +7026,7 @@ body { .errors-page .volume-head { display: flex; justify-content: space-between; align-items: center; font-family: var(--font-mono); - font-size: 0.7rem; color: var(--ink-muted); + font-size: 0.6875rem; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px; } @@ -7090,7 +7052,7 @@ body { } .errors-page .seg-tabs button { background: transparent; border: none; - padding: 6px 14px; font-size: 0.85rem; cursor: pointer; + padding: 6px 14px; font-size: 0.875rem; cursor: pointer; color: var(--color); font-family: var(--font-sans); } .errors-page .seg-tabs button + button { border-left: 1px solid var(--border-color); } @@ -7101,23 +7063,23 @@ body { padding: 6px 10px 6px 32px; border: 1px solid var(--border-color); border-radius: 8px; - font-family: var(--font-mono); font-size: 0.82rem; + font-family: var(--font-mono); font-size: 0.8125rem; background: var(--input-bg); color: var(--input-color); } -.errors-page .errors-toolbar .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--ink-muted); font-size: 0.85rem; } -.errors-page .errors-toolbar .filter-count { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); font-size: 0.72rem; color: var(--ink-muted); } +.errors-page .errors-toolbar .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--ink-muted); font-size: 0.875rem; } +.errors-page .errors-toolbar .filter-count { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); font-size: 0.75rem; color: var(--ink-muted); } .errors-page .errors-toolbar .select-wrap { display: inline-flex; align-items: center; gap: 6px; } .errors-page .errors-toolbar .select-wrap label { font-family: var(--font-mono); - font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); margin: 0; } .errors-page .errors-toolbar .select-wrap select { background: var(--input-bg); color: var(--input-color); border: 1px solid var(--border-color); border-radius: 8px; } -.errors-page .errors-toolbar .autoref { display: inline-flex; gap: 6px; align-items: center; font-size: 0.82rem; color: var(--color); cursor: pointer; margin: 0; } +.errors-page .errors-toolbar .autoref { display: inline-flex; gap: 6px; align-items: center; font-size: 0.8125rem; color: var(--color); cursor: pointer; margin: 0; } .errors-page .btn-icon { width: 32px; padding: 4px 0; } .errors-page .errors-list { @@ -7138,7 +7100,7 @@ body { .errors-page .errors-list-head { padding: 12px 20px; align-items: center; } .errors-page .errors-list-head { font-family: var(--font-mono); - font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); border-bottom: 1px solid var(--border-color); background: var(--paper-bg-alt); @@ -7148,18 +7110,18 @@ body { .errors-page .errors-row.open { background: var(--paper-bg-alt); } .errors-page .errors-row-main { cursor: pointer; } .errors-page .errors-row-main:hover { background: var(--hover-bg-color); } -.errors-page .col-when .when-rel { font-size: 0.95rem; color: var(--color); font-variant-numeric: tabular-nums; } -.errors-page .col-when .when-abs { font-family: var(--font-mono); font-size: 0.78rem; color: var(--ink-muted); font-variant-numeric: tabular-nums; margin-top: 2px; } +.errors-page .col-when .when-rel { font-size: 0.9375rem; color: var(--color); font-variant-numeric: tabular-nums; } +.errors-page .col-when .when-abs { font-family: var(--font-mono); font-size: 0.75rem; color: var(--ink-muted); font-variant-numeric: tabular-nums; margin-top: 2px; } .errors-page .col-sev { display: flex; align-items: center; gap: 8px; } .errors-page .sev-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .errors-page .sev-dot.sev-error { background: var(--bucket-error-dot); } .errors-page .sev-dot.sev-warn { background: var(--bucket-warn-dot); } .errors-page .sev-dot.sev-info { background: var(--bucket-info-dot); } -.errors-page .sev-label { font-family: var(--font-mono); font-size: 0.74rem; letter-spacing: 0.08em; color: var(--color); } +.errors-page .sev-label { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.08em; color: var(--color); } .errors-page .pill-module { display: inline-block; - font-family: var(--font-mono); font-size: 0.78rem; + font-family: var(--font-mono); font-size: 0.75rem; background: var(--paper-bg-alt); color: var(--ink-soft); border: 1px solid var(--border-color); padding: 3px 10px; border-radius: 6px; @@ -7170,17 +7132,17 @@ body { color: var(--color); font-weight: 600; } -.errors-page .col-msg .msg-context { color: var(--ink-muted); font-style: italic; font-size: 0.88rem; margin-left: 10px; } +.errors-page .col-msg .msg-context { color: var(--ink-muted); font-style: italic; font-size: 0.875rem; margin-left: 10px; } .errors-page .col-msg .msg-detail { color: var(--ink-soft); font-family: var(--font-mono); - font-size: 0.82rem; + font-size: 0.8125rem; margin-left: 10px; } .errors-page .col-msg .msg-url { color: var(--ink-muted); font-family: var(--font-mono); - font-size: 0.8rem; + font-size: 0.8125rem; margin-top: 6px; word-break: break-all; } @@ -7189,7 +7151,7 @@ body { display: inline-block; padding: 2px 8px; border-radius: 4px; background: var(--primary-bg); color: var(--primary-color); - font-family: var(--font-mono); font-size: 0.78rem; font-variant-numeric: tabular-nums; + font-family: var(--font-mono); font-size: 0.75rem; font-variant-numeric: tabular-nums; } .errors-page .count-pill.count-pill-muted { background: transparent; color: var(--ink-muted); @@ -7198,7 +7160,7 @@ body { .errors-page .status-pill { display: inline-block; padding: 2px 10px; border-radius: 4px; - font-family: var(--font-mono); font-size: 0.8rem; font-variant-numeric: tabular-nums; + font-family: var(--font-mono); font-size: 0.8125rem; font-variant-numeric: tabular-nums; border: 1px solid transparent; } .errors-page .status-pill.status-error { background: var(--bucket-error-bg); color: var(--bucket-error-fg); border-color: var(--bucket-error-bd); } @@ -7210,7 +7172,7 @@ body { .errors-page .detail-tabs button { background: transparent; border: 0; border-bottom: 2px solid transparent; - padding: 8px 12px; font-size: 0.85rem; cursor: pointer; + padding: 8px 12px; font-size: 0.875rem; cursor: pointer; color: var(--ink-muted); font-family: var(--font-sans); } .errors-page .detail-tabs button.active { color: var(--color); border-bottom-color: var(--color); } @@ -7221,7 +7183,7 @@ body { border-radius: 12px; padding: 18px 22px; font-family: var(--font-mono); - font-size: 0.85rem; + font-size: 0.875rem; line-height: 1.7; color: var(--color); max-height: 26em; @@ -7251,11 +7213,11 @@ body { .errors-page .related-row { display: flex; gap: 10px; padding: 6px 12px; align-items: center; border-bottom: 1px solid var(--border-soft); - font-size: 0.8rem; + font-size: 0.8125rem; } .errors-page .related-row:last-child { border-bottom: none; } .errors-page .detail-actions { display: flex; gap: 6px; align-items: center; margin-top: 10px; } -.errors-page .copy-hint { font-size: 0.78rem; color: var(--bucket-info-fg); } +.errors-page .copy-hint { font-size: 0.75rem; color: var(--bucket-info-fg); } .errors-page .detail-aside { background: var(--paper-card); @@ -7267,20 +7229,20 @@ body { .errors-page .aside-block + .aside-block { margin-top: 16px; } .errors-page .aside-label { font-family: var(--font-mono); - font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); margin-bottom: 4px; } .errors-page .aside-value { font-family: var(--font-serif); - font-size: 1.05rem; + font-size: 1rem; color: var(--color); word-break: break-word; line-height: 1.3; } -.errors-page .aside-value.mono { font-family: var(--font-mono); font-size: 0.82rem; } -.errors-page .aside-sub { color: var(--ink-muted); font-style: italic; font-size: 0.8rem; font-family: var(--font-sans); } +.errors-page .aside-value.mono { font-family: var(--font-mono); font-size: 0.8125rem; } +.errors-page .aside-sub { color: var(--ink-muted); font-style: italic; font-size: 0.8125rem; font-family: var(--font-sans); } -.errors-page .errors-pager { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; color: var(--ink-muted); font-size: 0.85rem; } +.errors-page .errors-pager { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; color: var(--ink-muted); font-size: 0.875rem; } @media (max-width: 720px) { .errors-page .errors-list-head, @@ -7338,7 +7300,7 @@ body { .overview-page .ov-kpi-card:hover { box-shadow: var(--card-shadow-hover); } .overview-page .ov-kpi-label { font-family: var(--font-mono); - font-size: 0.68rem; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); @@ -7354,7 +7316,7 @@ body { .overview-page .ov-kpi-value.ov-val-warn { color: #B42318; } .dark-mode .overview-page .ov-kpi-value.ov-val-warn { color: #F08A82; } .overview-page .ov-kpi-sub { - font-size: 0.76rem; + font-size: 0.75rem; color: var(--ink-muted); margin-top: 2px; } @@ -7375,21 +7337,21 @@ body { } .overview-page .ov-daily-label { font-family: var(--font-mono); - font-size: 0.68rem; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); } .overview-page .ov-daily-value { font-family: var(--font-serif); - font-size: 2.35rem; + font-size: 2.5rem; font-weight: 400; line-height: 1; color: var(--accent, #1F6A64); margin-top: 6px; } .overview-page .ov-daily-sub { - font-size: 0.76rem; + font-size: 0.75rem; color: var(--ink-muted); margin-top: 5px; } @@ -7422,11 +7384,11 @@ body { } .overview-page .ov-chart-title { font-family: var(--font-serif); - font-size: 1.05rem; + font-size: 1rem; } .overview-page .ov-chart-legend { font-family: var(--font-mono); - font-size: 0.7rem; + font-size: 0.6875rem; color: var(--ink-muted); display: flex; align-items: center; @@ -7485,7 +7447,7 @@ body { background: var(--color); color: var(--canvas-bg-color); font-family: var(--font-mono); - font-size: 0.68rem; + font-size: 0.6875rem; line-height: 1; white-space: nowrap; opacity: 0; @@ -7514,7 +7476,7 @@ body { display: flex; justify-content: space-between; font-family: var(--font-mono); - font-size: 0.66rem; + font-size: 0.6875rem; color: var(--ink-muted); margin-top: 6px; } @@ -7541,11 +7503,11 @@ body { } .overview-page .ov-panel-title { font-family: var(--font-serif); - font-size: 1.05rem; + font-size: 1rem; } .overview-page .ov-panel-meta { font-family: var(--font-mono); - font-size: 0.68rem; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-muted); @@ -7554,7 +7516,7 @@ body { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-color); - font-size: 0.82rem; + font-size: 0.8125rem; } .overview-page .ov-panel-foot a { color: var(--link-color); text-decoration: none; } .overview-page .ov-panel-foot a:hover { text-decoration: underline; } @@ -7706,7 +7668,7 @@ body { display: flex; align-items: center; gap: 8px; - font-size: 0.82rem; + font-size: 0.8125rem; text-decoration: none; color: inherit; } @@ -7720,7 +7682,7 @@ body { .overview-page .ov-legend-n { margin-left: auto; font-family: var(--font-mono); - font-size: 0.8rem; + font-size: 0.8125rem; font-weight: 600; } @@ -7731,7 +7693,7 @@ body { width: 30px; flex-shrink: 0; font-family: var(--font-mono); - font-size: 0.72rem; + font-size: 0.75rem; text-transform: uppercase; color: var(--ink-muted); } @@ -7756,7 +7718,7 @@ body { width: 36px; text-align: right; font-family: var(--font-mono); - font-size: 0.82rem; + font-size: 0.8125rem; font-weight: 600; flex-shrink: 0; } @@ -7770,14 +7732,14 @@ body { border-bottom: 1px solid var(--border-soft, var(--border-color)); text-decoration: none; color: inherit; - font-size: 0.85rem; + font-size: 0.875rem; align-items: center; } .overview-page .ov-route-row:last-child { border-bottom: none; } .overview-page .ov-route-row:hover { background: var(--hover-bg-color); text-decoration: none; color: inherit; } .overview-page .ov-route-head { font-family: var(--font-mono); - font-size: 0.68rem; + font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-muted); @@ -7786,12 +7748,12 @@ body { .overview-page .ov-route-head:hover { background: transparent; } .overview-page .ov-route-name { font-family: var(--font-mono); - font-size: 0.84rem; + font-size: 0.8125rem; } .overview-page .ov-route-n { text-align: right; font-family: var(--font-mono); - font-size: 0.84rem; + font-size: 0.8125rem; } .overview-page .ov-route-lat { color: var(--ink-muted); } .overview-page .ov-n-bad { color: #B42318; font-weight: 600; } @@ -7834,15 +7796,15 @@ body { .overview-page .ov-svc-info { display: flex; flex-direction: column; gap: 1px; flex: 1; } .overview-page .ov-svc-name { font-family: var(--font-mono); - font-size: 0.88rem; + font-size: 0.875rem; font-weight: 600; } .overview-page .ov-svc-meta { - font-size: 0.76rem; + font-size: 0.75rem; color: var(--ink-muted); } .overview-page .ov-svc-detail { - font-size: 0.74rem; + font-size: 0.75rem; color: var(--ink-muted); white-space: nowrap; } @@ -7893,7 +7855,7 @@ body { .connection-top > i { color: var(--ink-muted); font-size: 24px; } .connection-top > .fa-key { font-size: 18px; } .connection-title { margin-bottom: 10px; } -.connections-page h2 { font-family: inherit; font-size: 21px; font-weight: 600; letter-spacing: -.5px; margin: 0; } +.connections-page h2 { font-family: inherit; font-size: 22px; font-weight: 600; letter-spacing: -.5px; margin: 0; } .connections-page p { font-size: 14px; line-height: 1.65; color: var(--ink-muted); } .connection-badge { background: var(--accent-soft); color: var(--accent); font-size: 11px; font-weight: 600; border-radius: 5px; padding: 4px 8px; white-space: nowrap; } .connection-badge-muted { background: var(--paper-bg-alt); color: var(--ink-muted); } @@ -7942,7 +7904,7 @@ body { .auth-intro .paper-page-title { font-size: clamp(48px, 5vw, 76px); line-height: 1.06; margin: 18px 0 24px; } .auth-intro .paper-page-lede { max-width: 330px; } .auth-options { display: grid; gap: 28px; } -.auth-options h2 { font-family: inherit; font-size: 21px; font-weight: 600; margin: 0 0 10px; } +.auth-options h2 { font-family: inherit; font-size: 22px; font-weight: 600; margin: 0 0 10px; } .auth-options p { font-size: 14px; line-height: 1.65; color: var(--ink-muted); } .auth-options .auth-existing { font-size: 12px; border-left: 2px solid var(--accent); padding-left: 14px; margin: 0; } .auth-permissions { padding: 6px 0 12px; list-style: none; font-size: 13px; line-height: 1.6; } @@ -8031,11 +7993,11 @@ body { .paper-settings .paper-settings-footer { padding: 0 0 20px; } .paper-settings .form-control { font-size: 14px; border-color: var(--border-color); border-radius: var(--radius-control); } .paper-settings .form-check { padding-top: 8px; padding-bottom: 8px; } -.paper-page .btn-ink, .anonymize-page .btn-ink { background: var(--accent); color: #fff; border-color: var(--accent); } -.dark-mode .paper-page .btn-ink, .dark-mode .anonymize-page .btn-ink { color: #1A1815; } -.paper-page .btn-ink:hover, .anonymize-page .btn-ink:hover { background: var(--accent); border-color: var(--accent); color: #fff; filter: brightness(.94); } -.dark-mode .paper-page .btn-ink:hover, .dark-mode .anonymize-page .btn-ink:hover { color: #1A1815; } -.paper-page .btn-ink:active, .anonymize-page .btn-ink:active { filter: brightness(.9); } +.paper-page .btn-ink, .anonymize-page .btn-ink, .explorer-page .btn-ink { background: var(--accent); color: #fff; border-color: var(--accent); } +.dark-mode .paper-page .btn-ink, .dark-mode .anonymize-page .btn-ink, .dark-mode .explorer-page .btn-ink { color: #1A1815; } +.paper-page .btn-ink:hover, .anonymize-page .btn-ink:hover, .explorer-page .btn-ink:hover { background: var(--accent); border-color: var(--accent); color: #fff; filter: brightness(.94); } +.dark-mode .paper-page .btn-ink:hover, .dark-mode .anonymize-page .btn-ink:hover, .dark-mode .explorer-page .btn-ink:hover { color: #1A1815; } +.paper-page .btn-ink:active, .anonymize-page .btn-ink:active, .explorer-page .btn-ink:active { filter: brightness(.9); } input[type="date"], select { color-scheme: light; } .dark-mode input[type="date"], .dark-mode select { color-scheme: dark; } .draft-status { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; padding: 10px 0; } @@ -8043,6 +8005,10 @@ input[type="date"], select { color-scheme: light; } .conference-feedback { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px; margin-top: 16px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: var(--radius-surface); width: 100%; } .paper-table { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: var(--radius-surface); margin-top: 12px; } +.dashboard-page .paper-table-dashboard { width: 100%; } +.paper-account-email { margin: 20px 0; } +.paper-account-email .form-control { max-width: 420px; } +.paper-settings .paper-account-email .btn[type="submit"] { min-height: 36px; padding: 7px 18px; white-space: nowrap; } .paper-table .paper-table-head { font-family: var(--font-sans); text-transform: none; letter-spacing: 0; font-size: 12px; background: var(--paper-bg-alt); border-radius: var(--radius-surface) var(--radius-surface) 0 0; } .paper-table .paper-table-head, .paper-table .paper-table-row { padding-left: 16px; padding-right: 16px; } @@ -8408,7 +8374,6 @@ input[type="date"], select { color-scheme: light; } .q-range-btns { border-radius: var(--radius-control); } .q-range-btns .btn, .q-job-detail-actions .btn { font-family: var(--font-sans); font-size: 13px; } .q-range-btns .btn.active { background: var(--accent-soft); color: var(--accent); } -.btn-dark { border-radius: var(--radius-control); font-family: var(--font-sans); font-size: 12px; } @media (max-width: 900px) { .paper-settings .quota-usage { grid-template-columns: 1fr; gap: 16px; } .paper-settings .quota-usage .quota-item + .quota-item { padding-left: 0; border-left: 0; border-top: 1px solid var(--border-color); padding-top: 16px; } @@ -8425,8 +8390,9 @@ input[type="date"], select { color-scheme: light; } } @media (max-width: 767px) { .paper-page, .paper-faq { padding-top: 20px; } - .paper-page .paper-page-title, .paper-page.admin-page .paper-page-title, .anonymize-page .paper-page-title { font-size: 24px; } - .paper-faq-title { font-size: 40px; } + .paper-settings .paper-account-email .btn[type="submit"] { min-height: 40px; padding: 9px 18px; } + .paper-page .paper-page-title, .paper-page.admin-page .paper-page-title, .anonymize-page .paper-page-title { font-size: 24px; } + .paper-faq-title { font-size: 40px; } .paper-page .form-control, .anonymize-page .form-control { min-height: 44px; } .paper-page .btn, .anonymize-page .btn, .paper-settings-toc nav a, .connection-link, .draft-discard, .paper-faq-toc a { min-height: 44px; } .paper-page .btn, .anonymize-page .btn, .status-bar-actions .btn { @@ -8464,6 +8430,159 @@ input[type="date"], select { color-scheme: light; } .q-card { min-width: 130px; } .q-header-actions { flex-wrap: wrap; gap: 8px; } } + +/* ===== UI polish: explorer, errors, page alignment ===== */ +/* One content column for every app page, aligned with settings and the FAQ. */ +/* The sign-in page keeps its own narrow width. */ +.paper-page.container:not(.auth-page) { max-width: 1180px; } +.paper-page > .row { margin-left: 0; margin-right: 0; } +.paper-page > .row > .w-100, .paper-page > .row > [class*='col'] { padding-left: 0; padding-right: 0; } +.anonymize-landing { align-items: flex-start; } +.status-page .paper-support-grid { margin-bottom: 0; } +/* Explorer: readable code and Markdown. */ +.explorer-content .ace_editor { font-family: var(--font-mono); line-height: 1.6; } +.dark-mode .explorer-content .ace-nord-dark, .dark-mode .explorer-content .ace-nord-dark .ace_gutter { background: var(--main-bg-color); } +.explorer-content .file-content.markdown-body { max-width: 960px; padding: 32px 40px 96px; } +.leftCol-project-header .project-name { white-space: normal; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +/* Error states inside the explorer use the same card as the status page. */ +.file-error { font-size: inherit; font-weight: inherit; text-align: left; } +.file-error-card { width: 100%; max-width: 520px; margin-top: 0; } +.file-error-msg { margin: 12px 0 0; font-size: 14px; line-height: 1.55; color: var(--ink-soft); } +.file-error-actions { display: flex; gap: 8px; margin-top: 16px; } +.file-error .paper-empty-title { font-size: 20px; text-align: center; } +@media (min-width: 992px) { + /* Keep the sidebar footer clear of the floating Ko-fi button. */ + .leftCol-foot { padding-bottom: 72px; } +} +.explorer-content .ace-chrome, .explorer-content .ace-chrome .ace_gutter { background: var(--main-bg-color); } +@media (max-width: 991px) { + .explorer-content .file-content.markdown-body { padding: 20px 16px 96px; } + /* Same 18px inset as every other page. */ + .anonymize-landing { padding: 20px 18px; } + .anonymize-landing-inner { padding: 0; } + /* Section links scroll in one row instead of wrapping under a lone underline. */ + .paper-settings-toc nav, .paper-faq-toc nav { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; } + .paper-settings-toc nav a, .paper-faq-toc nav a { flex-shrink: 0; white-space: nowrap; } + .paper-settings-body, .paper-faq-body { grid-template-columns: minmax(0, 1fr); } + /* The TOC stacks above the content at the same breakpoint as the grid. */ + .paper-settings-toc, .paper-faq-toc { position: static; } + .paper-settings-toc nav, .paper-faq-toc nav { display: flex; flex-direction: row; gap: 0 16px; } + .paper-settings-toc nav a, .paper-faq-toc nav a { display: inline-flex; align-items: center; padding: 8px 0; margin-left: 0; border-left: 0; border-bottom: 2px solid transparent; } + .paper-settings-toc nav a.active, .paper-settings-toc nav a:hover, .paper-faq-toc nav a.active, .paper-faq-toc nav a:hover { border-bottom-color: var(--accent); } + .paper-page .dashboard-filter-controls .dropdown .dropdown-toggle { justify-content: flex-start; gap: 6px; } + .paper-page .dashboard-filter-controls .dropdown .dropdown-toggle::after { margin-left: auto; } +} +@media (max-width: 480px) { + .paper-page .paper-page-lede { display: block; -webkit-line-clamp: unset; overflow: visible; } +} + +/* ===== Design refinements ===== */ +/* One primary colour: accent for actions and progress; amber/red stay for states. */ +.navbar .btn-signin, .navbar .btn-signin:hover { background: var(--accent); border-color: var(--accent); color: #fff !important; } +.dark-mode .navbar .btn-signin, .dark-mode .navbar .btn-signin:hover { color: #1A1815 !important; } +.navbar .btn-signin:hover { filter: brightness(.94); } +.paper-progress .paper-progress-bar, .paper-progress.paper-progress-ready .paper-progress-bar { background: var(--accent); } +.paper-progress .paper-progress-label { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; } +/* Mono is for IDs, hashes and paths; labels use the sans face. */ +.paper-error-eyebrow, .paper-support-card .paper-support-eyebrow { font-family: var(--font-sans); font-size: 12px; font-weight: 500; letter-spacing: 0; text-transform: none; } +.leftCol-foot .last-update { font-family: var(--font-sans); font-size: 11px; letter-spacing: 0; } +.paper-proof-venues span { font-family: var(--font-sans); font-size: 18px; font-weight: 600; letter-spacing: .02em; color: var(--ink-soft); } +/* Redacted terms, in rendered documents and in code. */ +.markdown-body mark.redaction { background: var(--accent-soft); color: var(--accent); border-radius: 3px; padding: 0 2px; } +.ace_marker-layer .ace_redaction { position: absolute; background: var(--accent-soft); border-radius: 3px; } +/* Explorer chrome. */ +.leftCol-project-header { flex-wrap: wrap; row-gap: 2px; } +.project-expiry { flex: 1 0 100%; display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--ink-muted); } +.project-expiry .ui-icon { width: 12px; height: 12px; } +.status-bar-actions .btn-icon-only { width: 32px; padding: 0; justify-content: center; } +/* Status page: the share link leads; support links sit flat inside their section. */ +.share-link-row { display: flex; gap: 8px; margin-top: 4px; } +.share-link-input { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: 14px; background: var(--paper-bg-alt); } +.share-link-row .btn { flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; min-width: 112px; justify-content: center; } +.share-link-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } +.share-link-actions .btn { display: inline-flex; align-items: center; gap: 6px; } +.status-page .paper-support-grid { gap: 0 32px; } +.status-page .paper-support-card { padding: 16px 0 0; border: 0; border-top: 1px solid var(--border-color); border-radius: 0; background: transparent; } +.status-page .paper-support-card:hover { border-color: var(--color); } +@media (min-width: 992px) { .paper-table .cell-expires { white-space: nowrap; } } +/* Settings: plain sections separated by rules rather than a stack of cards. */ +.paper-settings .paper-settings-main .paper-settings-section { background: transparent; border: 0; border-bottom: 1px solid var(--border-color); border-radius: 0; padding: 0 0 28px; margin-bottom: 28px; } +.paper-settings .paper-settings-main .paper-settings-section:last-of-type { border-bottom: 0; } +@media (max-width: 991px) { + .paper-settings .paper-settings-main .paper-settings-section { padding: 0 0 20px; margin-bottom: 20px; } + .share-link-row { flex-direction: column; } + .share-link-actions { flex-direction: column; } + .share-link-actions .btn { justify-content: center; } + .status-page .paper-support-grid { row-gap: 20px; } +} +/* Segmented control: one outlined group, borderless segments, tinted selection. */ +.paper-page .q-range-btns { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--border-color); border-radius: var(--radius-control); background: var(--paper-card); overflow: visible; } +.paper-page .q-range-btns .btn, .paper-page .q-range-btns .btn + .btn { min-height: 0; height: 28px; padding: 0 10px; border: 0; border-radius: 4px; background: transparent; color: var(--ink-muted); font: 500 13px/1 var(--font-sans); } +.paper-page .q-range-btns .btn:hover { background: var(--hover-bg-color); color: var(--color); } +.paper-page .q-range-btns .btn.active { background: var(--accent-soft); color: var(--accent); } +.q-header-actions > .btn { height: 34px; min-height: 34px; } +/* Destructive actions are outlined in red rather than filled. */ +.paper-page .btn.btn-danger-soft { background: transparent; border-color: var(--status-error); color: var(--status-error); } +.paper-page .btn.btn-danger-soft:hover { background: var(--status-error-soft-hover); border-color: var(--status-error); color: var(--status-error); } +.q-section-label, .q-state-chip, .q-auto-refresh { font-family: var(--font-sans); letter-spacing: 0; text-transform: none; } +.q-state-chip { font-size: 12px; text-transform: capitalize; } + +/* ===== Admin consistency ===== */ +/* The error bar fills are spans: without display:block their width is ignored. */ +.overview-page .ov-ebar-fill { display: block; } +/* Error rows mark themselves with an inset edge so their content stays aligned. */ +.paper-table .paper-table-row.repo-error { border-left: 0; box-shadow: inset 2px 0 0 var(--status-error); padding-left: 16px; } +/* Labels, headers and chips use the sans face like the rest of the app. */ +.admin-page th, .admin-page .q-table th, .errors-page .col-when, .errors-page .col-sev, .errors-page .col-mod, .errors-page .col-msg, .errors-page .col-count, .errors-page .col-status, +.overview-page .ov-ebar-label, .overview-page .ov-route-n, .overview-page .ov-route-name, .overview-page .ov-chart-legend, +.q-section-right, .q-legend-completed, .q-legend-failed, .q-legend-exec, .q-stat-label, .q-card-sub, .q-delay-hint, +.admin-page .type-badge, .user-detail-grid .detail-label, .admin-page .user-detail-section h2, .admin-page > h2, .admin-page section > h2 { + font-family: var(--font-sans); letter-spacing: 0; text-transform: none; +} +.admin-page th, .errors-page [class^="col-"] { font-size: 12px; font-weight: 500; color: var(--ink-muted); } +.q-stat-label, .q-card-sub, .user-detail-grid .detail-label { font-size: 12px; } +.q-stat-value { font-family: var(--font-sans); font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; } +.q-state-badge { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: capitalize; } +.admin-page .type-badge { font-size: 12px; text-transform: none; } +.errors-page .sev-label, .errors-page .col-sev span { text-transform: capitalize; } +/* One accent for overview numbers and charts; colour is kept for states. */ +.overview-page .ov-daily-value, .dark-mode .overview-page .ov-daily-value { color: var(--color); } +.overview-page .ov-spark-fill-alt, .overview-page .ov-spark-fill-user, .overview-page .ov-spark-fill-new-user, +.dark-mode .overview-page .ov-spark-fill-alt, .dark-mode .overview-page .ov-spark-fill-user, .dark-mode .overview-page .ov-spark-fill-new-user { background: var(--accent); } +.overview-page .ov-dot-ok-fill, .overview-page .ov-dot-user-fill, .overview-page .ov-dot-new-user-fill, +.dark-mode .overview-page .ov-dot-ok-fill, .dark-mode .overview-page .ov-dot-user-fill, .dark-mode .overview-page .ov-dot-new-user-fill { background: var(--accent); } +/* Errors filter uses the same segmented control as the queue range. */ +.errors-page .seg-tabs { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--border-color); border-radius: var(--radius-control); background: var(--paper-card); } +.errors-page .seg-tabs button, .errors-page .seg-tabs button + button { min-height: 0; height: 30px; padding: 0 10px; border: 0; border-radius: 4px; background: transparent; color: var(--ink-muted); font: 500 13px/1 var(--font-sans); } +.errors-page .seg-tabs button:hover { background: var(--hover-bg-color); color: var(--color); } +.errors-page .seg-tabs button.active { background: var(--accent-soft); color: var(--accent); } +/* User detail: status reads as metadata next to the name; actions are spaced. */ +.user-detail-card .user-header .status-dot-wrap { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 400; color: var(--ink-soft); } +.user-detail-card .user-actions { display: flex; flex-wrap: wrap; gap: 8px; } +/* Row metadata wraps instead of truncating every item to a few letters. */ +.admin-page .paper-table .cell-anon .anon-sub { flex-wrap: wrap; row-gap: 2px; } +.admin-page .paper-table .anon-sub > span, .admin-page .paper-table .anon-sub > a { flex: 0 1 auto; max-width: 100%; white-space: nowrap; } +.admin-page .paper-table .cell-views .mobile-meta-icon { margin-right: 4px; } +.q-jobs { overflow-x: auto; } +.q-job-detail-label { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; } +.admin-section-header h2 { font-family: var(--font-sans); font-size: 16px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--color); } +.errors-page .sev-label { font-family: var(--font-sans); font-size: 13px; letter-spacing: 0; } + +/* Email setup after sign-in. Native dialog traps focus and makes the page inert. */ +.email-prompt { + width: min(560px, calc(100vw - 32px)); + padding: 28px; + border: 1px solid var(--paper-border, #e5e1d9); + border-radius: 12px; + background: var(--paper-bg, #faf9f6); + color: inherit; + box-shadow: 0 16px 64px #0003; +} +.email-prompt::backdrop { background: #0006; } +.email-prompt h2 { margin: 0 0 12px; font-size: 24px; } +.email-prompt p { margin-bottom: 20px; } +.email-prompt-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-top: 24px; } +body.dark-mode .email-prompt { background: #242320; color: #faf9f6; } .file-error .viewer-error { width: 100%; max-width: 65ch; @@ -8517,6 +8636,295 @@ body.dark-mode .markdown-body .hljs-comment { color: #b1bac4; } body:not(.dark-mode) .markdown-body .gh-dark-mode-only, body.dark-mode .markdown-body .gh-light-mode-only { display: none; } +/* Billing follows the settings page's typography and section rules. */ +.billing-plan { padding: 1.5rem 0; border-bottom: 1px solid var(--paper-border, #d8d4cd); } +.billing-plan h3 { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; justify-content: space-between; } +.billing-price { font-variant-numeric: tabular-nums; font-weight: normal; } +.billing-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1rem; } +#settings-billing p { max-width: 72ch; } + +/* Task-local hosting choices inherit the repository form's paper palette. */ +.repository-hosting { border-top: 1px solid var(--border-color); margin-top: 24px; } +.repository-hosting h2 { font-size: 1.5rem; } +.hosting-choices { border: 0; padding: 0; margin: 20px 0; min-width: 0; } +.hosting-choices legend { font-size: 1rem; font-weight: 600; } +.hosting-choice { display: flex; align-items: flex-start; gap: 12px; padding: 16px; border: 1px solid var(--border-color); background: var(--paper-card); cursor: pointer; } +.hosting-choice + .hosting-choice { border-top: 0; } +.hosting-choice input { margin-top: 5px; accent-color: var(--accent); } +.hosting-choice strong, .hosting-choice small { display: block; } +.hosting-choice small { margin-top: 6px; color: var(--ink-muted); } +.hosting-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin: 20px 0; } +.repository-pass { margin: 20px 0; } +.repository-pass summary { cursor: pointer; color: var(--link-color); } +.repository-pass p { margin-top: 12px; } + +/* Approved task-based payment flow: review content and a persistent summary. */ +.task-review-active.anonymize-page { height: auto !important; min-height: 100%; overflow: visible; } +.task-review-active .anonymize-workspace { flex: 0 0 auto; overflow: visible; } +.repository-task { width: 100%; max-width: 1020px; margin: 0 auto; padding: 32px 24px 42px; color: var(--color); font-size: 15px; line-height: 1.55; } +.repository-task .paper-crumbs { margin-bottom: 24px; } +.repository-task h1 { font: normal 40px/1.15 'Iowan Old Style', Georgia, serif; margin: 8px 0 10px; } +.repository-task h2 { font: normal 27px/1.25 'Iowan Old Style', Georgia, serif; margin: 0 0 12px; } +.repository-task h3 { font: 600 17px/1.5 var(--font-sans); margin: 0 0 10px; } +.repository-task p { margin: 0 0 16px; } +.task-lede, .task-muted { color: var(--ink-muted); } +.task-trail { list-style: none; display: flex; flex-wrap: wrap; gap: 24px; margin: 30px 0; padding: 18px 0; border-bottom: 1px solid var(--border-color); color: var(--ink-muted); font-size: 13px; } +.task-trail [aria-current] { color: var(--color); font-weight: 600; } +.task-trail button { color: inherit; padding: 0; border: 0; background: none; font: inherit; cursor: pointer; } +.task-columns { display: grid; grid-template-columns: minmax(0,1.6fr) minmax(245px,1fr); gap: 34px; align-items: start; } +.task-section-title { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } +.task-text-button { border: 0; padding: 7px 0; background: transparent; color: var(--link-color); cursor: pointer; font: inherit; text-align: left; } +.task-text-button:focus-visible, .task-trail button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; } +.task-summary { background: var(--paper-bg-alt); padding: 24px; border-radius: 5px; } +.task-summary dt { margin-top: 18px; font-size: 12px; font-weight: 400; color: var(--ink-muted); } +.task-summary dd { margin: 3px 0; overflow-wrap: anywhere; } +.task-summary h3, .task-redactions { overflow-wrap: anywhere; } +.task-summary dl { margin-bottom: 0; } +.task-summary-foot { border-top: 1px solid var(--border-color); margin-top: 28px; padding-top: 24px; } +.task-summary-foot p { color: var(--ink-muted); margin: 0; } +.task-hosting { border-top: 1px solid var(--border-color); margin-top: 22px; padding-top: 20px; } +.repository-task .hosting-choices { margin: 18px 0; } +.repository-task .hosting-choice { padding: 18px; gap: 13px; font-weight: 400; margin: 0; } +.repository-task .hosting-choice:first-of-type { border-radius: 6px 6px 0 0; } +.repository-task .hosting-choice:last-of-type { border-radius: 0 0 6px 6px; } +.repository-task .hosting-choice small { margin-top: 0; font-size: 13px; line-height: 1.55; } +.task-terms { font-size: 13px; color: var(--ink-soft); } +.task-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; margin: 24px 0 16px; } +.repository-task .btn { padding: 10px 16px; border-radius: 5px; font-size: 15px; } +.task-status { background: var(--accent-soft); padding: 16px 20px; margin: 20px 0; } +.task-status p:last-child { margin: 0; } +@media(max-width:650px) { + .repository-task { padding: 24px 18px; } + .repository-task h1 { font-size: 32px; } + .task-columns { display: block; } + .task-summary { margin-top: 28px; } + .task-trail { gap: 15px; font-size: 12px; } +} +.repository-task { scroll-margin-top: 70px; } +.task-columns { align-items: stretch; } +.repository-task .btn-ink { background: var(--color); border-color: var(--color); color: var(--canvas-bg-color); box-shadow: none; } +.repository-task .btn-ink:hover { background: var(--ink-soft); border-color: var(--ink-soft); } + +.task-field { margin-bottom: 24px; } +.task-field > label { display: block; font-weight: 600; margin-bottom: 8px; } +.task-field input, .task-field select, .task-field textarea { width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--paper-card); color: var(--color); font: inherit; } +.task-field textarea { resize: vertical; } +.task-field small { display: block; margin-top: 8px; font-size: 13px; color: var(--ink-muted); } +.task-field :is(input, select, textarea):focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; } + +.task-paybox { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 5px; padding: 22px; margin-top: 20px; } + +.task-ready { border-left: 3px solid var(--success, #2f7a44); padding: 16px 20px; background: var(--paper-bg-alt); margin-bottom: 22px; } +.repository-task.status-page .paper-progress { margin-top: 24px; } + +.task-date-options summary { cursor: pointer; color: var(--link-color); font-size: 13px; } +.task-date-options[open] summary { margin-bottom: 12px; } + +.task-result { padding: 18px; background: var(--paper-bg-alt); overflow-wrap: anywhere; } +.task-result .detail-label { font-size: 12px; color: var(--ink-muted); margin-bottom: 6px; } + +/* Step one keeps redaction feedback beside the fields being edited. */ +.task-configure-columns { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; } +.task-live-preview { min-width: 0; border: 1px solid var(--border-color); border-radius: 5px; background: var(--paper-card); } +.task-preview-head { padding: 20px 24px; border-bottom: 1px solid var(--border-color); } +.task-preview-head h2 { font-size: 23px; margin-bottom: 6px; } +.task-preview-head p { margin: 0; font-size: 13px; } +.task-preview-message, .task-preview-content { padding: 24px; } +.task-preview-content { overflow: auto; max-height: 650px; overflow-wrap: anywhere; } +.repository-task .task-preview-content h1 { font: 600 28px/1.3 var(--font-sans); } +.repository-task .task-preview-content h2 { font: 600 22px/1.3 var(--font-sans); } +@media(max-width:650px) { + .task-live-preview { margin-top: 28px; } + .task-preview-content { max-height: none; } +} + +/* Anonymization workspace: visible configuration and contextual hosting (mock v8). */ +.anonymize-page .anonymize-topbar { padding: 14px 22px; background: var(--canvas-bg-color); } +.anonymize-page .anonymize-topbar-inner { max-width: none; } +.anonymize-page .anonymize-topbar-head { justify-content: space-between; } +.anonymize-page .anonymize-topbar-title { font: 600 19px/1.25 var(--font-sans); margin: 0; } +.anonymize-page .anonymize-split { display: grid; grid-template-columns: minmax(320px,42%) minmax(0,1fr); overflow: hidden; } +.anonymize-page .anonymize-form-col { width: auto; min-height: 0; overflow: hidden !important; background: var(--paper-card); } +.anonymize-page .anonymize-form-col .paper-settings-main { display: flex; flex-direction: column; height: 100%; min-height: 0; max-width: none; padding: 0; } +.anonymize-fields { flex: 1; min-height: 0; overflow: auto; position: relative; scrollbar-width: thin; } +.anonymize-page .paper-settings-main .paper-settings-section { border: 0; border-top: 1px solid var(--border-color); border-radius: 0; padding: 18px 20px 20px; margin: 0; box-shadow: none; } +.anonymize-page .paper-settings-main .paper-settings-section:first-child { border-top: 0; padding-top: 18px; } +.anonymize-page .paper-section-eyebrow { font: 600 15px/1.4 var(--font-sans); margin: 0 0 16px; letter-spacing: 0; } +.anonymize-page .paper-field-label { font: 500 13px/1.5 var(--font-sans); margin-bottom: 8px; } +.anonymize-page .form-group { margin-bottom: 16px; } +.anonymize-page .form-group:last-child { margin-bottom: 0; } +.anonymize-page .form-control { font-size: 14px; min-height: 40px; padding: 9px 10px; border-radius: 4px; } +.anonymize-page textarea.form-control { line-height: 1.6; } +.anonymize-page .form-grid-2 { gap: 12px; } +.anonymize-page .form-check { padding-top: 10px; padding-bottom: 10px; font-size: 14px; } +.anonymize-page .form-check-label { font-size: 14px; line-height: 1.5; } +.anonymize-page .form-text { font-size: 12px; line-height: 1.6; } +.anonymize-page .repo-access-details { margin: 12px 0 16px; } +.anonymize-page .repo-access-note { font-size: 11px; } +.anonymize-page .anonymize-submit-bar { flex: none; position: static; margin: 0; padding: 16px 20px; background: var(--paper-card); border-top: 1px solid var(--border-color); } +.anonymize-page .anonymize-submit-bar .btn { width: 100%; font-size: 14px; padding: 10px 12px; min-height: 43px; } +.checkout-return-note { font-size: 11px; line-height: 1.5; color: var(--ink-muted); margin: 8px 0 0; text-align: center; } +.anonymize-fields > .alert, .anonymize-fields > .draft-status, .anonymize-fields > .anonymize-review-note { margin: 12px 20px; } +.anonymize-page .anonymize-preview-col { min-height: 0; padding: 20px; position: relative; background: var(--canvas-bg-color); } +.anonymize-page .anonymize-preview-head { border: 0; padding: 0; min-height: 32px; margin-bottom: 14px; display: flex; align-items: center; justify-content: space-between; } +.anonymize-preview-head strong, .anonymize-preview-head button { font-size: 13px; } +.anonymize-page .anonymize-preview-body { padding: 24px; border-radius: 6px; min-height: 480px; overflow-wrap: anywhere; font: 14px/1.7 var(--font-sans); } +.anonymize-page .anonymize-preview-body h1 { font-size: 27px; line-height: 1.2; margin: 0 0 20px; padding-bottom: 14px; } +.anonymize-page .anonymize-preview-body h2 { font-size: 18px; margin: 28px 0 12px; } +.anonymize-page .anonymize-preview-body pre { font-size: 12px; padding: 15px; } +.anonymize-page.preview-expanded .anonymize-split { grid-template-columns: 1fr; } +.anonymize-page.preview-expanded .anonymize-form-col { display: none; } +.hosting-usage { font-size: 12px; color: var(--ink-muted); margin: 0 0 4px; } +.expiration-choices { border: 0; padding: 0; margin: 0 0 16px; min-width: 0; } +.expiration-choice { display: flex; align-items: flex-start; gap: 10px; padding: 12px 0; margin: 0; cursor: pointer; font-size: 14px; font-weight: 500; line-height: 1.5; } +.expiration-choice input { width: 16px; height: 16px; margin: 4px 0 0; flex: none; accent-color: var(--accent); } +.expiration-choice small { display: block; font-size: 12px; font-weight: 400; color: var(--ink-muted); margin-top: 2px; } +.dated-options { padding: 2px 0 14px 26px; } +.hosting-property, .hosting-date { display: grid; grid-template-columns: 48px minmax(0,1fr); gap: 8px; align-items: center; margin-bottom: 10px; } +.hosting-property { margin-bottom: 0; } +.hosting-property label, .hosting-date .paper-field-label { font-size: 12px; font-weight: 400; margin: 0; } +.hosting-date .invalid-feedback { grid-column: 1/-1; } +.anonymize-page .conference-field { border-top: 1px solid var(--border-color); padding: 16px 0; margin: 0; } +.subscription-choice { padding: 16px; background: var(--paper-bg-alt); border-radius: 6px; margin-top: 4px; } +.subscription-choice h3 { font-size: 13px; font-weight: 600; margin: 0 0 10px; } +.billing-cycle { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px; border: 0; padding: 0; margin: 0 0 12px; min-width: 0; } +.cycle-option { display: flex; align-items: center; gap: 7px; padding: 10px 8px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--paper-card); font-size: 12px; cursor: pointer; margin: 0; } +.cycle-option.selected { border-color: var(--accent); background: var(--accent-soft); } +.cycle-option input { accent-color: var(--accent); margin: 0; flex: none; } +.subscription-choice p { font-size: 12px; line-height: 1.5; margin: 0 0 8px; } +.subscription-choice p:last-child { margin: 0; } +.hosting-terms { font-size: 12px; line-height: 1.5; color: var(--ink-muted); } +.capacity-reason { font-size: 13px; } +.capacity-options, .repository-pass { margin-top: 12px; font-size: 12px; } +.capacity-options summary, .repository-pass summary { cursor: pointer; } +.capacity-options p, .repository-pass p { margin: 8px 0; } +.capacity-options .task-text-button { display: block; } +@media(max-width:767px) { + .anonymize-page .anonymize-split { display: block; overflow: visible; } + .anonymize-page .anonymize-form-col .paper-settings-main { height: auto; } + .anonymize-fields { overflow: visible; } + .anonymize-page .paper-settings-main .paper-settings-section { padding: 18px 16px 20px; } + .anonymize-page .anonymize-topbar { padding: 14px 16px; } + .anonymize-page .anonymize-preview-col { padding: 16px; } + .anonymize-page .anonymize-preview-body { padding: 20px; } + .anonymize-page .anonymize-submit-bar { padding: 16px; } + .anonymize-page .preview-expand { display: none; } + .anonymize-page .anonymize-mobile-tabs { padding: 0; gap: 0; background: var(--paper-card); } + .anonymize-mobile-tabs button { border-radius: 0; padding: 12px; min-height: 44px; } + .anonymize-mobile-tabs button[aria-selected=true] { background: none; box-shadow: inset 0 -2px var(--accent); } + .cycle-option { min-height: 44px; } +} +/* Match the mock's typography and document rendering without changing the app shell. */ +.anonymize-page { --font-sans: system-ui, -apple-system, sans-serif; font-family: var(--font-sans); } +body:not(.dark-mode) .anonymize-page { --paper-card: #fff; --canvas-bg-color: #faf9f6; --paper-bg-alt: #f5f4f0; --border-color: #dedbd4; --color: #242320; --ink-muted: #68655e; } +.anonymize-page .repo-access-controls .btn { font-size: 12px; padding: 8px 12px; min-height: 36px; } +.anonymize-page .form-check-label { font-weight: 400; } +.anonymize-page .form-check { padding-left: 26px; } +.anonymize-page .form-check input { width: 16px; height: 16px; margin-left: -26px; accent-color: var(--accent); } +.anonymize-page .form-check small { margin-top: 6px; } +.anonymize-page .btn.btn-icon.refresh-branches { display: inline-flex; align-items: center; justify-content: center; width: 40px; min-width: 40px; height: 100%; min-height: 40px; padding: 0; } +@media (max-width: 767px) { + .anonymize-page #branch, .anonymize-page #commit { min-height: 44px; } + .anonymize-page .btn.btn-icon.refresh-branches { width: 44px; min-width: 44px; min-height: 44px; } +} +.anonymize-page .redaction-syntax { font-size: 12px; margin-top: 8px; } +.redaction-syntax summary { cursor: pointer; color: var(--link-color); min-height: 28px; } +.anonymize-page .anonymize-preview-body h2 { border: 0; padding: 0; font-weight: 700; } +.anonymize-page .anonymize-preview-body h1 { font-weight: 700; } +.anonymize-page .anonymize-preview-body pre { background: var(--paper-bg-alt); line-height: 1.7; } +.anonymize-page .anonymize-preview-body pre code { font: 12px/1.7 ui-monospace, monospace; background: none; padding: 0; } + +.anonymize-page .anonymize-preview-body { --main-bg-color: var(--paper-card); background: var(--paper-card); } +.anonymize-page .anonymize-submit-bar .btn { font-weight: 600; } +.anonymize-page .form-grid-2 .form-group { margin-bottom: 0; } +.anonymize-page .form-check small { margin-left: -26px; } +/* User settings: one sheet, with labels aligned beside their controls. */ +.user-settings { max-width: 1060px; padding: 28px 26px 40px; } +.user-settings .settings-title { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 22px; } +.user-settings .settings-title h1 { font: 650 26px/1.3 var(--font-sans); letter-spacing: -.025em; margin: 0; } +.settings-title > span { color: var(--ink-muted); font-size: 12px; } +.settings-nav { display: flex; gap: 28px; border-bottom: 1px solid var(--border-color); } +.settings-nav button { border: 0; border-radius: 0; background: transparent; color: var(--ink-muted); padding: 12px 0; min-height: 44px; font: 13px/1.4 var(--font-sans); } +.settings-nav button[aria-current=page] { color: var(--color); font-weight: 600; box-shadow: inset 0 -2px var(--accent); } +.settings-nav button:hover { color: var(--accent); } +.user-settings .settings-scope { color: var(--ink-muted); font-size: 12px; margin: 20px 0 4px; } +.user-settings .settings-row { display: grid; grid-template-columns: 210px minmax(0,1fr); gap: 32px; padding: 22px 0; border-bottom: 1px solid var(--border-color); } +.user-settings .settings-row > h2, .user-settings .settings-label { font-size: 13px; font-weight: 600; line-height: 1.5; margin: 8px 0 0; display: block; } +.user-settings .settings-help { font-size: 12px; color: var(--ink-muted); margin: 7px 0 0; line-height: 1.6; } +.user-settings .form-control { font-size: 14px; min-height: 40px; padding: 9px 10px; border-radius: 4px; background: var(--paper-card); } +.user-settings textarea.form-control { line-height: 1.6; } +.settings-syntax { font-size: 12px; margin-top: 8px; } +.settings-syntax summary { cursor: pointer; color: var(--link-color); min-height: 26px; } +.settings-syntax p { margin: 8px 0; } +.settings-checks { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; padding: 4px 0; margin-bottom: 12px; } +.user-settings .settings-check { display: flex; gap: 9px; align-items: center; font-size: 13px; font-weight: 400; margin: 0; } +.user-settings .settings-check .form-check-input { position: static; width: 16px; height: 16px; margin: 0; flex: none; accent-color: var(--accent); } +.settings-save { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 18px 0 22px; } +.settings-save-status { color: var(--ink-muted); font-size: 12px; } +.settings-save #save { margin-left: auto; min-width: 125px; } +.settings-save-error { color: var(--status-error); flex-basis: 100%; } +.user-settings .settings-inline { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } +.user-settings .btn { font-size: 13px; min-height: 40px; padding: 9px 13px; } +.settings-inline .btn { flex-shrink: 0; } +.user-settings #settings-quota { margin: 22px 0 16px; } +.user-settings .quota-row { gap: 20px; } +.user-settings .quota-item { padding: 0; background: transparent; border: 0; } +.user-settings .quota-label { font-size: 12px; } +.user-settings .quota-value { font-size: 13px; margin: 6px 0; } +.user-settings .quota-value strong { font-size: 14px; } +.user-settings .quota-percent, .user-settings .quota-remaining { font-size: 11px; } +.user-settings .settings-plan { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: 6px; padding: 20px; margin: 20px 0 0; } +.user-settings .settings-plan h3 { font-size: 15px; margin: 0 0 10px; } +.user-settings .settings-plan > p { font-size: 13px; } +.settings-cycles { display: flex; flex-wrap: wrap; gap: 10px; border: 0; padding: 0; margin: 16px 0; } +.settings-cycles label { display: flex; align-items: center; gap: 8px; border: 1px solid var(--border-color); background: var(--paper-card); border-radius: 5px; padding: 12px; font-size: 13px; margin: 0; cursor: pointer; flex: 1; } +.settings-cycles label.selected { border-color: var(--accent); background: var(--accent-soft); } +.settings-cycles input { accent-color: var(--accent); } +.settings-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; } +.settings-notice { background: var(--paper-bg-alt); border: 1px solid var(--border-color); border-radius: 5px; padding: 14px 16px; margin: 16px 0; font-size: 13px; } +.settings-notice p { margin: 6px 0 12px; } +.settings-text-button { border: 0; background: transparent; color: var(--link-color); padding: 8px 0; font-size: 12px; } +.settings-pass { margin-top: 18px; } +.settings-pass label { display: block; font-size: 13px; margin-bottom: 8px; } +.settings-pass .btn { margin-top: 12px; } +.settings-purchase { padding: 12px 0; border-bottom: 1px solid var(--border-color); font-size: 13px; overflow-wrap: anywhere; } +.settings-purchase:last-child { border: 0; } +.settings-purchase > span { display: block; color: var(--ink-muted); margin-top: 4px; } +.user-settings .paper-account-card { padding: 24px 0; border: 0; background: none; } +.user-settings .paper-danger-zone { margin-top: 24px; } +@media (max-width: 767px) { + .user-settings { padding: 22px 18px 90px; } + .user-settings .settings-row { grid-template-columns: 1fr; gap: 12px; padding: 20px 0; } + .user-settings .settings-row > h2, .user-settings .settings-label { margin-top: 0; } + .settings-nav { gap: 18px; justify-content: space-between; } + .settings-nav button { text-align: left; font-size: 12px; max-width: 124px; } + .user-settings .settings-check { font-size: 12px; min-height: 28px; } + .settings-save { gap: 8px; } + .settings-save-status { font-size: 11px; } + .settings-save #save { min-width: 120px; } + .user-settings .quota-row { grid-template-columns: 1fr; } + .settings-cycles label { flex-basis: 100%; } +} +.user-settings .settings-save .draft-status { margin: 0; padding: 0; } +.settings-delete-confirm { max-width: 480px; margin-top: 18px; } +.settings-delete-confirm label { display: block; font-size: 13px; margin-bottom: 8px; } +.user-settings .quota-usage { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); } +.user-settings .quota-usage .quota-item + .quota-item { border: 0; padding: 0; } +.user-settings .quota-percent, .user-settings .quota-remaining { display: none; } +@media (max-width: 767px) { .user-settings .quota-usage { grid-template-columns: 1fr; } } +.settings-header-actions { display: flex; align-items: center; gap: 16px; } +.settings-header-actions > span { color: var(--ink-muted); font-size: 12px; } +.user-settings .settings-upgrade { color: var(--accent); border-color: var(--accent); background: transparent; } +.user-settings .settings-comparison { width: 100%; table-layout: fixed; margin: 16px 0 20px; font-size: 12px; } +.settings-comparison th, .settings-comparison td { padding: 10px 8px; border-bottom: 1px solid var(--border-color); text-align: left; overflow-wrap: anywhere; vertical-align: top; } +.settings-comparison th:first-child { width: 42%; padding-left: 0; } +.settings-comparison tbody th { font-weight: 400; } +.settings-comparison thead th { font-weight: 600; } +@media(max-width: 580px) { .settings-header-actions > span { display: none; } .settings-header-actions { gap: 8px; } .user-settings .settings-comparison { font-size: 11px; } } +.navbar .nav-link.nav-upgrade { border: 1px solid var(--accent); border-radius: 5px; color: var(--accent) !important; padding: 7px 12px !important; margin-right: 12px; font-size: 12px; line-height: 1.5; } +.navbar .nav-link.nav-upgrade:hover { background: var(--accent-soft); } +@media(max-width:991px) { .navbar .nav-link.nav-upgrade { display: inline-block; min-height: 44px; padding: 11px 14px !important; margin: 8px 0; } } + @media (max-width: 600px) { .ov-cache .ov-performance-window { @@ -11685,3 +12093,4 @@ input::placeholder, textarea::placeholder, .form-control::placeholder, #sourceUr @media (min-width: 1100px) { .paper-faq-help { display: block !important; } } .q-throughput { position: relative; } .q-throughput .q-range-btns { position: absolute !important; top: 12px; right: 16px; float: none !important; margin: 0 !important; width: auto !important; display: inline-flex !important; z-index: 2; } +.anonymize-submit-bar .draft-status { order: -1; } diff --git a/public/i18n/locale-en.json b/public/i18n/locale-en.json index a470e96..b7c09b8 100644 --- a/public/i18n/locale-en.json +++ b/public/i18n/locale-en.json @@ -72,9 +72,12 @@ "invalid_request": "The request is missing required fields or is malformed.", "invalid_project_name": "Use a project name of up to 100 characters without line breaks or control characters.", "no_file_selected": "Please select a file.", + "submodule_not_available": "This public submodule file is no longer available at its recorded commit.", "file_not_found": "The requested file is not found.", "file_not_accessible": "The requested file is not accessible.", "file_not_supported": "The file type is not supported. Anonymous GitHub cannot handle it.", + "repository_tree_too_large": "The repository has too many pending folders to process safely. Share a smaller repository.", + "file_listing_too_large": "This folder has too many entries to list. Open a more specific folder or use file search.", "file_too_big": "The file size exceeds the limit of Anonymous GitHub.", "is_folder": "The path points to a folder.", "folder_not_supported": "The path points to a folder. Please select a file.", @@ -117,6 +120,15 @@ "gistId_not_specified": "A gist ID must be provided.", "gistId_already_used": "The gist ID is already used.", "missing_token": "An authentication token is required.", + "token_attempts_rate_limited": "Too many token authentication attempts. Wait before trying again.", + "refresh_rate_limited": "A refresh was already requested for this repository. Wait one minute before trying again.", + "session_required": "Sign in through your browser to manage maintenance tokens.", + "invalid_token_options": "Provide a token name, 1 to 20 repository IDs, and an expiration of 1 to 90 days.", + "token_limit_reached": "You have reached the limit of 50 API tokens. Revoke an unused token before creating another.", + "invalid_token_id": "The token ID is invalid.", + "invalid_settings": "Provide supported settings with boolean values, or a list of anonymization terms.", + "token_scope_forbidden": "This token does not allow this operation or repository.", + "authentication_unavailable": "Authentication is temporarily unavailable. Try again later.", "invalid_token": "The provided authentication token is invalid.", "login_failed": "Login failed. Please try again.", "server_error": "An unexpected server error occurred. Please try again later.", @@ -148,14 +160,23 @@ "github_grant_revocation_failed": "Unable to revoke the GitHub grant. Please retry.", "another_login_required": "Connect and verify another sign-in method before disconnecting OAuth.", "oauth_resources_remaining": "Some resources still require OAuth. Migrate or remove them before disconnecting.", - "invalid_github_path": "The GitHub repository path is invalid." + "invalid_github_path": "The GitHub repository path is invalid.", + "repository_tree_incomplete": "The repository file tree is incomplete. Reduce its size and refresh before downloading.", + "billing_environment_mismatch": "The payment event does not match this billing environment.", + "billing_unavailable": "Payments are not available. Please try again later.", + "invalid_billing_signature": "The payment notification could not be verified.", + "billing_webhook_failed": "The payment notification could not be processed.", + "billing_customer_not_found": "No billing account was found.", + "subscription_required_for_never_expiration": "Never-expiring repositories require the owner’s active monthly or annual author subscription. Choose a dated expiration or upgrade in Plans and billing.", + "invalid_billing_expiration": "Choose an expiration date within the next year, or select Never expire with an author subscription.", + "pending_payment": "Awaiting payment" }, "WARNINGS": { "page_not_enabled_on_repo": "GitHub Pages is not enabled on this repository. Enable it in the repository's Settings → Pages on GitHub, then refresh.", "page_branch_mismatch": "GitHub Pages on this repository is served from the '{{pageBranch}}' branch, but you selected '{{selectedBranch}}'. Switch the branch above to '{{pageBranch}}' to anonymize the Pages site.", "folder_truncated": "This folder has more than 10,000 entries; only a partial listing is shown.", "repo_truncated": "Some folders in this repository have too many files to be fully listed. Affected folders are marked with a warning icon.", - "submodules_not_included": "This repository uses git submodules. Submodule contents are not included in the anonymized repository and appear as empty folders." + "submodules_not_included": "Some submodule contents could not be included. Only direct public GitHub submodules within the size limits are supported. Private, external, nested, or unavailable submodules appear as empty folders." }, "VIEWER_LOADING": { "loading": "Loading file…", diff --git a/public/index.html b/public/index.html index 0c711b0..df53416 100644 --- a/public/index.html +++ b/public/index.html @@ -9,7 +9,6 @@ /> Anonymous Github -
- {{ fmt?.uppercase(row?._bucket) }} + {{ fmt?.title(row?._bucket) }}
{{ row?.module }}
diff --git a/public/partials/admin/overview.htm b/public/partials/admin/overview.htm index 2dbc9cf..093a1e3 100644 --- a/public/partials/admin/overview.htm +++ b/public/partials/admin/overview.htm @@ -111,11 +111,11 @@
Users · 30d - total users · peak {{ fmt?.number(historyMax('nbUsers')) }} + total users · {{ fmt?.number(historyRange('nbUsers').min) }}–{{ fmt?.number(historyRange('nbUsers').max) }}
- +
@@ -365,7 +365,7 @@ RouteRequestsAverageFirst byte p95p95p99Interrupted5xxSlow {{ row.route }}{{ row.service }} · {{ row.method }} - {{ fmt.number(row.count) }}{{ row.avgMs }} ms + {{ fmt.number(row.count) }}{{ formatMs(row.avgMs) }} {{ firstByteBound(row) }}{{ latencyBound(row, 'p95UpperMs') }}{{ latencyBound(row, 'p99UpperMs') }} {{ fmt.number(row.aborted) }}{{ fmt.number(row.errors) }}{{ fmt.number(row.slow) }} @@ -397,7 +397,7 @@ Work stages and cache results Work stageSamplesAveragep95 - {{ row.metric }}{{ row.service }}{{ fmt.number(row.count) }}{{ row.avgMs }} ms{{ latencyBound(row, 'p95UpperMs') }} + {{ row.metric }}{{ row.service }}{{ fmt.number(row.count) }}{{ formatMs(row.avgMs) }}{{ latencyBound(row, 'p95UpperMs') }}
diff --git a/public/partials/admin/queues.htm b/public/partials/admin/queues.htm index 646fd12..0a0b082 100644 --- a/public/partials/admin/queues.htm +++ b/public/partials/admin/queues.htm @@ -157,52 +157,52 @@
- JOB ID + Job id
- STATE + State
- REPO ID + Repo ID
- CREATED + Created
- RETRY AT + Retry at {{ humanTime(job?.delayUntil) }} ({{ delayCountdown(job?.delayUntil) }})
- PROCESSED + Processed
- FINISHED + Finished
- ATTEMPTS + Attempts
- STATUS + Status
- PROGRESS + Progress
- ERROR + Error
- STACKTRACE + Stacktrace
diff --git a/public/partials/admin/user.htm b/public/partials/admin/user.htm index 2801540..3dedc6c 100644 --- a/public/partials/admin/user.htm +++ b/public/partials/admin/user.htm @@ -98,7 +98,7 @@
- +
diff --git a/public/partials/anonymize.htm b/public/partials/anonymize.htm index c7b3504..110b103 100644 --- a/public/partials/anonymize.htm +++ b/public/partials/anonymize.htm @@ -1,4 +1,4 @@ -
+
@@ -24,14 +24,14 @@ Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted. -
@@ -40,14 +40,10 @@
-
- My work  /  - {{ isUpdate ? 'Edit anonymization' : 'New anonymization' }} -

- New anonymization - Edit anonymization + New anonymization + Edit anonymization

{{ detectedType === 'repo' ? 'Repository' : detectedType === 'pr' ? 'Pull request' : 'Gist' }} @@ -75,10 +71,10 @@
-
+
-
Source
+

Source & access

@@ -106,14 +102,37 @@
-
-
Identity
+

Anonymization

- Your share link will be anonymous.4open.science/r/{{ repoId }}. + anonymous.4open.science/r/{{ repoId }}
ID can only contain letters and numbers.
{{ repoId }} is already used.
@@ -183,31 +203,20 @@
{{ gistId }} is already used.
-
- - - - {{ conference_data?.name }} expires {{ fmt?.date(conference_data?.endDate) }}. - -
The conference is not activated.
- Link to a conference to apply its shared defaults. -
- -
-
Anonymization
+
- One term per line (regex allowed). Replaced by {{ site_options?.ANONYMIZATION_MASK }}-[N], or use term=>replacement to pick your own (e.g. Anonymous=>ABC). + One term per line.
Regex & replacement syntax Regex is supported. Replaced by {{ site_options?.ANONYMIZATION_MASK }}-[N], or use term=>replacement to pick your own (e.g. Anonymous=>ABC).
Regex characters detected. Escape them if unintentional.
Terms are in an invalid format.
-
Display
+

Display

@@ -217,12 +226,12 @@
- Images are served unchanged and may contain names or affiliations. +
- PDF contents and metadata are served unchanged. About supported formats +
@@ -240,6 +249,7 @@
+

Images and PDFs are served unchanged. Review their contents and metadata before sharing. Supported formats

@@ -299,31 +309,64 @@
-
-
Expiration
- -
-
- - -
-
- +
+

Hosting & expiration

+

+ Uses the owner's hosting + Your author subscription is active. + Conference hosting is checked on submission. + {{ hostingQuota.repository.used }} of {{ hostingQuota.repository.total }} active repositories · Free +

+
+ Expiration + +
+
+
Pick a date in the future.
Pick a date on or before {{ fmt?.date(maxExpirationDate) }}.
Enter a valid expiration date.
+
+
+ +
+

Visitors will be redirected to the original GitHub source on this date, revealing its identity.

+
+ + + + {{ conference_data?.name }} expires {{ fmt?.date(conference_data?.endDate) }}. + +
The conference is not activated.
+ Link to a conference to apply its shared defaults. +
+
+ + +

Confirming your payment…

Your draft is saved. Please do not pay again.

+

{{ billingNotice }}

+ +

No scheduled expiration while subscribed. A 90-day grace period applies if paid access ends.

+
Hosting just this repository?

{{ billingPrice(plan) }} · {{ billingBytes(plan.storage) }} for 180 days. No renewal. Does not include Never expire.

- After {{ fmt?.date(options?.expirationDate) }}, the content will be removed. - After {{ fmt?.date(options?.expirationDate) }}, visitors will be redirected to GitHub.
+
-
Co-authors
+

Coauthors

Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.

@@ -359,16 +402,14 @@ +
-
Your unsaved draft was restored.
-
{{ draftSaved ? "Draft saved ·" : "Unsaved changes ·" }}
- - + @@ -381,6 +422,8 @@ +
Your unsaved draft was restored.
+
{{ draftSaved ? "Draft saved ·" : "Unsaved changes ·" }}
@@ -390,6 +433,7 @@
Live preview {{ previewStatus !== 'ready' ? 'README preview' : (String(terms || '').trim() ? 'README with redactions applied' : 'No terms yet. Add some to see them redacted here.') }} +
Loading source…
Updating the anonymized preview…
@@ -403,8 +447,7 @@
- Live preview - {{ previewStatus === 'ready' ? 'Gist with redactions applied' : 'Gist preview' }} + Gist preview
Loading source…
Updating the anonymized preview…
@@ -456,8 +499,7 @@
- Live preview - {{ previewStatus === 'ready' ? 'Pull request with redactions applied' : 'Pull request preview' }} + Pull request preview
Loading source…
Updating the anonymized preview…
diff --git a/public/partials/claim.htm b/public/partials/claim.htm index 0b14013..6adc65f 100644 --- a/public/partials/claim.htm +++ b/public/partials/claim.htm @@ -17,7 +17,7 @@
- + The ID is the last part of the anonymized URL: anonymous.4open.science/r/<ID>.
diff --git a/public/partials/conference.htm b/public/partials/conference.htm index 043574a..5765346 100644 --- a/public/partials/conference.htm +++ b/public/partials/conference.htm @@ -26,6 +26,22 @@ Cost so far {{ fmt?.number(conference?.price, 2) }} €
+
+

{{ billingNotice }}

+ +
+

{{ billingError }}

+
+

Activate your review package

+

This conference is waiting for payment. Authors can join after payment is confirmed. Checkout returns to this conference. Authors within the package do not pay separately.

+

Loading payment options…

+

{{ billingError }}

+
+

{{ billingPrice(plan) }} for up to {{ plan.repositories }} repositories and {{ billingBytes(plan.storage) }} of source files during the review window. Taxes, if applicable, appear at checkout.

+ +
+

Manage invoices

+
Repositories
diff --git a/public/partials/connections.htm b/public/partials/connections.htm index eb1d989..b79a12d 100644 --- a/public/partials/connections.htm +++ b/public/partials/connections.htm @@ -8,6 +8,16 @@

Loading your connections…

+
+

Email alerts

+

We will email you if your GitHub connection stops working or a repository becomes inaccessible. Your address is never shown on anonymous pages.

+ + + + +

Email address saved.

+ +
Recommended
diff --git a/public/partials/explorer.htm b/public/partials/explorer.htm index ae31d66..0d7e001 100644 --- a/public/partials/explorer.htm +++ b/public/partials/explorer.htm @@ -5,7 +5,7 @@
- + Snapshot{{ fmt?.date(options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate) }} Support @@ -71,7 +71,7 @@
-
+
diff --git a/public/partials/faq.htm b/public/partials/faq.htm index 3c4bd80..451be81 100644 --- a/public/partials/faq.htm +++ b/public/partials/faq.htm @@ -12,6 +12,7 @@
Contents
+

Source updates

Latest commit, at most once an hour.

+
- - The expiration date itself is chosen per anonymization. Redirecting reveals the source repository, so pick it only once the review is over. - -
-
- -
Your unsaved draft was restored.
-
{{ draftSaved ? "Draft saved in this tab for 30 minutes." : "Unsaved changes. Keep this tab open until you save." }}
- - - -
-
Account
+

{{ options.expirationMode === 'redirect' ? 'Redirecting reveals the original repository. Choose the date per anonymization.' : 'Set the expiration date for each repository.' }}

+
+
Your unsaved draft was restored.Unsaved changes{{ draftSaved ? ' · Draft saved in this tab' : '' }}{{ message ? 'Defaults saved' : 'All changes saved' }}
+ + + +
+ +
+
+

Your plan and usage. Reviewer access is always free.

+

Loading billing…

+ +

{{ billingNotice }}

+
Confirming your payment

Your allowance will update after confirmation. Please do not pay again.

+

Current plan

+
{{ profileAuthorGrant() ? 'Author subscription' : (billing?.enabled ? 'Free' : 'Account allowance') }}

{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Renewal cancelled · Access until ' : 'Paid access through ' }}{{ billingDate(profileAuthorGrant().until) }}

+ +
+

{{ billing.allowance.repositories }} repositories · {{ billingBytes(billing.allowance.storage) }}

+

Loading usage…

+

Coauthored repositories use their owner's allowance. Conference hosting is separate.

+

Your repository allowance is full. Review your repositories to remove work you no longer need.

+

Your previous subscription is no longer active. Never-expiring repositories receive a 90-day grace period after paid access ends. Review repository expiration dates.

+

A subscription payment is overdue. Use Manage billing to update your payment details.

+

When paid access ends, never-expiring repositories enter a 90-day grace period. Dated repositories keep their expiration dates.

+
+

Compare plans

+ + + + + + +
Free and Author hosting
IncludedFreeAuthor
Active repositories{{ billing.allowance?.repositories }}{{ profileSelectedPlan()?.repositories }}
Source storage{{ billingBytes(billing.allowance?.storage) }}{{ billingBytes(profileSelectedPlan()?.storage) }}
ExpirationScheduledScheduled or never
CoauthorsIncludedIncluded
Reviewer accessFreeFree
+
Billing frequency
+

Renews {{ profileSelectedPlan()?.interval === 'year' ? 'annually' : 'monthly' }} until cancelled. Tax shown at checkout.

If paid access ends, never-expiring repositories expire after a 90-day grace period.

+
+
+

New purchases are not available. Existing billing can still be managed below.

+
+

Billing

Invoices, payment details, and renewal.

No billing account available.

+

Repository passes

One repository for 180 days.
No subscription or automatic renewal.

+

{{ billingPrice(profilePassPlan()) }} · {{ billingBytes(profilePassPlan().storage) }} · Never expire is not included.

+
+

Your purchases

{{ profileGrantName(grant) }}{{ grant.scope }}

{{ grant.status }}

+

Conference hosting

Shared hosting is managed per conference.

My conferences
+
+ -
-
+
diff --git a/public/partials/reviewConsent.htm b/public/partials/reviewConsent.htm new file mode 100644 index 0000000..26e2639 --- /dev/null +++ b/public/partials/reviewConsent.htm @@ -0,0 +1,25 @@ +
+ Anonymous GitHub +

Approve artifact access

+

This records your approval as the repository owner or an authorized coauthor.

+

Sign in first, then reopen the artifact link from your submission. Sign in

+

{{ error }}

+

Consent recorded. Continue to review to finish linking.Artifact linking is not complete. Return to your submission to check its status.

+ +
+ + +

Use the ID after /r/ in the anonymous repository URL.

+ +
+
+

{{ preview.repositoryName }}

+

Access: {{ preview.policy.access === 'anonymous-link' ? 'Anyone with the anonymous link' : 'Restricted review access' }}.

+

Retain until {{ preview.policy.retainUntil }}. Policy version {{ preview.policy.version }}.

+

This preview expired. Open a new artifact link from your submission.

+
+
+ +
+

Waiting for the server…

+
diff --git a/public/partials/status.htm b/public/partials/status.htm index 2f3f28d..1714e78 100644 --- a/public/partials/status.htm +++ b/public/partials/status.htm @@ -1,21 +1,16 @@ -
-
Anonymization  /  Status
-
-
-

Status of {{ repoId }}

-

Track progress as your anonymization is prepared.

-
- - - - Rate limited - -
+
+
My work / Repository
+

Prepare your anonymous review link

+

{{ repo?.status === 'ready' ? 'Your repository is ready to share.' : 'Your settings are saved. Track preparation before sharing the link.' }}

+
  1. 1. Repository
  2. 2. Review and hosting
  3. 3. Review link
+
+
+
Your review link is ready
+

{{ repoId }}

+

Share the link with reviewers. They do not need to sign in.

+ -
-
Progress
- -

+

The repository will take a few minutes to get ready, depending on its size. Visit the FAQ for more information.

@@ -31,7 +26,7 @@

Anonymization will resume automatically in {{ rateLimitCountdown }}. No action needed — the job is queued and will continue where it left off.

-
+
 ·  @@ -49,6 +44,7 @@

{{ fmt?.translate('ERRORS.' + repo?.statusMessage) }}

No additional details were reported. The most common causes are private repositories, missing branches, and rate limits.

+

Review this repository’s hosting, then retry preparation.

  • Make sure the source URL points to a repository or pull request you can access.
  • Check that the chosen branch and commit still exist on GitHub.
  • @@ -61,54 +57,31 @@
-
-
Repository
-
- /r/{{ repoId }}/ +
diff --git a/public/script/admin.js b/public/script/admin.js index 26801cf..ebd59f1 100644 --- a/public/script/admin.js +++ b/public/script/admin.js @@ -1817,9 +1817,14 @@ export const overviewAdminController = function (state, http, location, interval return "No samples"; }; state.performanceError = null; + // 18400 ms reads better as 18.4 s once a value passes one second. + state.formatMs = function (ms) { + if (ms == null) return ""; + return ms < 1000 ? ms + " ms" : +(ms / 1000).toFixed(1) + " s"; + }; state.latencyBound = function (row, field) { if (!row.count) return "No samples"; - return row[field] == null ? "> 60 s" : "\u2264 " + row[field] + " ms"; + return row[field] == null ? "> 60 s" : "\u2264 " + state.formatMs(row[field]); }; state.firstByteBound = function (row) { return state.latencyBound({ count: row.firstByteCount, p95UpperMs: row.firstByteP95UpperMs }, "p95UpperMs"); @@ -1957,10 +1962,22 @@ export const overviewAdminController = function (state, http, location, interval } var historyMaxes = {}; state.historyMax = function (field) { return historyMaxes[field] || 0; }; + var historyMins = {}; state.historyBarH = function (d, field) { if (!d || !historyMaxes[field]) return 0; return Math.max(1, Math.round((d[field] / historyMaxes[field]) * 140)); }; + // Cumulative totals barely move against a zero baseline, so they are + // drawn between the period's minimum and maximum (shown in the legend). + state.historyRangeH = function (d, field) { + if (!d || historyMaxes[field] == null) return 0; + var span = historyMaxes[field] - historyMins[field]; + if (!span) return 70; + return Math.round(8 + ((d[field] - historyMins[field]) / span) * 132); + }; + state.historyRange = function (field) { + return { min: historyMins[field], max: historyMaxes[field] }; + }; state.historyLabel = function (d) { if (!d || !d.date) return ""; var dt = new Date(d.date); @@ -1983,9 +2000,11 @@ export const overviewAdminController = function (state, http, location, interval state.loading = false; state.error = null; historyMaxes = {}; + historyMins = {}; (r.data.history || []).forEach(function (d) { ["dailyPageViews", "dailyRepositories", "dailyUsers", "nbUsers"].forEach(function (k) { if (!historyMaxes[k] || d[k] > historyMaxes[k]) historyMaxes[k] = d[k]; + if (historyMins[k] == null || d[k] < historyMins[k]) historyMins[k] = d[k]; }); }); }, function (err) { diff --git a/public/script/app.js b/public/script/app.js index 804797e..f7e90cd 100644 --- a/public/script/app.js +++ b/public/script/app.js @@ -1,3 +1,4 @@ +import { setupBilling, readBilling } from "./billing.js"; import { reactive, nextTick, markRaw } from "vue"; import { loadEditor } from "./lazy-assets.js"; import { createTimers, createListeners } from "./state.js"; @@ -28,6 +29,75 @@ function viewerErrorCode(error) { return "unknown_error"; } +// The mask comes from /api/options; mainController sets it once loaded. +let redactionMask = "XXXX"; +export const setRedactionMask = (mask) => { redactionMask = mask || "XXXX"; }; + +// Matches the replacements the anonymizer writes: MASK and MASK-. +export const redactionPattern = (mask = redactionMask) => { + if (!mask) return null; + const escaped = mask.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(escaped + "(?:-\\d+)?", "g"); +}; + +// Wraps redacted terms under root in , touching text +// nodes only so attributes and URLs stay intact. Mermaid sources are left +// alone (Mermaid parses their text), and so are code blocks Prism will +// rewrite unless root is that highlighted block itself. +export const markRedactionsIn = (root, mask = redactionMask) => { + const pattern = redactionPattern(mask); + if (!pattern || !root || !root.textContent.includes(mask)) return; + const doc = root.ownerDocument; + const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT); + const nodes = []; + while (walker.nextNode()) nodes.push(walker.currentNode); + for (const node of nodes) { + const text = node.nodeValue; + const parent = node.parentElement; + if (!text.includes(mask) || !parent || parent.closest("script, style, mark.redaction, .mermaid")) continue; + const highlighted = parent.closest('code[class*="language-"]'); + if (highlighted && !highlighted.contains(root)) continue; + const fragment = doc.createDocumentFragment(); + let last = 0; + for (const match of text.matchAll(pattern)) { + fragment.append(text.slice(last, match.index)); + const mark = doc.createElement("mark"); + mark.className = "redaction"; + mark.title = "Redacted by Anonymous GitHub"; + mark.textContent = match[0]; + fragment.append(mark); + last = match.index + match[0].length; + } + fragment.append(text.slice(last)); + node.replaceWith(fragment); + } +}; + +export const highlightRedactions = (html, mask = redactionMask) => { + if (!html || !mask || !html.includes(mask)) return html; + const doc = new DOMParser().parseFromString("" + html + "", "text/html"); + markRedactionsIn(doc.body, mask); + return doc.body.innerHTML; +}; + +async function copyText(text) { + if (window.navigator.clipboard?.writeText) { + await window.navigator.clipboard.writeText(text); + return; + } + const field = document.createElement("textarea"); + field.value = text; + field.setAttribute("readonly", ""); + field.style.position = "fixed"; + field.style.opacity = "0"; + const focused = document.activeElement; + document.body.appendChild(field); + try { + field.select(); + if (!document.execCommand?.("copy")) throw Error("Copy unavailable"); + } finally { field.remove(); focused?.focus(); } +} + function previewTabKeydown(event) { if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; const tabs = [...event.currentTarget.closest('[role="tablist"]').querySelectorAll('[role="tab"]')]; @@ -42,6 +112,21 @@ function previewTabKeydown(event) { // Page setup functions run inside Vue effect scopes. export const mainController = function (state, http, location, timeout) { state.previewTabKeydown = previewTabKeydown; + state.headerCanUpgrade = false; + state.headerShowSupport = false; + state.headerBillingPending = ["success", "portal"].includes(new URLSearchParams(window.location.search).get("billing")); + const updateHeaderBilling = billing => { + state.headerShowSupport = !(billing?.grants || []).some(g => ["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now()); + const subscribed = (billing?.grants || []).some(g => ["author_monthly", "author_annual"].includes(g.planId) && ["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now()); + state.headerCanUpgrade = !!billing?.enabled && billing.checkoutEnabled !== false && !subscribed && (billing.plans || []).some(p => ["author_monthly", "author_annual"].includes(p.id)); + }; + state.on("billing:updated", (_, billing) => updateHeaderBilling(billing)); + state.on("billing:pending", (_, pending) => { state.headerBillingPending = pending; }); + state.openHeaderUpgrade = () => { + if (location.path() === "/profile") state.emit("settings:upgrade"); + else location.url("/profile?upgrade=1"); + }; + state.title = "Main"; state.user = { status: "connection" }; state.site_options; @@ -67,8 +152,11 @@ export const mainController = function (state, http, location, timeout) { return toast; }; + // Anonymized content that signed-out reviewers browse. + const reviewerRoutes = /^\/(r|repository|pr|gist)\//; state.path = location.url(); state.paths = location.path().substring(1).split("/"); + state.isReviewerRoute = reviewerRoutes.test(location.path()); state.darkMode = function (on) { localStorage.setItem("darkMode", on); @@ -90,23 +178,51 @@ export const mainController = function (state, http, location, timeout) { document.head.append(link); $(`link[href='${darkPrismLink}']`).remove(); } - const kofiBtn = document.querySelector("[class*='floatingchat-container-wrap'] [class*='floating-chat-kofi-text-container-wrap']"); - if (kofiBtn) { - kofiBtn.style.backgroundColor = on ? "#FAF9F6" : "#1A1815"; - kofiBtn.style.color = on ? "#1A1815" : "#FAF9F6"; - } state.emit("dark-mode", on); }; state.darkMode(localStorage.getItem("darkMode") == "true"); + state.emailPromptOpen = false; + state.emailPromptAddress = ""; + state.emailPromptBusy = false; + state.emailPromptError = ""; + let emailPromptFocus; + state.respondToEmailPrompt = async (action) => { + if (state.emailPromptBusy) return; + state.emailPromptBusy = true; + state.emailPromptError = ""; + try { + const res = await http.post("/github/connections/email", { action, email: state.emailPromptAddress }, + { headers: { "X-CSRF-Token": state.user.emailPromptCSRF } }); + if (action === "save") state.user.notificationEmail = res.data.notificationEmail; + state.emailPromptOpen = false; + state.user.showEmailPrompt = false; + emailPromptFocus?.focus(); + } catch (error) { state.emailPromptError = error.data?.error || "Unable to save your preference. Please try again."; } + finally { state.emailPromptBusy = false; } + }; + function getUser() { http.get("/api/user").then( (res) => { if (res) state.user = res.data; + if (state.user?.username) readBilling(http).then(res => updateHeaderBilling(res.data)).catch(() => {}); + else state.headerShowSupport = true; + if (state.user?.emailNotificationsEnabled && state.user.showEmailPrompt && !state.user.notificationEmail) { + emailPromptFocus = document.activeElement; + state.emailPromptOpen = true; + timeout(() => { + const dialog = document.getElementById("email-prompt"); + if (dialog?.showModal) dialog.showModal(); + else dialog?.setAttribute("open", ""); + document.getElementById("email-prompt-address")?.focus(); + }, 0); + } }, () => { state.user = null; + state.headerShowSupport = true; } ); } @@ -116,6 +232,8 @@ export const mainController = function (state, http, location, timeout) { http.get("/api/options").then( (res) => { if (res) state.site_options = res.data; + setRedactionMask(res?.data?.ANONYMIZATION_MASK); + state.emit("site-options"); }, () => { state.site_options = null; @@ -142,6 +260,7 @@ export const mainController = function (state, http, location, timeout) { } state.path = location.url(); state.paths = location.path().substring(1).split("/"); + state.isReviewerRoute = reviewerRoutes.test(location.path()); } state.on("routeChange", changedUrl); @@ -168,6 +287,26 @@ export const faqController = function (state, http) { }; export const profileController = function (state, http, translate, timeout, quotaService) { + state.settingsTab = /(?:billing|upgrade)=/.test(window.location.search) ? "billing" : "defaults"; + state.confirmDeleteAccount = false; + state.deleteUsername = ""; + state.profilePastSubscription = () => !state.profileAuthorGrant() && (state.billing?.grants || []).some(g => ["author_monthly", "author_annual"].includes(g.planId)); + state.showAuthorPlans = new URLSearchParams(window.location.search).get("upgrade") === "1"; + state.showRepositoryPass = false; + state.profilePlan = "author_monthly"; + state.openProfileUpgrade = () => { + state.settingsTab = "billing"; + state.showAuthorPlans = true; + nextTick(() => document.getElementById("settings-plan-comparison")?.focus()); + }; + state.on("settings:upgrade", () => state.openProfileUpgrade()); + state.profileAuthorPlans = () => (state.billing?.plans || []).filter(p => ["author_monthly", "author_annual"].includes(p.id)); + state.profileSelectedPlan = () => state.profileAuthorPlans().find(p => p.id === state.profilePlan) || state.profileAuthorPlans()[0]; + state.profilePassPlan = () => (state.billing?.plans || []).find(p => p.id === "author_pass"); + state.profileAuthorGrant = () => (state.billing?.grants || []).find(g => ["author_monthly", "author_annual"].includes(g.planId) && ["active", "paid", "past_due"].includes(g.status) && new Date(g.until).getTime() > Date.now()); + state.profileGrantName = grant => ({ author_monthly: "Author monthly", author_annual: "Author annual", author_pass: "Repository pass", conference: "Conference hosting" }[grant.planId] || grant.planId); + + setupBilling(state, http, () => quotaService.load().then(quota => { state.quota = quota; }, console.error)); state.terms = ""; state.options = { expirationMode: "remove", @@ -182,6 +321,42 @@ export const profileController = function (state, http, translate, timeout, quot state.message = null; state.error = null; + state.accountEmail = ""; + state.emailLoading = true; + state.emailCSRFReady = false; + state.emailSaving = false; + state.emailSaved = false; + state.emailError = ""; + let emailCSRF = ""; + state.loadAccountEmail = async () => { + state.emailLoading = true; + state.emailError = ""; + try { + const res = await http.get("/github/connections/email"); + state.accountEmail = res.data.notificationEmail || ""; + emailCSRF = res.data.csrf; + state.emailCSRFReady = !!emailCSRF; + } catch { + state.emailError = "Unable to load your email address. Please try again."; + } finally { state.emailLoading = false; } + }; + state.saveAccountEmail = async () => { + if (state.emailSaving || state.emailLoading || !emailCSRF) return; + state.emailSaving = true; + state.emailSaved = false; + state.emailError = ""; + try { + const res = await http.post("/github/connections/email", { email: state.accountEmail }, + { headers: { "X-CSRF-Token": emailCSRF } }); + state.accountEmail = res.data.notificationEmail; + if (state.user) state.user.notificationEmail = res.data.notificationEmail; + state.emailSaved = true; + } catch { + state.emailError = "Unable to save your email address. Check the address and try again."; + } finally { state.emailSaving = false; } + }; + state.loadAccountEmail(); + quotaService.load().then((quota) => { state.quota = quota; }, console.error); @@ -227,23 +402,20 @@ export const profileController = function (state, http, translate, timeout, quot (error) => { state.saving = false; const code = error && error.data && error.data.error; - const key = "ERRORS." + code; - translate(key).then((translation) => { - state.error = translation && translation !== key ? translation : "Unable to save your defaults. Please try again."; + const fallback = "Unable to save your defaults. Please try again."; + // Unknown codes translate to the generic unknown_error message; + // this page has a more specific one. + Promise.all([translate("ERRORS." + code), translate("ERRORS.unknown_error")]).then(([translation, unknown]) => { + state.error = code && translation !== unknown ? translation : fallback; }, () => { - state.error = "Unable to save your defaults. Please try again."; + state.error = fallback; }); } ); }; state.deleteAccount = () => { - if ( - !confirm( - "Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone." - ) - ) - return; + if (state.deletingAccount || !state.confirmDeleteAccount || state.deleteUsername !== state.user?.username) return; state.deletingAccount = true; http.delete("/api/user").then( () => { @@ -561,21 +733,7 @@ export const unifiedDashboardController = function (state, http, location, promi const link = new URL(item._viewUrl, window.location.origin).href; state.copyingId = item._type + ":" + item._id; try { - if (window.navigator.clipboard?.writeText) { - await window.navigator.clipboard.writeText(link); - } else { - const field = document.createElement("textarea"); - field.value = link; - field.setAttribute("readonly", ""); - field.style.position = "fixed"; - field.style.opacity = "0"; - const focused = document.activeElement; - document.body.appendChild(field); - try { - field.select(); - if (!document.execCommand?.("copy")) throw Error("Copy unavailable"); - } finally { field.remove(); focused?.focus(); } - } + await copyText(link); state.addToast({ title: "Link copied", body: "The anonymous link is ready to share." }); } catch { state.addToast({ title: "Copy this anonymous link", body: link }); @@ -1046,10 +1204,24 @@ export const prDashboardController = function (state, location) { export const statusController = function (state, http, params) { const timers = createTimers(); state.repoId = params.repoId; + state.reviewUrl = window.location.origin + "/r/" + encodeURIComponent(params.repoId) + "/"; state.repo = null; state.progress = 0; state.rateLimitResetAt = 0; state.rateLimitCountdown = ""; + state.shareUrl = (window.location?.origin || "") + "/r/" + encodeURIComponent(state.repoId) + "/"; + state.copied = false; + let copiedTimer = null; + state.copyShareUrl = async () => { + try { + await copyText(state.shareUrl); + state.copied = true; + if (copiedTimer) timers.timeout.cancel(copiedTimer); + copiedTimer = timers.timeout(() => { state.copied = false; }, 2000); + } catch { + state.addToast?.({ title: "Copy this anonymous link", body: state.shareUrl }); + } + }; var countdownTimer = null; let pollTimer = null; @@ -1174,6 +1346,72 @@ export const anonymizeController = function (state, http, html, params, location ? "gist:" + parsed.gistId.toLowerCase() : (parsed.owner + "/" + parsed.repo).toLowerCase() + (parsed.pullRequestId ? "/pull/" + parsed.pullRequestId : ""); const draft = formDraft(state, ["sourceUrl", "terms", "repoId", "pullRequestId", "gistId", "source", "options", "conference", "githubConnection"], "anonymize:" + (location.path?.() || "")); + let restoredCheckoutConference = null; + let checkoutDraftId = null; + let checkoutDraftSnapshot = null; + state.hostingReview = false; + state.previewExpanded = false; + state.taskValidationError = ""; + state.editRepository = () => { + state.mobilePane = "settings"; + state.previewExpanded = false; + nextTick(() => document.querySelector(".anonymize-fields .is-invalid")?.focus()); + }; + state.submitRepository = (event) => { + state.taskValidationError = ""; + if (state.sourceLoading || state.billingBusy || state.hostingSaving || state.billingPending) return; + if (expirationDateInvalid() || state.anonymize?.invalid || state.sourceUrlError) { + state.taskValidationError = "Check the highlighted fields before continuing."; + state.editRepository(); + return; + } + if (state.hostingNeedsPayment()) return state.checkoutRepository(); + return state.anonymizeRepo(event); + }; + state.hostingPayment = false; + state.hostingAlternatives = false; + state.hostingPrice = plan => { + if (plan.amount == null) return "Price shown at checkout"; + const amount = new Intl.NumberFormat(undefined, { style: "currency", currency: plan.currency, minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(plan.amount / 100); + return amount + (plan.interval === "year" ? " billed annually" : " billed monthly"); + }; + state.hostingPlan = "author_monthly"; + state.hostingSaving = false; + state.selectedHostingPlan = () => state.authorPlans().find(p => p.id === state.hostingPlan) || state.authorPlans()[0]; + const loadHostingQuota = () => http.get("/api/user/quota").then(res => { state.hostingQuota = res.data; }); + setupBilling(state, http, async () => { + await loadHostingQuota(); + state.hostingQuotaBlocked = false; + }, { confirmedMessage: "Hosting status refreshed. Your draft is ready to continue." }); + loadHostingQuota().catch(() => {}); + state.authorPlans = () => (state.billing?.plans || []).filter(p => ["author_monthly", "author_annual"].includes(p.id)); + state.hasAuthorHosting = () => ["author_monthly", "author_annual"].includes(state.billing?.allowance?.plan); + state.hostingNeedsPayment = () => { + if (!state.billing?.enabled || state.detectedType !== "repo" || state.hasAuthorHosting() || + (state.isUpdate && state.role !== "owner")) return false; + const needsLifetime = state.options?.expirationMode === "never" && !state._existingNever; + // Reported usage may include removed reservations; admission reclaims them. + return !!(needsLifetime || (!state.conference && state.hostingQuotaBlocked)); + }; + state.checkoutRepository = async (pass = false) => { + if (state.hostingSaving || state.billingBusy || !state.billing?.csrf || state.sourceLoading) return; + state.hostingSaving = true; + state.billingError = ""; + try { + const values = draft.checkpoint(); + const serialized = JSON.stringify(values); + if (!checkoutDraftId || serialized !== checkoutDraftSnapshot) { + const saved = await http.post("/api/billing/draft", { path: location.path(), values }, { headers: { "x-csrf-token": state.billing.csrf } }); + checkoutDraftId = saved.data.id; + checkoutDraftSnapshot = serialized; + } + const plan = state.authorPlans().find(p => p.id === state.hostingPlan) || state.authorPlans()[0]; + if (!pass && !plan) throw new Error("No author plan available"); + await state.checkout(pass ? "author_pass" : plan.id, pass ? state.repoId : "", { draftId: checkoutDraftId }); + } catch { state.billingError = "Your draft could not be saved for checkout. Your form is unchanged. Please try again."; } + finally { state.hostingSaving = false; } + }; + state.useDatedHosting = () => { state.options.expirationMode = "remove"; }; state.discardDraft = async () => { draft.discard(); state.workspaceOpen = !!state.sourceUrl; @@ -1273,6 +1511,33 @@ export const anonymizeController = function (state, http, html, params, location state.html_readme = ""; state.isUpdate = false; + state.previousConfigurations = []; + state.previousConfigurationId = ""; + let previousRepositories; + const normalizedRepositoryName = name => String(name || "").replace(/\/$/, "").replace(/\.git$/i, "").toLowerCase(); + async function findPreviousConfigurations() { + if (state.isUpdate || params.repoId || params.pullRequestId || params.gistId) return; + const request = { generation: sourceGeneration, url: state.sourceUrl.trim(), connection: state.githubConnection }; + try { + // This endpoint only returns repositories the signed-in user can manage. + previousRepositories ||= http.get("/api/user/anonymized_repositories").then(res => res.data); + const repositories = await previousRepositories; + if (!isCurrentSource(request)) return; + const name = normalizedRepositoryName(parseRepoFullName(request.url)); + state.previousConfigurations = repositories.filter(repo => repo.repoId && repo.options + && normalizedRepositoryName(repo.source?.fullName) === name) + .sort((a, b) => (Date.parse(b.anonymizeDate) || 0) - (Date.parse(a.anonymizeDate) || 0)); + state.previousConfigurationId = state.previousConfigurations[0]?.repoId || ""; + } catch (_) { + // A suggestion failure must not prevent creating a repository. Retry on the next URL selection. + previousRepositories = undefined; + } + } + state.existingConfigurationUrl = () => { + const previous = state.previousConfigurations.find(repo => repo.repoId === state.previousConfigurationId); + return previous ? "/anonymize/" + encodeURIComponent(previous.repoId) : null; + }; + function getDefault(cb) { http.get("/api/user/default").then((res) => { const data = res.data; @@ -1309,6 +1574,38 @@ export const anonymizeController = function (state, http, html, params, location return null; } + state.accessRepairAllowed = false; + state.repairingGitHubAccess = false; + state.accessRepairMessage = ""; + state.repairGitHubAccess = async () => { + if (!state.accessRepairAllowed || state.repairingGitHubAccess || !state.githubConnections?.appEnabled || !params.repoId || state.role !== "owner" || + state.githubConnection !== "github-app" || parseRepoFullName(state.sourceUrl) !== state._originalFullName) return; + state.repairingGitHubAccess = true; + state.accessRepairMessage = ""; + const request = beginSourceRequest(); + try { + const connections = await http.get("/github/connections"); + state.githubConnections = connections.data; + if (!connections.data.appEnabled) return; + const repaired = await http.post("/github/connections/migrate", { type: "repository", id: params.repoId, connection: "github-app" }, + { headers: { "X-CSRF-Token": connections.data.csrf } }); + if (!isCurrentSource(request)) return; + state.error = null; + if (repaired.data.fullName) { + state._originalFullName = repaired.data.fullName; + state.source.fullName = repaired.data.fullName; + state.sourceUrl = "https://github.com/" + repaired.data.fullName; + } + state.accessRepairMessage = "GitHub access repaired. Your anonymous URL and published commit are unchanged."; + await state.urlSelected(true); + } catch (_) { + if (isCurrentSource(request)) state.accessRepairMessage = "Unable to repair GitHub access. Check the App's repository permissions in Manage access, then retry. Your published snapshot has not been replaced."; + } finally { + state.repairingGitHubAccess = false; + finishSourceRequest(request); + } + }; + state.reconnectingSource = false; state.reconnectRepositoryId = undefined; state.reconnectSource = async () => { @@ -1339,6 +1636,27 @@ export const anonymizeController = function (state, http, html, params, location } async function restoreGitHubDraft() { + const billingDraft = new URLSearchParams(window.location.search).get("billingDraft"); + if (billingDraft) { + draft.restore(); + try { + const saved = (await http.get("/api/billing/draft/" + encodeURIComponent(billingDraft))).data; + if (saved.path !== location.path()) throw new Error("Wrong task"); + restoredCheckoutConference = saved.values.conference || null; + for (const key of ["sourceUrl", "terms", "repoId", "source", "options", "conference", "githubConnection"]) { + if (Object.prototype.hasOwnProperty.call(saved.values, key)) state[key] = saved.values[key]; + } + if (state.options?.expirationDate) state.options.expirationDate = new Date(state.options.expirationDate); + checkoutDraftId = saved.id; + checkoutDraftSnapshot = JSON.stringify(draft.checkpoint()); + state.draftRestored = true; + state.hostingReview = true; + if (state.sourceUrl) await refreshGitHubAccess(); + // Consume the return token so revisiting the form cannot overwrite newer edits. + location.replace?.(location.path()); + return true; + } catch { state.billingError = "The checkout draft could not be restored. Retry by reloading this page; any local draft is still available."; } + } let saved; try { saved = JSON.parse(sessionStorage.getItem("github-access-draft") || "null"); } catch (_) { return false; } @@ -1366,12 +1684,14 @@ export const anonymizeController = function (state, http, html, params, location state.repoId = params.repoId; http.get("/api/repo/" + state.repoId).then( async (res) => { + state.accessRepairAllowed = !["removed", "archived", "preparing", "queue", "download", "removing", "expiring"].includes(res.data.status); state.githubConnection = res.data.connection || "oauth"; state.sourceUrl = "https://github.com/" + res.data.source.fullName; state._originalFullName = res.data.source.fullName; state.terms = res.data.options.terms.filter((f) => f).join("\n"); state.source = res.data.source; state.role = res.data.role || "owner"; + state._existingNever = res.data.options.expirationMode === "never"; state.coauthors = res.data.coauthors || []; // Remember the saved branch so the source.branch watcher knows // not to bump source.commit to GitHub HEAD on edit-page load @@ -1461,6 +1781,8 @@ export const anonymizeController = function (state, http, html, params, location // Keep the same input focused until a complete GitHub URL can be loaded. state.urlSelected = async (preserveDraft = false) => { const generation = ++sourceGeneration; + state.previousConfigurations = []; + state.previousConfigurationId = ""; let o; try { o = parseGithubUrl(state.sourceUrl.trim()); } catch (_) { @@ -1518,6 +1840,7 @@ export const anonymizeController = function (state, http, html, params, location await getPrDetails(); } else { state.detectedType = "repo"; + void findPreviousConfigurations(); await Promise.all([getRepoDetails(), getReadme()]); anonymizeReadme(); } @@ -1929,7 +2252,9 @@ export const anonymizeController = function (state, http, html, params, location state.conference_data = null; if (!conference) return; const preserveSavedOptions = - state.isUpdate && conference === state._originalConference; + conference === restoredCheckoutConference || + (state.isUpdate && conference === state._originalConference); + if (conference !== restoredCheckoutConference) restoredCheckoutConference = null; http.get("/api/conferences/" + conference).then( (res) => { if (state.conference !== conference) return; @@ -1974,6 +2299,7 @@ export const anonymizeController = function (state, http, html, params, location // When the picked date fails min/max validation AngularJS sets the model // to undefined, so we check both the field validity and the model value. function expirationDateInvalid() { + if (state.detectedType === "repo" && state.options.expirationMode === "never") return false; const field = state.anonymize && state.anonymize.expirationDate; if (!state.options.expirationDate || (field && field.invalid)) { if (field && field.setDirty) field.setDirty(); @@ -2072,11 +2398,18 @@ export const anonymizeController = function (state, http, html, params, location state.anonymizeRepo = (event) => { if (state.sourceLoading) { state.error = "Wait for the source to finish loading before saving."; return; } if (state.reconnectingSource && !state.reconnectRepositoryId) return; - if (expirationDateInvalid()) return; + if (expirationDateInvalid()) { state.editRepository(); return; } if (state.anonymize?.invalid || state.sourceUrlError) { + state.editRepository(); state.error = "Please correct the highlighted fields before saving."; return; } + // Capacity is enforced on the server, which can release removed reservations. + if (state.hostingNeedsPayment() && state.options.expirationMode === "never") { + state.hostingReview = true; + nextTick(() => document.getElementById("repository-hosting")?.scrollIntoView({ block: "center" })); + return; + } const button = event.currentTarget || event.target.closest?.("button") || event.target; button.disabled = true; const o = parseGithubUrl(state.sourceUrl); @@ -2100,6 +2433,12 @@ export const anonymizeController = function (state, http, html, params, location if (error.data) { translate("ERRORS." + error.data.error).then((t) => { state.error = t; }, console.error); displayErrorMessage(error.data.error); + if (["quota_exceeded", "subscription_required_for_never_expiration"].includes(error.data.error)) { + if (error.data.error === "subscription_required_for_never_expiration") state._existingNever = false; + state.hostingQuotaBlocked = true; + state.hostingReview = true; + nextTick(() => document.getElementById("repository-hosting")?.scrollIntoView({ block: "center" })); + } } } ).finally(() => { button.disabled = false; }); @@ -2192,6 +2531,11 @@ export const anonymizeController = function (state, http, html, params, location export const exploreController = function (state, http, location, params, html, promises) { const timers = createTimers(); + // Prism rewrites fenced code blocks, so mark redactions once it is done. + if (window.Prism?.hooks && !window.Prism.redactionHook) { + window.Prism.hooks.add("complete", (env) => markRedactionsIn(env.element)); + window.Prism.redactionHook = true; + } const listen = createListeners(); state.on("dark-mode", (event, on) => { if (!state.aceOption) return; @@ -2432,18 +2776,24 @@ export const exploreController = function (state, http, location, params, html, ); } } - state.getFiles = function (path, background = false) { + state.fileCounts = null; + let listingGeneration = 0; + let countRequests = new Set(); + let countsUnavailable = false; + state.getFiles = function (path, background = false) { const repoId = state.repoId; + const generation = listingGeneration; return http.get( `/api/repo/${state.repoId}/files/?path=${encodeURIComponent(path)}&v=${state.options.lastUpdateDate}` ).then(function (res) { - if (destroyed || repoId !== state.repoId) return []; + if (destroyed || repoId !== state.repoId || generation !== listingGeneration) return []; const normalized = path || ""; state.files = state.files.filter((f) => f.path !== normalized); state.files.push(...res.data); + fetchFileCounts(normalized, generation); return res.data; }, function (err) { - if (destroyed || repoId !== state.repoId) return []; + if (destroyed || repoId !== state.repoId || generation !== listingGeneration) return []; if (!background) { state.type = "error"; state.content = viewerErrorCode(err); @@ -2451,6 +2801,23 @@ export const exploreController = function (state, http, location, params, html, } }); }; + function fetchFileCounts(path, generation) { + if (countsUnavailable || countRequests.has(path)) return; + countRequests.add(path); + const repoId = state.repoId; + http.get( + `/api/repo/${state.repoId}/files/counts?path=${encodeURIComponent(path)}` + ).then(function (res) { + if (destroyed || repoId !== state.repoId || generation !== listingGeneration) return; + state.fileCounts = { ...state.fileCounts, ...res.data }; + }, function () { + if (destroyed || repoId !== state.repoId || generation !== listingGeneration) return; + // Counts are optional. Do not retry a guarded or rate-limited query + // every time a visitor opens another directory. + countsUnavailable = true; + }); + } + function getSelectedFile() { return state.files.filter( (f) => @@ -3184,6 +3551,10 @@ export const exploreController = function (state, http, location, params, html, cancelContentLoad(); state.files = []; state.content = null; + state.fileCounts = null; + listingGeneration++; + countRequests = new Set(); + countsUnavailable = false; state.fileSearchQuery = ""; state.onFileSearchChange(); state.repoId = params.repoId; @@ -3433,7 +3804,7 @@ export const newConferenceController = function (state, http, location, params) state.error = ""; function getConference() { - http + return http .get("/api/conferences/" + params.conferenceId) .then((res) => { state.options = { ...state.options, ...res.data, billing: res.data.billing || {} }; @@ -3460,7 +3831,7 @@ export const newConferenceController = function (state, http, location, params) start.setDate(1); start.setMonth(start.getMonth() + 1); const end = new Date(start); - end.setMonth(start.getMonth() + 7, 0); + end.setTime(start.getTime() + 179 * 86400000); state.options = { billing: {}, startDate: start, @@ -3572,6 +3943,7 @@ export const newConferenceController = function (state, http, location, params) }; export const conferenceController = function (state, http, location, params) { + setupBilling(state, http, () => getConference(), { confirmedMessage: "Payment status checked. Your conference is ready for submissions once payment is confirmed." }); state.watch("user.status", () => { if (state.user == null) { location.url("/"); @@ -3612,11 +3984,27 @@ export const conferenceController = function (state, http, location, params) { export const connectionsController = function (state, http) { + state.notificationEmail = ""; + state.emailSaved = false; + state.saveNotificationEmail = async () => { + state.busy = true; + state.emailSaved = false; + state.connectionError = ""; + try { + const res = await http.post("/github/connections/email", { email: state.notificationEmail }, + { headers: { "X-CSRF-Token": state.connections.csrf } }); + state.notificationEmail = res.data.notificationEmail; + if (state.user) state.user.notificationEmail = res.data.notificationEmail; + state.emailSaved = true; + } catch (error) { state.connectionError = error.data?.error || "Unable to save email address."; } + finally { state.busy = false; } + }; state.connections = null; state.connectionError = ""; state.busy = false; state.loadConnections = () => http.get("/github/connections").then(res => { state.connections = res.data; + state.notificationEmail = res.data.notificationEmail || ""; }).catch(error => { state.connectionError = error.data?.error || "Unable to load connections."; }); state.changeConnection = async (resource, connection, preview) => { state.busy = true; diff --git a/public/script/billing.js b/public/script/billing.js new file mode 100644 index 0000000..856313c --- /dev/null +++ b/public/script/billing.js @@ -0,0 +1,83 @@ +// Share concurrent catalog reads between the app header and the current page. +const billingReads = new WeakMap(); +export function readBilling(http) { + if (!billingReads.has(http)) { + const request = http.get("/api/billing/").finally(() => billingReads.delete(http)); + billingReads.set(http, request); + } + return billingReads.get(http); +} +export function setupBilling(state, http, onRefresh = () => {}, context = {}) { + const outcome = new URLSearchParams(window.location.search).get("billing"); + state.billing = null; + state.billingLoading = true; + state.billingBusy = false; + state.billingError = ""; + state.billingNotice = ""; + state.billingPending = outcome === "success" || outcome === "portal"; + state.billingRepository = ""; + const messages = { + billing_invalid_draft: "This draft could not be saved. Your form is unchanged. Try again.", + billing_draft_not_found: "This checkout draft is unavailable or expired. Your local form is unchanged.", + billing_recovery_required: "Your earlier payment needs verification. Please contact support before paying again.", + billing_checkout_disabled: "New purchases are temporarily unavailable. You can still manage existing billing.", + billing_unavailable: "Payments are not available yet. Please try again later.", + billing_subscription_exists: "You already have a subscription. Use Manage billing to change or cancel it.", + billing_checkout_in_progress: "You have another checkout open. Complete it or wait for it to expire before choosing a different plan.", + billing_busy: "Another billing request is processing. Please try again shortly.", + billing_already_paid: "This repository or conference already has an active paid package.", + billing_invalid_review_window: "Conference packages cover up to 180 days. Check the review dates.", + billing_invalid_conference: "Only a conference owner can pay for a conference awaiting payment.", + repo_not_found: "Enter the ID of an existing repository that you own.", + }; + state.billingPrice = plan => { + if (plan.amount == null) return "Price shown at checkout"; + const amount = new Intl.NumberFormat(undefined, { style: "currency", currency: plan.currency }).format(plan.amount / 100); + return amount + (plan.interval ? ` / ${plan.intervalCount > 1 ? plan.intervalCount + " " : ""}${plan.interval}` : " once"); + }; + state.billingBytes = value => { + const bytes = Number(value) || 0; + if (bytes < 1024) return `${bytes} B`; + const exponent = Math.min(4, Math.floor(Math.log2(bytes) / 10)); + return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(bytes / 1024 ** exponent)} ${["B", "KiB", "MiB", "GiB", "TiB"][exponent]}`; + }; + state.billingDate = value => value ? new Date(value).toLocaleDateString() : ""; + state.loadBilling = () => { state.billingError = ""; state.billingLoading = true; return readBilling(http).then(res => { state.billing = res.data; state.emit?.("billing:updated", res.data); }) + .catch(() => { state.billingError = "Billing could not be loaded. Try again."; }) + .finally(() => { state.billingLoading = false; }); }; + async function action(path, body, redirect = true) { + if (state.billingBusy || !state.billing?.csrf) return; + state.billingBusy = true; + state.billingError = ""; + try { + const response = await http.post(`/api/billing/${path}`, body, { headers: { "x-csrf-token": state.billing.csrf } }); + if (redirect) window.location.assign(response.data.url); + else { + await state.loadBilling(); + await onRefresh(); + state.billingPending = !!response.data.pending; + state.emit?.("billing:pending", state.billingPending); + state.billingNotice = response.data.pending ? "Your payment is still pending. Please do not pay again." : (context.confirmedMessage || "Billing status refreshed. Your confirmed access appears below."); + return response.data; + } + } catch (error) { state.billingError = messages[error.data?.error] || "The billing request failed. Please try again."; } + finally { state.billingBusy = false; } + } + state.checkout = (planId, target = "", extra = {}) => action("checkout", { planId, target, ...extra }); + state.billingPortal = () => action("portal", {}); + state.refreshBilling = () => action("refresh", {}, false); + async function confirmPayment() { + for (let attempt = 0; attempt < 5; attempt++) { + const result = await state.refreshBilling(); + if (!result || !result.pending) return; + await new Promise(resolve => window.setTimeout(resolve, 3000)); + } + state.billingNotice = "Confirmation is taking longer than usual. You can refresh payment status later. Please do not pay again."; + } + state.loadBilling().then(() => { + if (outcome === "success" || outcome === "portal") { + state.billingNotice = "Checking your payment. Paid access starts after confirmation."; + confirmPayment(); + } else if (outcome === "cancelled") state.billingNotice = "Checkout was closed. No new paid access has been activated."; + }); +} diff --git a/public/script/components.js b/public/script/components.js index f4f8768..a4c80b3 100644 --- a/public/script/components.js +++ b/public/script/components.js @@ -8,7 +8,7 @@ const QuotaUsage = { props: ["quota", "fmt"], setup(props) { return { formatAmount: (value, kind) => kind === "bytes" - ? props.fmt.humanFileSize(value).replace(/([a-zA-Z]+)$/, " $1") : props.fmt.number(value) }; + ? props.fmt.humanFileSize(props.quota?.storage?.unit === "bytes" ? value * 8 : value).replace(/([a-zA-Z]+)$/, " $1") : props.fmt.number(value) }; }, render: quotaUsageTemplate, }; diff --git a/public/script/core.min.js b/public/script/core.min.js index bb5115c..a08545e 100644 --- a/public/script/core.min.js +++ b/public/script/core.min.js @@ -1,4 +1,4 @@ -let githubEmojiData={100:"https://github.githubassets.com/images/icons/emoji/unicode/1f4af.png?v8",1234:"https://github.githubassets.com/images/icons/emoji/unicode/1f522.png?v8","+1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8","-1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8","1st_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f947.png?v8","2nd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f948.png?v8","3rd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f949.png?v8","8ball":"https://github.githubassets.com/images/icons/emoji/unicode/1f3b1.png?v8",a:"https://github.githubassets.com/images/icons/emoji/unicode/1f170.png?v8",ab:"https://github.githubassets.com/images/icons/emoji/unicode/1f18e.png?v8",abacus:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ee.png?v8",abc:"https://github.githubassets.com/images/icons/emoji/unicode/1f524.png?v8",abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f521.png?v8",accept:"https://github.githubassets.com/images/icons/emoji/unicode/1f251.png?v8",accessibility:"https://github.githubassets.com/images/icons/emoji/accessibility.png?v8",accordion:"https://github.githubassets.com/images/icons/emoji/unicode/1fa97.png?v8",adhesive_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1fa79.png?v8",adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1.png?v8",aerial_tramway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a1.png?v8",afghanistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1eb.png?v8",airplane:"https://github.githubassets.com/images/icons/emoji/unicode/2708.png?v8",aland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fd.png?v8",alarm_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f0.png?v8",albania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f1.png?v8",alembic:"https://github.githubassets.com/images/icons/emoji/unicode/2697.png?v8",algeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ff.png?v8",alien:"https://github.githubassets.com/images/icons/emoji/unicode/1f47d.png?v8",ambulance:"https://github.githubassets.com/images/icons/emoji/unicode/1f691.png?v8",american_samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f8.png?v8",amphora:"https://github.githubassets.com/images/icons/emoji/unicode/1f3fa.png?v8",anatomical_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fac0.png?v8",anchor:"https://github.githubassets.com/images/icons/emoji/unicode/2693.png?v8",andorra:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e9.png?v8",angel:"https://github.githubassets.com/images/icons/emoji/unicode/1f47c.png?v8",anger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a2.png?v8",angola:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f4.png?v8",angry:"https://github.githubassets.com/images/icons/emoji/unicode/1f620.png?v8",anguilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ee.png?v8",anguished:"https://github.githubassets.com/images/icons/emoji/unicode/1f627.png?v8",ant:"https://github.githubassets.com/images/icons/emoji/unicode/1f41c.png?v8",antarctica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f6.png?v8",antigua_barbuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ec.png?v8",apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34e.png?v8",aquarius:"https://github.githubassets.com/images/icons/emoji/unicode/2652.png?v8",argentina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f7.png?v8",aries:"https://github.githubassets.com/images/icons/emoji/unicode/2648.png?v8",armenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f2.png?v8",arrow_backward:"https://github.githubassets.com/images/icons/emoji/unicode/25c0.png?v8",arrow_double_down:"https://github.githubassets.com/images/icons/emoji/unicode/23ec.png?v8",arrow_double_up:"https://github.githubassets.com/images/icons/emoji/unicode/23eb.png?v8",arrow_down:"https://github.githubassets.com/images/icons/emoji/unicode/2b07.png?v8",arrow_down_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53d.png?v8",arrow_forward:"https://github.githubassets.com/images/icons/emoji/unicode/25b6.png?v8",arrow_heading_down:"https://github.githubassets.com/images/icons/emoji/unicode/2935.png?v8",arrow_heading_up:"https://github.githubassets.com/images/icons/emoji/unicode/2934.png?v8",arrow_left:"https://github.githubassets.com/images/icons/emoji/unicode/2b05.png?v8",arrow_lower_left:"https://github.githubassets.com/images/icons/emoji/unicode/2199.png?v8",arrow_lower_right:"https://github.githubassets.com/images/icons/emoji/unicode/2198.png?v8",arrow_right:"https://github.githubassets.com/images/icons/emoji/unicode/27a1.png?v8",arrow_right_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21aa.png?v8",arrow_up:"https://github.githubassets.com/images/icons/emoji/unicode/2b06.png?v8",arrow_up_down:"https://github.githubassets.com/images/icons/emoji/unicode/2195.png?v8",arrow_up_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53c.png?v8",arrow_upper_left:"https://github.githubassets.com/images/icons/emoji/unicode/2196.png?v8",arrow_upper_right:"https://github.githubassets.com/images/icons/emoji/unicode/2197.png?v8",arrows_clockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f503.png?v8",arrows_counterclockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f504.png?v8",art:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a8.png?v8",articulated_lorry:"https://github.githubassets.com/images/icons/emoji/unicode/1f69b.png?v8",artificial_satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f0.png?v8",artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a8.png?v8",aruba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fc.png?v8",ascension_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e8.png?v8",asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/002a-20e3.png?v8",astonished:"https://github.githubassets.com/images/icons/emoji/unicode/1f632.png?v8",astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f680.png?v8",athletic_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45f.png?v8",atm:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e7.png?v8",atom:"https://github.githubassets.com/images/icons/emoji/atom.png?v8",atom_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/269b.png?v8",australia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fa.png?v8",austria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f9.png?v8",auto_rickshaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fa.png?v8",avocado:"https://github.githubassets.com/images/icons/emoji/unicode/1f951.png?v8",axe:"https://github.githubassets.com/images/icons/emoji/unicode/1fa93.png?v8",azerbaijan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ff.png?v8",b:"https://github.githubassets.com/images/icons/emoji/unicode/1f171.png?v8",baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f476.png?v8",baby_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f37c.png?v8",baby_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f424.png?v8",baby_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bc.png?v8",back:"https://github.githubassets.com/images/icons/emoji/unicode/1f519.png?v8",bacon:"https://github.githubassets.com/images/icons/emoji/unicode/1f953.png?v8",badger:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a1.png?v8",badminton:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f8.png?v8",bagel:"https://github.githubassets.com/images/icons/emoji/unicode/1f96f.png?v8",baggage_claim:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c4.png?v8",baguette_bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f956.png?v8",bahamas:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f8.png?v8",bahrain:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ed.png?v8",balance_scale:"https://github.githubassets.com/images/icons/emoji/unicode/2696.png?v8",bald_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b2.png?v8",bald_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b2.png?v8",ballet_shoes:"https://github.githubassets.com/images/icons/emoji/unicode/1fa70.png?v8",balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f388.png?v8",ballot_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5f3.png?v8",ballot_box_with_check:"https://github.githubassets.com/images/icons/emoji/unicode/2611.png?v8",bamboo:"https://github.githubassets.com/images/icons/emoji/unicode/1f38d.png?v8",banana:"https://github.githubassets.com/images/icons/emoji/unicode/1f34c.png?v8",bangbang:"https://github.githubassets.com/images/icons/emoji/unicode/203c.png?v8",bangladesh:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e9.png?v8",banjo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa95.png?v8",bank:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e6.png?v8",bar_chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ca.png?v8",barbados:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e7.png?v8",barber:"https://github.githubassets.com/images/icons/emoji/unicode/1f488.png?v8",baseball:"https://github.githubassets.com/images/icons/emoji/unicode/26be.png?v8",basecamp:"https://github.githubassets.com/images/icons/emoji/basecamp.png?v8",basecampy:"https://github.githubassets.com/images/icons/emoji/basecampy.png?v8",basket:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fa.png?v8",basketball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c0.png?v8",basketball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8",basketball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8",bat:"https://github.githubassets.com/images/icons/emoji/unicode/1f987.png?v8",bath:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c0.png?v8",bathtub:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c1.png?v8",battery:"https://github.githubassets.com/images/icons/emoji/unicode/1f50b.png?v8",beach_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d6.png?v8",beans:"https://github.githubassets.com/images/icons/emoji/unicode/1fad8.png?v8",bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f43b.png?v8",bearded_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4.png?v8",beaver:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ab.png?v8",bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cf.png?v8",bee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8",beer:"https://github.githubassets.com/images/icons/emoji/unicode/1f37a.png?v8",beers:"https://github.githubassets.com/images/icons/emoji/unicode/1f37b.png?v8",beetle:"https://github.githubassets.com/images/icons/emoji/unicode/1fab2.png?v8",beginner:"https://github.githubassets.com/images/icons/emoji/unicode/1f530.png?v8",belarus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fe.png?v8",belgium:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ea.png?v8",belize:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ff.png?v8",bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f514.png?v8",bell_pepper:"https://github.githubassets.com/images/icons/emoji/unicode/1fad1.png?v8",bellhop_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ce.png?v8",benin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ef.png?v8",bento:"https://github.githubassets.com/images/icons/emoji/unicode/1f371.png?v8",bermuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f2.png?v8",beverage_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c3.png?v8",bhutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f9.png?v8",bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4.png?v8",bike:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b2.png?v8",biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2642.png?v8",biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2640.png?v8",bikini:"https://github.githubassets.com/images/icons/emoji/unicode/1f459.png?v8",billed_cap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e2.png?v8",biohazard:"https://github.githubassets.com/images/icons/emoji/unicode/2623.png?v8",bird:"https://github.githubassets.com/images/icons/emoji/unicode/1f426.png?v8",birthday:"https://github.githubassets.com/images/icons/emoji/unicode/1f382.png?v8",bison:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ac.png?v8",biting_lip:"https://github.githubassets.com/images/icons/emoji/unicode/1fae6.png?v8",black_bird:"https://github.githubassets.com/images/icons/emoji/unicode/1f426-2b1b.png?v8",black_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f408-2b1b.png?v8",black_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26ab.png?v8",black_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4.png?v8",black_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a4.png?v8",black_joker:"https://github.githubassets.com/images/icons/emoji/unicode/1f0cf.png?v8",black_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1b.png?v8",black_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fe.png?v8",black_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fc.png?v8",black_nib:"https://github.githubassets.com/images/icons/emoji/unicode/2712.png?v8",black_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25aa.png?v8",black_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f532.png?v8",blond_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2642.png?v8",blond_haired_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f471.png?v8",blond_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8",blonde_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8",blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f33c.png?v8",blowfish:"https://github.githubassets.com/images/icons/emoji/unicode/1f421.png?v8",blue_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d8.png?v8",blue_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f699.png?v8",blue_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f499.png?v8",blue_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e6.png?v8",blueberries:"https://github.githubassets.com/images/icons/emoji/unicode/1fad0.png?v8",blush:"https://github.githubassets.com/images/icons/emoji/unicode/1f60a.png?v8",boar:"https://github.githubassets.com/images/icons/emoji/unicode/1f417.png?v8",boat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8",bolivia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f4.png?v8",bomb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a3.png?v8",bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b4.png?v8",book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8",bookmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f516.png?v8",bookmark_tabs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d1.png?v8",books:"https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png?v8",boom:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8",boomerang:"https://github.githubassets.com/images/icons/emoji/unicode/1fa83.png?v8",boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f462.png?v8",bosnia_herzegovina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e6.png?v8",botswana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fc.png?v8",bouncing_ball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8",bouncing_ball_person:"https://github.githubassets.com/images/icons/emoji/unicode/26f9.png?v8",bouncing_ball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8",bouquet:"https://github.githubassets.com/images/icons/emoji/unicode/1f490.png?v8",bouvet_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fb.png?v8",bow:"https://github.githubassets.com/images/icons/emoji/unicode/1f647.png?v8",bow_and_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f9.png?v8",bowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2642.png?v8",bowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2640.png?v8",bowl_with_spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f963.png?v8",bowling:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b3.png?v8",bowtie:"https://github.githubassets.com/images/icons/emoji/bowtie.png?v8",boxing_glove:"https://github.githubassets.com/images/icons/emoji/unicode/1f94a.png?v8",boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f466.png?v8",brain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e0.png?v8",brazil:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f7.png?v8",bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f35e.png?v8",breast_feeding:"https://github.githubassets.com/images/icons/emoji/unicode/1f931.png?v8",bricks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f1.png?v8",bride_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470-2640.png?v8",bridge_at_night:"https://github.githubassets.com/images/icons/emoji/unicode/1f309.png?v8",briefcase:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bc.png?v8",british_indian_ocean_territory:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f4.png?v8",british_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ec.png?v8",broccoli:"https://github.githubassets.com/images/icons/emoji/unicode/1f966.png?v8",broken_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f494.png?v8",broom:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f9.png?v8",brown_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e4.png?v8",brown_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90e.png?v8",brown_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7eb.png?v8",brunei:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f3.png?v8",bubble_tea:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cb.png?v8",bubbles:"https://github.githubassets.com/images/icons/emoji/unicode/1fae7.png?v8",bucket:"https://github.githubassets.com/images/icons/emoji/unicode/1faa3.png?v8",bug:"https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png?v8",building_construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d7.png?v8",bulb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a1.png?v8",bulgaria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ec.png?v8",bullettrain_front:"https://github.githubassets.com/images/icons/emoji/unicode/1f685.png?v8",bullettrain_side:"https://github.githubassets.com/images/icons/emoji/unicode/1f684.png?v8",burkina_faso:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1eb.png?v8",burrito:"https://github.githubassets.com/images/icons/emoji/unicode/1f32f.png?v8",burundi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ee.png?v8",bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68c.png?v8",business_suit_levitating:"https://github.githubassets.com/images/icons/emoji/unicode/1f574.png?v8",busstop:"https://github.githubassets.com/images/icons/emoji/unicode/1f68f.png?v8",bust_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f464.png?v8",busts_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f465.png?v8",butter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c8.png?v8",butterfly:"https://github.githubassets.com/images/icons/emoji/unicode/1f98b.png?v8",cactus:"https://github.githubassets.com/images/icons/emoji/unicode/1f335.png?v8",cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f370.png?v8",calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c6.png?v8",call_me_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f919.png?v8",calling:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f2.png?v8",cambodia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ed.png?v8",camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42b.png?v8",camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f7.png?v8",camera_flash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f8.png?v8",cameroon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f2.png?v8",camping:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d5.png?v8",canada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e6.png?v8",canary_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e8.png?v8",cancer:"https://github.githubassets.com/images/icons/emoji/unicode/264b.png?v8",candle:"https://github.githubassets.com/images/icons/emoji/unicode/1f56f.png?v8",candy:"https://github.githubassets.com/images/icons/emoji/unicode/1f36c.png?v8",canned_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f96b.png?v8",canoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f6.png?v8",cape_verde:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fb.png?v8",capital_abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f520.png?v8",capricorn:"https://github.githubassets.com/images/icons/emoji/unicode/2651.png?v8",car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8",card_file_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c3.png?v8",card_index:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c7.png?v8",card_index_dividers:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c2.png?v8",caribbean_netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f6.png?v8",carousel_horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a0.png?v8",carpentry_saw:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9a.png?v8",carrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f955.png?v8",cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938.png?v8",cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f431.png?v8",cat2:"https://github.githubassets.com/images/icons/emoji/unicode/1f408.png?v8",cayman_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fe.png?v8",cd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bf.png?v8",central_african_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1eb.png?v8",ceuta_melilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e6.png?v8",chad:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e9.png?v8",chains:"https://github.githubassets.com/images/icons/emoji/unicode/26d3.png?v8",chair:"https://github.githubassets.com/images/icons/emoji/unicode/1fa91.png?v8",champagne:"https://github.githubassets.com/images/icons/emoji/unicode/1f37e.png?v8",chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b9.png?v8",chart_with_downwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c9.png?v8",chart_with_upwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c8.png?v8",checkered_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c1.png?v8",cheese:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c0.png?v8",cherries:"https://github.githubassets.com/images/icons/emoji/unicode/1f352.png?v8",cherry_blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f338.png?v8",chess_pawn:"https://github.githubassets.com/images/icons/emoji/unicode/265f.png?v8",chestnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f330.png?v8",chicken:"https://github.githubassets.com/images/icons/emoji/unicode/1f414.png?v8",child:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d2.png?v8",children_crossing:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b8.png?v8",chile:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f1.png?v8",chipmunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f43f.png?v8",chocolate_bar:"https://github.githubassets.com/images/icons/emoji/unicode/1f36b.png?v8",chopsticks:"https://github.githubassets.com/images/icons/emoji/unicode/1f962.png?v8",christmas_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fd.png?v8",christmas_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f384.png?v8",church:"https://github.githubassets.com/images/icons/emoji/unicode/26ea.png?v8",cinema:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a6.png?v8",circus_tent:"https://github.githubassets.com/images/icons/emoji/unicode/1f3aa.png?v8",city_sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f307.png?v8",city_sunset:"https://github.githubassets.com/images/icons/emoji/unicode/1f306.png?v8",cityscape:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d9.png?v8",cl:"https://github.githubassets.com/images/icons/emoji/unicode/1f191.png?v8",clamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dc.png?v8",clap:"https://github.githubassets.com/images/icons/emoji/unicode/1f44f.png?v8",clapper:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ac.png?v8",classical_building:"https://github.githubassets.com/images/icons/emoji/unicode/1f3db.png?v8",climbing:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7.png?v8",climbing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2642.png?v8",climbing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2640.png?v8",clinking_glasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f942.png?v8",clipboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cb.png?v8",clipperton_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f5.png?v8",clock1:"https://github.githubassets.com/images/icons/emoji/unicode/1f550.png?v8",clock10:"https://github.githubassets.com/images/icons/emoji/unicode/1f559.png?v8",clock1030:"https://github.githubassets.com/images/icons/emoji/unicode/1f565.png?v8",clock11:"https://github.githubassets.com/images/icons/emoji/unicode/1f55a.png?v8",clock1130:"https://github.githubassets.com/images/icons/emoji/unicode/1f566.png?v8",clock12:"https://github.githubassets.com/images/icons/emoji/unicode/1f55b.png?v8",clock1230:"https://github.githubassets.com/images/icons/emoji/unicode/1f567.png?v8",clock130:"https://github.githubassets.com/images/icons/emoji/unicode/1f55c.png?v8",clock2:"https://github.githubassets.com/images/icons/emoji/unicode/1f551.png?v8",clock230:"https://github.githubassets.com/images/icons/emoji/unicode/1f55d.png?v8",clock3:"https://github.githubassets.com/images/icons/emoji/unicode/1f552.png?v8",clock330:"https://github.githubassets.com/images/icons/emoji/unicode/1f55e.png?v8",clock4:"https://github.githubassets.com/images/icons/emoji/unicode/1f553.png?v8",clock430:"https://github.githubassets.com/images/icons/emoji/unicode/1f55f.png?v8",clock5:"https://github.githubassets.com/images/icons/emoji/unicode/1f554.png?v8",clock530:"https://github.githubassets.com/images/icons/emoji/unicode/1f560.png?v8",clock6:"https://github.githubassets.com/images/icons/emoji/unicode/1f555.png?v8",clock630:"https://github.githubassets.com/images/icons/emoji/unicode/1f561.png?v8",clock7:"https://github.githubassets.com/images/icons/emoji/unicode/1f556.png?v8",clock730:"https://github.githubassets.com/images/icons/emoji/unicode/1f562.png?v8",clock8:"https://github.githubassets.com/images/icons/emoji/unicode/1f557.png?v8",clock830:"https://github.githubassets.com/images/icons/emoji/unicode/1f563.png?v8",clock9:"https://github.githubassets.com/images/icons/emoji/unicode/1f558.png?v8",clock930:"https://github.githubassets.com/images/icons/emoji/unicode/1f564.png?v8",closed_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d5.png?v8",closed_lock_with_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f510.png?v8",closed_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f302.png?v8",cloud:"https://github.githubassets.com/images/icons/emoji/unicode/2601.png?v8",cloud_with_lightning:"https://github.githubassets.com/images/icons/emoji/unicode/1f329.png?v8",cloud_with_lightning_and_rain:"https://github.githubassets.com/images/icons/emoji/unicode/26c8.png?v8",cloud_with_rain:"https://github.githubassets.com/images/icons/emoji/unicode/1f327.png?v8",cloud_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f328.png?v8",clown_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f921.png?v8",clubs:"https://github.githubassets.com/images/icons/emoji/unicode/2663.png?v8",cn:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f3.png?v8",coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e5.png?v8",cockroach:"https://github.githubassets.com/images/icons/emoji/unicode/1fab3.png?v8",cocktail:"https://github.githubassets.com/images/icons/emoji/unicode/1f378.png?v8",coconut:"https://github.githubassets.com/images/icons/emoji/unicode/1f965.png?v8",cocos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e8.png?v8",coffee:"https://github.githubassets.com/images/icons/emoji/unicode/2615.png?v8",coffin:"https://github.githubassets.com/images/icons/emoji/unicode/26b0.png?v8",coin:"https://github.githubassets.com/images/icons/emoji/unicode/1fa99.png?v8",cold_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f976.png?v8",cold_sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f630.png?v8",collision:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8",colombia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f4.png?v8",comet:"https://github.githubassets.com/images/icons/emoji/unicode/2604.png?v8",comoros:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f2.png?v8",compass:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ed.png?v8",computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bb.png?v8",computer_mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b1.png?v8",confetti_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f38a.png?v8",confounded:"https://github.githubassets.com/images/icons/emoji/unicode/1f616.png?v8",confused:"https://github.githubassets.com/images/icons/emoji/unicode/1f615.png?v8",congo_brazzaville:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ec.png?v8",congo_kinshasa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e9.png?v8",congratulations:"https://github.githubassets.com/images/icons/emoji/unicode/3297.png?v8",construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png?v8",construction_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f477.png?v8",construction_worker_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2642.png?v8",construction_worker_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2640.png?v8",control_knobs:"https://github.githubassets.com/images/icons/emoji/unicode/1f39b.png?v8",convenience_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ea.png?v8",cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f373.png?v8",cook_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f0.png?v8",cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f36a.png?v8",cool:"https://github.githubassets.com/images/icons/emoji/unicode/1f192.png?v8",cop:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8",copilot:"https://github.githubassets.com/images/icons/emoji/copilot.png?v8",copyright:"https://github.githubassets.com/images/icons/emoji/unicode/00a9.png?v8",coral:"https://github.githubassets.com/images/icons/emoji/unicode/1fab8.png?v8",corn:"https://github.githubassets.com/images/icons/emoji/unicode/1f33d.png?v8",costa_rica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f7.png?v8",cote_divoire:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ee.png?v8",couch_and_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cb.png?v8",couple:"https://github.githubassets.com/images/icons/emoji/unicode/1f46b.png?v8",couple_with_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f491.png?v8",couple_with_heart_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f468.png?v8",couple_with_heart_woman_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f468.png?v8",couple_with_heart_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f469.png?v8",couplekiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48f.png?v8",couplekiss_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f48b-1f468.png?v8",couplekiss_man_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f468.png?v8",couplekiss_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f469.png?v8",cow:"https://github.githubassets.com/images/icons/emoji/unicode/1f42e.png?v8",cow2:"https://github.githubassets.com/images/icons/emoji/unicode/1f404.png?v8",cowboy_hat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f920.png?v8",crab:"https://github.githubassets.com/images/icons/emoji/unicode/1f980.png?v8",crayon:"https://github.githubassets.com/images/icons/emoji/unicode/1f58d.png?v8",credit_card:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b3.png?v8",crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f319.png?v8",cricket:"https://github.githubassets.com/images/icons/emoji/unicode/1f997.png?v8",cricket_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cf.png?v8",croatia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f7.png?v8",crocodile:"https://github.githubassets.com/images/icons/emoji/unicode/1f40a.png?v8",croissant:"https://github.githubassets.com/images/icons/emoji/unicode/1f950.png?v8",crossed_fingers:"https://github.githubassets.com/images/icons/emoji/unicode/1f91e.png?v8",crossed_flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38c.png?v8",crossed_swords:"https://github.githubassets.com/images/icons/emoji/unicode/2694.png?v8",crown:"https://github.githubassets.com/images/icons/emoji/unicode/1f451.png?v8",crutch:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7c.png?v8",cry:"https://github.githubassets.com/images/icons/emoji/unicode/1f622.png?v8",crying_cat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f63f.png?v8",crystal_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f52e.png?v8",cuba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fa.png?v8",cucumber:"https://github.githubassets.com/images/icons/emoji/unicode/1f952.png?v8",cup_with_straw:"https://github.githubassets.com/images/icons/emoji/unicode/1f964.png?v8",cupcake:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c1.png?v8",cupid:"https://github.githubassets.com/images/icons/emoji/unicode/1f498.png?v8",curacao:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fc.png?v8",curling_stone:"https://github.githubassets.com/images/icons/emoji/unicode/1f94c.png?v8",curly_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b1.png?v8",curly_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b1.png?v8",curly_loop:"https://github.githubassets.com/images/icons/emoji/unicode/27b0.png?v8",currency_exchange:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b1.png?v8",curry:"https://github.githubassets.com/images/icons/emoji/unicode/1f35b.png?v8",cursing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92c.png?v8",custard:"https://github.githubassets.com/images/icons/emoji/unicode/1f36e.png?v8",customs:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c3.png?v8",cut_of_meat:"https://github.githubassets.com/images/icons/emoji/unicode/1f969.png?v8",cyclone:"https://github.githubassets.com/images/icons/emoji/unicode/1f300.png?v8",cyprus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fe.png?v8",czech_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ff.png?v8",dagger:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e1.png?v8",dancer:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8",dancers:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f.png?v8",dancing_men:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2642.png?v8",dancing_women:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2640.png?v8",dango:"https://github.githubassets.com/images/icons/emoji/unicode/1f361.png?v8",dark_sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f576.png?v8",dart:"https://github.githubassets.com/images/icons/emoji/unicode/1f3af.png?v8",dash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a8.png?v8",date:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c5.png?v8",de:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ea.png?v8",deaf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2642.png?v8",deaf_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf.png?v8",deaf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2640.png?v8",deciduous_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f333.png?v8",deer:"https://github.githubassets.com/images/icons/emoji/unicode/1f98c.png?v8",denmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f0.png?v8",department_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ec.png?v8",dependabot:"https://github.githubassets.com/images/icons/emoji/dependabot.png?v8",derelict_house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3da.png?v8",desert:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dc.png?v8",desert_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dd.png?v8",desktop_computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a5.png?v8",detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575.png?v8",diamond_shape_with_a_dot_inside:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a0.png?v8",diamonds:"https://github.githubassets.com/images/icons/emoji/unicode/2666.png?v8",diego_garcia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ec.png?v8",disappointed:"https://github.githubassets.com/images/icons/emoji/unicode/1f61e.png?v8",disappointed_relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f625.png?v8",disguised_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f978.png?v8",diving_mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f93f.png?v8",diya_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1fa94.png?v8",dizzy:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ab.png?v8",dizzy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f635.png?v8",djibouti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ef.png?v8",dna:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ec.png?v8",do_not_litter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6af.png?v8",dodo:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a4.png?v8",dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f436.png?v8",dog2:"https://github.githubassets.com/images/icons/emoji/unicode/1f415.png?v8",dollar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b5.png?v8",dolls:"https://github.githubassets.com/images/icons/emoji/unicode/1f38e.png?v8",dolphin:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8",dominica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f2.png?v8",dominican_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f4.png?v8",donkey:"https://github.githubassets.com/images/icons/emoji/unicode/1facf.png?v8",door:"https://github.githubassets.com/images/icons/emoji/unicode/1f6aa.png?v8",dotted_line_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae5.png?v8",doughnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f369.png?v8",dove:"https://github.githubassets.com/images/icons/emoji/unicode/1f54a.png?v8",dragon:"https://github.githubassets.com/images/icons/emoji/unicode/1f409.png?v8",dragon_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f432.png?v8",dress:"https://github.githubassets.com/images/icons/emoji/unicode/1f457.png?v8",dromedary_camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42a.png?v8",drooling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f924.png?v8",drop_of_blood:"https://github.githubassets.com/images/icons/emoji/unicode/1fa78.png?v8",droplet:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a7.png?v8",drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f941.png?v8",duck:"https://github.githubassets.com/images/icons/emoji/unicode/1f986.png?v8",dumpling:"https://github.githubassets.com/images/icons/emoji/unicode/1f95f.png?v8",dvd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c0.png?v8","e-mail":"https://github.githubassets.com/images/icons/emoji/unicode/1f4e7.png?v8",eagle:"https://github.githubassets.com/images/icons/emoji/unicode/1f985.png?v8",ear:"https://github.githubassets.com/images/icons/emoji/unicode/1f442.png?v8",ear_of_rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f33e.png?v8",ear_with_hearing_aid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bb.png?v8",earth_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f30d.png?v8",earth_americas:"https://github.githubassets.com/images/icons/emoji/unicode/1f30e.png?v8",earth_asia:"https://github.githubassets.com/images/icons/emoji/unicode/1f30f.png?v8",ecuador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e8.png?v8",egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f95a.png?v8",eggplant:"https://github.githubassets.com/images/icons/emoji/unicode/1f346.png?v8",egypt:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ec.png?v8",eight:"https://github.githubassets.com/images/icons/emoji/unicode/0038-20e3.png?v8",eight_pointed_black_star:"https://github.githubassets.com/images/icons/emoji/unicode/2734.png?v8",eight_spoked_asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/2733.png?v8",eject_button:"https://github.githubassets.com/images/icons/emoji/unicode/23cf.png?v8",el_salvador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fb.png?v8",electric_plug:"https://github.githubassets.com/images/icons/emoji/unicode/1f50c.png?v8",electron:"https://github.githubassets.com/images/icons/emoji/electron.png?v8",elephant:"https://github.githubassets.com/images/icons/emoji/unicode/1f418.png?v8",elevator:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d7.png?v8",elf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd.png?v8",elf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2642.png?v8",elf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2640.png?v8",email:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e7.png?v8",empty_nest:"https://github.githubassets.com/images/icons/emoji/unicode/1fab9.png?v8",end:"https://github.githubassets.com/images/icons/emoji/unicode/1f51a.png?v8",england:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png?v8",envelope:"https://github.githubassets.com/images/icons/emoji/unicode/2709.png?v8",envelope_with_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e9.png?v8",equatorial_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f6.png?v8",eritrea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f7.png?v8",es:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f8.png?v8",estonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ea.png?v8",ethiopia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f9.png?v8",eu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8",euro:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b6.png?v8",european_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f0.png?v8",european_post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e4.png?v8",european_union:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8",evergreen_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f332.png?v8",exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8",exploding_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f92f.png?v8",expressionless:"https://github.githubassets.com/images/icons/emoji/unicode/1f611.png?v8",eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f441.png?v8",eye_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f441-1f5e8.png?v8",eyeglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f453.png?v8",eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f440.png?v8",face_exhaling:"https://github.githubassets.com/images/icons/emoji/unicode/1f62e-1f4a8.png?v8",face_holding_back_tears:"https://github.githubassets.com/images/icons/emoji/unicode/1f979.png?v8",face_in_clouds:"https://github.githubassets.com/images/icons/emoji/unicode/1f636-1f32b.png?v8",face_with_diagonal_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1fae4.png?v8",face_with_head_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1f915.png?v8",face_with_open_eyes_and_hand_over_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1fae2.png?v8",face_with_peeking_eye:"https://github.githubassets.com/images/icons/emoji/unicode/1fae3.png?v8",face_with_spiral_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f635-1f4ab.png?v8",face_with_thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f912.png?v8",facepalm:"https://github.githubassets.com/images/icons/emoji/unicode/1f926.png?v8",facepunch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8",factory:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ed.png?v8",factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3ed.png?v8",fairy:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da.png?v8",fairy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2642.png?v8",fairy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2640.png?v8",falafel:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c6.png?v8",falkland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f0.png?v8",fallen_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f342.png?v8",family:"https://github.githubassets.com/images/icons/emoji/unicode/1f46a.png?v8",family_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466.png?v8",family_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466-1f466.png?v8",family_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467.png?v8",family_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f466.png?v8",family_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f467.png?v8",family_man_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466.png?v8",family_man_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466-1f466.png?v8",family_man_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467.png?v8",family_man_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f466.png?v8",family_man_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f467.png?v8",family_man_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466.png?v8",family_man_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466-1f466.png?v8",family_man_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467.png?v8",family_man_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f466.png?v8",family_man_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f467.png?v8",family_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466.png?v8",family_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466-1f466.png?v8",family_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467.png?v8",family_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f466.png?v8",family_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f467.png?v8",family_woman_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466.png?v8",family_woman_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466-1f466.png?v8",family_woman_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467.png?v8",family_woman_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f466.png?v8",family_woman_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f467.png?v8",farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f33e.png?v8",faroe_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f4.png?v8",fast_forward:"https://github.githubassets.com/images/icons/emoji/unicode/23e9.png?v8",fax:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e0.png?v8",fearful:"https://github.githubassets.com/images/icons/emoji/unicode/1f628.png?v8",feather:"https://github.githubassets.com/images/icons/emoji/unicode/1fab6.png?v8",feelsgood:"https://github.githubassets.com/images/icons/emoji/feelsgood.png?v8",feet:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8",female_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2640.png?v8",female_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2640.png?v8",ferris_wheel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a1.png?v8",ferry:"https://github.githubassets.com/images/icons/emoji/unicode/26f4.png?v8",field_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d1.png?v8",fiji:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ef.png?v8",file_cabinet:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c4.png?v8",file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c1.png?v8",film_projector:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fd.png?v8",film_strip:"https://github.githubassets.com/images/icons/emoji/unicode/1f39e.png?v8",finland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ee.png?v8",finnadie:"https://github.githubassets.com/images/icons/emoji/finnadie.png?v8",fire:"https://github.githubassets.com/images/icons/emoji/unicode/1f525.png?v8",fire_engine:"https://github.githubassets.com/images/icons/emoji/unicode/1f692.png?v8",fire_extinguisher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ef.png?v8",firecracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e8.png?v8",firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f692.png?v8",fireworks:"https://github.githubassets.com/images/icons/emoji/unicode/1f386.png?v8",first_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f313.png?v8",first_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31b.png?v8",fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f41f.png?v8",fish_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f365.png?v8",fishing_pole_and_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a3.png?v8",fishsticks:"https://github.githubassets.com/images/icons/emoji/fishsticks.png?v8",fist:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8",fist_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f91b.png?v8",fist_oncoming:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8",fist_raised:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8",fist_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f91c.png?v8",five:"https://github.githubassets.com/images/icons/emoji/unicode/0035-20e3.png?v8",flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38f.png?v8",flamingo:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a9.png?v8",flashlight:"https://github.githubassets.com/images/icons/emoji/unicode/1f526.png?v8",flat_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f97f.png?v8",flatbread:"https://github.githubassets.com/images/icons/emoji/unicode/1fad3.png?v8",fleur_de_lis:"https://github.githubassets.com/images/icons/emoji/unicode/269c.png?v8",flight_arrival:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ec.png?v8",flight_departure:"https://github.githubassets.com/images/icons/emoji/unicode/1f6eb.png?v8",flipper:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8",floppy_disk:"https://github.githubassets.com/images/icons/emoji/unicode/1f4be.png?v8",flower_playing_cards:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b4.png?v8",flushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f633.png?v8",flute:"https://github.githubassets.com/images/icons/emoji/unicode/1fa88.png?v8",fly:"https://github.githubassets.com/images/icons/emoji/unicode/1fab0.png?v8",flying_disc:"https://github.githubassets.com/images/icons/emoji/unicode/1f94f.png?v8",flying_saucer:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f8.png?v8",fog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32b.png?v8",foggy:"https://github.githubassets.com/images/icons/emoji/unicode/1f301.png?v8",folding_hand_fan:"https://github.githubassets.com/images/icons/emoji/unicode/1faad.png?v8",fondue:"https://github.githubassets.com/images/icons/emoji/unicode/1fad5.png?v8",foot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b6.png?v8",football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c8.png?v8",footprints:"https://github.githubassets.com/images/icons/emoji/unicode/1f463.png?v8",fork_and_knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f374.png?v8",fortune_cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f960.png?v8",fountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f2.png?v8",fountain_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58b.png?v8",four:"https://github.githubassets.com/images/icons/emoji/unicode/0034-20e3.png?v8",four_leaf_clover:"https://github.githubassets.com/images/icons/emoji/unicode/1f340.png?v8",fox_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f98a.png?v8",fr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f7.png?v8",framed_picture:"https://github.githubassets.com/images/icons/emoji/unicode/1f5bc.png?v8",free:"https://github.githubassets.com/images/icons/emoji/unicode/1f193.png?v8",french_guiana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1eb.png?v8",french_polynesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1eb.png?v8",french_southern_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1eb.png?v8",fried_egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f373.png?v8",fried_shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f364.png?v8",fries:"https://github.githubassets.com/images/icons/emoji/unicode/1f35f.png?v8",frog:"https://github.githubassets.com/images/icons/emoji/unicode/1f438.png?v8",frowning:"https://github.githubassets.com/images/icons/emoji/unicode/1f626.png?v8",frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/2639.png?v8",frowning_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2642.png?v8",frowning_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d.png?v8",frowning_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2640.png?v8",fu:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8",fuelpump:"https://github.githubassets.com/images/icons/emoji/unicode/26fd.png?v8",full_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f315.png?v8",full_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31d.png?v8",funeral_urn:"https://github.githubassets.com/images/icons/emoji/unicode/26b1.png?v8",gabon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e6.png?v8",gambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f2.png?v8",game_die:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b2.png?v8",garlic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c4.png?v8",gb:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8",gear:"https://github.githubassets.com/images/icons/emoji/unicode/2699.png?v8",gem:"https://github.githubassets.com/images/icons/emoji/unicode/1f48e.png?v8",gemini:"https://github.githubassets.com/images/icons/emoji/unicode/264a.png?v8",genie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de.png?v8",genie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2642.png?v8",genie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2640.png?v8",georgia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ea.png?v8",ghana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ed.png?v8",ghost:"https://github.githubassets.com/images/icons/emoji/unicode/1f47b.png?v8",gibraltar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ee.png?v8",gift:"https://github.githubassets.com/images/icons/emoji/unicode/1f381.png?v8",gift_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49d.png?v8",ginger_root:"https://github.githubassets.com/images/icons/emoji/unicode/1fada.png?v8",giraffe:"https://github.githubassets.com/images/icons/emoji/unicode/1f992.png?v8",girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f467.png?v8",globe_with_meridians:"https://github.githubassets.com/images/icons/emoji/unicode/1f310.png?v8",gloves:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e4.png?v8",goal_net:"https://github.githubassets.com/images/icons/emoji/unicode/1f945.png?v8",goat:"https://github.githubassets.com/images/icons/emoji/unicode/1f410.png?v8",goberserk:"https://github.githubassets.com/images/icons/emoji/goberserk.png?v8",godmode:"https://github.githubassets.com/images/icons/emoji/godmode.png?v8",goggles:"https://github.githubassets.com/images/icons/emoji/unicode/1f97d.png?v8",golf:"https://github.githubassets.com/images/icons/emoji/unicode/26f3.png?v8",golfing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc.png?v8",golfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2642.png?v8",golfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2640.png?v8",goose:"https://github.githubassets.com/images/icons/emoji/unicode/1fabf.png?v8",gorilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f98d.png?v8",grapes:"https://github.githubassets.com/images/icons/emoji/unicode/1f347.png?v8",greece:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f7.png?v8",green_apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34f.png?v8",green_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d7.png?v8",green_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e2.png?v8",green_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49a.png?v8",green_salad:"https://github.githubassets.com/images/icons/emoji/unicode/1f957.png?v8",green_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e9.png?v8",greenland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f1.png?v8",grenada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e9.png?v8",grey_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2755.png?v8",grey_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fa76.png?v8",grey_question:"https://github.githubassets.com/images/icons/emoji/unicode/2754.png?v8",grimacing:"https://github.githubassets.com/images/icons/emoji/unicode/1f62c.png?v8",grin:"https://github.githubassets.com/images/icons/emoji/unicode/1f601.png?v8",grinning:"https://github.githubassets.com/images/icons/emoji/unicode/1f600.png?v8",guadeloupe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f5.png?v8",guam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fa.png?v8",guard:"https://github.githubassets.com/images/icons/emoji/unicode/1f482.png?v8",guardsman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2642.png?v8",guardswoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2640.png?v8",guatemala:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f9.png?v8",guernsey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ec.png?v8",guide_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ae.png?v8",guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f3.png?v8",guinea_bissau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fc.png?v8",guitar:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b8.png?v8",gun:"https://github.githubassets.com/images/icons/emoji/unicode/1f52b.png?v8",guyana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fe.png?v8",hair_pick:"https://github.githubassets.com/images/icons/emoji/unicode/1faae.png?v8",haircut:"https://github.githubassets.com/images/icons/emoji/unicode/1f487.png?v8",haircut_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2642.png?v8",haircut_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2640.png?v8",haiti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f9.png?v8",hamburger:"https://github.githubassets.com/images/icons/emoji/unicode/1f354.png?v8",hammer:"https://github.githubassets.com/images/icons/emoji/unicode/1f528.png?v8",hammer_and_pick:"https://github.githubassets.com/images/icons/emoji/unicode/2692.png?v8",hammer_and_wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e0.png?v8",hamsa:"https://github.githubassets.com/images/icons/emoji/unicode/1faac.png?v8",hamster:"https://github.githubassets.com/images/icons/emoji/unicode/1f439.png?v8",hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8",hand_over_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f92d.png?v8",hand_with_index_finger_and_thumb_crossed:"https://github.githubassets.com/images/icons/emoji/unicode/1faf0.png?v8",handbag:"https://github.githubassets.com/images/icons/emoji/unicode/1f45c.png?v8",handball_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e.png?v8",handshake:"https://github.githubassets.com/images/icons/emoji/unicode/1f91d.png?v8",hankey:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8",hash:"https://github.githubassets.com/images/icons/emoji/unicode/0023-20e3.png?v8",hatched_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f425.png?v8",hatching_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f423.png?v8",headphones:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a7.png?v8",headstone:"https://github.githubassets.com/images/icons/emoji/unicode/1faa6.png?v8",health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2695.png?v8",hear_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f649.png?v8",heard_mcdonald_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f2.png?v8",heart:"https://github.githubassets.com/images/icons/emoji/unicode/2764.png?v8",heart_decoration:"https://github.githubassets.com/images/icons/emoji/unicode/1f49f.png?v8",heart_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f60d.png?v8",heart_eyes_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63b.png?v8",heart_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1faf6.png?v8",heart_on_fire:"https://github.githubassets.com/images/icons/emoji/unicode/2764-1f525.png?v8",heartbeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f493.png?v8",heartpulse:"https://github.githubassets.com/images/icons/emoji/unicode/1f497.png?v8",hearts:"https://github.githubassets.com/images/icons/emoji/unicode/2665.png?v8",heavy_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2714.png?v8",heavy_division_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2797.png?v8",heavy_dollar_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b2.png?v8",heavy_equals_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f7f0.png?v8",heavy_exclamation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8",heavy_heart_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2763.png?v8",heavy_minus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2796.png?v8",heavy_multiplication_x:"https://github.githubassets.com/images/icons/emoji/unicode/2716.png?v8",heavy_plus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2795.png?v8",hedgehog:"https://github.githubassets.com/images/icons/emoji/unicode/1f994.png?v8",helicopter:"https://github.githubassets.com/images/icons/emoji/unicode/1f681.png?v8",herb:"https://github.githubassets.com/images/icons/emoji/unicode/1f33f.png?v8",hibiscus:"https://github.githubassets.com/images/icons/emoji/unicode/1f33a.png?v8",high_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f506.png?v8",high_heel:"https://github.githubassets.com/images/icons/emoji/unicode/1f460.png?v8",hiking_boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f97e.png?v8",hindu_temple:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d5.png?v8",hippopotamus:"https://github.githubassets.com/images/icons/emoji/unicode/1f99b.png?v8",hocho:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8",hole:"https://github.githubassets.com/images/icons/emoji/unicode/1f573.png?v8",honduras:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f3.png?v8",honey_pot:"https://github.githubassets.com/images/icons/emoji/unicode/1f36f.png?v8",honeybee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8",hong_kong:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f0.png?v8",hook:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9d.png?v8",horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f434.png?v8",horse_racing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c7.png?v8",hospital:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e5.png?v8",hot_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f975.png?v8",hot_pepper:"https://github.githubassets.com/images/icons/emoji/unicode/1f336.png?v8",hotdog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32d.png?v8",hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e8.png?v8",hotsprings:"https://github.githubassets.com/images/icons/emoji/unicode/2668.png?v8",hourglass:"https://github.githubassets.com/images/icons/emoji/unicode/231b.png?v8",hourglass_flowing_sand:"https://github.githubassets.com/images/icons/emoji/unicode/23f3.png?v8",house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e0.png?v8",house_with_garden:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e1.png?v8",houses:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d8.png?v8",hugs:"https://github.githubassets.com/images/icons/emoji/unicode/1f917.png?v8",hungary:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1fa.png?v8",hurtrealbad:"https://github.githubassets.com/images/icons/emoji/hurtrealbad.png?v8",hushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f62f.png?v8",hut:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d6.png?v8",hyacinth:"https://github.githubassets.com/images/icons/emoji/unicode/1fabb.png?v8",ice_cream:"https://github.githubassets.com/images/icons/emoji/unicode/1f368.png?v8",ice_cube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ca.png?v8",ice_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d2.png?v8",ice_skate:"https://github.githubassets.com/images/icons/emoji/unicode/26f8.png?v8",icecream:"https://github.githubassets.com/images/icons/emoji/unicode/1f366.png?v8",iceland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f8.png?v8",id:"https://github.githubassets.com/images/icons/emoji/unicode/1f194.png?v8",identification_card:"https://github.githubassets.com/images/icons/emoji/unicode/1faaa.png?v8",ideograph_advantage:"https://github.githubassets.com/images/icons/emoji/unicode/1f250.png?v8",imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f47f.png?v8",inbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e5.png?v8",incoming_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e8.png?v8",index_pointing_at_the_viewer:"https://github.githubassets.com/images/icons/emoji/unicode/1faf5.png?v8",india:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f3.png?v8",indonesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e9.png?v8",infinity:"https://github.githubassets.com/images/icons/emoji/unicode/267e.png?v8",information_desk_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8",information_source:"https://github.githubassets.com/images/icons/emoji/unicode/2139.png?v8",innocent:"https://github.githubassets.com/images/icons/emoji/unicode/1f607.png?v8",interrobang:"https://github.githubassets.com/images/icons/emoji/unicode/2049.png?v8",iphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f1.png?v8",iran:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f7.png?v8",iraq:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f6.png?v8",ireland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1ea.png?v8",isle_of_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f2.png?v8",israel:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f1.png?v8",it:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f9.png?v8",izakaya_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8",jack_o_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f383.png?v8",jamaica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f2.png?v8",japan:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fe.png?v8",japanese_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ef.png?v8",japanese_goblin:"https://github.githubassets.com/images/icons/emoji/unicode/1f47a.png?v8",japanese_ogre:"https://github.githubassets.com/images/icons/emoji/unicode/1f479.png?v8",jar:"https://github.githubassets.com/images/icons/emoji/unicode/1fad9.png?v8",jeans:"https://github.githubassets.com/images/icons/emoji/unicode/1f456.png?v8",jellyfish:"https://github.githubassets.com/images/icons/emoji/unicode/1fabc.png?v8",jersey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1ea.png?v8",jigsaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e9.png?v8",jordan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f4.png?v8",joy:"https://github.githubassets.com/images/icons/emoji/unicode/1f602.png?v8",joy_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f639.png?v8",joystick:"https://github.githubassets.com/images/icons/emoji/unicode/1f579.png?v8",jp:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f5.png?v8",judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2696.png?v8",juggling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f939.png?v8",kaaba:"https://github.githubassets.com/images/icons/emoji/unicode/1f54b.png?v8",kangaroo:"https://github.githubassets.com/images/icons/emoji/unicode/1f998.png?v8",kazakhstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ff.png?v8",kenya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ea.png?v8",key:"https://github.githubassets.com/images/icons/emoji/unicode/1f511.png?v8",keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/2328.png?v8",keycap_ten:"https://github.githubassets.com/images/icons/emoji/unicode/1f51f.png?v8",khanda:"https://github.githubassets.com/images/icons/emoji/unicode/1faaf.png?v8",kick_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f4.png?v8",kimono:"https://github.githubassets.com/images/icons/emoji/unicode/1f458.png?v8",kiribati:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ee.png?v8",kiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48b.png?v8",kissing:"https://github.githubassets.com/images/icons/emoji/unicode/1f617.png?v8",kissing_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63d.png?v8",kissing_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61a.png?v8",kissing_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f618.png?v8",kissing_smiling_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f619.png?v8",kite:"https://github.githubassets.com/images/icons/emoji/unicode/1fa81.png?v8",kiwi_fruit:"https://github.githubassets.com/images/icons/emoji/unicode/1f95d.png?v8",kneeling_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2642.png?v8",kneeling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce.png?v8",kneeling_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2640.png?v8",knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8",knot:"https://github.githubassets.com/images/icons/emoji/unicode/1faa2.png?v8",koala:"https://github.githubassets.com/images/icons/emoji/unicode/1f428.png?v8",koko:"https://github.githubassets.com/images/icons/emoji/unicode/1f201.png?v8",kosovo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fd-1f1f0.png?v8",kr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f7.png?v8",kuwait:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fc.png?v8",kyrgyzstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ec.png?v8",lab_coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f97c.png?v8",label:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f7.png?v8",lacrosse:"https://github.githubassets.com/images/icons/emoji/unicode/1f94d.png?v8",ladder:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9c.png?v8",lady_beetle:"https://github.githubassets.com/images/icons/emoji/unicode/1f41e.png?v8",lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8",laos:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e6.png?v8",large_blue_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f535.png?v8",large_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f537.png?v8",large_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f536.png?v8",last_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f317.png?v8",last_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31c.png?v8",latin_cross:"https://github.githubassets.com/images/icons/emoji/unicode/271d.png?v8",latvia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fb.png?v8",laughing:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8",leafy_green:"https://github.githubassets.com/images/icons/emoji/unicode/1f96c.png?v8",leaves:"https://github.githubassets.com/images/icons/emoji/unicode/1f343.png?v8",lebanon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e7.png?v8",ledger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d2.png?v8",left_luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c5.png?v8",left_right_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/2194.png?v8",left_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e8.png?v8",leftwards_arrow_with_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21a9.png?v8",leftwards_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf2.png?v8",leftwards_pushing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf7.png?v8",leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b5.png?v8",lemon:"https://github.githubassets.com/images/icons/emoji/unicode/1f34b.png?v8",leo:"https://github.githubassets.com/images/icons/emoji/unicode/264c.png?v8",leopard:"https://github.githubassets.com/images/icons/emoji/unicode/1f406.png?v8",lesotho:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f8.png?v8",level_slider:"https://github.githubassets.com/images/icons/emoji/unicode/1f39a.png?v8",liberia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f7.png?v8",libra:"https://github.githubassets.com/images/icons/emoji/unicode/264e.png?v8",libya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fe.png?v8",liechtenstein:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1ee.png?v8",light_blue_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fa75.png?v8",light_rail:"https://github.githubassets.com/images/icons/emoji/unicode/1f688.png?v8",link:"https://github.githubassets.com/images/icons/emoji/unicode/1f517.png?v8",lion:"https://github.githubassets.com/images/icons/emoji/unicode/1f981.png?v8",lips:"https://github.githubassets.com/images/icons/emoji/unicode/1f444.png?v8",lipstick:"https://github.githubassets.com/images/icons/emoji/unicode/1f484.png?v8",lithuania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f9.png?v8",lizard:"https://github.githubassets.com/images/icons/emoji/unicode/1f98e.png?v8",llama:"https://github.githubassets.com/images/icons/emoji/unicode/1f999.png?v8",lobster:"https://github.githubassets.com/images/icons/emoji/unicode/1f99e.png?v8",lock:"https://github.githubassets.com/images/icons/emoji/unicode/1f512.png?v8",lock_with_ink_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f50f.png?v8",lollipop:"https://github.githubassets.com/images/icons/emoji/unicode/1f36d.png?v8",long_drum:"https://github.githubassets.com/images/icons/emoji/unicode/1fa98.png?v8",loop:"https://github.githubassets.com/images/icons/emoji/unicode/27bf.png?v8",lotion_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f4.png?v8",lotus:"https://github.githubassets.com/images/icons/emoji/unicode/1fab7.png?v8",lotus_position:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8.png?v8",lotus_position_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2642.png?v8",lotus_position_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2640.png?v8",loud_sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f50a.png?v8",loudspeaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e2.png?v8",love_hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e9.png?v8",love_letter:"https://github.githubassets.com/images/icons/emoji/unicode/1f48c.png?v8",love_you_gesture:"https://github.githubassets.com/images/icons/emoji/unicode/1f91f.png?v8",low_battery:"https://github.githubassets.com/images/icons/emoji/unicode/1faab.png?v8",low_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f505.png?v8",luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f3.png?v8",lungs:"https://github.githubassets.com/images/icons/emoji/unicode/1fac1.png?v8",luxembourg:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fa.png?v8",lying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f925.png?v8",m:"https://github.githubassets.com/images/icons/emoji/unicode/24c2.png?v8",macau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f4.png?v8",macedonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f0.png?v8",madagascar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ec.png?v8",mag:"https://github.githubassets.com/images/icons/emoji/unicode/1f50d.png?v8",mag_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f50e.png?v8",mage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9.png?v8",mage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2642.png?v8",mage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2640.png?v8",magic_wand:"https://github.githubassets.com/images/icons/emoji/unicode/1fa84.png?v8",magnet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f2.png?v8",mahjong:"https://github.githubassets.com/images/icons/emoji/unicode/1f004.png?v8",mailbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4eb.png?v8",mailbox_closed:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ea.png?v8",mailbox_with_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ec.png?v8",mailbox_with_no_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ed.png?v8",malawi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fc.png?v8",malaysia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fe.png?v8",maldives:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fb.png?v8",male_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2642.png?v8",male_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2642.png?v8",mali:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f1.png?v8",malta:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f9.png?v8",mammoth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a3.png?v8",man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468.png?v8",man_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a8.png?v8",man_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f680.png?v8",man_beard:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4-2642.png?v8",man_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2642.png?v8",man_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f373.png?v8",man_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f57a.png?v8",man_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2642.png?v8",man_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3ed.png?v8",man_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f33e.png?v8",man_feeding_baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f37c.png?v8",man_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f692.png?v8",man_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2695.png?v8",man_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bd.png?v8",man_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bc.png?v8",man_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935-2642.png?v8",man_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2696.png?v8",man_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2642.png?v8",man_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f527.png?v8",man_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bc.png?v8",man_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2708.png?v8",man_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2642.png?v8",man_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2642.png?v8",man_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f52c.png?v8",man_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2642.png?v8",man_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a4.png?v8",man_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f393.png?v8",man_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3eb.png?v8",man_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bb.png?v8",man_with_gua_pi_mao:"https://github.githubassets.com/images/icons/emoji/unicode/1f472.png?v8",man_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9af.png?v8",man_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2642.png?v8",man_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470-2642.png?v8",mandarin:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8",mango:"https://github.githubassets.com/images/icons/emoji/unicode/1f96d.png?v8",mans_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8",mantelpiece_clock:"https://github.githubassets.com/images/icons/emoji/unicode/1f570.png?v8",manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bd.png?v8",maple_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f341.png?v8",maracas:"https://github.githubassets.com/images/icons/emoji/unicode/1fa87.png?v8",marshall_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ed.png?v8",martial_arts_uniform:"https://github.githubassets.com/images/icons/emoji/unicode/1f94b.png?v8",martinique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f6.png?v8",mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f637.png?v8",massage:"https://github.githubassets.com/images/icons/emoji/unicode/1f486.png?v8",massage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2642.png?v8",massage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2640.png?v8",mate:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c9.png?v8",mauritania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f7.png?v8",mauritius:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fa.png?v8",mayotte:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1f9.png?v8",meat_on_bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f356.png?v8",mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f527.png?v8",mechanical_arm:"https://github.githubassets.com/images/icons/emoji/unicode/1f9be.png?v8",mechanical_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bf.png?v8",medal_military:"https://github.githubassets.com/images/icons/emoji/unicode/1f396.png?v8",medal_sports:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c5.png?v8",medical_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/2695.png?v8",mega:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e3.png?v8",melon:"https://github.githubassets.com/images/icons/emoji/unicode/1f348.png?v8",melting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae0.png?v8",memo:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8",men_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2642.png?v8",mending_heart:"https://github.githubassets.com/images/icons/emoji/unicode/2764-1fa79.png?v8",menorah:"https://github.githubassets.com/images/icons/emoji/unicode/1f54e.png?v8",mens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b9.png?v8",mermaid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2640.png?v8",merman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2642.png?v8",merperson:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc.png?v8",metal:"https://github.githubassets.com/images/icons/emoji/unicode/1f918.png?v8",metro:"https://github.githubassets.com/images/icons/emoji/unicode/1f687.png?v8",mexico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fd.png?v8",microbe:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a0.png?v8",micronesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f2.png?v8",microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a4.png?v8",microscope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52c.png?v8",middle_finger:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8",military_helmet:"https://github.githubassets.com/images/icons/emoji/unicode/1fa96.png?v8",milk_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f95b.png?v8",milky_way:"https://github.githubassets.com/images/icons/emoji/unicode/1f30c.png?v8",minibus:"https://github.githubassets.com/images/icons/emoji/unicode/1f690.png?v8",minidisc:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bd.png?v8",mirror:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9e.png?v8",mirror_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1faa9.png?v8",mobile_phone_off:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f4.png?v8",moldova:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e9.png?v8",monaco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e8.png?v8",money_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f911.png?v8",money_with_wings:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b8.png?v8",moneybag:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b0.png?v8",mongolia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f3.png?v8",monkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f412.png?v8",monkey_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f435.png?v8",monocle_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d0.png?v8",monorail:"https://github.githubassets.com/images/icons/emoji/unicode/1f69d.png?v8",montenegro:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ea.png?v8",montserrat:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f8.png?v8",moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8",moon_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f96e.png?v8",moose:"https://github.githubassets.com/images/icons/emoji/unicode/1face.png?v8",morocco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e6.png?v8",mortar_board:"https://github.githubassets.com/images/icons/emoji/unicode/1f393.png?v8",mosque:"https://github.githubassets.com/images/icons/emoji/unicode/1f54c.png?v8",mosquito:"https://github.githubassets.com/images/icons/emoji/unicode/1f99f.png?v8",motor_boat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e5.png?v8",motor_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f5.png?v8",motorcycle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cd.png?v8",motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bc.png?v8",motorway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e3.png?v8",mount_fuji:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fb.png?v8",mountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f0.png?v8",mountain_bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5.png?v8",mountain_biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2642.png?v8",mountain_biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2640.png?v8",mountain_cableway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a0.png?v8",mountain_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69e.png?v8",mountain_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d4.png?v8",mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f42d.png?v8",mouse2:"https://github.githubassets.com/images/icons/emoji/unicode/1f401.png?v8",mouse_trap:"https://github.githubassets.com/images/icons/emoji/unicode/1faa4.png?v8",movie_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a5.png?v8",moyai:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ff.png?v8",mozambique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ff.png?v8",mrs_claus:"https://github.githubassets.com/images/icons/emoji/unicode/1f936.png?v8",muscle:"https://github.githubassets.com/images/icons/emoji/unicode/1f4aa.png?v8",mushroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f344.png?v8",musical_keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b9.png?v8",musical_note:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b5.png?v8",musical_score:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bc.png?v8",mute:"https://github.githubassets.com/images/icons/emoji/unicode/1f507.png?v8",mx_claus:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f384.png?v8",myanmar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f2.png?v8",nail_care:"https://github.githubassets.com/images/icons/emoji/unicode/1f485.png?v8",name_badge:"https://github.githubassets.com/images/icons/emoji/unicode/1f4db.png?v8",namibia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e6.png?v8",national_park:"https://github.githubassets.com/images/icons/emoji/unicode/1f3de.png?v8",nauru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f7.png?v8",nauseated_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f922.png?v8",nazar_amulet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ff.png?v8",neckbeard:"https://github.githubassets.com/images/icons/emoji/neckbeard.png?v8",necktie:"https://github.githubassets.com/images/icons/emoji/unicode/1f454.png?v8",negative_squared_cross_mark:"https://github.githubassets.com/images/icons/emoji/unicode/274e.png?v8",nepal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f5.png?v8",nerd_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f913.png?v8",nest_with_eggs:"https://github.githubassets.com/images/icons/emoji/unicode/1faba.png?v8",nesting_dolls:"https://github.githubassets.com/images/icons/emoji/unicode/1fa86.png?v8",netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f1.png?v8",neutral_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f610.png?v8",new:"https://github.githubassets.com/images/icons/emoji/unicode/1f195.png?v8",new_caledonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e8.png?v8",new_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f311.png?v8",new_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31a.png?v8",new_zealand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ff.png?v8",newspaper:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f0.png?v8",newspaper_roll:"https://github.githubassets.com/images/icons/emoji/unicode/1f5de.png?v8",next_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ed.png?v8",ng:"https://github.githubassets.com/images/icons/emoji/unicode/1f196.png?v8",ng_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8",ng_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8",nicaragua:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ee.png?v8",niger:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ea.png?v8",nigeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ec.png?v8",night_with_stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f303.png?v8",nine:"https://github.githubassets.com/images/icons/emoji/unicode/0039-20e3.png?v8",ninja:"https://github.githubassets.com/images/icons/emoji/unicode/1f977.png?v8",niue:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1fa.png?v8",no_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f515.png?v8",no_bicycles:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b3.png?v8",no_entry:"https://github.githubassets.com/images/icons/emoji/unicode/26d4.png?v8",no_entry_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ab.png?v8",no_good:"https://github.githubassets.com/images/icons/emoji/unicode/1f645.png?v8",no_good_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8",no_good_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8",no_mobile_phones:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f5.png?v8",no_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f636.png?v8",no_pedestrians:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b7.png?v8",no_smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ad.png?v8","non-potable_water":"https://github.githubassets.com/images/icons/emoji/unicode/1f6b1.png?v8",norfolk_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1eb.png?v8",north_korea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f5.png?v8",northern_mariana_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f5.png?v8",norway:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f4.png?v8",nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f443.png?v8",notebook:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d3.png?v8",notebook_with_decorative_cover:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d4.png?v8",notes:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b6.png?v8",nut_and_bolt:"https://github.githubassets.com/images/icons/emoji/unicode/1f529.png?v8",o:"https://github.githubassets.com/images/icons/emoji/unicode/2b55.png?v8",o2:"https://github.githubassets.com/images/icons/emoji/unicode/1f17e.png?v8",ocean:"https://github.githubassets.com/images/icons/emoji/unicode/1f30a.png?v8",octocat:"https://github.githubassets.com/images/icons/emoji/octocat.png?v8",octopus:"https://github.githubassets.com/images/icons/emoji/unicode/1f419.png?v8",oden:"https://github.githubassets.com/images/icons/emoji/unicode/1f362.png?v8",office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e2.png?v8",office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bc.png?v8",oil_drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e2.png?v8",ok:"https://github.githubassets.com/images/icons/emoji/unicode/1f197.png?v8",ok_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f44c.png?v8",ok_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2642.png?v8",ok_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f646.png?v8",ok_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2640.png?v8",old_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dd.png?v8",older_adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d3.png?v8",older_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f474.png?v8",older_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f475.png?v8",olive:"https://github.githubassets.com/images/icons/emoji/unicode/1fad2.png?v8",om:"https://github.githubassets.com/images/icons/emoji/unicode/1f549.png?v8",oman:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f4-1f1f2.png?v8",on:"https://github.githubassets.com/images/icons/emoji/unicode/1f51b.png?v8",oncoming_automobile:"https://github.githubassets.com/images/icons/emoji/unicode/1f698.png?v8",oncoming_bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68d.png?v8",oncoming_police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f694.png?v8",oncoming_taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f696.png?v8",one:"https://github.githubassets.com/images/icons/emoji/unicode/0031-20e3.png?v8",one_piece_swimsuit:"https://github.githubassets.com/images/icons/emoji/unicode/1fa71.png?v8",onion:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c5.png?v8",open_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8",open_file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c2.png?v8",open_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f450.png?v8",open_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f62e.png?v8",open_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2602.png?v8",ophiuchus:"https://github.githubassets.com/images/icons/emoji/unicode/26ce.png?v8",orange:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8",orange_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d9.png?v8",orange_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e0.png?v8",orange_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e1.png?v8",orange_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e7.png?v8",orangutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a7.png?v8",orthodox_cross:"https://github.githubassets.com/images/icons/emoji/unicode/2626.png?v8",otter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a6.png?v8",outbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e4.png?v8",owl:"https://github.githubassets.com/images/icons/emoji/unicode/1f989.png?v8",ox:"https://github.githubassets.com/images/icons/emoji/unicode/1f402.png?v8",oyster:"https://github.githubassets.com/images/icons/emoji/unicode/1f9aa.png?v8",package:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e6.png?v8",page_facing_up:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c4.png?v8",page_with_curl:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c3.png?v8",pager:"https://github.githubassets.com/images/icons/emoji/unicode/1f4df.png?v8",paintbrush:"https://github.githubassets.com/images/icons/emoji/unicode/1f58c.png?v8",pakistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f0.png?v8",palau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fc.png?v8",palestinian_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f8.png?v8",palm_down_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf3.png?v8",palm_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f334.png?v8",palm_up_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf4.png?v8",palms_up_together:"https://github.githubassets.com/images/icons/emoji/unicode/1f932.png?v8",panama:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1e6.png?v8",pancakes:"https://github.githubassets.com/images/icons/emoji/unicode/1f95e.png?v8",panda_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f43c.png?v8",paperclip:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png?v8",paperclips:"https://github.githubassets.com/images/icons/emoji/unicode/1f587.png?v8",papua_new_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ec.png?v8",parachute:"https://github.githubassets.com/images/icons/emoji/unicode/1fa82.png?v8",paraguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fe.png?v8",parasol_on_ground:"https://github.githubassets.com/images/icons/emoji/unicode/26f1.png?v8",parking:"https://github.githubassets.com/images/icons/emoji/unicode/1f17f.png?v8",parrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f99c.png?v8",part_alternation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/303d.png?v8",partly_sunny:"https://github.githubassets.com/images/icons/emoji/unicode/26c5.png?v8",partying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f973.png?v8",passenger_ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f3.png?v8",passport_control:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c2.png?v8",pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f8.png?v8",paw_prints:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8",pea_pod:"https://github.githubassets.com/images/icons/emoji/unicode/1fadb.png?v8",peace_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/262e.png?v8",peach:"https://github.githubassets.com/images/icons/emoji/unicode/1f351.png?v8",peacock:"https://github.githubassets.com/images/icons/emoji/unicode/1f99a.png?v8",peanuts:"https://github.githubassets.com/images/icons/emoji/unicode/1f95c.png?v8",pear:"https://github.githubassets.com/images/icons/emoji/unicode/1f350.png?v8",pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58a.png?v8",pencil:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8",pencil2:"https://github.githubassets.com/images/icons/emoji/unicode/270f.png?v8",penguin:"https://github.githubassets.com/images/icons/emoji/unicode/1f427.png?v8",pensive:"https://github.githubassets.com/images/icons/emoji/unicode/1f614.png?v8",people_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f91d-1f9d1.png?v8",people_hugging:"https://github.githubassets.com/images/icons/emoji/unicode/1fac2.png?v8",performing_arts:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ad.png?v8",persevere:"https://github.githubassets.com/images/icons/emoji/unicode/1f623.png?v8",person_bald:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b2.png?v8",person_curly_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b1.png?v8",person_feeding_baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f37c.png?v8",person_fencing:"https://github.githubassets.com/images/icons/emoji/unicode/1f93a.png?v8",person_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bd.png?v8",person_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bc.png?v8",person_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935.png?v8",person_red_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b0.png?v8",person_white_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b3.png?v8",person_with_crown:"https://github.githubassets.com/images/icons/emoji/unicode/1fac5.png?v8",person_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9af.png?v8",person_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473.png?v8",person_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470.png?v8",peru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ea.png?v8",petri_dish:"https://github.githubassets.com/images/icons/emoji/unicode/1f9eb.png?v8",philippines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ed.png?v8",phone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8",pick:"https://github.githubassets.com/images/icons/emoji/unicode/26cf.png?v8",pickup_truck:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fb.png?v8",pie:"https://github.githubassets.com/images/icons/emoji/unicode/1f967.png?v8",pig:"https://github.githubassets.com/images/icons/emoji/unicode/1f437.png?v8",pig2:"https://github.githubassets.com/images/icons/emoji/unicode/1f416.png?v8",pig_nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f43d.png?v8",pill:"https://github.githubassets.com/images/icons/emoji/unicode/1f48a.png?v8",pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2708.png?v8",pinata:"https://github.githubassets.com/images/icons/emoji/unicode/1fa85.png?v8",pinched_fingers:"https://github.githubassets.com/images/icons/emoji/unicode/1f90c.png?v8",pinching_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f90f.png?v8",pineapple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34d.png?v8",ping_pong:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d3.png?v8",pink_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fa77.png?v8",pirate_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-2620.png?v8",pisces:"https://github.githubassets.com/images/icons/emoji/unicode/2653.png?v8",pitcairn_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f3.png?v8",pizza:"https://github.githubassets.com/images/icons/emoji/unicode/1f355.png?v8",placard:"https://github.githubassets.com/images/icons/emoji/unicode/1faa7.png?v8",place_of_worship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d0.png?v8",plate_with_cutlery:"https://github.githubassets.com/images/icons/emoji/unicode/1f37d.png?v8",play_or_pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ef.png?v8",playground_slide:"https://github.githubassets.com/images/icons/emoji/unicode/1f6dd.png?v8",pleading_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f97a.png?v8",plunger:"https://github.githubassets.com/images/icons/emoji/unicode/1faa0.png?v8",point_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f447.png?v8",point_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f448.png?v8",point_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f449.png?v8",point_up:"https://github.githubassets.com/images/icons/emoji/unicode/261d.png?v8",point_up_2:"https://github.githubassets.com/images/icons/emoji/unicode/1f446.png?v8",poland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f1.png?v8",polar_bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f43b-2744.png?v8",police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f693.png?v8",police_officer:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8",policeman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2642.png?v8",policewoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2640.png?v8",poodle:"https://github.githubassets.com/images/icons/emoji/unicode/1f429.png?v8",poop:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8",popcorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f37f.png?v8",portugal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f9.png?v8",post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e3.png?v8",postal_horn:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ef.png?v8",postbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ee.png?v8",potable_water:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b0.png?v8",potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f954.png?v8",potted_plant:"https://github.githubassets.com/images/icons/emoji/unicode/1fab4.png?v8",pouch:"https://github.githubassets.com/images/icons/emoji/unicode/1f45d.png?v8",poultry_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f357.png?v8",pound:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b7.png?v8",pouring_liquid:"https://github.githubassets.com/images/icons/emoji/unicode/1fad7.png?v8",pout:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8",pouting_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63e.png?v8",pouting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e.png?v8",pouting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2642.png?v8",pouting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2640.png?v8",pray:"https://github.githubassets.com/images/icons/emoji/unicode/1f64f.png?v8",prayer_beads:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ff.png?v8",pregnant_man:"https://github.githubassets.com/images/icons/emoji/unicode/1fac3.png?v8",pregnant_person:"https://github.githubassets.com/images/icons/emoji/unicode/1fac4.png?v8",pregnant_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f930.png?v8",pretzel:"https://github.githubassets.com/images/icons/emoji/unicode/1f968.png?v8",previous_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ee.png?v8",prince:"https://github.githubassets.com/images/icons/emoji/unicode/1f934.png?v8",princess:"https://github.githubassets.com/images/icons/emoji/unicode/1f478.png?v8",printer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a8.png?v8",probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9af.png?v8",puerto_rico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f7.png?v8",punch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8",purple_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e3.png?v8",purple_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49c.png?v8",purple_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7ea.png?v8",purse:"https://github.githubassets.com/images/icons/emoji/unicode/1f45b.png?v8",pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cc.png?v8",put_litter_in_its_place:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ae.png?v8",qatar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f6-1f1e6.png?v8",question:"https://github.githubassets.com/images/icons/emoji/unicode/2753.png?v8",rabbit:"https://github.githubassets.com/images/icons/emoji/unicode/1f430.png?v8",rabbit2:"https://github.githubassets.com/images/icons/emoji/unicode/1f407.png?v8",raccoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f99d.png?v8",racehorse:"https://github.githubassets.com/images/icons/emoji/unicode/1f40e.png?v8",racing_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ce.png?v8",radio:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fb.png?v8",radio_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f518.png?v8",radioactive:"https://github.githubassets.com/images/icons/emoji/unicode/2622.png?v8",rage:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8",rage1:"https://github.githubassets.com/images/icons/emoji/rage1.png?v8",rage2:"https://github.githubassets.com/images/icons/emoji/rage2.png?v8",rage3:"https://github.githubassets.com/images/icons/emoji/rage3.png?v8",rage4:"https://github.githubassets.com/images/icons/emoji/rage4.png?v8",railway_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f683.png?v8",railway_track:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e4.png?v8",rainbow:"https://github.githubassets.com/images/icons/emoji/unicode/1f308.png?v8",rainbow_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3-1f308.png?v8",raised_back_of_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f91a.png?v8",raised_eyebrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f928.png?v8",raised_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8",raised_hand_with_fingers_splayed:"https://github.githubassets.com/images/icons/emoji/unicode/1f590.png?v8",raised_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f64c.png?v8",raising_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b.png?v8",raising_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2642.png?v8",raising_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2640.png?v8",ram:"https://github.githubassets.com/images/icons/emoji/unicode/1f40f.png?v8",ramen:"https://github.githubassets.com/images/icons/emoji/unicode/1f35c.png?v8",rat:"https://github.githubassets.com/images/icons/emoji/unicode/1f400.png?v8",razor:"https://github.githubassets.com/images/icons/emoji/unicode/1fa92.png?v8",receipt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fe.png?v8",record_button:"https://github.githubassets.com/images/icons/emoji/unicode/23fa.png?v8",recycle:"https://github.githubassets.com/images/icons/emoji/unicode/267b.png?v8",red_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8",red_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f534.png?v8",red_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e7.png?v8",red_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b0.png?v8",red_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b0.png?v8",red_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e5.png?v8",registered:"https://github.githubassets.com/images/icons/emoji/unicode/00ae.png?v8",relaxed:"https://github.githubassets.com/images/icons/emoji/unicode/263a.png?v8",relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f60c.png?v8",reminder_ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f397.png?v8",repeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f501.png?v8",repeat_one:"https://github.githubassets.com/images/icons/emoji/unicode/1f502.png?v8",rescue_worker_helmet:"https://github.githubassets.com/images/icons/emoji/unicode/26d1.png?v8",restroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bb.png?v8",reunion:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1ea.png?v8",revolving_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f49e.png?v8",rewind:"https://github.githubassets.com/images/icons/emoji/unicode/23ea.png?v8",rhinoceros:"https://github.githubassets.com/images/icons/emoji/unicode/1f98f.png?v8",ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f380.png?v8",rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f35a.png?v8",rice_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f359.png?v8",rice_cracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f358.png?v8",rice_scene:"https://github.githubassets.com/images/icons/emoji/unicode/1f391.png?v8",right_anger_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ef.png?v8",rightwards_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf1.png?v8",rightwards_pushing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf8.png?v8",ring:"https://github.githubassets.com/images/icons/emoji/unicode/1f48d.png?v8",ring_buoy:"https://github.githubassets.com/images/icons/emoji/unicode/1f6df.png?v8",ringed_planet:"https://github.githubassets.com/images/icons/emoji/unicode/1fa90.png?v8",robot:"https://github.githubassets.com/images/icons/emoji/unicode/1f916.png?v8",rock:"https://github.githubassets.com/images/icons/emoji/unicode/1faa8.png?v8",rocket:"https://github.githubassets.com/images/icons/emoji/unicode/1f680.png?v8",rofl:"https://github.githubassets.com/images/icons/emoji/unicode/1f923.png?v8",roll_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f644.png?v8",roll_of_paper:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fb.png?v8",roller_coaster:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a2.png?v8",roller_skate:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fc.png?v8",romania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f4.png?v8",rooster:"https://github.githubassets.com/images/icons/emoji/unicode/1f413.png?v8",rose:"https://github.githubassets.com/images/icons/emoji/unicode/1f339.png?v8",rosette:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f5.png?v8",rotating_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a8.png?v8",round_pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cd.png?v8",rowboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3.png?v8",rowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2642.png?v8",rowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2640.png?v8",ru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fa.png?v8",rugby_football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c9.png?v8",runner:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8",running:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8",running_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2642.png?v8",running_shirt_with_sash:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bd.png?v8",running_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2640.png?v8",rwanda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fc.png?v8",sa:"https://github.githubassets.com/images/icons/emoji/unicode/1f202.png?v8",safety_pin:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f7.png?v8",safety_vest:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ba.png?v8",sagittarius:"https://github.githubassets.com/images/icons/emoji/unicode/2650.png?v8",sailboat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8",sake:"https://github.githubassets.com/images/icons/emoji/unicode/1f376.png?v8",salt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c2.png?v8",saluting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae1.png?v8",samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1f8.png?v8",san_marino:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f2.png?v8",sandal:"https://github.githubassets.com/images/icons/emoji/unicode/1f461.png?v8",sandwich:"https://github.githubassets.com/images/icons/emoji/unicode/1f96a.png?v8",santa:"https://github.githubassets.com/images/icons/emoji/unicode/1f385.png?v8",sao_tome_principe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f9.png?v8",sari:"https://github.githubassets.com/images/icons/emoji/unicode/1f97b.png?v8",sassy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8",sassy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8",satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e1.png?v8",satisfied:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8",saudi_arabia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e6.png?v8",sauna_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2642.png?v8",sauna_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6.png?v8",sauna_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2640.png?v8",sauropod:"https://github.githubassets.com/images/icons/emoji/unicode/1f995.png?v8",saxophone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b7.png?v8",scarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e3.png?v8",school:"https://github.githubassets.com/images/icons/emoji/unicode/1f3eb.png?v8",school_satchel:"https://github.githubassets.com/images/icons/emoji/unicode/1f392.png?v8",scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f52c.png?v8",scissors:"https://github.githubassets.com/images/icons/emoji/unicode/2702.png?v8",scorpion:"https://github.githubassets.com/images/icons/emoji/unicode/1f982.png?v8",scorpius:"https://github.githubassets.com/images/icons/emoji/unicode/264f.png?v8",scotland:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png?v8",scream:"https://github.githubassets.com/images/icons/emoji/unicode/1f631.png?v8",scream_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f640.png?v8",screwdriver:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9b.png?v8",scroll:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dc.png?v8",seal:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ad.png?v8",seat:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ba.png?v8",secret:"https://github.githubassets.com/images/icons/emoji/unicode/3299.png?v8",see_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f648.png?v8",seedling:"https://github.githubassets.com/images/icons/emoji/unicode/1f331.png?v8",selfie:"https://github.githubassets.com/images/icons/emoji/unicode/1f933.png?v8",senegal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f3.png?v8",serbia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f8.png?v8",service_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f415-1f9ba.png?v8",seven:"https://github.githubassets.com/images/icons/emoji/unicode/0037-20e3.png?v8",sewing_needle:"https://github.githubassets.com/images/icons/emoji/unicode/1faa1.png?v8",seychelles:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e8.png?v8",shaking_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae8.png?v8",shallow_pan_of_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f958.png?v8",shamrock:"https://github.githubassets.com/images/icons/emoji/unicode/2618.png?v8",shark:"https://github.githubassets.com/images/icons/emoji/unicode/1f988.png?v8",shaved_ice:"https://github.githubassets.com/images/icons/emoji/unicode/1f367.png?v8",sheep:"https://github.githubassets.com/images/icons/emoji/unicode/1f411.png?v8",shell:"https://github.githubassets.com/images/icons/emoji/unicode/1f41a.png?v8",shield:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e1.png?v8",shinto_shrine:"https://github.githubassets.com/images/icons/emoji/unicode/26e9.png?v8",ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a2.png?v8",shipit:"https://github.githubassets.com/images/icons/emoji/shipit.png?v8",shirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8",shit:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8",shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8",shopping:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cd.png?v8",shopping_cart:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d2.png?v8",shorts:"https://github.githubassets.com/images/icons/emoji/unicode/1fa73.png?v8",shower:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bf.png?v8",shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f990.png?v8",shrug:"https://github.githubassets.com/images/icons/emoji/unicode/1f937.png?v8",shushing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92b.png?v8",sierra_leone:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f1.png?v8",signal_strength:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f6.png?v8",singapore:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ec.png?v8",singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a4.png?v8",sint_maarten:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fd.png?v8",six:"https://github.githubassets.com/images/icons/emoji/unicode/0036-20e3.png?v8",six_pointed_star:"https://github.githubassets.com/images/icons/emoji/unicode/1f52f.png?v8",skateboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f9.png?v8",ski:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bf.png?v8",skier:"https://github.githubassets.com/images/icons/emoji/unicode/26f7.png?v8",skull:"https://github.githubassets.com/images/icons/emoji/unicode/1f480.png?v8",skull_and_crossbones:"https://github.githubassets.com/images/icons/emoji/unicode/2620.png?v8",skunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a8.png?v8",sled:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f7.png?v8",sleeping:"https://github.githubassets.com/images/icons/emoji/unicode/1f634.png?v8",sleeping_bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cc.png?v8",sleepy:"https://github.githubassets.com/images/icons/emoji/unicode/1f62a.png?v8",slightly_frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f641.png?v8",slightly_smiling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f642.png?v8",slot_machine:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b0.png?v8",sloth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a5.png?v8",slovakia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f0.png?v8",slovenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ee.png?v8",small_airplane:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e9.png?v8",small_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f539.png?v8",small_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f538.png?v8",small_red_triangle:"https://github.githubassets.com/images/icons/emoji/unicode/1f53a.png?v8",small_red_triangle_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f53b.png?v8",smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f604.png?v8",smile_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f638.png?v8",smiley:"https://github.githubassets.com/images/icons/emoji/unicode/1f603.png?v8",smiley_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63a.png?v8",smiling_face_with_tear:"https://github.githubassets.com/images/icons/emoji/unicode/1f972.png?v8",smiling_face_with_three_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f970.png?v8",smiling_imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f608.png?v8",smirk:"https://github.githubassets.com/images/icons/emoji/unicode/1f60f.png?v8",smirk_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63c.png?v8",smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ac.png?v8",snail:"https://github.githubassets.com/images/icons/emoji/unicode/1f40c.png?v8",snake:"https://github.githubassets.com/images/icons/emoji/unicode/1f40d.png?v8",sneezing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f927.png?v8",snowboarder:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c2.png?v8",snowflake:"https://github.githubassets.com/images/icons/emoji/unicode/2744.png?v8",snowman:"https://github.githubassets.com/images/icons/emoji/unicode/26c4.png?v8",snowman_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/2603.png?v8",soap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fc.png?v8",sob:"https://github.githubassets.com/images/icons/emoji/unicode/1f62d.png?v8",soccer:"https://github.githubassets.com/images/icons/emoji/unicode/26bd.png?v8",socks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e6.png?v8",softball:"https://github.githubassets.com/images/icons/emoji/unicode/1f94e.png?v8",solomon_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e7.png?v8",somalia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f4.png?v8",soon:"https://github.githubassets.com/images/icons/emoji/unicode/1f51c.png?v8",sos:"https://github.githubassets.com/images/icons/emoji/unicode/1f198.png?v8",sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f509.png?v8",south_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1e6.png?v8",south_georgia_south_sandwich_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f8.png?v8",south_sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f8.png?v8",space_invader:"https://github.githubassets.com/images/icons/emoji/unicode/1f47e.png?v8",spades:"https://github.githubassets.com/images/icons/emoji/unicode/2660.png?v8",spaghetti:"https://github.githubassets.com/images/icons/emoji/unicode/1f35d.png?v8",sparkle:"https://github.githubassets.com/images/icons/emoji/unicode/2747.png?v8",sparkler:"https://github.githubassets.com/images/icons/emoji/unicode/1f387.png?v8",sparkles:"https://github.githubassets.com/images/icons/emoji/unicode/2728.png?v8",sparkling_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f496.png?v8",speak_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f64a.png?v8",speaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f508.png?v8",speaking_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e3.png?v8",speech_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ac.png?v8",speedboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a4.png?v8",spider:"https://github.githubassets.com/images/icons/emoji/unicode/1f577.png?v8",spider_web:"https://github.githubassets.com/images/icons/emoji/unicode/1f578.png?v8",spiral_calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d3.png?v8",spiral_notepad:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d2.png?v8",sponge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fd.png?v8",spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f944.png?v8",squid:"https://github.githubassets.com/images/icons/emoji/unicode/1f991.png?v8",sri_lanka:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f0.png?v8",st_barthelemy:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f1.png?v8",st_helena:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ed.png?v8",st_kitts_nevis:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f3.png?v8",st_lucia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e8.png?v8",st_martin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1eb.png?v8",st_pierre_miquelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f2.png?v8",st_vincent_grenadines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e8.png?v8",stadium:"https://github.githubassets.com/images/icons/emoji/unicode/1f3df.png?v8",standing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2642.png?v8",standing_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd.png?v8",standing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2640.png?v8",star:"https://github.githubassets.com/images/icons/emoji/unicode/2b50.png?v8",star2:"https://github.githubassets.com/images/icons/emoji/unicode/1f31f.png?v8",star_and_crescent:"https://github.githubassets.com/images/icons/emoji/unicode/262a.png?v8",star_of_david:"https://github.githubassets.com/images/icons/emoji/unicode/2721.png?v8",star_struck:"https://github.githubassets.com/images/icons/emoji/unicode/1f929.png?v8",stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f320.png?v8",station:"https://github.githubassets.com/images/icons/emoji/unicode/1f689.png?v8",statue_of_liberty:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fd.png?v8",steam_locomotive:"https://github.githubassets.com/images/icons/emoji/unicode/1f682.png?v8",stethoscope:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7a.png?v8",stew:"https://github.githubassets.com/images/icons/emoji/unicode/1f372.png?v8",stop_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f9.png?v8",stop_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d1.png?v8",stopwatch:"https://github.githubassets.com/images/icons/emoji/unicode/23f1.png?v8",straight_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cf.png?v8",strawberry:"https://github.githubassets.com/images/icons/emoji/unicode/1f353.png?v8",stuck_out_tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f61b.png?v8",stuck_out_tongue_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61d.png?v8",stuck_out_tongue_winking_eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f61c.png?v8",student:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f393.png?v8",studio_microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f399.png?v8",stuffed_flatbread:"https://github.githubassets.com/images/icons/emoji/unicode/1f959.png?v8",sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e9.png?v8",sun_behind_large_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f325.png?v8",sun_behind_rain_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f326.png?v8",sun_behind_small_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f324.png?v8",sun_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31e.png?v8",sunflower:"https://github.githubassets.com/images/icons/emoji/unicode/1f33b.png?v8",sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f60e.png?v8",sunny:"https://github.githubassets.com/images/icons/emoji/unicode/2600.png?v8",sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f305.png?v8",sunrise_over_mountains:"https://github.githubassets.com/images/icons/emoji/unicode/1f304.png?v8",superhero:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8.png?v8",superhero_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2642.png?v8",superhero_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2640.png?v8",supervillain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9.png?v8",supervillain_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2642.png?v8",supervillain_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2640.png?v8",surfer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4.png?v8",surfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2642.png?v8",surfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2640.png?v8",suriname:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f7.png?v8",sushi:"https://github.githubassets.com/images/icons/emoji/unicode/1f363.png?v8",suspect:"https://github.githubassets.com/images/icons/emoji/suspect.png?v8",suspension_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69f.png?v8",svalbard_jan_mayen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ef.png?v8",swan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a2.png?v8",swaziland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ff.png?v8",sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f613.png?v8",sweat_drops:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a6.png?v8",sweat_smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f605.png?v8",sweden:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ea.png?v8",sweet_potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f360.png?v8",swim_brief:"https://github.githubassets.com/images/icons/emoji/unicode/1fa72.png?v8",swimmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca.png?v8",swimming_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2642.png?v8",swimming_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2640.png?v8",switzerland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ed.png?v8",symbols:"https://github.githubassets.com/images/icons/emoji/unicode/1f523.png?v8",synagogue:"https://github.githubassets.com/images/icons/emoji/unicode/1f54d.png?v8",syria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fe.png?v8",syringe:"https://github.githubassets.com/images/icons/emoji/unicode/1f489.png?v8","t-rex":"https://github.githubassets.com/images/icons/emoji/unicode/1f996.png?v8",taco:"https://github.githubassets.com/images/icons/emoji/unicode/1f32e.png?v8",tada:"https://github.githubassets.com/images/icons/emoji/unicode/1f389.png?v8",taiwan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fc.png?v8",tajikistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ef.png?v8",takeout_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f961.png?v8",tamale:"https://github.githubassets.com/images/icons/emoji/unicode/1fad4.png?v8",tanabata_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f38b.png?v8",tangerine:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8",tanzania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ff.png?v8",taurus:"https://github.githubassets.com/images/icons/emoji/unicode/2649.png?v8",taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f695.png?v8",tea:"https://github.githubassets.com/images/icons/emoji/unicode/1f375.png?v8",teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3eb.png?v8",teapot:"https://github.githubassets.com/images/icons/emoji/unicode/1fad6.png?v8",technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bb.png?v8",teddy_bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f8.png?v8",telephone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8",telephone_receiver:"https://github.githubassets.com/images/icons/emoji/unicode/1f4de.png?v8",telescope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52d.png?v8",tennis:"https://github.githubassets.com/images/icons/emoji/unicode/1f3be.png?v8",tent:"https://github.githubassets.com/images/icons/emoji/unicode/26fa.png?v8",test_tube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ea.png?v8",thailand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ed.png?v8",thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f321.png?v8",thinking:"https://github.githubassets.com/images/icons/emoji/unicode/1f914.png?v8",thong_sandal:"https://github.githubassets.com/images/icons/emoji/unicode/1fa74.png?v8",thought_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ad.png?v8",thread:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f5.png?v8",three:"https://github.githubassets.com/images/icons/emoji/unicode/0033-20e3.png?v8",thumbsdown:"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8",thumbsup:"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8",ticket:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ab.png?v8",tickets:"https://github.githubassets.com/images/icons/emoji/unicode/1f39f.png?v8",tiger:"https://github.githubassets.com/images/icons/emoji/unicode/1f42f.png?v8",tiger2:"https://github.githubassets.com/images/icons/emoji/unicode/1f405.png?v8",timer_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f2.png?v8",timor_leste:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f1.png?v8",tipping_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8",tipping_hand_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8",tipping_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8",tired_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f62b.png?v8",tm:"https://github.githubassets.com/images/icons/emoji/unicode/2122.png?v8",togo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ec.png?v8",toilet:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bd.png?v8",tokelau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f0.png?v8",tokyo_tower:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fc.png?v8",tomato:"https://github.githubassets.com/images/icons/emoji/unicode/1f345.png?v8",tonga:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f4.png?v8",tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f445.png?v8",toolbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f0.png?v8",tooth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b7.png?v8",toothbrush:"https://github.githubassets.com/images/icons/emoji/unicode/1faa5.png?v8",top:"https://github.githubassets.com/images/icons/emoji/unicode/1f51d.png?v8",tophat:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a9.png?v8",tornado:"https://github.githubassets.com/images/icons/emoji/unicode/1f32a.png?v8",tr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f7.png?v8",trackball:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b2.png?v8",tractor:"https://github.githubassets.com/images/icons/emoji/unicode/1f69c.png?v8",traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a5.png?v8",train:"https://github.githubassets.com/images/icons/emoji/unicode/1f68b.png?v8",train2:"https://github.githubassets.com/images/icons/emoji/unicode/1f686.png?v8",tram:"https://github.githubassets.com/images/icons/emoji/unicode/1f68a.png?v8",transgender_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3-26a7.png?v8",transgender_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/26a7.png?v8",triangular_flag_on_post:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a9.png?v8",triangular_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d0.png?v8",trident:"https://github.githubassets.com/images/icons/emoji/unicode/1f531.png?v8",trinidad_tobago:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f9.png?v8",tristan_da_cunha:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e6.png?v8",triumph:"https://github.githubassets.com/images/icons/emoji/unicode/1f624.png?v8",troll:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cc.png?v8",trolleybus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68e.png?v8",trollface:"https://github.githubassets.com/images/icons/emoji/trollface.png?v8",trophy:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c6.png?v8",tropical_drink:"https://github.githubassets.com/images/icons/emoji/unicode/1f379.png?v8",tropical_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f420.png?v8",truck:"https://github.githubassets.com/images/icons/emoji/unicode/1f69a.png?v8",trumpet:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ba.png?v8",tshirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8",tulip:"https://github.githubassets.com/images/icons/emoji/unicode/1f337.png?v8",tumbler_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f943.png?v8",tunisia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f3.png?v8",turkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f983.png?v8",turkmenistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f2.png?v8",turks_caicos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e8.png?v8",turtle:"https://github.githubassets.com/images/icons/emoji/unicode/1f422.png?v8",tuvalu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fb.png?v8",tv:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fa.png?v8",twisted_rightwards_arrows:"https://github.githubassets.com/images/icons/emoji/unicode/1f500.png?v8",two:"https://github.githubassets.com/images/icons/emoji/unicode/0032-20e3.png?v8",two_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f495.png?v8",two_men_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46c.png?v8",two_women_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46d.png?v8",u5272:"https://github.githubassets.com/images/icons/emoji/unicode/1f239.png?v8",u5408:"https://github.githubassets.com/images/icons/emoji/unicode/1f234.png?v8",u55b6:"https://github.githubassets.com/images/icons/emoji/unicode/1f23a.png?v8",u6307:"https://github.githubassets.com/images/icons/emoji/unicode/1f22f.png?v8",u6708:"https://github.githubassets.com/images/icons/emoji/unicode/1f237.png?v8",u6709:"https://github.githubassets.com/images/icons/emoji/unicode/1f236.png?v8",u6e80:"https://github.githubassets.com/images/icons/emoji/unicode/1f235.png?v8",u7121:"https://github.githubassets.com/images/icons/emoji/unicode/1f21a.png?v8",u7533:"https://github.githubassets.com/images/icons/emoji/unicode/1f238.png?v8",u7981:"https://github.githubassets.com/images/icons/emoji/unicode/1f232.png?v8",u7a7a:"https://github.githubassets.com/images/icons/emoji/unicode/1f233.png?v8",uganda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ec.png?v8",uk:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8",ukraine:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1e6.png?v8",umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2614.png?v8",unamused:"https://github.githubassets.com/images/icons/emoji/unicode/1f612.png?v8",underage:"https://github.githubassets.com/images/icons/emoji/unicode/1f51e.png?v8",unicorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f984.png?v8",united_arab_emirates:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ea.png?v8",united_nations:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f3.png?v8",unlock:"https://github.githubassets.com/images/icons/emoji/unicode/1f513.png?v8",up:"https://github.githubassets.com/images/icons/emoji/unicode/1f199.png?v8",upside_down_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f643.png?v8",uruguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1fe.png?v8",us:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f8.png?v8",us_outlying_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f2.png?v8",us_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ee.png?v8",uzbekistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ff.png?v8",v:"https://github.githubassets.com/images/icons/emoji/unicode/270c.png?v8",vampire:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db.png?v8",vampire_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2642.png?v8",vampire_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2640.png?v8",vanuatu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1fa.png?v8",vatican_city:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e6.png?v8",venezuela:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ea.png?v8",vertical_traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a6.png?v8",vhs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fc.png?v8",vibration_mode:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f3.png?v8",video_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f9.png?v8",video_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ae.png?v8",vietnam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1f3.png?v8",violin:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bb.png?v8",virgo:"https://github.githubassets.com/images/icons/emoji/unicode/264d.png?v8",volcano:"https://github.githubassets.com/images/icons/emoji/unicode/1f30b.png?v8",volleyball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d0.png?v8",vomiting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92e.png?v8",vs:"https://github.githubassets.com/images/icons/emoji/unicode/1f19a.png?v8",vulcan_salute:"https://github.githubassets.com/images/icons/emoji/unicode/1f596.png?v8",waffle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c7.png?v8",wales:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png?v8",walking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6.png?v8",walking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2642.png?v8",walking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2640.png?v8",wallis_futuna:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1eb.png?v8",waning_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f318.png?v8",waning_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f316.png?v8",warning:"https://github.githubassets.com/images/icons/emoji/unicode/26a0.png?v8",wastebasket:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d1.png?v8",watch:"https://github.githubassets.com/images/icons/emoji/unicode/231a.png?v8",water_buffalo:"https://github.githubassets.com/images/icons/emoji/unicode/1f403.png?v8",water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d.png?v8",watermelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f349.png?v8",wave:"https://github.githubassets.com/images/icons/emoji/unicode/1f44b.png?v8",wavy_dash:"https://github.githubassets.com/images/icons/emoji/unicode/3030.png?v8",waxing_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f312.png?v8",waxing_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8",wc:"https://github.githubassets.com/images/icons/emoji/unicode/1f6be.png?v8",weary:"https://github.githubassets.com/images/icons/emoji/unicode/1f629.png?v8",wedding:"https://github.githubassets.com/images/icons/emoji/unicode/1f492.png?v8",weight_lifting:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb.png?v8",weight_lifting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2642.png?v8",weight_lifting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2640.png?v8",western_sahara:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ed.png?v8",whale:"https://github.githubassets.com/images/icons/emoji/unicode/1f433.png?v8",whale2:"https://github.githubassets.com/images/icons/emoji/unicode/1f40b.png?v8",wheel:"https://github.githubassets.com/images/icons/emoji/unicode/1f6de.png?v8",wheel_of_dharma:"https://github.githubassets.com/images/icons/emoji/unicode/2638.png?v8",wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/267f.png?v8",white_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2705.png?v8",white_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26aa.png?v8",white_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3.png?v8",white_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ae.png?v8",white_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b3.png?v8",white_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b3.png?v8",white_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90d.png?v8",white_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1c.png?v8",white_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fd.png?v8",white_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fb.png?v8",white_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25ab.png?v8",white_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f533.png?v8",wilted_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f940.png?v8",wind_chime:"https://github.githubassets.com/images/icons/emoji/unicode/1f390.png?v8",wind_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f32c.png?v8",window:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9f.png?v8",wine_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f377.png?v8",wing:"https://github.githubassets.com/images/icons/emoji/unicode/1fabd.png?v8",wink:"https://github.githubassets.com/images/icons/emoji/unicode/1f609.png?v8",wireless:"https://github.githubassets.com/images/icons/emoji/unicode/1f6dc.png?v8",wolf:"https://github.githubassets.com/images/icons/emoji/unicode/1f43a.png?v8",woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469.png?v8",woman_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a8.png?v8",woman_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f680.png?v8",woman_beard:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4-2640.png?v8",woman_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2640.png?v8",woman_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f373.png?v8",woman_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8",woman_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2640.png?v8",woman_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3ed.png?v8",woman_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f33e.png?v8",woman_feeding_baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f37c.png?v8",woman_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f692.png?v8",woman_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2695.png?v8",woman_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bd.png?v8",woman_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bc.png?v8",woman_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935-2640.png?v8",woman_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2696.png?v8",woman_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2640.png?v8",woman_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f527.png?v8",woman_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bc.png?v8",woman_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2708.png?v8",woman_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2640.png?v8",woman_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2640.png?v8",woman_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f52c.png?v8",woman_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2640.png?v8",woman_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a4.png?v8",woman_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f393.png?v8",woman_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3eb.png?v8",woman_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bb.png?v8",woman_with_headscarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d5.png?v8",woman_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9af.png?v8",woman_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2640.png?v8",woman_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470-2640.png?v8",womans_clothes:"https://github.githubassets.com/images/icons/emoji/unicode/1f45a.png?v8",womans_hat:"https://github.githubassets.com/images/icons/emoji/unicode/1f452.png?v8",women_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2640.png?v8",womens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ba.png?v8",wood:"https://github.githubassets.com/images/icons/emoji/unicode/1fab5.png?v8",woozy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f974.png?v8",world_map:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fa.png?v8",worm:"https://github.githubassets.com/images/icons/emoji/unicode/1fab1.png?v8",worried:"https://github.githubassets.com/images/icons/emoji/unicode/1f61f.png?v8",wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f527.png?v8",wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c.png?v8",writing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270d.png?v8",x:"https://github.githubassets.com/images/icons/emoji/unicode/274c.png?v8",x_ray:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7b.png?v8",yarn:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f6.png?v8",yawning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f971.png?v8",yellow_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e1.png?v8",yellow_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49b.png?v8",yellow_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e8.png?v8",yemen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1ea.png?v8",yen:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b4.png?v8",yin_yang:"https://github.githubassets.com/images/icons/emoji/unicode/262f.png?v8",yo_yo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa80.png?v8",yum:"https://github.githubassets.com/images/icons/emoji/unicode/1f60b.png?v8",zambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1f2.png?v8",zany_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92a.png?v8",zap:"https://github.githubassets.com/images/icons/emoji/unicode/26a1.png?v8",zebra:"https://github.githubassets.com/images/icons/emoji/unicode/1f993.png?v8",zero:"https://github.githubassets.com/images/icons/emoji/unicode/0030-20e3.png?v8",zimbabwe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1fc.png?v8",zipper_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f910.png?v8",zombie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df.png?v8",zombie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2642.png?v8",zombie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2640.png?v8",zzz:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a4.png?v8"},defaultOptions=("undefined"!=typeof window&&(window.githubEmojis=githubEmojiData),"undefined"!=typeof module&&(module.exports=githubEmojiData),{unicode:!1});function markedEmoji(n){if((n={...defaultOptions,...n}).emojis)return{extensions:[{name:"emoji",level:"inline",start(e){return e.indexOf(":")},tokenizer(e,t){e=/^:(.+?):/.exec(e);if(e){var i=e[1],s=n.emojis[i];if(s)return{type:"emoji",raw:e[0],name:i,emoji:s}}},renderer(e){return n.unicode?e.emoji:`${e.name}`}}]};throw new Error("Must provide emojis to markedEmoji")}function baseUrl(i){i=i.trim().replace(/\/+$/,"/");let s=/^[\w+]+:\/\//,n=s.test(i);var e="http://__dummy__";let o=new URL(i,e),a=e.length+(i.startsWith("/")?0:1);return{walkTokens(e){if(["link","image"].includes(e.type)&&!s.test(e.href)&&!e.href.startsWith("#"))if(n)try{e.href=new URL(e.href,i).href}catch(e){}else if(!e.href.startsWith("/"))try{var t=new URL(e.href,o).href;e.href=t.slice(a)}catch(e){}}}}((e,t)=>{"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})})(this,function(t){function e(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function i(e){t.defaults=e}t.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};let s=/[&<>"']/,R=new RegExp(s.source,"g"),n=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,F=new RegExp(n.source,"g"),q={"&":"&","<":"<",">":">",'"':""","'":"'"},o=e=>q[e];function a(e,t){if(t){if(s.test(e))return e.replace(R,o)}else if(n.test(e))return e.replace(F,o);return e}let H=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;let M=/(^|[^\[])\^/g;function c(e,t){let s="string"==typeof e?e:e.source,n=(t=t||"",{replace:(e,t)=>{let i="string"==typeof t?t:t.source;return i=i.replace(M,"$1"),s=s.replace(e,i),n},getRegex:()=>new RegExp(s,t)});return n}function u(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}var g={exec:()=>null};function r(e,t){var i=e.replace(/\|/g,(e,t,i)=>{let s=!1,n=t;for(;0<=--n&&"\\"===i[n];)s=!s;return s?"|":" |"}).split(/ \|/);let s=0;if(i[0].trim()||i.shift(),0t)i.splice(t);else for(;i.length{if(null===(e=e.match(/^(\s+)(?:```)/)))return t;let i=e[1];return t.split("\n").map(e=>{var t=e.match(/^\s+/);return null!==t&&([t]=t,t.length>=i.length)?e.slice(i.length):e}).join("\n")})(e,i[3]||"");return{type:"code",raw:e,lang:i[2]&&i[2].trim().replace(this.rules.inline.anyPunctuation,"$1"),text:t}}}heading(e){let i=this.rules.block.heading.exec(e);if(i){let t=i[2].trim();if(/#$/.test(t)){let e=h(t,"#");!this.options.pedantic&&e&&!/ $/.test(e)||(t=e.trim())}return{type:"heading",raw:i[0],depth:i[1].length,text:t,tokens:this.lexer.inline(t)}}}hr(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}}blockquote(e){var i=this.rules.block.blockquote.exec(e);if(i){let e=h(i[0].replace(/^ *>[ \t]?/gm,""),"\n"),t=this.lexer.state.top;this.lexer.state.top=!0;var s=this.lexer.blockTokens(e);return this.lexer.state.top=t,{type:"blockquote",raw:i[0],tokens:s,text:e}}}list(r){let h=this.rules.block.list.exec(r);if(h){let e=h[1].trim();var t=1" ".repeat(3*e.length)),o=r.split("\n",1)[0],a=0,c=(this.options.pedantic?(a=2,g=n.trimStart()):(a=4<(a=h[2].search(/[^ ]/))?1:a,g=n.slice(a),a+=h[1].length),!1);if(!n&&/^ *$/.test(o)&&(u+=o+"\n",r=r.substring(o.length+1),e=!0),!e){let e=new RegExp(`^ {0,${Math.min(3,a-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),t=new RegExp(`^ {0,${Math.min(3,a-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),i=new RegExp(`^ {0,${Math.min(3,a-1)}}(?:\`\`\`|~~~)`),s=new RegExp(`^ {0,${Math.min(3,a-1)}}#`);for(;r;){var l=r.split("\n",1)[0];if(o=l,this.options.pedantic&&(o=o.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),i.test(o))break;if(s.test(o))break;if(e.test(o))break;if(t.test(r))break;if(o.search(/[^ ]/)>=a||!o.trim())g+="\n"+o.slice(a);else{if(c)break;if(4<=n.search(/[^ ]/))break;if(i.test(n))break;if(s.test(n))break;if(t.test(n))break;g+="\n"+o}c||o.trim()||(c=!0),u+=l+"\n",r=r.substring(l.length+1),n=o.slice(a)}}m.loose||(s?m.loose=!0:/\n *\n *$/.test(u)&&(s=!0));let t,i=null;this.options.gfm&&(i=/^\[[ xX]\] /.exec(g))&&(t="[ ] "!==i[0],g=g.replace(/^\[[ xX]\] +/,"")),m.items.push({type:"list_item",raw:u,task:!!i,checked:t,loose:!1,text:g,tokens:[]}),m.raw+=u}m.items[m.items.length-1].raw=u.trimEnd(),m.items[m.items.length-1].text=g.trimEnd(),m.raw=m.raw.trimEnd();for(let i=0;i"space"===e.type),t=0/\n.*\n/.test(e.raw));m.loose=t}if(m.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=s[3]&&s[3].substring(1,s[3].length-1).replace(this.rules.inline.anyPunctuation,"$1");return{type:"def",tag:e,raw:s[0],href:t,title:i}}}table(e){e=this.rules.block.table.exec(e);if(e&&/[:|]/.test(e[2])){var t=r(e[1]),i=e[2].replace(/^\||\| *$/g,"").split("|"),s=e[3]&&e[3].trim()?e[3].replace(/\n[ \t]*$/,"").split("\n"):[],n={type:"table",raw:e[0],header:[],align:[],rows:[]};if(t.length===i.length){for(let e of i)/^ *-+: *$/.test(e)?n.align.push("right"):/^ *:-+: *$/.test(e)?n.align.push("center"):/^ *:-+ *$/.test(e)?n.align.push("left"):n.align.push(null);for(let e of t)n.header.push({text:e,tokens:this.lexer.inline(e)});for(let e of s)n.rows.push(r(e,n.header.length).map(e=>({text:e,tokens:this.lexer.inline(e)})));return n}}}lheading(e){e=this.rules.block.lheading.exec(e);if(e)return{type:"heading",raw:e[0],depth:"="===e[2].charAt(0)?1:2,text:e[1],tokens:this.lexer.inline(e[1])}}paragraph(e){var t=this.rules.block.paragraph.exec(e);if(t){let e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){e=this.rules.block.text.exec(e);if(e)return{type:"text",raw:e[0],text:e[0],tokens:this.lexer.inline(e[0])}}escape(e){e=this.rules.inline.escape.exec(e);if(e)return{type:"escape",raw:e[0],text:a(e[1])}}tag(e){e=this.rules.inline.tag.exec(e);if(e)return!this.lexer.state.inLink&&/^/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:e[0]}}link(e){let n=this.rules.inline.link.exec(e);if(n){let t=n[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;let e=h(t.slice(0,-1),"\\");if((t.length-e.length)%2==0)return}else{let e=(i=>{if(-1!==i.indexOf("()"[1])){let t=0;for(let e=0;e$/.test(t)?i.slice(1):i.slice(1,-1)),m(n,{href:i&&i.replace(this.rules.inline.anyPunctuation,"$1"),title:s&&s.replace(this.rules.inline.anyPunctuation,"$1")},n[0],this.lexer)}}reflink(e,t){let i;if(i=(i=this.rules.inline.reflink.exec(e))||this.rules.inline.nolink.exec(e)){let e=t[(i[2]||i[1]).replace(/\s+/g," ").toLowerCase()];if(e)return m(i,e,i[0],this.lexer);{let e=i[0].charAt(0);return{type:"text",raw:e,text:e}}}}emStrong(a,t,e=""){let c=this.rules.inline.emStrongLDelim.exec(a);if(c&&(!c[3]||!e.match(/[\p{L}\p{N}]/u))&&(!c[1]&&!c[2]||!e||this.rules.inline.punctuation.exec(e))){let i=[...c[0]].length-1,e,s,n=i,o=0;var u="*"===c[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*a.length+i);null!=(c=u.exec(t));)if(e=c[1]||c[2]||c[3]||c[4]||c[5]||c[6])if(s=[...e].length,c[3]||c[4])n+=s;else if((c[5]||c[6])&&i%3&&!((i+s)%3))o+=s;else if(!(0<(n-=s))){s=Math.min(s,s+n+o);let e=[...c[0]][0].length,t=a.slice(0,i+c.index+e+s);if(Math.min(i,s)%2){let e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}var g=t.slice(2,-2);return{type:"strong",raw:t,text:g,tokens:this.lexer.inlineTokens(g)}}}}codespan(t){t=this.rules.inline.code.exec(t);if(t){let e=t[2].replace(/\n/g," ");var i=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return e=a(e=i&&s?e.substring(1,e.length-1):e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}}del(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}}autolink(i){i=this.rules.inline.autolink.exec(i);if(i){let e,t;return t="@"===i[2]?"mailto:"+(e=a(i[1])):e=a(i[1]),{type:"link",raw:i[0],text:e,href:t,tokens:[{type:"text",raw:e,text:e}]}}}url(e){var i,s;if(i=this.rules.inline.url.exec(e)){let e,t;if("@"===i[2])e=a(i[0]),t="mailto:"+e;else{for(;s=i[0],i[0]=this.rules.inline._backpedal.exec(i[0])?.[0]??"",s!==i[0];);e=a(i[0]),t="www."===i[1]?"http://"+i[0]:i[0]}return{type:"link",raw:i[0],text:e,href:t,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){var t,e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?e[0]:a(e[0]),{type:"text",raw:e[0],text:t}}}let l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,f=/(?:[*+-]|\d{1,9}[.)])/,d=c(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,f).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),b=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,v=/(?!\s*\])(?:\\.|[^\[\]\\])+/,z=c(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",v).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),$=c(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,f).getRegex(),_="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",j=/|$))/,B=c("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",j).replace("tag",_).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y=c(b).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),w={blockquote:c(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",y).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:z,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:B,lheading:d,list:$,newline:/^(?: *(?:\n|$))+/,paragraph:y,table:g,text:/^[^\n]+/},W=c("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),U={...w,table:W,paragraph:c(b).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",W).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex()},Q={...w,html:c("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",j).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:g,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:c(b).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",d).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},K=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,V=/^( {2,}|\\)\n(?!\s*$)/,k="\\p{P}\\p{S}",Y=c(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,k).getRegex(),X=c(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,k).getRegex(),Z=c("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,k).getRegex(),G=c("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,k).getRegex(),J=c(/\\([punct])/,"gu").replace(/punct/g,k).getRegex(),ee=c(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),te=c(j).replace("(?:--\x3e|$)","--\x3e").getRegex(),ie=c("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",te).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),x=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,se=c(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",x).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ne=c(/^!?\[(label)\]\[(ref)\]/).replace("label",x).replace("ref",v).getRegex(),oe=c(/^!?\[(ref)\](?:\[\])?/).replace("ref",v).getRegex(),T={_backpedal:g,anyPunctuation:J,autolink:ee,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:V,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:g,emStrongLDelim:X,emStrongRDelimAst:Z,emStrongRDelimUnd:G,escape:K,link:se,nolink:oe,punctuation:Y,reflink:ne,reflinkSearch:c("reflink|nolink(?!\\()","g").replace("reflink",ne).replace("nolink",oe).getRegex(),tag:ie,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(i.length));n;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some(e=>!!(i=e.call({lexer:this},n,t))&&(n=n.substring(i.raw.length),t.push(i),!0))))if(i=this.tokenizer.space(n))n=n.substring(i.raw.length),1===i.raw.length&&0{"number"==typeof(s=e.call({lexer:this},i))&&0<=s&&(t=Math.min(t,s))}),t<1/0&&0<=t&&(o=n.substring(0,t+1))}if(this.state.top&&(i=this.tokenizer.paragraph(o)))e=t[t.length-1],s&&"paragraph"===e.type?(e.raw+="\n"+i.raw,e.text+="\n"+i.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=e.text):t.push(i),s=o.length!==n.length,n=n.substring(i.raw.length);else if(i=this.tokenizer.text(n))n=n.substring(i.raw.length),(e=t[t.length-1])&&"text"===e.type?(e.raw+="\n"+i.raw,e.text+="\n"+i.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=e.text):t.push(i);else if(n){let e="Infinite loop on byte: "+n.charCodeAt(0);if(this.options.silent){console.error(e);break}throw new Error(e)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(n,t=[]){let i,e,o,s,a,c,u=n;if(this.tokens.links){let e=Object.keys(this.tokens.links);if(0!!(i=e.call({lexer:this},n,t))&&(n=n.substring(i.raw.length),t.push(i),!0))))if(i=this.tokenizer.escape(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.tag(n))n=n.substring(i.raw.length),(e=t[t.length-1])&&"text"===i.type&&"text"===e.type?(e.raw+=i.raw,e.text+=i.text):t.push(i);else if(i=this.tokenizer.link(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.reflink(n,this.tokens.links))n=n.substring(i.raw.length),(e=t[t.length-1])&&"text"===i.type&&"text"===e.type?(e.raw+=i.raw,e.text+=i.text):t.push(i);else if(i=this.tokenizer.emStrong(n,u,c))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.codespan(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.br(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.del(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.autolink(n))n=n.substring(i.raw.length),t.push(i);else if(this.state.inLink||!(i=this.tokenizer.url(n))){if(o=n,this.options.extensions&&this.options.extensions.startInline){let t=1/0,i=n.slice(1),s;this.options.extensions.startInline.forEach(e=>{"number"==typeof(s=e.call({lexer:this},i))&&0<=s&&(t=Math.min(t,s))}),t<1/0&&0<=t&&(o=n.substring(0,t+1))}if(i=this.tokenizer.inlineText(o))n=n.substring(i.raw.length),"_"!==i.raw.slice(-1)&&(c=i.raw.slice(-1)),a=!0,(e=t[t.length-1])&&"text"===e.type?(e.raw+=i.raw,e.text+=i.text):t.push(i);else if(n){let e="Infinite loop on byte: "+n.charCodeAt(0);if(this.options.silent){console.error(e);break}throw new Error(e)}}else n=n.substring(i.raw.length),t.push(i);return t}}class D{options;constructor(e){this.options=e||t.defaults}code(e,t,i){t=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",t?'
'+(i?e:a(e,!0))+"
\n":"
"+(i?e:a(e,!0))+"
\n"}blockquote(e){return`
+let githubEmojiData={100:"https://github.githubassets.com/images/icons/emoji/unicode/1f4af.png?v8",1234:"https://github.githubassets.com/images/icons/emoji/unicode/1f522.png?v8","+1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8","-1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8","1st_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f947.png?v8","2nd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f948.png?v8","3rd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f949.png?v8","8ball":"https://github.githubassets.com/images/icons/emoji/unicode/1f3b1.png?v8",a:"https://github.githubassets.com/images/icons/emoji/unicode/1f170.png?v8",ab:"https://github.githubassets.com/images/icons/emoji/unicode/1f18e.png?v8",abacus:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ee.png?v8",abc:"https://github.githubassets.com/images/icons/emoji/unicode/1f524.png?v8",abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f521.png?v8",accept:"https://github.githubassets.com/images/icons/emoji/unicode/1f251.png?v8",accessibility:"https://github.githubassets.com/images/icons/emoji/accessibility.png?v8",accordion:"https://github.githubassets.com/images/icons/emoji/unicode/1fa97.png?v8",adhesive_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1fa79.png?v8",adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1.png?v8",aerial_tramway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a1.png?v8",afghanistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1eb.png?v8",airplane:"https://github.githubassets.com/images/icons/emoji/unicode/2708.png?v8",aland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fd.png?v8",alarm_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f0.png?v8",albania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f1.png?v8",alembic:"https://github.githubassets.com/images/icons/emoji/unicode/2697.png?v8",algeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ff.png?v8",alien:"https://github.githubassets.com/images/icons/emoji/unicode/1f47d.png?v8",ambulance:"https://github.githubassets.com/images/icons/emoji/unicode/1f691.png?v8",american_samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f8.png?v8",amphora:"https://github.githubassets.com/images/icons/emoji/unicode/1f3fa.png?v8",anatomical_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fac0.png?v8",anchor:"https://github.githubassets.com/images/icons/emoji/unicode/2693.png?v8",andorra:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e9.png?v8",angel:"https://github.githubassets.com/images/icons/emoji/unicode/1f47c.png?v8",anger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a2.png?v8",angola:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f4.png?v8",angry:"https://github.githubassets.com/images/icons/emoji/unicode/1f620.png?v8",anguilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ee.png?v8",anguished:"https://github.githubassets.com/images/icons/emoji/unicode/1f627.png?v8",ant:"https://github.githubassets.com/images/icons/emoji/unicode/1f41c.png?v8",antarctica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f6.png?v8",antigua_barbuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ec.png?v8",apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34e.png?v8",aquarius:"https://github.githubassets.com/images/icons/emoji/unicode/2652.png?v8",argentina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f7.png?v8",aries:"https://github.githubassets.com/images/icons/emoji/unicode/2648.png?v8",armenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f2.png?v8",arrow_backward:"https://github.githubassets.com/images/icons/emoji/unicode/25c0.png?v8",arrow_double_down:"https://github.githubassets.com/images/icons/emoji/unicode/23ec.png?v8",arrow_double_up:"https://github.githubassets.com/images/icons/emoji/unicode/23eb.png?v8",arrow_down:"https://github.githubassets.com/images/icons/emoji/unicode/2b07.png?v8",arrow_down_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53d.png?v8",arrow_forward:"https://github.githubassets.com/images/icons/emoji/unicode/25b6.png?v8",arrow_heading_down:"https://github.githubassets.com/images/icons/emoji/unicode/2935.png?v8",arrow_heading_up:"https://github.githubassets.com/images/icons/emoji/unicode/2934.png?v8",arrow_left:"https://github.githubassets.com/images/icons/emoji/unicode/2b05.png?v8",arrow_lower_left:"https://github.githubassets.com/images/icons/emoji/unicode/2199.png?v8",arrow_lower_right:"https://github.githubassets.com/images/icons/emoji/unicode/2198.png?v8",arrow_right:"https://github.githubassets.com/images/icons/emoji/unicode/27a1.png?v8",arrow_right_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21aa.png?v8",arrow_up:"https://github.githubassets.com/images/icons/emoji/unicode/2b06.png?v8",arrow_up_down:"https://github.githubassets.com/images/icons/emoji/unicode/2195.png?v8",arrow_up_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53c.png?v8",arrow_upper_left:"https://github.githubassets.com/images/icons/emoji/unicode/2196.png?v8",arrow_upper_right:"https://github.githubassets.com/images/icons/emoji/unicode/2197.png?v8",arrows_clockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f503.png?v8",arrows_counterclockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f504.png?v8",art:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a8.png?v8",articulated_lorry:"https://github.githubassets.com/images/icons/emoji/unicode/1f69b.png?v8",artificial_satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f0.png?v8",artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a8.png?v8",aruba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fc.png?v8",ascension_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e8.png?v8",asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/002a-20e3.png?v8",astonished:"https://github.githubassets.com/images/icons/emoji/unicode/1f632.png?v8",astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f680.png?v8",athletic_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45f.png?v8",atm:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e7.png?v8",atom:"https://github.githubassets.com/images/icons/emoji/atom.png?v8",atom_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/269b.png?v8",australia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fa.png?v8",austria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f9.png?v8",auto_rickshaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fa.png?v8",avocado:"https://github.githubassets.com/images/icons/emoji/unicode/1f951.png?v8",axe:"https://github.githubassets.com/images/icons/emoji/unicode/1fa93.png?v8",azerbaijan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ff.png?v8",b:"https://github.githubassets.com/images/icons/emoji/unicode/1f171.png?v8",baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f476.png?v8",baby_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f37c.png?v8",baby_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f424.png?v8",baby_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bc.png?v8",back:"https://github.githubassets.com/images/icons/emoji/unicode/1f519.png?v8",bacon:"https://github.githubassets.com/images/icons/emoji/unicode/1f953.png?v8",badger:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a1.png?v8",badminton:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f8.png?v8",bagel:"https://github.githubassets.com/images/icons/emoji/unicode/1f96f.png?v8",baggage_claim:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c4.png?v8",baguette_bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f956.png?v8",bahamas:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f8.png?v8",bahrain:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ed.png?v8",balance_scale:"https://github.githubassets.com/images/icons/emoji/unicode/2696.png?v8",bald_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b2.png?v8",bald_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b2.png?v8",ballet_shoes:"https://github.githubassets.com/images/icons/emoji/unicode/1fa70.png?v8",balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f388.png?v8",ballot_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5f3.png?v8",ballot_box_with_check:"https://github.githubassets.com/images/icons/emoji/unicode/2611.png?v8",bamboo:"https://github.githubassets.com/images/icons/emoji/unicode/1f38d.png?v8",banana:"https://github.githubassets.com/images/icons/emoji/unicode/1f34c.png?v8",bangbang:"https://github.githubassets.com/images/icons/emoji/unicode/203c.png?v8",bangladesh:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e9.png?v8",banjo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa95.png?v8",bank:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e6.png?v8",bar_chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ca.png?v8",barbados:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e7.png?v8",barber:"https://github.githubassets.com/images/icons/emoji/unicode/1f488.png?v8",baseball:"https://github.githubassets.com/images/icons/emoji/unicode/26be.png?v8",basecamp:"https://github.githubassets.com/images/icons/emoji/basecamp.png?v8",basecampy:"https://github.githubassets.com/images/icons/emoji/basecampy.png?v8",basket:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fa.png?v8",basketball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c0.png?v8",basketball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8",basketball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8",bat:"https://github.githubassets.com/images/icons/emoji/unicode/1f987.png?v8",bath:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c0.png?v8",bathtub:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c1.png?v8",battery:"https://github.githubassets.com/images/icons/emoji/unicode/1f50b.png?v8",beach_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d6.png?v8",beans:"https://github.githubassets.com/images/icons/emoji/unicode/1fad8.png?v8",bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f43b.png?v8",bearded_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4.png?v8",beaver:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ab.png?v8",bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cf.png?v8",bee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8",beer:"https://github.githubassets.com/images/icons/emoji/unicode/1f37a.png?v8",beers:"https://github.githubassets.com/images/icons/emoji/unicode/1f37b.png?v8",beetle:"https://github.githubassets.com/images/icons/emoji/unicode/1fab2.png?v8",beginner:"https://github.githubassets.com/images/icons/emoji/unicode/1f530.png?v8",belarus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fe.png?v8",belgium:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ea.png?v8",belize:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ff.png?v8",bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f514.png?v8",bell_pepper:"https://github.githubassets.com/images/icons/emoji/unicode/1fad1.png?v8",bellhop_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ce.png?v8",benin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ef.png?v8",bento:"https://github.githubassets.com/images/icons/emoji/unicode/1f371.png?v8",bermuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f2.png?v8",beverage_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c3.png?v8",bhutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f9.png?v8",bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4.png?v8",bike:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b2.png?v8",biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2642.png?v8",biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2640.png?v8",bikini:"https://github.githubassets.com/images/icons/emoji/unicode/1f459.png?v8",billed_cap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e2.png?v8",biohazard:"https://github.githubassets.com/images/icons/emoji/unicode/2623.png?v8",bird:"https://github.githubassets.com/images/icons/emoji/unicode/1f426.png?v8",birthday:"https://github.githubassets.com/images/icons/emoji/unicode/1f382.png?v8",bison:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ac.png?v8",biting_lip:"https://github.githubassets.com/images/icons/emoji/unicode/1fae6.png?v8",black_bird:"https://github.githubassets.com/images/icons/emoji/unicode/1f426-2b1b.png?v8",black_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f408-2b1b.png?v8",black_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26ab.png?v8",black_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4.png?v8",black_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a4.png?v8",black_joker:"https://github.githubassets.com/images/icons/emoji/unicode/1f0cf.png?v8",black_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1b.png?v8",black_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fe.png?v8",black_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fc.png?v8",black_nib:"https://github.githubassets.com/images/icons/emoji/unicode/2712.png?v8",black_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25aa.png?v8",black_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f532.png?v8",blond_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2642.png?v8",blond_haired_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f471.png?v8",blond_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8",blonde_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8",blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f33c.png?v8",blowfish:"https://github.githubassets.com/images/icons/emoji/unicode/1f421.png?v8",blue_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d8.png?v8",blue_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f699.png?v8",blue_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f499.png?v8",blue_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e6.png?v8",blueberries:"https://github.githubassets.com/images/icons/emoji/unicode/1fad0.png?v8",blush:"https://github.githubassets.com/images/icons/emoji/unicode/1f60a.png?v8",boar:"https://github.githubassets.com/images/icons/emoji/unicode/1f417.png?v8",boat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8",bolivia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f4.png?v8",bomb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a3.png?v8",bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b4.png?v8",book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8",bookmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f516.png?v8",bookmark_tabs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d1.png?v8",books:"https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png?v8",boom:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8",boomerang:"https://github.githubassets.com/images/icons/emoji/unicode/1fa83.png?v8",boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f462.png?v8",bosnia_herzegovina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e6.png?v8",botswana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fc.png?v8",bouncing_ball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8",bouncing_ball_person:"https://github.githubassets.com/images/icons/emoji/unicode/26f9.png?v8",bouncing_ball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8",bouquet:"https://github.githubassets.com/images/icons/emoji/unicode/1f490.png?v8",bouvet_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fb.png?v8",bow:"https://github.githubassets.com/images/icons/emoji/unicode/1f647.png?v8",bow_and_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f9.png?v8",bowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2642.png?v8",bowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2640.png?v8",bowl_with_spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f963.png?v8",bowling:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b3.png?v8",bowtie:"https://github.githubassets.com/images/icons/emoji/bowtie.png?v8",boxing_glove:"https://github.githubassets.com/images/icons/emoji/unicode/1f94a.png?v8",boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f466.png?v8",brain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e0.png?v8",brazil:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f7.png?v8",bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f35e.png?v8",breast_feeding:"https://github.githubassets.com/images/icons/emoji/unicode/1f931.png?v8",bricks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f1.png?v8",bride_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470-2640.png?v8",bridge_at_night:"https://github.githubassets.com/images/icons/emoji/unicode/1f309.png?v8",briefcase:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bc.png?v8",british_indian_ocean_territory:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f4.png?v8",british_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ec.png?v8",broccoli:"https://github.githubassets.com/images/icons/emoji/unicode/1f966.png?v8",broken_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f494.png?v8",broom:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f9.png?v8",brown_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e4.png?v8",brown_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90e.png?v8",brown_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7eb.png?v8",brunei:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f3.png?v8",bubble_tea:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cb.png?v8",bubbles:"https://github.githubassets.com/images/icons/emoji/unicode/1fae7.png?v8",bucket:"https://github.githubassets.com/images/icons/emoji/unicode/1faa3.png?v8",bug:"https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png?v8",building_construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d7.png?v8",bulb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a1.png?v8",bulgaria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ec.png?v8",bullettrain_front:"https://github.githubassets.com/images/icons/emoji/unicode/1f685.png?v8",bullettrain_side:"https://github.githubassets.com/images/icons/emoji/unicode/1f684.png?v8",burkina_faso:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1eb.png?v8",burrito:"https://github.githubassets.com/images/icons/emoji/unicode/1f32f.png?v8",burundi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ee.png?v8",bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68c.png?v8",business_suit_levitating:"https://github.githubassets.com/images/icons/emoji/unicode/1f574.png?v8",busstop:"https://github.githubassets.com/images/icons/emoji/unicode/1f68f.png?v8",bust_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f464.png?v8",busts_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f465.png?v8",butter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c8.png?v8",butterfly:"https://github.githubassets.com/images/icons/emoji/unicode/1f98b.png?v8",cactus:"https://github.githubassets.com/images/icons/emoji/unicode/1f335.png?v8",cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f370.png?v8",calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c6.png?v8",call_me_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f919.png?v8",calling:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f2.png?v8",cambodia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ed.png?v8",camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42b.png?v8",camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f7.png?v8",camera_flash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f8.png?v8",cameroon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f2.png?v8",camping:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d5.png?v8",canada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e6.png?v8",canary_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e8.png?v8",cancer:"https://github.githubassets.com/images/icons/emoji/unicode/264b.png?v8",candle:"https://github.githubassets.com/images/icons/emoji/unicode/1f56f.png?v8",candy:"https://github.githubassets.com/images/icons/emoji/unicode/1f36c.png?v8",canned_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f96b.png?v8",canoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f6.png?v8",cape_verde:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fb.png?v8",capital_abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f520.png?v8",capricorn:"https://github.githubassets.com/images/icons/emoji/unicode/2651.png?v8",car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8",card_file_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c3.png?v8",card_index:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c7.png?v8",card_index_dividers:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c2.png?v8",caribbean_netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f6.png?v8",carousel_horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a0.png?v8",carpentry_saw:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9a.png?v8",carrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f955.png?v8",cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938.png?v8",cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f431.png?v8",cat2:"https://github.githubassets.com/images/icons/emoji/unicode/1f408.png?v8",cayman_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fe.png?v8",cd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bf.png?v8",central_african_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1eb.png?v8",ceuta_melilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e6.png?v8",chad:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e9.png?v8",chains:"https://github.githubassets.com/images/icons/emoji/unicode/26d3.png?v8",chair:"https://github.githubassets.com/images/icons/emoji/unicode/1fa91.png?v8",champagne:"https://github.githubassets.com/images/icons/emoji/unicode/1f37e.png?v8",chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b9.png?v8",chart_with_downwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c9.png?v8",chart_with_upwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c8.png?v8",checkered_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c1.png?v8",cheese:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c0.png?v8",cherries:"https://github.githubassets.com/images/icons/emoji/unicode/1f352.png?v8",cherry_blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f338.png?v8",chess_pawn:"https://github.githubassets.com/images/icons/emoji/unicode/265f.png?v8",chestnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f330.png?v8",chicken:"https://github.githubassets.com/images/icons/emoji/unicode/1f414.png?v8",child:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d2.png?v8",children_crossing:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b8.png?v8",chile:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f1.png?v8",chipmunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f43f.png?v8",chocolate_bar:"https://github.githubassets.com/images/icons/emoji/unicode/1f36b.png?v8",chopsticks:"https://github.githubassets.com/images/icons/emoji/unicode/1f962.png?v8",christmas_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fd.png?v8",christmas_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f384.png?v8",church:"https://github.githubassets.com/images/icons/emoji/unicode/26ea.png?v8",cinema:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a6.png?v8",circus_tent:"https://github.githubassets.com/images/icons/emoji/unicode/1f3aa.png?v8",city_sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f307.png?v8",city_sunset:"https://github.githubassets.com/images/icons/emoji/unicode/1f306.png?v8",cityscape:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d9.png?v8",cl:"https://github.githubassets.com/images/icons/emoji/unicode/1f191.png?v8",clamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dc.png?v8",clap:"https://github.githubassets.com/images/icons/emoji/unicode/1f44f.png?v8",clapper:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ac.png?v8",classical_building:"https://github.githubassets.com/images/icons/emoji/unicode/1f3db.png?v8",climbing:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7.png?v8",climbing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2642.png?v8",climbing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2640.png?v8",clinking_glasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f942.png?v8",clipboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cb.png?v8",clipperton_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f5.png?v8",clock1:"https://github.githubassets.com/images/icons/emoji/unicode/1f550.png?v8",clock10:"https://github.githubassets.com/images/icons/emoji/unicode/1f559.png?v8",clock1030:"https://github.githubassets.com/images/icons/emoji/unicode/1f565.png?v8",clock11:"https://github.githubassets.com/images/icons/emoji/unicode/1f55a.png?v8",clock1130:"https://github.githubassets.com/images/icons/emoji/unicode/1f566.png?v8",clock12:"https://github.githubassets.com/images/icons/emoji/unicode/1f55b.png?v8",clock1230:"https://github.githubassets.com/images/icons/emoji/unicode/1f567.png?v8",clock130:"https://github.githubassets.com/images/icons/emoji/unicode/1f55c.png?v8",clock2:"https://github.githubassets.com/images/icons/emoji/unicode/1f551.png?v8",clock230:"https://github.githubassets.com/images/icons/emoji/unicode/1f55d.png?v8",clock3:"https://github.githubassets.com/images/icons/emoji/unicode/1f552.png?v8",clock330:"https://github.githubassets.com/images/icons/emoji/unicode/1f55e.png?v8",clock4:"https://github.githubassets.com/images/icons/emoji/unicode/1f553.png?v8",clock430:"https://github.githubassets.com/images/icons/emoji/unicode/1f55f.png?v8",clock5:"https://github.githubassets.com/images/icons/emoji/unicode/1f554.png?v8",clock530:"https://github.githubassets.com/images/icons/emoji/unicode/1f560.png?v8",clock6:"https://github.githubassets.com/images/icons/emoji/unicode/1f555.png?v8",clock630:"https://github.githubassets.com/images/icons/emoji/unicode/1f561.png?v8",clock7:"https://github.githubassets.com/images/icons/emoji/unicode/1f556.png?v8",clock730:"https://github.githubassets.com/images/icons/emoji/unicode/1f562.png?v8",clock8:"https://github.githubassets.com/images/icons/emoji/unicode/1f557.png?v8",clock830:"https://github.githubassets.com/images/icons/emoji/unicode/1f563.png?v8",clock9:"https://github.githubassets.com/images/icons/emoji/unicode/1f558.png?v8",clock930:"https://github.githubassets.com/images/icons/emoji/unicode/1f564.png?v8",closed_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d5.png?v8",closed_lock_with_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f510.png?v8",closed_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f302.png?v8",cloud:"https://github.githubassets.com/images/icons/emoji/unicode/2601.png?v8",cloud_with_lightning:"https://github.githubassets.com/images/icons/emoji/unicode/1f329.png?v8",cloud_with_lightning_and_rain:"https://github.githubassets.com/images/icons/emoji/unicode/26c8.png?v8",cloud_with_rain:"https://github.githubassets.com/images/icons/emoji/unicode/1f327.png?v8",cloud_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f328.png?v8",clown_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f921.png?v8",clubs:"https://github.githubassets.com/images/icons/emoji/unicode/2663.png?v8",cn:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f3.png?v8",coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e5.png?v8",cockroach:"https://github.githubassets.com/images/icons/emoji/unicode/1fab3.png?v8",cocktail:"https://github.githubassets.com/images/icons/emoji/unicode/1f378.png?v8",coconut:"https://github.githubassets.com/images/icons/emoji/unicode/1f965.png?v8",cocos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e8.png?v8",coffee:"https://github.githubassets.com/images/icons/emoji/unicode/2615.png?v8",coffin:"https://github.githubassets.com/images/icons/emoji/unicode/26b0.png?v8",coin:"https://github.githubassets.com/images/icons/emoji/unicode/1fa99.png?v8",cold_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f976.png?v8",cold_sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f630.png?v8",collision:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8",colombia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f4.png?v8",comet:"https://github.githubassets.com/images/icons/emoji/unicode/2604.png?v8",comoros:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f2.png?v8",compass:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ed.png?v8",computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bb.png?v8",computer_mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b1.png?v8",confetti_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f38a.png?v8",confounded:"https://github.githubassets.com/images/icons/emoji/unicode/1f616.png?v8",confused:"https://github.githubassets.com/images/icons/emoji/unicode/1f615.png?v8",congo_brazzaville:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ec.png?v8",congo_kinshasa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e9.png?v8",congratulations:"https://github.githubassets.com/images/icons/emoji/unicode/3297.png?v8",construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png?v8",construction_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f477.png?v8",construction_worker_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2642.png?v8",construction_worker_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2640.png?v8",control_knobs:"https://github.githubassets.com/images/icons/emoji/unicode/1f39b.png?v8",convenience_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ea.png?v8",cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f373.png?v8",cook_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f0.png?v8",cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f36a.png?v8",cool:"https://github.githubassets.com/images/icons/emoji/unicode/1f192.png?v8",cop:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8",copilot:"https://github.githubassets.com/images/icons/emoji/copilot.png?v8",copyright:"https://github.githubassets.com/images/icons/emoji/unicode/00a9.png?v8",coral:"https://github.githubassets.com/images/icons/emoji/unicode/1fab8.png?v8",corn:"https://github.githubassets.com/images/icons/emoji/unicode/1f33d.png?v8",costa_rica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f7.png?v8",cote_divoire:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ee.png?v8",couch_and_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cb.png?v8",couple:"https://github.githubassets.com/images/icons/emoji/unicode/1f46b.png?v8",couple_with_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f491.png?v8",couple_with_heart_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f468.png?v8",couple_with_heart_woman_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f468.png?v8",couple_with_heart_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f469.png?v8",couplekiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48f.png?v8",couplekiss_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f48b-1f468.png?v8",couplekiss_man_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f468.png?v8",couplekiss_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f469.png?v8",cow:"https://github.githubassets.com/images/icons/emoji/unicode/1f42e.png?v8",cow2:"https://github.githubassets.com/images/icons/emoji/unicode/1f404.png?v8",cowboy_hat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f920.png?v8",crab:"https://github.githubassets.com/images/icons/emoji/unicode/1f980.png?v8",crayon:"https://github.githubassets.com/images/icons/emoji/unicode/1f58d.png?v8",credit_card:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b3.png?v8",crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f319.png?v8",cricket:"https://github.githubassets.com/images/icons/emoji/unicode/1f997.png?v8",cricket_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cf.png?v8",croatia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f7.png?v8",crocodile:"https://github.githubassets.com/images/icons/emoji/unicode/1f40a.png?v8",croissant:"https://github.githubassets.com/images/icons/emoji/unicode/1f950.png?v8",crossed_fingers:"https://github.githubassets.com/images/icons/emoji/unicode/1f91e.png?v8",crossed_flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38c.png?v8",crossed_swords:"https://github.githubassets.com/images/icons/emoji/unicode/2694.png?v8",crown:"https://github.githubassets.com/images/icons/emoji/unicode/1f451.png?v8",crutch:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7c.png?v8",cry:"https://github.githubassets.com/images/icons/emoji/unicode/1f622.png?v8",crying_cat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f63f.png?v8",crystal_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f52e.png?v8",cuba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fa.png?v8",cucumber:"https://github.githubassets.com/images/icons/emoji/unicode/1f952.png?v8",cup_with_straw:"https://github.githubassets.com/images/icons/emoji/unicode/1f964.png?v8",cupcake:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c1.png?v8",cupid:"https://github.githubassets.com/images/icons/emoji/unicode/1f498.png?v8",curacao:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fc.png?v8",curling_stone:"https://github.githubassets.com/images/icons/emoji/unicode/1f94c.png?v8",curly_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b1.png?v8",curly_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b1.png?v8",curly_loop:"https://github.githubassets.com/images/icons/emoji/unicode/27b0.png?v8",currency_exchange:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b1.png?v8",curry:"https://github.githubassets.com/images/icons/emoji/unicode/1f35b.png?v8",cursing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92c.png?v8",custard:"https://github.githubassets.com/images/icons/emoji/unicode/1f36e.png?v8",customs:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c3.png?v8",cut_of_meat:"https://github.githubassets.com/images/icons/emoji/unicode/1f969.png?v8",cyclone:"https://github.githubassets.com/images/icons/emoji/unicode/1f300.png?v8",cyprus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fe.png?v8",czech_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ff.png?v8",dagger:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e1.png?v8",dancer:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8",dancers:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f.png?v8",dancing_men:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2642.png?v8",dancing_women:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2640.png?v8",dango:"https://github.githubassets.com/images/icons/emoji/unicode/1f361.png?v8",dark_sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f576.png?v8",dart:"https://github.githubassets.com/images/icons/emoji/unicode/1f3af.png?v8",dash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a8.png?v8",date:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c5.png?v8",de:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ea.png?v8",deaf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2642.png?v8",deaf_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf.png?v8",deaf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2640.png?v8",deciduous_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f333.png?v8",deer:"https://github.githubassets.com/images/icons/emoji/unicode/1f98c.png?v8",denmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f0.png?v8",department_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ec.png?v8",dependabot:"https://github.githubassets.com/images/icons/emoji/dependabot.png?v8",derelict_house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3da.png?v8",desert:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dc.png?v8",desert_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dd.png?v8",desktop_computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a5.png?v8",detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575.png?v8",diamond_shape_with_a_dot_inside:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a0.png?v8",diamonds:"https://github.githubassets.com/images/icons/emoji/unicode/2666.png?v8",diego_garcia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ec.png?v8",disappointed:"https://github.githubassets.com/images/icons/emoji/unicode/1f61e.png?v8",disappointed_relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f625.png?v8",disguised_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f978.png?v8",diving_mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f93f.png?v8",diya_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1fa94.png?v8",dizzy:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ab.png?v8",dizzy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f635.png?v8",djibouti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ef.png?v8",dna:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ec.png?v8",do_not_litter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6af.png?v8",dodo:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a4.png?v8",dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f436.png?v8",dog2:"https://github.githubassets.com/images/icons/emoji/unicode/1f415.png?v8",dollar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b5.png?v8",dolls:"https://github.githubassets.com/images/icons/emoji/unicode/1f38e.png?v8",dolphin:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8",dominica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f2.png?v8",dominican_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f4.png?v8",donkey:"https://github.githubassets.com/images/icons/emoji/unicode/1facf.png?v8",door:"https://github.githubassets.com/images/icons/emoji/unicode/1f6aa.png?v8",dotted_line_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae5.png?v8",doughnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f369.png?v8",dove:"https://github.githubassets.com/images/icons/emoji/unicode/1f54a.png?v8",dragon:"https://github.githubassets.com/images/icons/emoji/unicode/1f409.png?v8",dragon_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f432.png?v8",dress:"https://github.githubassets.com/images/icons/emoji/unicode/1f457.png?v8",dromedary_camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42a.png?v8",drooling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f924.png?v8",drop_of_blood:"https://github.githubassets.com/images/icons/emoji/unicode/1fa78.png?v8",droplet:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a7.png?v8",drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f941.png?v8",duck:"https://github.githubassets.com/images/icons/emoji/unicode/1f986.png?v8",dumpling:"https://github.githubassets.com/images/icons/emoji/unicode/1f95f.png?v8",dvd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c0.png?v8","e-mail":"https://github.githubassets.com/images/icons/emoji/unicode/1f4e7.png?v8",eagle:"https://github.githubassets.com/images/icons/emoji/unicode/1f985.png?v8",ear:"https://github.githubassets.com/images/icons/emoji/unicode/1f442.png?v8",ear_of_rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f33e.png?v8",ear_with_hearing_aid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bb.png?v8",earth_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f30d.png?v8",earth_americas:"https://github.githubassets.com/images/icons/emoji/unicode/1f30e.png?v8",earth_asia:"https://github.githubassets.com/images/icons/emoji/unicode/1f30f.png?v8",ecuador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e8.png?v8",egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f95a.png?v8",eggplant:"https://github.githubassets.com/images/icons/emoji/unicode/1f346.png?v8",egypt:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ec.png?v8",eight:"https://github.githubassets.com/images/icons/emoji/unicode/0038-20e3.png?v8",eight_pointed_black_star:"https://github.githubassets.com/images/icons/emoji/unicode/2734.png?v8",eight_spoked_asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/2733.png?v8",eject_button:"https://github.githubassets.com/images/icons/emoji/unicode/23cf.png?v8",el_salvador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fb.png?v8",electric_plug:"https://github.githubassets.com/images/icons/emoji/unicode/1f50c.png?v8",electron:"https://github.githubassets.com/images/icons/emoji/electron.png?v8",elephant:"https://github.githubassets.com/images/icons/emoji/unicode/1f418.png?v8",elevator:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d7.png?v8",elf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd.png?v8",elf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2642.png?v8",elf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2640.png?v8",email:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e7.png?v8",empty_nest:"https://github.githubassets.com/images/icons/emoji/unicode/1fab9.png?v8",end:"https://github.githubassets.com/images/icons/emoji/unicode/1f51a.png?v8",england:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png?v8",envelope:"https://github.githubassets.com/images/icons/emoji/unicode/2709.png?v8",envelope_with_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e9.png?v8",equatorial_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f6.png?v8",eritrea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f7.png?v8",es:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f8.png?v8",estonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ea.png?v8",ethiopia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f9.png?v8",eu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8",euro:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b6.png?v8",european_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f0.png?v8",european_post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e4.png?v8",european_union:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8",evergreen_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f332.png?v8",exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8",exploding_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f92f.png?v8",expressionless:"https://github.githubassets.com/images/icons/emoji/unicode/1f611.png?v8",eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f441.png?v8",eye_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f441-1f5e8.png?v8",eyeglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f453.png?v8",eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f440.png?v8",face_exhaling:"https://github.githubassets.com/images/icons/emoji/unicode/1f62e-1f4a8.png?v8",face_holding_back_tears:"https://github.githubassets.com/images/icons/emoji/unicode/1f979.png?v8",face_in_clouds:"https://github.githubassets.com/images/icons/emoji/unicode/1f636-1f32b.png?v8",face_with_diagonal_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1fae4.png?v8",face_with_head_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1f915.png?v8",face_with_open_eyes_and_hand_over_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1fae2.png?v8",face_with_peeking_eye:"https://github.githubassets.com/images/icons/emoji/unicode/1fae3.png?v8",face_with_spiral_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f635-1f4ab.png?v8",face_with_thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f912.png?v8",facepalm:"https://github.githubassets.com/images/icons/emoji/unicode/1f926.png?v8",facepunch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8",factory:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ed.png?v8",factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3ed.png?v8",fairy:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da.png?v8",fairy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2642.png?v8",fairy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2640.png?v8",falafel:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c6.png?v8",falkland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f0.png?v8",fallen_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f342.png?v8",family:"https://github.githubassets.com/images/icons/emoji/unicode/1f46a.png?v8",family_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466.png?v8",family_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466-1f466.png?v8",family_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467.png?v8",family_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f466.png?v8",family_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f467.png?v8",family_man_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466.png?v8",family_man_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466-1f466.png?v8",family_man_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467.png?v8",family_man_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f466.png?v8",family_man_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f467.png?v8",family_man_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466.png?v8",family_man_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466-1f466.png?v8",family_man_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467.png?v8",family_man_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f466.png?v8",family_man_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f467.png?v8",family_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466.png?v8",family_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466-1f466.png?v8",family_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467.png?v8",family_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f466.png?v8",family_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f467.png?v8",family_woman_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466.png?v8",family_woman_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466-1f466.png?v8",family_woman_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467.png?v8",family_woman_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f466.png?v8",family_woman_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f467.png?v8",farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f33e.png?v8",faroe_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f4.png?v8",fast_forward:"https://github.githubassets.com/images/icons/emoji/unicode/23e9.png?v8",fax:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e0.png?v8",fearful:"https://github.githubassets.com/images/icons/emoji/unicode/1f628.png?v8",feather:"https://github.githubassets.com/images/icons/emoji/unicode/1fab6.png?v8",feelsgood:"https://github.githubassets.com/images/icons/emoji/feelsgood.png?v8",feet:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8",female_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2640.png?v8",female_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2640.png?v8",ferris_wheel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a1.png?v8",ferry:"https://github.githubassets.com/images/icons/emoji/unicode/26f4.png?v8",field_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d1.png?v8",fiji:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ef.png?v8",file_cabinet:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c4.png?v8",file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c1.png?v8",film_projector:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fd.png?v8",film_strip:"https://github.githubassets.com/images/icons/emoji/unicode/1f39e.png?v8",finland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ee.png?v8",finnadie:"https://github.githubassets.com/images/icons/emoji/finnadie.png?v8",fire:"https://github.githubassets.com/images/icons/emoji/unicode/1f525.png?v8",fire_engine:"https://github.githubassets.com/images/icons/emoji/unicode/1f692.png?v8",fire_extinguisher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ef.png?v8",firecracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e8.png?v8",firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f692.png?v8",fireworks:"https://github.githubassets.com/images/icons/emoji/unicode/1f386.png?v8",first_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f313.png?v8",first_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31b.png?v8",fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f41f.png?v8",fish_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f365.png?v8",fishing_pole_and_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a3.png?v8",fishsticks:"https://github.githubassets.com/images/icons/emoji/fishsticks.png?v8",fist:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8",fist_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f91b.png?v8",fist_oncoming:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8",fist_raised:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8",fist_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f91c.png?v8",five:"https://github.githubassets.com/images/icons/emoji/unicode/0035-20e3.png?v8",flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38f.png?v8",flamingo:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a9.png?v8",flashlight:"https://github.githubassets.com/images/icons/emoji/unicode/1f526.png?v8",flat_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f97f.png?v8",flatbread:"https://github.githubassets.com/images/icons/emoji/unicode/1fad3.png?v8",fleur_de_lis:"https://github.githubassets.com/images/icons/emoji/unicode/269c.png?v8",flight_arrival:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ec.png?v8",flight_departure:"https://github.githubassets.com/images/icons/emoji/unicode/1f6eb.png?v8",flipper:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8",floppy_disk:"https://github.githubassets.com/images/icons/emoji/unicode/1f4be.png?v8",flower_playing_cards:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b4.png?v8",flushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f633.png?v8",flute:"https://github.githubassets.com/images/icons/emoji/unicode/1fa88.png?v8",fly:"https://github.githubassets.com/images/icons/emoji/unicode/1fab0.png?v8",flying_disc:"https://github.githubassets.com/images/icons/emoji/unicode/1f94f.png?v8",flying_saucer:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f8.png?v8",fog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32b.png?v8",foggy:"https://github.githubassets.com/images/icons/emoji/unicode/1f301.png?v8",folding_hand_fan:"https://github.githubassets.com/images/icons/emoji/unicode/1faad.png?v8",fondue:"https://github.githubassets.com/images/icons/emoji/unicode/1fad5.png?v8",foot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b6.png?v8",football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c8.png?v8",footprints:"https://github.githubassets.com/images/icons/emoji/unicode/1f463.png?v8",fork_and_knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f374.png?v8",fortune_cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f960.png?v8",fountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f2.png?v8",fountain_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58b.png?v8",four:"https://github.githubassets.com/images/icons/emoji/unicode/0034-20e3.png?v8",four_leaf_clover:"https://github.githubassets.com/images/icons/emoji/unicode/1f340.png?v8",fox_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f98a.png?v8",fr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f7.png?v8",framed_picture:"https://github.githubassets.com/images/icons/emoji/unicode/1f5bc.png?v8",free:"https://github.githubassets.com/images/icons/emoji/unicode/1f193.png?v8",french_guiana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1eb.png?v8",french_polynesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1eb.png?v8",french_southern_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1eb.png?v8",fried_egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f373.png?v8",fried_shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f364.png?v8",fries:"https://github.githubassets.com/images/icons/emoji/unicode/1f35f.png?v8",frog:"https://github.githubassets.com/images/icons/emoji/unicode/1f438.png?v8",frowning:"https://github.githubassets.com/images/icons/emoji/unicode/1f626.png?v8",frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/2639.png?v8",frowning_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2642.png?v8",frowning_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d.png?v8",frowning_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2640.png?v8",fu:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8",fuelpump:"https://github.githubassets.com/images/icons/emoji/unicode/26fd.png?v8",full_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f315.png?v8",full_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31d.png?v8",funeral_urn:"https://github.githubassets.com/images/icons/emoji/unicode/26b1.png?v8",gabon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e6.png?v8",gambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f2.png?v8",game_die:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b2.png?v8",garlic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c4.png?v8",gb:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8",gear:"https://github.githubassets.com/images/icons/emoji/unicode/2699.png?v8",gem:"https://github.githubassets.com/images/icons/emoji/unicode/1f48e.png?v8",gemini:"https://github.githubassets.com/images/icons/emoji/unicode/264a.png?v8",genie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de.png?v8",genie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2642.png?v8",genie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2640.png?v8",georgia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ea.png?v8",ghana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ed.png?v8",ghost:"https://github.githubassets.com/images/icons/emoji/unicode/1f47b.png?v8",gibraltar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ee.png?v8",gift:"https://github.githubassets.com/images/icons/emoji/unicode/1f381.png?v8",gift_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49d.png?v8",ginger_root:"https://github.githubassets.com/images/icons/emoji/unicode/1fada.png?v8",giraffe:"https://github.githubassets.com/images/icons/emoji/unicode/1f992.png?v8",girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f467.png?v8",globe_with_meridians:"https://github.githubassets.com/images/icons/emoji/unicode/1f310.png?v8",gloves:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e4.png?v8",goal_net:"https://github.githubassets.com/images/icons/emoji/unicode/1f945.png?v8",goat:"https://github.githubassets.com/images/icons/emoji/unicode/1f410.png?v8",goberserk:"https://github.githubassets.com/images/icons/emoji/goberserk.png?v8",godmode:"https://github.githubassets.com/images/icons/emoji/godmode.png?v8",goggles:"https://github.githubassets.com/images/icons/emoji/unicode/1f97d.png?v8",golf:"https://github.githubassets.com/images/icons/emoji/unicode/26f3.png?v8",golfing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc.png?v8",golfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2642.png?v8",golfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2640.png?v8",goose:"https://github.githubassets.com/images/icons/emoji/unicode/1fabf.png?v8",gorilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f98d.png?v8",grapes:"https://github.githubassets.com/images/icons/emoji/unicode/1f347.png?v8",greece:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f7.png?v8",green_apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34f.png?v8",green_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d7.png?v8",green_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e2.png?v8",green_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49a.png?v8",green_salad:"https://github.githubassets.com/images/icons/emoji/unicode/1f957.png?v8",green_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e9.png?v8",greenland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f1.png?v8",grenada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e9.png?v8",grey_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2755.png?v8",grey_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fa76.png?v8",grey_question:"https://github.githubassets.com/images/icons/emoji/unicode/2754.png?v8",grimacing:"https://github.githubassets.com/images/icons/emoji/unicode/1f62c.png?v8",grin:"https://github.githubassets.com/images/icons/emoji/unicode/1f601.png?v8",grinning:"https://github.githubassets.com/images/icons/emoji/unicode/1f600.png?v8",guadeloupe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f5.png?v8",guam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fa.png?v8",guard:"https://github.githubassets.com/images/icons/emoji/unicode/1f482.png?v8",guardsman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2642.png?v8",guardswoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2640.png?v8",guatemala:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f9.png?v8",guernsey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ec.png?v8",guide_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ae.png?v8",guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f3.png?v8",guinea_bissau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fc.png?v8",guitar:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b8.png?v8",gun:"https://github.githubassets.com/images/icons/emoji/unicode/1f52b.png?v8",guyana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fe.png?v8",hair_pick:"https://github.githubassets.com/images/icons/emoji/unicode/1faae.png?v8",haircut:"https://github.githubassets.com/images/icons/emoji/unicode/1f487.png?v8",haircut_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2642.png?v8",haircut_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2640.png?v8",haiti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f9.png?v8",hamburger:"https://github.githubassets.com/images/icons/emoji/unicode/1f354.png?v8",hammer:"https://github.githubassets.com/images/icons/emoji/unicode/1f528.png?v8",hammer_and_pick:"https://github.githubassets.com/images/icons/emoji/unicode/2692.png?v8",hammer_and_wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e0.png?v8",hamsa:"https://github.githubassets.com/images/icons/emoji/unicode/1faac.png?v8",hamster:"https://github.githubassets.com/images/icons/emoji/unicode/1f439.png?v8",hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8",hand_over_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f92d.png?v8",hand_with_index_finger_and_thumb_crossed:"https://github.githubassets.com/images/icons/emoji/unicode/1faf0.png?v8",handbag:"https://github.githubassets.com/images/icons/emoji/unicode/1f45c.png?v8",handball_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e.png?v8",handshake:"https://github.githubassets.com/images/icons/emoji/unicode/1f91d.png?v8",hankey:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8",hash:"https://github.githubassets.com/images/icons/emoji/unicode/0023-20e3.png?v8",hatched_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f425.png?v8",hatching_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f423.png?v8",headphones:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a7.png?v8",headstone:"https://github.githubassets.com/images/icons/emoji/unicode/1faa6.png?v8",health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2695.png?v8",hear_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f649.png?v8",heard_mcdonald_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f2.png?v8",heart:"https://github.githubassets.com/images/icons/emoji/unicode/2764.png?v8",heart_decoration:"https://github.githubassets.com/images/icons/emoji/unicode/1f49f.png?v8",heart_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f60d.png?v8",heart_eyes_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63b.png?v8",heart_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1faf6.png?v8",heart_on_fire:"https://github.githubassets.com/images/icons/emoji/unicode/2764-1f525.png?v8",heartbeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f493.png?v8",heartpulse:"https://github.githubassets.com/images/icons/emoji/unicode/1f497.png?v8",hearts:"https://github.githubassets.com/images/icons/emoji/unicode/2665.png?v8",heavy_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2714.png?v8",heavy_division_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2797.png?v8",heavy_dollar_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b2.png?v8",heavy_equals_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f7f0.png?v8",heavy_exclamation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8",heavy_heart_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2763.png?v8",heavy_minus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2796.png?v8",heavy_multiplication_x:"https://github.githubassets.com/images/icons/emoji/unicode/2716.png?v8",heavy_plus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2795.png?v8",hedgehog:"https://github.githubassets.com/images/icons/emoji/unicode/1f994.png?v8",helicopter:"https://github.githubassets.com/images/icons/emoji/unicode/1f681.png?v8",herb:"https://github.githubassets.com/images/icons/emoji/unicode/1f33f.png?v8",hibiscus:"https://github.githubassets.com/images/icons/emoji/unicode/1f33a.png?v8",high_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f506.png?v8",high_heel:"https://github.githubassets.com/images/icons/emoji/unicode/1f460.png?v8",hiking_boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f97e.png?v8",hindu_temple:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d5.png?v8",hippopotamus:"https://github.githubassets.com/images/icons/emoji/unicode/1f99b.png?v8",hocho:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8",hole:"https://github.githubassets.com/images/icons/emoji/unicode/1f573.png?v8",honduras:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f3.png?v8",honey_pot:"https://github.githubassets.com/images/icons/emoji/unicode/1f36f.png?v8",honeybee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8",hong_kong:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f0.png?v8",hook:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9d.png?v8",horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f434.png?v8",horse_racing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c7.png?v8",hospital:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e5.png?v8",hot_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f975.png?v8",hot_pepper:"https://github.githubassets.com/images/icons/emoji/unicode/1f336.png?v8",hotdog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32d.png?v8",hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e8.png?v8",hotsprings:"https://github.githubassets.com/images/icons/emoji/unicode/2668.png?v8",hourglass:"https://github.githubassets.com/images/icons/emoji/unicode/231b.png?v8",hourglass_flowing_sand:"https://github.githubassets.com/images/icons/emoji/unicode/23f3.png?v8",house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e0.png?v8",house_with_garden:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e1.png?v8",houses:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d8.png?v8",hugs:"https://github.githubassets.com/images/icons/emoji/unicode/1f917.png?v8",hungary:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1fa.png?v8",hurtrealbad:"https://github.githubassets.com/images/icons/emoji/hurtrealbad.png?v8",hushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f62f.png?v8",hut:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d6.png?v8",hyacinth:"https://github.githubassets.com/images/icons/emoji/unicode/1fabb.png?v8",ice_cream:"https://github.githubassets.com/images/icons/emoji/unicode/1f368.png?v8",ice_cube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ca.png?v8",ice_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d2.png?v8",ice_skate:"https://github.githubassets.com/images/icons/emoji/unicode/26f8.png?v8",icecream:"https://github.githubassets.com/images/icons/emoji/unicode/1f366.png?v8",iceland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f8.png?v8",id:"https://github.githubassets.com/images/icons/emoji/unicode/1f194.png?v8",identification_card:"https://github.githubassets.com/images/icons/emoji/unicode/1faaa.png?v8",ideograph_advantage:"https://github.githubassets.com/images/icons/emoji/unicode/1f250.png?v8",imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f47f.png?v8",inbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e5.png?v8",incoming_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e8.png?v8",index_pointing_at_the_viewer:"https://github.githubassets.com/images/icons/emoji/unicode/1faf5.png?v8",india:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f3.png?v8",indonesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e9.png?v8",infinity:"https://github.githubassets.com/images/icons/emoji/unicode/267e.png?v8",information_desk_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8",information_source:"https://github.githubassets.com/images/icons/emoji/unicode/2139.png?v8",innocent:"https://github.githubassets.com/images/icons/emoji/unicode/1f607.png?v8",interrobang:"https://github.githubassets.com/images/icons/emoji/unicode/2049.png?v8",iphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f1.png?v8",iran:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f7.png?v8",iraq:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f6.png?v8",ireland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1ea.png?v8",isle_of_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f2.png?v8",israel:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f1.png?v8",it:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f9.png?v8",izakaya_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8",jack_o_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f383.png?v8",jamaica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f2.png?v8",japan:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fe.png?v8",japanese_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ef.png?v8",japanese_goblin:"https://github.githubassets.com/images/icons/emoji/unicode/1f47a.png?v8",japanese_ogre:"https://github.githubassets.com/images/icons/emoji/unicode/1f479.png?v8",jar:"https://github.githubassets.com/images/icons/emoji/unicode/1fad9.png?v8",jeans:"https://github.githubassets.com/images/icons/emoji/unicode/1f456.png?v8",jellyfish:"https://github.githubassets.com/images/icons/emoji/unicode/1fabc.png?v8",jersey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1ea.png?v8",jigsaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e9.png?v8",jordan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f4.png?v8",joy:"https://github.githubassets.com/images/icons/emoji/unicode/1f602.png?v8",joy_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f639.png?v8",joystick:"https://github.githubassets.com/images/icons/emoji/unicode/1f579.png?v8",jp:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f5.png?v8",judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2696.png?v8",juggling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f939.png?v8",kaaba:"https://github.githubassets.com/images/icons/emoji/unicode/1f54b.png?v8",kangaroo:"https://github.githubassets.com/images/icons/emoji/unicode/1f998.png?v8",kazakhstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ff.png?v8",kenya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ea.png?v8",key:"https://github.githubassets.com/images/icons/emoji/unicode/1f511.png?v8",keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/2328.png?v8",keycap_ten:"https://github.githubassets.com/images/icons/emoji/unicode/1f51f.png?v8",khanda:"https://github.githubassets.com/images/icons/emoji/unicode/1faaf.png?v8",kick_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f4.png?v8",kimono:"https://github.githubassets.com/images/icons/emoji/unicode/1f458.png?v8",kiribati:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ee.png?v8",kiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48b.png?v8",kissing:"https://github.githubassets.com/images/icons/emoji/unicode/1f617.png?v8",kissing_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63d.png?v8",kissing_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61a.png?v8",kissing_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f618.png?v8",kissing_smiling_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f619.png?v8",kite:"https://github.githubassets.com/images/icons/emoji/unicode/1fa81.png?v8",kiwi_fruit:"https://github.githubassets.com/images/icons/emoji/unicode/1f95d.png?v8",kneeling_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2642.png?v8",kneeling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce.png?v8",kneeling_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2640.png?v8",knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8",knot:"https://github.githubassets.com/images/icons/emoji/unicode/1faa2.png?v8",koala:"https://github.githubassets.com/images/icons/emoji/unicode/1f428.png?v8",koko:"https://github.githubassets.com/images/icons/emoji/unicode/1f201.png?v8",kosovo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fd-1f1f0.png?v8",kr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f7.png?v8",kuwait:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fc.png?v8",kyrgyzstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ec.png?v8",lab_coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f97c.png?v8",label:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f7.png?v8",lacrosse:"https://github.githubassets.com/images/icons/emoji/unicode/1f94d.png?v8",ladder:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9c.png?v8",lady_beetle:"https://github.githubassets.com/images/icons/emoji/unicode/1f41e.png?v8",lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8",laos:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e6.png?v8",large_blue_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f535.png?v8",large_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f537.png?v8",large_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f536.png?v8",last_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f317.png?v8",last_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31c.png?v8",latin_cross:"https://github.githubassets.com/images/icons/emoji/unicode/271d.png?v8",latvia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fb.png?v8",laughing:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8",leafy_green:"https://github.githubassets.com/images/icons/emoji/unicode/1f96c.png?v8",leaves:"https://github.githubassets.com/images/icons/emoji/unicode/1f343.png?v8",lebanon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e7.png?v8",ledger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d2.png?v8",left_luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c5.png?v8",left_right_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/2194.png?v8",left_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e8.png?v8",leftwards_arrow_with_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21a9.png?v8",leftwards_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf2.png?v8",leftwards_pushing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf7.png?v8",leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b5.png?v8",lemon:"https://github.githubassets.com/images/icons/emoji/unicode/1f34b.png?v8",leo:"https://github.githubassets.com/images/icons/emoji/unicode/264c.png?v8",leopard:"https://github.githubassets.com/images/icons/emoji/unicode/1f406.png?v8",lesotho:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f8.png?v8",level_slider:"https://github.githubassets.com/images/icons/emoji/unicode/1f39a.png?v8",liberia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f7.png?v8",libra:"https://github.githubassets.com/images/icons/emoji/unicode/264e.png?v8",libya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fe.png?v8",liechtenstein:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1ee.png?v8",light_blue_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fa75.png?v8",light_rail:"https://github.githubassets.com/images/icons/emoji/unicode/1f688.png?v8",link:"https://github.githubassets.com/images/icons/emoji/unicode/1f517.png?v8",lion:"https://github.githubassets.com/images/icons/emoji/unicode/1f981.png?v8",lips:"https://github.githubassets.com/images/icons/emoji/unicode/1f444.png?v8",lipstick:"https://github.githubassets.com/images/icons/emoji/unicode/1f484.png?v8",lithuania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f9.png?v8",lizard:"https://github.githubassets.com/images/icons/emoji/unicode/1f98e.png?v8",llama:"https://github.githubassets.com/images/icons/emoji/unicode/1f999.png?v8",lobster:"https://github.githubassets.com/images/icons/emoji/unicode/1f99e.png?v8",lock:"https://github.githubassets.com/images/icons/emoji/unicode/1f512.png?v8",lock_with_ink_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f50f.png?v8",lollipop:"https://github.githubassets.com/images/icons/emoji/unicode/1f36d.png?v8",long_drum:"https://github.githubassets.com/images/icons/emoji/unicode/1fa98.png?v8",loop:"https://github.githubassets.com/images/icons/emoji/unicode/27bf.png?v8",lotion_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f4.png?v8",lotus:"https://github.githubassets.com/images/icons/emoji/unicode/1fab7.png?v8",lotus_position:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8.png?v8",lotus_position_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2642.png?v8",lotus_position_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2640.png?v8",loud_sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f50a.png?v8",loudspeaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e2.png?v8",love_hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e9.png?v8",love_letter:"https://github.githubassets.com/images/icons/emoji/unicode/1f48c.png?v8",love_you_gesture:"https://github.githubassets.com/images/icons/emoji/unicode/1f91f.png?v8",low_battery:"https://github.githubassets.com/images/icons/emoji/unicode/1faab.png?v8",low_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f505.png?v8",luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f3.png?v8",lungs:"https://github.githubassets.com/images/icons/emoji/unicode/1fac1.png?v8",luxembourg:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fa.png?v8",lying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f925.png?v8",m:"https://github.githubassets.com/images/icons/emoji/unicode/24c2.png?v8",macau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f4.png?v8",macedonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f0.png?v8",madagascar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ec.png?v8",mag:"https://github.githubassets.com/images/icons/emoji/unicode/1f50d.png?v8",mag_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f50e.png?v8",mage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9.png?v8",mage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2642.png?v8",mage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2640.png?v8",magic_wand:"https://github.githubassets.com/images/icons/emoji/unicode/1fa84.png?v8",magnet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f2.png?v8",mahjong:"https://github.githubassets.com/images/icons/emoji/unicode/1f004.png?v8",mailbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4eb.png?v8",mailbox_closed:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ea.png?v8",mailbox_with_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ec.png?v8",mailbox_with_no_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ed.png?v8",malawi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fc.png?v8",malaysia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fe.png?v8",maldives:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fb.png?v8",male_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2642.png?v8",male_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2642.png?v8",mali:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f1.png?v8",malta:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f9.png?v8",mammoth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a3.png?v8",man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468.png?v8",man_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a8.png?v8",man_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f680.png?v8",man_beard:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4-2642.png?v8",man_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2642.png?v8",man_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f373.png?v8",man_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f57a.png?v8",man_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2642.png?v8",man_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3ed.png?v8",man_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f33e.png?v8",man_feeding_baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f37c.png?v8",man_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f692.png?v8",man_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2695.png?v8",man_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bd.png?v8",man_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bc.png?v8",man_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935-2642.png?v8",man_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2696.png?v8",man_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2642.png?v8",man_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f527.png?v8",man_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bc.png?v8",man_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2708.png?v8",man_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2642.png?v8",man_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2642.png?v8",man_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f52c.png?v8",man_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2642.png?v8",man_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a4.png?v8",man_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f393.png?v8",man_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3eb.png?v8",man_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bb.png?v8",man_with_gua_pi_mao:"https://github.githubassets.com/images/icons/emoji/unicode/1f472.png?v8",man_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9af.png?v8",man_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2642.png?v8",man_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470-2642.png?v8",mandarin:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8",mango:"https://github.githubassets.com/images/icons/emoji/unicode/1f96d.png?v8",mans_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8",mantelpiece_clock:"https://github.githubassets.com/images/icons/emoji/unicode/1f570.png?v8",manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bd.png?v8",maple_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f341.png?v8",maracas:"https://github.githubassets.com/images/icons/emoji/unicode/1fa87.png?v8",marshall_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ed.png?v8",martial_arts_uniform:"https://github.githubassets.com/images/icons/emoji/unicode/1f94b.png?v8",martinique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f6.png?v8",mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f637.png?v8",massage:"https://github.githubassets.com/images/icons/emoji/unicode/1f486.png?v8",massage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2642.png?v8",massage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2640.png?v8",mate:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c9.png?v8",mauritania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f7.png?v8",mauritius:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fa.png?v8",mayotte:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1f9.png?v8",meat_on_bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f356.png?v8",mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f527.png?v8",mechanical_arm:"https://github.githubassets.com/images/icons/emoji/unicode/1f9be.png?v8",mechanical_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bf.png?v8",medal_military:"https://github.githubassets.com/images/icons/emoji/unicode/1f396.png?v8",medal_sports:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c5.png?v8",medical_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/2695.png?v8",mega:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e3.png?v8",melon:"https://github.githubassets.com/images/icons/emoji/unicode/1f348.png?v8",melting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae0.png?v8",memo:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8",men_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2642.png?v8",mending_heart:"https://github.githubassets.com/images/icons/emoji/unicode/2764-1fa79.png?v8",menorah:"https://github.githubassets.com/images/icons/emoji/unicode/1f54e.png?v8",mens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b9.png?v8",mermaid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2640.png?v8",merman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2642.png?v8",merperson:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc.png?v8",metal:"https://github.githubassets.com/images/icons/emoji/unicode/1f918.png?v8",metro:"https://github.githubassets.com/images/icons/emoji/unicode/1f687.png?v8",mexico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fd.png?v8",microbe:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a0.png?v8",micronesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f2.png?v8",microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a4.png?v8",microscope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52c.png?v8",middle_finger:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8",military_helmet:"https://github.githubassets.com/images/icons/emoji/unicode/1fa96.png?v8",milk_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f95b.png?v8",milky_way:"https://github.githubassets.com/images/icons/emoji/unicode/1f30c.png?v8",minibus:"https://github.githubassets.com/images/icons/emoji/unicode/1f690.png?v8",minidisc:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bd.png?v8",mirror:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9e.png?v8",mirror_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1faa9.png?v8",mobile_phone_off:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f4.png?v8",moldova:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e9.png?v8",monaco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e8.png?v8",money_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f911.png?v8",money_with_wings:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b8.png?v8",moneybag:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b0.png?v8",mongolia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f3.png?v8",monkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f412.png?v8",monkey_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f435.png?v8",monocle_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d0.png?v8",monorail:"https://github.githubassets.com/images/icons/emoji/unicode/1f69d.png?v8",montenegro:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ea.png?v8",montserrat:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f8.png?v8",moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8",moon_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f96e.png?v8",moose:"https://github.githubassets.com/images/icons/emoji/unicode/1face.png?v8",morocco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e6.png?v8",mortar_board:"https://github.githubassets.com/images/icons/emoji/unicode/1f393.png?v8",mosque:"https://github.githubassets.com/images/icons/emoji/unicode/1f54c.png?v8",mosquito:"https://github.githubassets.com/images/icons/emoji/unicode/1f99f.png?v8",motor_boat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e5.png?v8",motor_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f5.png?v8",motorcycle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cd.png?v8",motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bc.png?v8",motorway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e3.png?v8",mount_fuji:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fb.png?v8",mountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f0.png?v8",mountain_bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5.png?v8",mountain_biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2642.png?v8",mountain_biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2640.png?v8",mountain_cableway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a0.png?v8",mountain_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69e.png?v8",mountain_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d4.png?v8",mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f42d.png?v8",mouse2:"https://github.githubassets.com/images/icons/emoji/unicode/1f401.png?v8",mouse_trap:"https://github.githubassets.com/images/icons/emoji/unicode/1faa4.png?v8",movie_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a5.png?v8",moyai:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ff.png?v8",mozambique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ff.png?v8",mrs_claus:"https://github.githubassets.com/images/icons/emoji/unicode/1f936.png?v8",muscle:"https://github.githubassets.com/images/icons/emoji/unicode/1f4aa.png?v8",mushroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f344.png?v8",musical_keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b9.png?v8",musical_note:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b5.png?v8",musical_score:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bc.png?v8",mute:"https://github.githubassets.com/images/icons/emoji/unicode/1f507.png?v8",mx_claus:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f384.png?v8",myanmar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f2.png?v8",nail_care:"https://github.githubassets.com/images/icons/emoji/unicode/1f485.png?v8",name_badge:"https://github.githubassets.com/images/icons/emoji/unicode/1f4db.png?v8",namibia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e6.png?v8",national_park:"https://github.githubassets.com/images/icons/emoji/unicode/1f3de.png?v8",nauru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f7.png?v8",nauseated_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f922.png?v8",nazar_amulet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ff.png?v8",neckbeard:"https://github.githubassets.com/images/icons/emoji/neckbeard.png?v8",necktie:"https://github.githubassets.com/images/icons/emoji/unicode/1f454.png?v8",negative_squared_cross_mark:"https://github.githubassets.com/images/icons/emoji/unicode/274e.png?v8",nepal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f5.png?v8",nerd_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f913.png?v8",nest_with_eggs:"https://github.githubassets.com/images/icons/emoji/unicode/1faba.png?v8",nesting_dolls:"https://github.githubassets.com/images/icons/emoji/unicode/1fa86.png?v8",netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f1.png?v8",neutral_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f610.png?v8",new:"https://github.githubassets.com/images/icons/emoji/unicode/1f195.png?v8",new_caledonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e8.png?v8",new_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f311.png?v8",new_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31a.png?v8",new_zealand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ff.png?v8",newspaper:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f0.png?v8",newspaper_roll:"https://github.githubassets.com/images/icons/emoji/unicode/1f5de.png?v8",next_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ed.png?v8",ng:"https://github.githubassets.com/images/icons/emoji/unicode/1f196.png?v8",ng_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8",ng_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8",nicaragua:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ee.png?v8",niger:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ea.png?v8",nigeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ec.png?v8",night_with_stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f303.png?v8",nine:"https://github.githubassets.com/images/icons/emoji/unicode/0039-20e3.png?v8",ninja:"https://github.githubassets.com/images/icons/emoji/unicode/1f977.png?v8",niue:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1fa.png?v8",no_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f515.png?v8",no_bicycles:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b3.png?v8",no_entry:"https://github.githubassets.com/images/icons/emoji/unicode/26d4.png?v8",no_entry_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ab.png?v8",no_good:"https://github.githubassets.com/images/icons/emoji/unicode/1f645.png?v8",no_good_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8",no_good_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8",no_mobile_phones:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f5.png?v8",no_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f636.png?v8",no_pedestrians:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b7.png?v8",no_smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ad.png?v8","non-potable_water":"https://github.githubassets.com/images/icons/emoji/unicode/1f6b1.png?v8",norfolk_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1eb.png?v8",north_korea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f5.png?v8",northern_mariana_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f5.png?v8",norway:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f4.png?v8",nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f443.png?v8",notebook:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d3.png?v8",notebook_with_decorative_cover:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d4.png?v8",notes:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b6.png?v8",nut_and_bolt:"https://github.githubassets.com/images/icons/emoji/unicode/1f529.png?v8",o:"https://github.githubassets.com/images/icons/emoji/unicode/2b55.png?v8",o2:"https://github.githubassets.com/images/icons/emoji/unicode/1f17e.png?v8",ocean:"https://github.githubassets.com/images/icons/emoji/unicode/1f30a.png?v8",octocat:"https://github.githubassets.com/images/icons/emoji/octocat.png?v8",octopus:"https://github.githubassets.com/images/icons/emoji/unicode/1f419.png?v8",oden:"https://github.githubassets.com/images/icons/emoji/unicode/1f362.png?v8",office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e2.png?v8",office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bc.png?v8",oil_drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e2.png?v8",ok:"https://github.githubassets.com/images/icons/emoji/unicode/1f197.png?v8",ok_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f44c.png?v8",ok_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2642.png?v8",ok_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f646.png?v8",ok_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2640.png?v8",old_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dd.png?v8",older_adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d3.png?v8",older_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f474.png?v8",older_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f475.png?v8",olive:"https://github.githubassets.com/images/icons/emoji/unicode/1fad2.png?v8",om:"https://github.githubassets.com/images/icons/emoji/unicode/1f549.png?v8",oman:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f4-1f1f2.png?v8",on:"https://github.githubassets.com/images/icons/emoji/unicode/1f51b.png?v8",oncoming_automobile:"https://github.githubassets.com/images/icons/emoji/unicode/1f698.png?v8",oncoming_bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68d.png?v8",oncoming_police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f694.png?v8",oncoming_taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f696.png?v8",one:"https://github.githubassets.com/images/icons/emoji/unicode/0031-20e3.png?v8",one_piece_swimsuit:"https://github.githubassets.com/images/icons/emoji/unicode/1fa71.png?v8",onion:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c5.png?v8",open_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8",open_file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c2.png?v8",open_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f450.png?v8",open_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f62e.png?v8",open_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2602.png?v8",ophiuchus:"https://github.githubassets.com/images/icons/emoji/unicode/26ce.png?v8",orange:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8",orange_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d9.png?v8",orange_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e0.png?v8",orange_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e1.png?v8",orange_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e7.png?v8",orangutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a7.png?v8",orthodox_cross:"https://github.githubassets.com/images/icons/emoji/unicode/2626.png?v8",otter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a6.png?v8",outbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e4.png?v8",owl:"https://github.githubassets.com/images/icons/emoji/unicode/1f989.png?v8",ox:"https://github.githubassets.com/images/icons/emoji/unicode/1f402.png?v8",oyster:"https://github.githubassets.com/images/icons/emoji/unicode/1f9aa.png?v8",package:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e6.png?v8",page_facing_up:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c4.png?v8",page_with_curl:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c3.png?v8",pager:"https://github.githubassets.com/images/icons/emoji/unicode/1f4df.png?v8",paintbrush:"https://github.githubassets.com/images/icons/emoji/unicode/1f58c.png?v8",pakistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f0.png?v8",palau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fc.png?v8",palestinian_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f8.png?v8",palm_down_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf3.png?v8",palm_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f334.png?v8",palm_up_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf4.png?v8",palms_up_together:"https://github.githubassets.com/images/icons/emoji/unicode/1f932.png?v8",panama:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1e6.png?v8",pancakes:"https://github.githubassets.com/images/icons/emoji/unicode/1f95e.png?v8",panda_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f43c.png?v8",paperclip:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png?v8",paperclips:"https://github.githubassets.com/images/icons/emoji/unicode/1f587.png?v8",papua_new_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ec.png?v8",parachute:"https://github.githubassets.com/images/icons/emoji/unicode/1fa82.png?v8",paraguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fe.png?v8",parasol_on_ground:"https://github.githubassets.com/images/icons/emoji/unicode/26f1.png?v8",parking:"https://github.githubassets.com/images/icons/emoji/unicode/1f17f.png?v8",parrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f99c.png?v8",part_alternation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/303d.png?v8",partly_sunny:"https://github.githubassets.com/images/icons/emoji/unicode/26c5.png?v8",partying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f973.png?v8",passenger_ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f3.png?v8",passport_control:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c2.png?v8",pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f8.png?v8",paw_prints:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8",pea_pod:"https://github.githubassets.com/images/icons/emoji/unicode/1fadb.png?v8",peace_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/262e.png?v8",peach:"https://github.githubassets.com/images/icons/emoji/unicode/1f351.png?v8",peacock:"https://github.githubassets.com/images/icons/emoji/unicode/1f99a.png?v8",peanuts:"https://github.githubassets.com/images/icons/emoji/unicode/1f95c.png?v8",pear:"https://github.githubassets.com/images/icons/emoji/unicode/1f350.png?v8",pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58a.png?v8",pencil:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8",pencil2:"https://github.githubassets.com/images/icons/emoji/unicode/270f.png?v8",penguin:"https://github.githubassets.com/images/icons/emoji/unicode/1f427.png?v8",pensive:"https://github.githubassets.com/images/icons/emoji/unicode/1f614.png?v8",people_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f91d-1f9d1.png?v8",people_hugging:"https://github.githubassets.com/images/icons/emoji/unicode/1fac2.png?v8",performing_arts:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ad.png?v8",persevere:"https://github.githubassets.com/images/icons/emoji/unicode/1f623.png?v8",person_bald:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b2.png?v8",person_curly_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b1.png?v8",person_feeding_baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f37c.png?v8",person_fencing:"https://github.githubassets.com/images/icons/emoji/unicode/1f93a.png?v8",person_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bd.png?v8",person_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bc.png?v8",person_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935.png?v8",person_red_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b0.png?v8",person_white_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b3.png?v8",person_with_crown:"https://github.githubassets.com/images/icons/emoji/unicode/1fac5.png?v8",person_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9af.png?v8",person_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473.png?v8",person_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470.png?v8",peru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ea.png?v8",petri_dish:"https://github.githubassets.com/images/icons/emoji/unicode/1f9eb.png?v8",philippines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ed.png?v8",phone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8",pick:"https://github.githubassets.com/images/icons/emoji/unicode/26cf.png?v8",pickup_truck:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fb.png?v8",pie:"https://github.githubassets.com/images/icons/emoji/unicode/1f967.png?v8",pig:"https://github.githubassets.com/images/icons/emoji/unicode/1f437.png?v8",pig2:"https://github.githubassets.com/images/icons/emoji/unicode/1f416.png?v8",pig_nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f43d.png?v8",pill:"https://github.githubassets.com/images/icons/emoji/unicode/1f48a.png?v8",pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2708.png?v8",pinata:"https://github.githubassets.com/images/icons/emoji/unicode/1fa85.png?v8",pinched_fingers:"https://github.githubassets.com/images/icons/emoji/unicode/1f90c.png?v8",pinching_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f90f.png?v8",pineapple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34d.png?v8",ping_pong:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d3.png?v8",pink_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1fa77.png?v8",pirate_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-2620.png?v8",pisces:"https://github.githubassets.com/images/icons/emoji/unicode/2653.png?v8",pitcairn_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f3.png?v8",pizza:"https://github.githubassets.com/images/icons/emoji/unicode/1f355.png?v8",placard:"https://github.githubassets.com/images/icons/emoji/unicode/1faa7.png?v8",place_of_worship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d0.png?v8",plate_with_cutlery:"https://github.githubassets.com/images/icons/emoji/unicode/1f37d.png?v8",play_or_pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ef.png?v8",playground_slide:"https://github.githubassets.com/images/icons/emoji/unicode/1f6dd.png?v8",pleading_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f97a.png?v8",plunger:"https://github.githubassets.com/images/icons/emoji/unicode/1faa0.png?v8",point_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f447.png?v8",point_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f448.png?v8",point_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f449.png?v8",point_up:"https://github.githubassets.com/images/icons/emoji/unicode/261d.png?v8",point_up_2:"https://github.githubassets.com/images/icons/emoji/unicode/1f446.png?v8",poland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f1.png?v8",polar_bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f43b-2744.png?v8",police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f693.png?v8",police_officer:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8",policeman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2642.png?v8",policewoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2640.png?v8",poodle:"https://github.githubassets.com/images/icons/emoji/unicode/1f429.png?v8",poop:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8",popcorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f37f.png?v8",portugal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f9.png?v8",post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e3.png?v8",postal_horn:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ef.png?v8",postbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ee.png?v8",potable_water:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b0.png?v8",potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f954.png?v8",potted_plant:"https://github.githubassets.com/images/icons/emoji/unicode/1fab4.png?v8",pouch:"https://github.githubassets.com/images/icons/emoji/unicode/1f45d.png?v8",poultry_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f357.png?v8",pound:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b7.png?v8",pouring_liquid:"https://github.githubassets.com/images/icons/emoji/unicode/1fad7.png?v8",pout:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8",pouting_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63e.png?v8",pouting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e.png?v8",pouting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2642.png?v8",pouting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2640.png?v8",pray:"https://github.githubassets.com/images/icons/emoji/unicode/1f64f.png?v8",prayer_beads:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ff.png?v8",pregnant_man:"https://github.githubassets.com/images/icons/emoji/unicode/1fac3.png?v8",pregnant_person:"https://github.githubassets.com/images/icons/emoji/unicode/1fac4.png?v8",pregnant_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f930.png?v8",pretzel:"https://github.githubassets.com/images/icons/emoji/unicode/1f968.png?v8",previous_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ee.png?v8",prince:"https://github.githubassets.com/images/icons/emoji/unicode/1f934.png?v8",princess:"https://github.githubassets.com/images/icons/emoji/unicode/1f478.png?v8",printer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a8.png?v8",probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9af.png?v8",puerto_rico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f7.png?v8",punch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8",purple_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e3.png?v8",purple_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49c.png?v8",purple_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7ea.png?v8",purse:"https://github.githubassets.com/images/icons/emoji/unicode/1f45b.png?v8",pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cc.png?v8",put_litter_in_its_place:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ae.png?v8",qatar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f6-1f1e6.png?v8",question:"https://github.githubassets.com/images/icons/emoji/unicode/2753.png?v8",rabbit:"https://github.githubassets.com/images/icons/emoji/unicode/1f430.png?v8",rabbit2:"https://github.githubassets.com/images/icons/emoji/unicode/1f407.png?v8",raccoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f99d.png?v8",racehorse:"https://github.githubassets.com/images/icons/emoji/unicode/1f40e.png?v8",racing_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ce.png?v8",radio:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fb.png?v8",radio_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f518.png?v8",radioactive:"https://github.githubassets.com/images/icons/emoji/unicode/2622.png?v8",rage:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8",rage1:"https://github.githubassets.com/images/icons/emoji/rage1.png?v8",rage2:"https://github.githubassets.com/images/icons/emoji/rage2.png?v8",rage3:"https://github.githubassets.com/images/icons/emoji/rage3.png?v8",rage4:"https://github.githubassets.com/images/icons/emoji/rage4.png?v8",railway_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f683.png?v8",railway_track:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e4.png?v8",rainbow:"https://github.githubassets.com/images/icons/emoji/unicode/1f308.png?v8",rainbow_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3-1f308.png?v8",raised_back_of_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f91a.png?v8",raised_eyebrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f928.png?v8",raised_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8",raised_hand_with_fingers_splayed:"https://github.githubassets.com/images/icons/emoji/unicode/1f590.png?v8",raised_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f64c.png?v8",raising_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b.png?v8",raising_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2642.png?v8",raising_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2640.png?v8",ram:"https://github.githubassets.com/images/icons/emoji/unicode/1f40f.png?v8",ramen:"https://github.githubassets.com/images/icons/emoji/unicode/1f35c.png?v8",rat:"https://github.githubassets.com/images/icons/emoji/unicode/1f400.png?v8",razor:"https://github.githubassets.com/images/icons/emoji/unicode/1fa92.png?v8",receipt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fe.png?v8",record_button:"https://github.githubassets.com/images/icons/emoji/unicode/23fa.png?v8",recycle:"https://github.githubassets.com/images/icons/emoji/unicode/267b.png?v8",red_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8",red_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f534.png?v8",red_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e7.png?v8",red_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b0.png?v8",red_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b0.png?v8",red_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e5.png?v8",registered:"https://github.githubassets.com/images/icons/emoji/unicode/00ae.png?v8",relaxed:"https://github.githubassets.com/images/icons/emoji/unicode/263a.png?v8",relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f60c.png?v8",reminder_ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f397.png?v8",repeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f501.png?v8",repeat_one:"https://github.githubassets.com/images/icons/emoji/unicode/1f502.png?v8",rescue_worker_helmet:"https://github.githubassets.com/images/icons/emoji/unicode/26d1.png?v8",restroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bb.png?v8",reunion:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1ea.png?v8",revolving_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f49e.png?v8",rewind:"https://github.githubassets.com/images/icons/emoji/unicode/23ea.png?v8",rhinoceros:"https://github.githubassets.com/images/icons/emoji/unicode/1f98f.png?v8",ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f380.png?v8",rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f35a.png?v8",rice_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f359.png?v8",rice_cracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f358.png?v8",rice_scene:"https://github.githubassets.com/images/icons/emoji/unicode/1f391.png?v8",right_anger_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ef.png?v8",rightwards_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf1.png?v8",rightwards_pushing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1faf8.png?v8",ring:"https://github.githubassets.com/images/icons/emoji/unicode/1f48d.png?v8",ring_buoy:"https://github.githubassets.com/images/icons/emoji/unicode/1f6df.png?v8",ringed_planet:"https://github.githubassets.com/images/icons/emoji/unicode/1fa90.png?v8",robot:"https://github.githubassets.com/images/icons/emoji/unicode/1f916.png?v8",rock:"https://github.githubassets.com/images/icons/emoji/unicode/1faa8.png?v8",rocket:"https://github.githubassets.com/images/icons/emoji/unicode/1f680.png?v8",rofl:"https://github.githubassets.com/images/icons/emoji/unicode/1f923.png?v8",roll_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f644.png?v8",roll_of_paper:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fb.png?v8",roller_coaster:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a2.png?v8",roller_skate:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fc.png?v8",romania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f4.png?v8",rooster:"https://github.githubassets.com/images/icons/emoji/unicode/1f413.png?v8",rose:"https://github.githubassets.com/images/icons/emoji/unicode/1f339.png?v8",rosette:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f5.png?v8",rotating_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a8.png?v8",round_pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cd.png?v8",rowboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3.png?v8",rowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2642.png?v8",rowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2640.png?v8",ru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fa.png?v8",rugby_football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c9.png?v8",runner:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8",running:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8",running_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2642.png?v8",running_shirt_with_sash:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bd.png?v8",running_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2640.png?v8",rwanda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fc.png?v8",sa:"https://github.githubassets.com/images/icons/emoji/unicode/1f202.png?v8",safety_pin:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f7.png?v8",safety_vest:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ba.png?v8",sagittarius:"https://github.githubassets.com/images/icons/emoji/unicode/2650.png?v8",sailboat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8",sake:"https://github.githubassets.com/images/icons/emoji/unicode/1f376.png?v8",salt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c2.png?v8",saluting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae1.png?v8",samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1f8.png?v8",san_marino:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f2.png?v8",sandal:"https://github.githubassets.com/images/icons/emoji/unicode/1f461.png?v8",sandwich:"https://github.githubassets.com/images/icons/emoji/unicode/1f96a.png?v8",santa:"https://github.githubassets.com/images/icons/emoji/unicode/1f385.png?v8",sao_tome_principe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f9.png?v8",sari:"https://github.githubassets.com/images/icons/emoji/unicode/1f97b.png?v8",sassy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8",sassy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8",satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e1.png?v8",satisfied:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8",saudi_arabia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e6.png?v8",sauna_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2642.png?v8",sauna_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6.png?v8",sauna_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2640.png?v8",sauropod:"https://github.githubassets.com/images/icons/emoji/unicode/1f995.png?v8",saxophone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b7.png?v8",scarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e3.png?v8",school:"https://github.githubassets.com/images/icons/emoji/unicode/1f3eb.png?v8",school_satchel:"https://github.githubassets.com/images/icons/emoji/unicode/1f392.png?v8",scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f52c.png?v8",scissors:"https://github.githubassets.com/images/icons/emoji/unicode/2702.png?v8",scorpion:"https://github.githubassets.com/images/icons/emoji/unicode/1f982.png?v8",scorpius:"https://github.githubassets.com/images/icons/emoji/unicode/264f.png?v8",scotland:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png?v8",scream:"https://github.githubassets.com/images/icons/emoji/unicode/1f631.png?v8",scream_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f640.png?v8",screwdriver:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9b.png?v8",scroll:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dc.png?v8",seal:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ad.png?v8",seat:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ba.png?v8",secret:"https://github.githubassets.com/images/icons/emoji/unicode/3299.png?v8",see_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f648.png?v8",seedling:"https://github.githubassets.com/images/icons/emoji/unicode/1f331.png?v8",selfie:"https://github.githubassets.com/images/icons/emoji/unicode/1f933.png?v8",senegal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f3.png?v8",serbia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f8.png?v8",service_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f415-1f9ba.png?v8",seven:"https://github.githubassets.com/images/icons/emoji/unicode/0037-20e3.png?v8",sewing_needle:"https://github.githubassets.com/images/icons/emoji/unicode/1faa1.png?v8",seychelles:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e8.png?v8",shaking_face:"https://github.githubassets.com/images/icons/emoji/unicode/1fae8.png?v8",shallow_pan_of_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f958.png?v8",shamrock:"https://github.githubassets.com/images/icons/emoji/unicode/2618.png?v8",shark:"https://github.githubassets.com/images/icons/emoji/unicode/1f988.png?v8",shaved_ice:"https://github.githubassets.com/images/icons/emoji/unicode/1f367.png?v8",sheep:"https://github.githubassets.com/images/icons/emoji/unicode/1f411.png?v8",shell:"https://github.githubassets.com/images/icons/emoji/unicode/1f41a.png?v8",shield:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e1.png?v8",shinto_shrine:"https://github.githubassets.com/images/icons/emoji/unicode/26e9.png?v8",ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a2.png?v8",shipit:"https://github.githubassets.com/images/icons/emoji/shipit.png?v8",shirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8",shit:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8",shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8",shopping:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cd.png?v8",shopping_cart:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d2.png?v8",shorts:"https://github.githubassets.com/images/icons/emoji/unicode/1fa73.png?v8",shower:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bf.png?v8",shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f990.png?v8",shrug:"https://github.githubassets.com/images/icons/emoji/unicode/1f937.png?v8",shushing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92b.png?v8",sierra_leone:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f1.png?v8",signal_strength:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f6.png?v8",singapore:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ec.png?v8",singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a4.png?v8",sint_maarten:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fd.png?v8",six:"https://github.githubassets.com/images/icons/emoji/unicode/0036-20e3.png?v8",six_pointed_star:"https://github.githubassets.com/images/icons/emoji/unicode/1f52f.png?v8",skateboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f9.png?v8",ski:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bf.png?v8",skier:"https://github.githubassets.com/images/icons/emoji/unicode/26f7.png?v8",skull:"https://github.githubassets.com/images/icons/emoji/unicode/1f480.png?v8",skull_and_crossbones:"https://github.githubassets.com/images/icons/emoji/unicode/2620.png?v8",skunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a8.png?v8",sled:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f7.png?v8",sleeping:"https://github.githubassets.com/images/icons/emoji/unicode/1f634.png?v8",sleeping_bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cc.png?v8",sleepy:"https://github.githubassets.com/images/icons/emoji/unicode/1f62a.png?v8",slightly_frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f641.png?v8",slightly_smiling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f642.png?v8",slot_machine:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b0.png?v8",sloth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a5.png?v8",slovakia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f0.png?v8",slovenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ee.png?v8",small_airplane:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e9.png?v8",small_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f539.png?v8",small_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f538.png?v8",small_red_triangle:"https://github.githubassets.com/images/icons/emoji/unicode/1f53a.png?v8",small_red_triangle_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f53b.png?v8",smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f604.png?v8",smile_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f638.png?v8",smiley:"https://github.githubassets.com/images/icons/emoji/unicode/1f603.png?v8",smiley_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63a.png?v8",smiling_face_with_tear:"https://github.githubassets.com/images/icons/emoji/unicode/1f972.png?v8",smiling_face_with_three_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f970.png?v8",smiling_imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f608.png?v8",smirk:"https://github.githubassets.com/images/icons/emoji/unicode/1f60f.png?v8",smirk_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63c.png?v8",smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ac.png?v8",snail:"https://github.githubassets.com/images/icons/emoji/unicode/1f40c.png?v8",snake:"https://github.githubassets.com/images/icons/emoji/unicode/1f40d.png?v8",sneezing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f927.png?v8",snowboarder:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c2.png?v8",snowflake:"https://github.githubassets.com/images/icons/emoji/unicode/2744.png?v8",snowman:"https://github.githubassets.com/images/icons/emoji/unicode/26c4.png?v8",snowman_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/2603.png?v8",soap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fc.png?v8",sob:"https://github.githubassets.com/images/icons/emoji/unicode/1f62d.png?v8",soccer:"https://github.githubassets.com/images/icons/emoji/unicode/26bd.png?v8",socks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e6.png?v8",softball:"https://github.githubassets.com/images/icons/emoji/unicode/1f94e.png?v8",solomon_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e7.png?v8",somalia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f4.png?v8",soon:"https://github.githubassets.com/images/icons/emoji/unicode/1f51c.png?v8",sos:"https://github.githubassets.com/images/icons/emoji/unicode/1f198.png?v8",sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f509.png?v8",south_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1e6.png?v8",south_georgia_south_sandwich_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f8.png?v8",south_sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f8.png?v8",space_invader:"https://github.githubassets.com/images/icons/emoji/unicode/1f47e.png?v8",spades:"https://github.githubassets.com/images/icons/emoji/unicode/2660.png?v8",spaghetti:"https://github.githubassets.com/images/icons/emoji/unicode/1f35d.png?v8",sparkle:"https://github.githubassets.com/images/icons/emoji/unicode/2747.png?v8",sparkler:"https://github.githubassets.com/images/icons/emoji/unicode/1f387.png?v8",sparkles:"https://github.githubassets.com/images/icons/emoji/unicode/2728.png?v8",sparkling_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f496.png?v8",speak_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f64a.png?v8",speaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f508.png?v8",speaking_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e3.png?v8",speech_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ac.png?v8",speedboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a4.png?v8",spider:"https://github.githubassets.com/images/icons/emoji/unicode/1f577.png?v8",spider_web:"https://github.githubassets.com/images/icons/emoji/unicode/1f578.png?v8",spiral_calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d3.png?v8",spiral_notepad:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d2.png?v8",sponge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fd.png?v8",spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f944.png?v8",squid:"https://github.githubassets.com/images/icons/emoji/unicode/1f991.png?v8",sri_lanka:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f0.png?v8",st_barthelemy:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f1.png?v8",st_helena:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ed.png?v8",st_kitts_nevis:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f3.png?v8",st_lucia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e8.png?v8",st_martin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1eb.png?v8",st_pierre_miquelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f2.png?v8",st_vincent_grenadines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e8.png?v8",stadium:"https://github.githubassets.com/images/icons/emoji/unicode/1f3df.png?v8",standing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2642.png?v8",standing_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd.png?v8",standing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2640.png?v8",star:"https://github.githubassets.com/images/icons/emoji/unicode/2b50.png?v8",star2:"https://github.githubassets.com/images/icons/emoji/unicode/1f31f.png?v8",star_and_crescent:"https://github.githubassets.com/images/icons/emoji/unicode/262a.png?v8",star_of_david:"https://github.githubassets.com/images/icons/emoji/unicode/2721.png?v8",star_struck:"https://github.githubassets.com/images/icons/emoji/unicode/1f929.png?v8",stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f320.png?v8",station:"https://github.githubassets.com/images/icons/emoji/unicode/1f689.png?v8",statue_of_liberty:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fd.png?v8",steam_locomotive:"https://github.githubassets.com/images/icons/emoji/unicode/1f682.png?v8",stethoscope:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7a.png?v8",stew:"https://github.githubassets.com/images/icons/emoji/unicode/1f372.png?v8",stop_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f9.png?v8",stop_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d1.png?v8",stopwatch:"https://github.githubassets.com/images/icons/emoji/unicode/23f1.png?v8",straight_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cf.png?v8",strawberry:"https://github.githubassets.com/images/icons/emoji/unicode/1f353.png?v8",stuck_out_tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f61b.png?v8",stuck_out_tongue_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61d.png?v8",stuck_out_tongue_winking_eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f61c.png?v8",student:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f393.png?v8",studio_microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f399.png?v8",stuffed_flatbread:"https://github.githubassets.com/images/icons/emoji/unicode/1f959.png?v8",sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e9.png?v8",sun_behind_large_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f325.png?v8",sun_behind_rain_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f326.png?v8",sun_behind_small_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f324.png?v8",sun_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31e.png?v8",sunflower:"https://github.githubassets.com/images/icons/emoji/unicode/1f33b.png?v8",sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f60e.png?v8",sunny:"https://github.githubassets.com/images/icons/emoji/unicode/2600.png?v8",sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f305.png?v8",sunrise_over_mountains:"https://github.githubassets.com/images/icons/emoji/unicode/1f304.png?v8",superhero:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8.png?v8",superhero_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2642.png?v8",superhero_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2640.png?v8",supervillain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9.png?v8",supervillain_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2642.png?v8",supervillain_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2640.png?v8",surfer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4.png?v8",surfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2642.png?v8",surfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2640.png?v8",suriname:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f7.png?v8",sushi:"https://github.githubassets.com/images/icons/emoji/unicode/1f363.png?v8",suspect:"https://github.githubassets.com/images/icons/emoji/suspect.png?v8",suspension_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69f.png?v8",svalbard_jan_mayen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ef.png?v8",swan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a2.png?v8",swaziland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ff.png?v8",sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f613.png?v8",sweat_drops:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a6.png?v8",sweat_smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f605.png?v8",sweden:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ea.png?v8",sweet_potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f360.png?v8",swim_brief:"https://github.githubassets.com/images/icons/emoji/unicode/1fa72.png?v8",swimmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca.png?v8",swimming_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2642.png?v8",swimming_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2640.png?v8",switzerland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ed.png?v8",symbols:"https://github.githubassets.com/images/icons/emoji/unicode/1f523.png?v8",synagogue:"https://github.githubassets.com/images/icons/emoji/unicode/1f54d.png?v8",syria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fe.png?v8",syringe:"https://github.githubassets.com/images/icons/emoji/unicode/1f489.png?v8","t-rex":"https://github.githubassets.com/images/icons/emoji/unicode/1f996.png?v8",taco:"https://github.githubassets.com/images/icons/emoji/unicode/1f32e.png?v8",tada:"https://github.githubassets.com/images/icons/emoji/unicode/1f389.png?v8",taiwan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fc.png?v8",tajikistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ef.png?v8",takeout_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f961.png?v8",tamale:"https://github.githubassets.com/images/icons/emoji/unicode/1fad4.png?v8",tanabata_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f38b.png?v8",tangerine:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8",tanzania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ff.png?v8",taurus:"https://github.githubassets.com/images/icons/emoji/unicode/2649.png?v8",taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f695.png?v8",tea:"https://github.githubassets.com/images/icons/emoji/unicode/1f375.png?v8",teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3eb.png?v8",teapot:"https://github.githubassets.com/images/icons/emoji/unicode/1fad6.png?v8",technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bb.png?v8",teddy_bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f8.png?v8",telephone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8",telephone_receiver:"https://github.githubassets.com/images/icons/emoji/unicode/1f4de.png?v8",telescope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52d.png?v8",tennis:"https://github.githubassets.com/images/icons/emoji/unicode/1f3be.png?v8",tent:"https://github.githubassets.com/images/icons/emoji/unicode/26fa.png?v8",test_tube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ea.png?v8",thailand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ed.png?v8",thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f321.png?v8",thinking:"https://github.githubassets.com/images/icons/emoji/unicode/1f914.png?v8",thong_sandal:"https://github.githubassets.com/images/icons/emoji/unicode/1fa74.png?v8",thought_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ad.png?v8",thread:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f5.png?v8",three:"https://github.githubassets.com/images/icons/emoji/unicode/0033-20e3.png?v8",thumbsdown:"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8",thumbsup:"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8",ticket:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ab.png?v8",tickets:"https://github.githubassets.com/images/icons/emoji/unicode/1f39f.png?v8",tiger:"https://github.githubassets.com/images/icons/emoji/unicode/1f42f.png?v8",tiger2:"https://github.githubassets.com/images/icons/emoji/unicode/1f405.png?v8",timer_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f2.png?v8",timor_leste:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f1.png?v8",tipping_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8",tipping_hand_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8",tipping_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8",tired_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f62b.png?v8",tm:"https://github.githubassets.com/images/icons/emoji/unicode/2122.png?v8",togo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ec.png?v8",toilet:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bd.png?v8",tokelau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f0.png?v8",tokyo_tower:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fc.png?v8",tomato:"https://github.githubassets.com/images/icons/emoji/unicode/1f345.png?v8",tonga:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f4.png?v8",tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f445.png?v8",toolbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f0.png?v8",tooth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b7.png?v8",toothbrush:"https://github.githubassets.com/images/icons/emoji/unicode/1faa5.png?v8",top:"https://github.githubassets.com/images/icons/emoji/unicode/1f51d.png?v8",tophat:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a9.png?v8",tornado:"https://github.githubassets.com/images/icons/emoji/unicode/1f32a.png?v8",tr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f7.png?v8",trackball:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b2.png?v8",tractor:"https://github.githubassets.com/images/icons/emoji/unicode/1f69c.png?v8",traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a5.png?v8",train:"https://github.githubassets.com/images/icons/emoji/unicode/1f68b.png?v8",train2:"https://github.githubassets.com/images/icons/emoji/unicode/1f686.png?v8",tram:"https://github.githubassets.com/images/icons/emoji/unicode/1f68a.png?v8",transgender_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3-26a7.png?v8",transgender_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/26a7.png?v8",triangular_flag_on_post:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a9.png?v8",triangular_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d0.png?v8",trident:"https://github.githubassets.com/images/icons/emoji/unicode/1f531.png?v8",trinidad_tobago:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f9.png?v8",tristan_da_cunha:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e6.png?v8",triumph:"https://github.githubassets.com/images/icons/emoji/unicode/1f624.png?v8",troll:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cc.png?v8",trolleybus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68e.png?v8",trollface:"https://github.githubassets.com/images/icons/emoji/trollface.png?v8",trophy:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c6.png?v8",tropical_drink:"https://github.githubassets.com/images/icons/emoji/unicode/1f379.png?v8",tropical_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f420.png?v8",truck:"https://github.githubassets.com/images/icons/emoji/unicode/1f69a.png?v8",trumpet:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ba.png?v8",tshirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8",tulip:"https://github.githubassets.com/images/icons/emoji/unicode/1f337.png?v8",tumbler_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f943.png?v8",tunisia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f3.png?v8",turkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f983.png?v8",turkmenistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f2.png?v8",turks_caicos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e8.png?v8",turtle:"https://github.githubassets.com/images/icons/emoji/unicode/1f422.png?v8",tuvalu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fb.png?v8",tv:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fa.png?v8",twisted_rightwards_arrows:"https://github.githubassets.com/images/icons/emoji/unicode/1f500.png?v8",two:"https://github.githubassets.com/images/icons/emoji/unicode/0032-20e3.png?v8",two_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f495.png?v8",two_men_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46c.png?v8",two_women_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46d.png?v8",u5272:"https://github.githubassets.com/images/icons/emoji/unicode/1f239.png?v8",u5408:"https://github.githubassets.com/images/icons/emoji/unicode/1f234.png?v8",u55b6:"https://github.githubassets.com/images/icons/emoji/unicode/1f23a.png?v8",u6307:"https://github.githubassets.com/images/icons/emoji/unicode/1f22f.png?v8",u6708:"https://github.githubassets.com/images/icons/emoji/unicode/1f237.png?v8",u6709:"https://github.githubassets.com/images/icons/emoji/unicode/1f236.png?v8",u6e80:"https://github.githubassets.com/images/icons/emoji/unicode/1f235.png?v8",u7121:"https://github.githubassets.com/images/icons/emoji/unicode/1f21a.png?v8",u7533:"https://github.githubassets.com/images/icons/emoji/unicode/1f238.png?v8",u7981:"https://github.githubassets.com/images/icons/emoji/unicode/1f232.png?v8",u7a7a:"https://github.githubassets.com/images/icons/emoji/unicode/1f233.png?v8",uganda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ec.png?v8",uk:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8",ukraine:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1e6.png?v8",umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2614.png?v8",unamused:"https://github.githubassets.com/images/icons/emoji/unicode/1f612.png?v8",underage:"https://github.githubassets.com/images/icons/emoji/unicode/1f51e.png?v8",unicorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f984.png?v8",united_arab_emirates:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ea.png?v8",united_nations:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f3.png?v8",unlock:"https://github.githubassets.com/images/icons/emoji/unicode/1f513.png?v8",up:"https://github.githubassets.com/images/icons/emoji/unicode/1f199.png?v8",upside_down_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f643.png?v8",uruguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1fe.png?v8",us:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f8.png?v8",us_outlying_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f2.png?v8",us_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ee.png?v8",uzbekistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ff.png?v8",v:"https://github.githubassets.com/images/icons/emoji/unicode/270c.png?v8",vampire:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db.png?v8",vampire_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2642.png?v8",vampire_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2640.png?v8",vanuatu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1fa.png?v8",vatican_city:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e6.png?v8",venezuela:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ea.png?v8",vertical_traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a6.png?v8",vhs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fc.png?v8",vibration_mode:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f3.png?v8",video_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f9.png?v8",video_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ae.png?v8",vietnam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1f3.png?v8",violin:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bb.png?v8",virgo:"https://github.githubassets.com/images/icons/emoji/unicode/264d.png?v8",volcano:"https://github.githubassets.com/images/icons/emoji/unicode/1f30b.png?v8",volleyball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d0.png?v8",vomiting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92e.png?v8",vs:"https://github.githubassets.com/images/icons/emoji/unicode/1f19a.png?v8",vulcan_salute:"https://github.githubassets.com/images/icons/emoji/unicode/1f596.png?v8",waffle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c7.png?v8",wales:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png?v8",walking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6.png?v8",walking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2642.png?v8",walking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2640.png?v8",wallis_futuna:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1eb.png?v8",waning_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f318.png?v8",waning_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f316.png?v8",warning:"https://github.githubassets.com/images/icons/emoji/unicode/26a0.png?v8",wastebasket:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d1.png?v8",watch:"https://github.githubassets.com/images/icons/emoji/unicode/231a.png?v8",water_buffalo:"https://github.githubassets.com/images/icons/emoji/unicode/1f403.png?v8",water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d.png?v8",watermelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f349.png?v8",wave:"https://github.githubassets.com/images/icons/emoji/unicode/1f44b.png?v8",wavy_dash:"https://github.githubassets.com/images/icons/emoji/unicode/3030.png?v8",waxing_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f312.png?v8",waxing_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8",wc:"https://github.githubassets.com/images/icons/emoji/unicode/1f6be.png?v8",weary:"https://github.githubassets.com/images/icons/emoji/unicode/1f629.png?v8",wedding:"https://github.githubassets.com/images/icons/emoji/unicode/1f492.png?v8",weight_lifting:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb.png?v8",weight_lifting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2642.png?v8",weight_lifting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2640.png?v8",western_sahara:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ed.png?v8",whale:"https://github.githubassets.com/images/icons/emoji/unicode/1f433.png?v8",whale2:"https://github.githubassets.com/images/icons/emoji/unicode/1f40b.png?v8",wheel:"https://github.githubassets.com/images/icons/emoji/unicode/1f6de.png?v8",wheel_of_dharma:"https://github.githubassets.com/images/icons/emoji/unicode/2638.png?v8",wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/267f.png?v8",white_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2705.png?v8",white_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26aa.png?v8",white_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3.png?v8",white_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ae.png?v8",white_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b3.png?v8",white_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b3.png?v8",white_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90d.png?v8",white_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1c.png?v8",white_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fd.png?v8",white_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fb.png?v8",white_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25ab.png?v8",white_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f533.png?v8",wilted_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f940.png?v8",wind_chime:"https://github.githubassets.com/images/icons/emoji/unicode/1f390.png?v8",wind_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f32c.png?v8",window:"https://github.githubassets.com/images/icons/emoji/unicode/1fa9f.png?v8",wine_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f377.png?v8",wing:"https://github.githubassets.com/images/icons/emoji/unicode/1fabd.png?v8",wink:"https://github.githubassets.com/images/icons/emoji/unicode/1f609.png?v8",wireless:"https://github.githubassets.com/images/icons/emoji/unicode/1f6dc.png?v8",wolf:"https://github.githubassets.com/images/icons/emoji/unicode/1f43a.png?v8",woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469.png?v8",woman_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a8.png?v8",woman_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f680.png?v8",woman_beard:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4-2640.png?v8",woman_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2640.png?v8",woman_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f373.png?v8",woman_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8",woman_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2640.png?v8",woman_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3ed.png?v8",woman_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f33e.png?v8",woman_feeding_baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f37c.png?v8",woman_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f692.png?v8",woman_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2695.png?v8",woman_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bd.png?v8",woman_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bc.png?v8",woman_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935-2640.png?v8",woman_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2696.png?v8",woman_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2640.png?v8",woman_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f527.png?v8",woman_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bc.png?v8",woman_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2708.png?v8",woman_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2640.png?v8",woman_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2640.png?v8",woman_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f52c.png?v8",woman_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2640.png?v8",woman_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a4.png?v8",woman_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f393.png?v8",woman_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3eb.png?v8",woman_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bb.png?v8",woman_with_headscarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d5.png?v8",woman_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9af.png?v8",woman_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2640.png?v8",woman_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470-2640.png?v8",womans_clothes:"https://github.githubassets.com/images/icons/emoji/unicode/1f45a.png?v8",womans_hat:"https://github.githubassets.com/images/icons/emoji/unicode/1f452.png?v8",women_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2640.png?v8",womens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ba.png?v8",wood:"https://github.githubassets.com/images/icons/emoji/unicode/1fab5.png?v8",woozy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f974.png?v8",world_map:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fa.png?v8",worm:"https://github.githubassets.com/images/icons/emoji/unicode/1fab1.png?v8",worried:"https://github.githubassets.com/images/icons/emoji/unicode/1f61f.png?v8",wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f527.png?v8",wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c.png?v8",writing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270d.png?v8",x:"https://github.githubassets.com/images/icons/emoji/unicode/274c.png?v8",x_ray:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7b.png?v8",yarn:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f6.png?v8",yawning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f971.png?v8",yellow_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e1.png?v8",yellow_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49b.png?v8",yellow_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e8.png?v8",yemen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1ea.png?v8",yen:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b4.png?v8",yin_yang:"https://github.githubassets.com/images/icons/emoji/unicode/262f.png?v8",yo_yo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa80.png?v8",yum:"https://github.githubassets.com/images/icons/emoji/unicode/1f60b.png?v8",zambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1f2.png?v8",zany_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92a.png?v8",zap:"https://github.githubassets.com/images/icons/emoji/unicode/26a1.png?v8",zebra:"https://github.githubassets.com/images/icons/emoji/unicode/1f993.png?v8",zero:"https://github.githubassets.com/images/icons/emoji/unicode/0030-20e3.png?v8",zimbabwe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1fc.png?v8",zipper_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f910.png?v8",zombie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df.png?v8",zombie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2642.png?v8",zombie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2640.png?v8",zzz:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a4.png?v8"},defaultOptions=("undefined"!=typeof window&&(window.githubEmojis=githubEmojiData),"undefined"!=typeof module&&(module.exports=githubEmojiData),{unicode:!1});function markedEmoji(n){if((n={...defaultOptions,...n}).emojis)return{extensions:[{name:"emoji",level:"inline",start(e){return e.indexOf(":")},tokenizer(e,t){e=/^:(.+?):/.exec(e);if(e){var i=e[1],s=n.emojis[i];if(s)return{type:"emoji",raw:e[0],name:i,emoji:s}}},renderer(e){return n.unicode?e.emoji:`${e.name}`}}]};throw new Error("Must provide emojis to markedEmoji")}function baseUrl(i){i=i.trim().replace(/\/+$/,"/");let s=/^[\w+]+:\/\//,n=s.test(i);var e="http://__dummy__";let o=new URL(i,e),a=e.length+(i.startsWith("/")?0:1);return{walkTokens(e){if(["link","image"].includes(e.type)&&!s.test(e.href)&&!e.href.startsWith("#"))if(n)try{e.href=new URL(e.href,i).href}catch(e){}else if(!e.href.startsWith("/"))try{var t=new URL(e.href,o).href;e.href=t.slice(a)}catch(e){}}}}((e,t)=>{"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})})(this,function(t){function e(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function i(e){t.defaults=e}t.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};let s=/[&<>"']/,R=new RegExp(s.source,"g"),n=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,F=new RegExp(n.source,"g"),q={"&":"&","<":"<",">":">",'"':""","'":"'"},o=e=>q[e];function a(e,t){if(t){if(s.test(e))return e.replace(R,o)}else if(n.test(e))return e.replace(F,o);return e}let z=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;let H=/(^|[^\[])\^/g;function c(e,t){let s="string"==typeof e?e:e.source,n=(t=t||"",{replace:(e,t)=>{let i="string"==typeof t?t:t.source;return i=i.replace(H,"$1"),s=s.replace(e,i),n},getRegex:()=>new RegExp(s,t)});return n}function u(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}var g={exec:()=>null};function r(e,t){var i=e.replace(/\|/g,(e,t,i)=>{let s=!1,n=t;for(;0<=--n&&"\\"===i[n];)s=!s;return s?"|":" |"}).split(/ \|/);let s=0;if(i[0].trim()||i.shift(),0t)i.splice(t);else for(;i.length{if(null===(e=e.match(/^(\s+)(?:```)/)))return t;let i=e[1];return t.split("\n").map(e=>{var t=e.match(/^\s+/);return null!==t&&([t]=t,t.length>=i.length)?e.slice(i.length):e}).join("\n")})(e,i[3]||"");return{type:"code",raw:e,lang:i[2]&&i[2].trim().replace(this.rules.inline.anyPunctuation,"$1"),text:t}}}heading(e){let i=this.rules.block.heading.exec(e);if(i){let t=i[2].trim();if(/#$/.test(t)){let e=h(t,"#");!this.options.pedantic&&e&&!/ $/.test(e)||(t=e.trim())}return{type:"heading",raw:i[0],depth:i[1].length,text:t,tokens:this.lexer.inline(t)}}}hr(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}}blockquote(e){var i=this.rules.block.blockquote.exec(e);if(i){let e=h(i[0].replace(/^ *>[ \t]?/gm,""),"\n"),t=this.lexer.state.top;this.lexer.state.top=!0;var s=this.lexer.blockTokens(e);return this.lexer.state.top=t,{type:"blockquote",raw:i[0],tokens:s,text:e}}}list(r){let h=this.rules.block.list.exec(r);if(h){let e=h[1].trim();var t=1" ".repeat(3*e.length)),o=r.split("\n",1)[0],a=0,c=(this.options.pedantic?(a=2,g=n.trimStart()):(a=4<(a=h[2].search(/[^ ]/))?1:a,g=n.slice(a),a+=h[1].length),!1);if(!n&&/^ *$/.test(o)&&(u+=o+"\n",r=r.substring(o.length+1),e=!0),!e){let e=new RegExp(`^ {0,${Math.min(3,a-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),t=new RegExp(`^ {0,${Math.min(3,a-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),i=new RegExp(`^ {0,${Math.min(3,a-1)}}(?:\`\`\`|~~~)`),s=new RegExp(`^ {0,${Math.min(3,a-1)}}#`);for(;r;){var l=r.split("\n",1)[0];if(o=l,this.options.pedantic&&(o=o.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),i.test(o))break;if(s.test(o))break;if(e.test(o))break;if(t.test(r))break;if(o.search(/[^ ]/)>=a||!o.trim())g+="\n"+o.slice(a);else{if(c)break;if(4<=n.search(/[^ ]/))break;if(i.test(n))break;if(s.test(n))break;if(t.test(n))break;g+="\n"+o}c||o.trim()||(c=!0),u+=l+"\n",r=r.substring(l.length+1),n=o.slice(a)}}m.loose||(s?m.loose=!0:/\n *\n *$/.test(u)&&(s=!0));let t,i=null;this.options.gfm&&(i=/^\[[ xX]\] /.exec(g))&&(t="[ ] "!==i[0],g=g.replace(/^\[[ xX]\] +/,"")),m.items.push({type:"list_item",raw:u,task:!!i,checked:t,loose:!1,text:g,tokens:[]}),m.raw+=u}m.items[m.items.length-1].raw=u.trimEnd(),m.items[m.items.length-1].text=g.trimEnd(),m.raw=m.raw.trimEnd();for(let i=0;i"space"===e.type),t=0/\n.*\n/.test(e.raw));m.loose=t}if(m.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=s[3]&&s[3].substring(1,s[3].length-1).replace(this.rules.inline.anyPunctuation,"$1");return{type:"def",tag:e,raw:s[0],href:t,title:i}}}table(e){e=this.rules.block.table.exec(e);if(e&&/[:|]/.test(e[2])){var t=r(e[1]),i=e[2].replace(/^\||\| *$/g,"").split("|"),s=e[3]&&e[3].trim()?e[3].replace(/\n[ \t]*$/,"").split("\n"):[],n={type:"table",raw:e[0],header:[],align:[],rows:[]};if(t.length===i.length){for(let e of i)/^ *-+: *$/.test(e)?n.align.push("right"):/^ *:-+: *$/.test(e)?n.align.push("center"):/^ *:-+ *$/.test(e)?n.align.push("left"):n.align.push(null);for(let e of t)n.header.push({text:e,tokens:this.lexer.inline(e)});for(let e of s)n.rows.push(r(e,n.header.length).map(e=>({text:e,tokens:this.lexer.inline(e)})));return n}}}lheading(e){e=this.rules.block.lheading.exec(e);if(e)return{type:"heading",raw:e[0],depth:"="===e[2].charAt(0)?1:2,text:e[1],tokens:this.lexer.inline(e[1])}}paragraph(e){var t=this.rules.block.paragraph.exec(e);if(t){let e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){e=this.rules.block.text.exec(e);if(e)return{type:"text",raw:e[0],text:e[0],tokens:this.lexer.inline(e[0])}}escape(e){e=this.rules.inline.escape.exec(e);if(e)return{type:"escape",raw:e[0],text:a(e[1])}}tag(e){e=this.rules.inline.tag.exec(e);if(e)return!this.lexer.state.inLink&&/^/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:e[0]}}link(e){let n=this.rules.inline.link.exec(e);if(n){let t=n[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;let e=h(t.slice(0,-1),"\\");if((t.length-e.length)%2==0)return}else{let e=(i=>{if(-1!==i.indexOf("()"[1])){let t=0;for(let e=0;e$/.test(t)?i.slice(1):i.slice(1,-1)),m(n,{href:i&&i.replace(this.rules.inline.anyPunctuation,"$1"),title:s&&s.replace(this.rules.inline.anyPunctuation,"$1")},n[0],this.lexer)}}reflink(e,t){let i;if(i=(i=this.rules.inline.reflink.exec(e))||this.rules.inline.nolink.exec(e)){let e=t[(i[2]||i[1]).replace(/\s+/g," ").toLowerCase()];if(e)return m(i,e,i[0],this.lexer);{let e=i[0].charAt(0);return{type:"text",raw:e,text:e}}}}emStrong(a,t,e=""){let c=this.rules.inline.emStrongLDelim.exec(a);if(c&&(!c[3]||!e.match(/[\p{L}\p{N}]/u))&&(!c[1]&&!c[2]||!e||this.rules.inline.punctuation.exec(e))){let i=[...c[0]].length-1,e,s,n=i,o=0;var u="*"===c[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*a.length+i);null!=(c=u.exec(t));)if(e=c[1]||c[2]||c[3]||c[4]||c[5]||c[6])if(s=[...e].length,c[3]||c[4])n+=s;else if((c[5]||c[6])&&i%3&&!((i+s)%3))o+=s;else if(!(0<(n-=s))){s=Math.min(s,s+n+o);let e=[...c[0]][0].length,t=a.slice(0,i+c.index+e+s);if(Math.min(i,s)%2){let e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}var g=t.slice(2,-2);return{type:"strong",raw:t,text:g,tokens:this.lexer.inlineTokens(g)}}}}codespan(t){t=this.rules.inline.code.exec(t);if(t){let e=t[2].replace(/\n/g," ");var i=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return e=a(e=i&&s?e.substring(1,e.length-1):e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}}del(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}}autolink(i){i=this.rules.inline.autolink.exec(i);if(i){let e,t;return t="@"===i[2]?"mailto:"+(e=a(i[1])):e=a(i[1]),{type:"link",raw:i[0],text:e,href:t,tokens:[{type:"text",raw:e,text:e}]}}}url(e){var i,s;if(i=this.rules.inline.url.exec(e)){let e,t;if("@"===i[2])e=a(i[0]),t="mailto:"+e;else{for(;s=i[0],i[0]=this.rules.inline._backpedal.exec(i[0])?.[0]??"",s!==i[0];);e=a(i[0]),t="www."===i[1]?"http://"+i[0]:i[0]}return{type:"link",raw:i[0],text:e,href:t,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){var t,e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?e[0]:a(e[0]),{type:"text",raw:e[0],text:t}}}let l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,f=/(?:[*+-]|\d{1,9}[.)])/,d=c(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,f).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),b=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,v=/(?!\s*\])(?:\\.|[^\[\]\\])+/,M=c(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",v).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),$=c(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,f).getRegex(),_="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",j=/|$))/,B=c("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",j).replace("tag",_).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y=c(b).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),w={blockquote:c(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",y).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:M,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:B,lheading:d,list:$,newline:/^(?: *(?:\n|$))+/,paragraph:y,table:g,text:/^[^\n]+/},W=c("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),U={...w,table:W,paragraph:c(b).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",W).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex()},Q={...w,html:c("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",j).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:g,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:c(b).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",d).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Y=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,K=/^( {2,}|\\)\n(?!\s*$)/,k="\\p{P}\\p{S}",V=c(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,k).getRegex(),X=c(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,k).getRegex(),Z=c("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,k).getRegex(),G=c("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,k).getRegex(),J=c(/\\([punct])/,"gu").replace(/punct/g,k).getRegex(),ee=c(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),te=c(j).replace("(?:--\x3e|$)","--\x3e").getRegex(),ie=c("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",te).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),x=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,se=c(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",x).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ne=c(/^!?\[(label)\]\[(ref)\]/).replace("label",x).replace("ref",v).getRegex(),oe=c(/^!?\[(ref)\](?:\[\])?/).replace("ref",v).getRegex(),E={_backpedal:g,anyPunctuation:J,autolink:ee,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:K,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:g,emStrongLDelim:X,emStrongRDelimAst:Z,emStrongRDelimUnd:G,escape:Y,link:se,nolink:oe,punctuation:V,reflink:ne,reflinkSearch:c("reflink|nolink(?!\\()","g").replace("reflink",ne).replace("nolink",oe).getRegex(),tag:ie,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(i.length));n;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some(e=>!!(i=e.call({lexer:this},n,t))&&(n=n.substring(i.raw.length),t.push(i),!0))))if(i=this.tokenizer.space(n))n=n.substring(i.raw.length),1===i.raw.length&&0{"number"==typeof(s=e.call({lexer:this},i))&&0<=s&&(t=Math.min(t,s))}),t<1/0&&0<=t&&(o=n.substring(0,t+1))}if(this.state.top&&(i=this.tokenizer.paragraph(o)))e=t[t.length-1],s&&"paragraph"===e.type?(e.raw+="\n"+i.raw,e.text+="\n"+i.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=e.text):t.push(i),s=o.length!==n.length,n=n.substring(i.raw.length);else if(i=this.tokenizer.text(n))n=n.substring(i.raw.length),(e=t[t.length-1])&&"text"===e.type?(e.raw+="\n"+i.raw,e.text+="\n"+i.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=e.text):t.push(i);else if(n){let e="Infinite loop on byte: "+n.charCodeAt(0);if(this.options.silent){console.error(e);break}throw new Error(e)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(n,t=[]){let i,e,o,s,a,c,u=n;if(this.tokens.links){let e=Object.keys(this.tokens.links);if(0!!(i=e.call({lexer:this},n,t))&&(n=n.substring(i.raw.length),t.push(i),!0))))if(i=this.tokenizer.escape(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.tag(n))n=n.substring(i.raw.length),(e=t[t.length-1])&&"text"===i.type&&"text"===e.type?(e.raw+=i.raw,e.text+=i.text):t.push(i);else if(i=this.tokenizer.link(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.reflink(n,this.tokens.links))n=n.substring(i.raw.length),(e=t[t.length-1])&&"text"===i.type&&"text"===e.type?(e.raw+=i.raw,e.text+=i.text):t.push(i);else if(i=this.tokenizer.emStrong(n,u,c))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.codespan(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.br(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.del(n))n=n.substring(i.raw.length),t.push(i);else if(i=this.tokenizer.autolink(n))n=n.substring(i.raw.length),t.push(i);else if(this.state.inLink||!(i=this.tokenizer.url(n))){if(o=n,this.options.extensions&&this.options.extensions.startInline){let t=1/0,i=n.slice(1),s;this.options.extensions.startInline.forEach(e=>{"number"==typeof(s=e.call({lexer:this},i))&&0<=s&&(t=Math.min(t,s))}),t<1/0&&0<=t&&(o=n.substring(0,t+1))}if(i=this.tokenizer.inlineText(o))n=n.substring(i.raw.length),"_"!==i.raw.slice(-1)&&(c=i.raw.slice(-1)),a=!0,(e=t[t.length-1])&&"text"===e.type?(e.raw+=i.raw,e.text+=i.text):t.push(i);else if(n){let e="Infinite loop on byte: "+n.charCodeAt(0);if(this.options.silent){console.error(e);break}throw new Error(e)}}else n=n.substring(i.raw.length),t.push(i);return t}}class D{options;constructor(e){this.options=e||t.defaults}code(e,t,i){t=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",t?'
'+(i?e:a(e,!0))+"
\n":"
"+(i?e:a(e,!0))+"
\n"}blockquote(e){return`
${e}
`}html(e,t){return e}heading(e,t,i){return`${e} `}hr(){return"
\n"}list(e,t,i){var s=t?"ol":"ul";return"<"+s+(t&&1!==i?' start="'+i+'"':"")+">\n"+e+"\n"}listitem(e,t,i){return`
  • ${e}
  • @@ -6,4 +6,6 @@ ${e}
    `}table(e,t){return"\n\n"+e+"\n"+(t=t&&`${t}`)+"
    \n"}tablerow(e){return` ${e} `}tablecell(e,t){var i=t.header?"th":"td";return(t.align?`<${i} align="${t.align}">`:`<${i}>`)+e+` -`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,i){var s=u(e);if(null===s)return i;let n='
    "}image(e,t,i){var s=u(e);if(null===s)return i;let n=`${i}"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""));continue}case"code":{let e=g;u+=this.renderer.code(e.text,e.lang,!!e.escaped);continue}case"table":{let i=g,e="",s="";for(let e=0;e{e=t[e].flat(1/0);s=s.concat(this.walkTokens(e,i))}):t.tokens&&(s=s.concat(this.walkTokens(t.tokens,i)))}}return s}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(o=>{let e={...o};if(e.async=this.defaults.async||e.async||!1,o.extensions&&(o.extensions.forEach(s=>{if(!s.name)throw new Error("extension name required");if("renderer"in s){let i=t.renderers[s.name];t.renderers[s.name]=i?function(...e){let t=s.renderer.apply(this,e);return t=!1===t?i.apply(this,e):t}:s.renderer}if("tokenizer"in s){if(!s.level||"block"!==s.level&&"inline"!==s.level)throw new Error("extension level must be 'block' or 'inline'");var e=t[s.level];e?e.unshift(s.tokenizer):t[s.level]=[s.tokenizer],s.start&&("block"===s.level?t.startBlock?t.startBlock.push(s.start):t.startBlock=[s.start]:"inline"===s.level&&(t.startInline?t.startInline.push(s.start):t.startInline=[s.start]))}"childTokens"in s&&s.childTokens&&(t.childTokens[s.name]=s.childTokens)}),e.extensions=t),o.renderer){let n=this.defaults.renderer||new D(this.defaults);for(let t in o.renderer){if(!(t in n))throw new Error(`renderer '${t}' does not exist`);if("options"!==t){let e=t,i=o.renderer[e],s=n[e];n[e]=(...e)=>{let t=i.apply(n,e);return(t=!1===t?s.apply(n,e):t)||""}}}e.renderer=n}if(o.tokenizer){let n=this.defaults.tokenizer||new p(this.defaults);for(let t in o.tokenizer){if(!(t in n))throw new Error(`tokenizer '${t}' does not exist`);if(!["options","rules","lexer"].includes(t)){let e=t,i=o.tokenizer[e],s=n[e];n[e]=(...e)=>{let t=i.apply(n,e);return t=!1===t?s.apply(n,e):t}}}e.tokenizer=n}if(o.hooks){let n=this.defaults.hooks||new O;for(let t in o.hooks){if(!(t in n))throw new Error(`hook '${t}' does not exist`);if("options"!==t){let e=t,i=o.hooks[e],s=n[e];O.passThroughHooks.has(t)?n[e]=e=>this.defaults.async?Promise.resolve(i.call(n,e)).then(e=>s.call(n,e)):(e=i.call(n,e),s.call(n,e)):n[e]=(...e)=>{let t=i.apply(n,e);return t=!1===t?s.apply(n,e):t}}}e.hooks=n}if(o.walkTokens){let i=this.defaults.walkTokens,s=o.walkTokens;e.walkTokens=function(e){let t=[];return t.push(s.call(this,e)),t=i?t.concat(i.call(this,e)):t}}this.defaults={...this.defaults,...e}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return S.lex(e,t??this.defaults)}parser(e,t){return I.parse(e,t??this.defaults)}#e(n,o){return(i,t)=>{let e={...t},s={...this.defaults,...e};!0===this.defaults.async&&!1===e.async&&(s.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),s.async=!0);t=this.#t(!!s.silent,!!s.async);if(null==i)return t(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof i)return t(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(i)+", string expected"));if(s.hooks&&(s.hooks.options=s),s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(i):i).then(e=>n(e,s)).then(e=>s.hooks?s.hooks.processAllTokens(e):e).then(e=>s.walkTokens?Promise.all(this.walkTokens(e,s.walkTokens)).then(()=>e):e).then(e=>o(e,s)).then(e=>s.hooks?s.hooks.postprocess(e):e).catch(t);try{s.hooks&&(i=s.hooks.preprocess(i));let e=n(i,s),t=(s.hooks&&(e=s.hooks.processAllTokens(e)),s.walkTokens&&this.walkTokens(e,s.walkTokens),o(e,s));return t=s.hooks?s.hooks.postprocess(t):t}catch(e){return t(e)}}}#t(e,i){return t=>{if(t.message+="\nPlease report this to https://github.com/markedjs/marked.",e){let e="

    An error occurred:

    "+a(t.message+"",!0)+"
    ";return i?Promise.resolve(e):e}if(i)return Promise.reject(t);throw t}}}let L=new ue;function P(e,t){return L.parse(e,t)}P.options=P.setOptions=function(e){return L.setOptions(e),i(P.defaults=L.defaults),P},P.getDefaults=e,P.defaults=t.defaults,P.use=function(...e){return L.use(...e),i(P.defaults=L.defaults),P},P.walkTokens=function(e,t){return L.walkTokens(e,t)},P.parseInline=L.parseInline,P.Parser=I,P.parser=I.parse,P.Renderer=D,P.TextRenderer=N,P.Lexer=S,P.lexer=S.lex,P.Tokenizer=p,P.Hooks=O;var g=(P.parse=P).options,ge=P.setOptions,re=P.use,he=P.walkTokens,me=P.parseInline,pe=P,le=I.parse,fe=S.lex;t.Hooks=O,t.Lexer=S,t.Marked=ue,t.Parser=I,t.Renderer=D,t.TextRenderer=N,t.Tokenizer=p,t.getDefaults=e,t.lexer=fe,t.marked=P,t.options=g,t.parse=pe,t.parseInline=me,t.parser=le,t.setOptions=ge,t.use=re,t.walkTokens=he}),((e,t)=>{"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()})(this,function(){function Ae(){for(var e=arguments.length,t=Array(e),i=0;i{if(Array.isArray(e)){for(var t=0,i=Array(e.length);t/gm),it=e(/^data-[\-\w.\u00B7-\uFFFF]/),st=e(/^aria-[\-\w]+$/),nt=e(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ot=e(/^(?:\w+script|data):/i),at=e(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Ye="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function Xe(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t{if("object"!==(void 0===e?"undefined":Ye(e))||"function"!=typeof e.createPolicy)return null;var i=null,s="data-tt-policy-suffix",t="dompurify"+((i=t.currentScript&&t.currentScript.hasAttribute(s)?t.currentScript.getAttribute(s):i)?"#"+i:"");try{return e.createPolicy(t,{createHTML:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+t+" could not be created."),null}})(i,u)),Q=h&&T?h.createHTML(""):"",e=o,a=e.implementation,K=e.createNodeIterator,V=e.getElementsByTagName,Y=e.createDocumentFragment,X=u.importNode,s={};try{s=qe(o).documentMode?o.documentMode:{}}catch(P){}var n={},m=(r.isSupported=a&&void 0!==a.createHTMLDocument&&9!==s,et),p=tt,Z=it,G=st,J=ot,ee=at,l=nt,f=null,te=Fe({},[].concat(Xe(Me),Xe(ze),Xe($e),Xe(Be),Xe(We))),d=null,ie=Fe({},[].concat(Xe(Ue),Xe(Qe),Xe(Ke),Xe(Ve))),b=null,se=null,ne=!0,v=!0,oe=!1,_=!1,j=!1,y=!1,w=!1,k=!1,x=!1,ae=!0,T=!1,ce=!0,E=!0,A=!1,C={},ue=Fe({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),ge=null,re=Fe({},["audio","video","img","source","image","track"]),S=null,he=Fe({},["alt","class","for","id","label","name","pattern","placeholder","summary","title","value","style","xmlns"]),D=null,me=o.createElement("form"),pe=function(e){D&&D===e||(e=qe(e=e&&"object"===(void 0===e?"undefined":Ye(e))?e:{}),f="ALLOWED_TAGS"in e?Fe({},e.ALLOWED_TAGS):te,d="ALLOWED_ATTR"in e?Fe({},e.ALLOWED_ATTR):ie,S="ADD_URI_SAFE_ATTR"in e?Fe(qe(he),e.ADD_URI_SAFE_ATTR):he,ge="ADD_DATA_URI_TAGS"in e?Fe(qe(re),e.ADD_DATA_URI_TAGS):re,b="FORBID_TAGS"in e?Fe({},e.FORBID_TAGS):{},se="FORBID_ATTR"in e?Fe({},e.FORBID_ATTR):{},C="USE_PROFILES"in e&&e.USE_PROFILES,ne=!1!==e.ALLOW_ARIA_ATTR,v=!1!==e.ALLOW_DATA_ATTR,oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,_=e.SAFE_FOR_TEMPLATES||!1,j=e.WHOLE_DOCUMENT||!1,k=e.RETURN_DOM||!1,x=e.RETURN_DOM_FRAGMENT||!1,ae=!1!==e.RETURN_DOM_IMPORT,T=e.RETURN_TRUSTED_TYPE||!1,w=e.FORCE_BODY||!1,ce=!1!==e.SANITIZE_DOM,E=!1!==e.KEEP_CONTENT,A=e.IN_PLACE||!1,l=e.ALLOWED_URI_REGEXP||l,_&&(v=!1),x&&(k=!0),C&&(f=Fe({},[].concat(Xe(We))),d=[],!0===C.html&&(Fe(f,Me),Fe(d,Ue)),!0===C.svg&&(Fe(f,ze),Fe(d,Qe),Fe(d,Ve)),!0===C.svgFilters&&(Fe(f,$e),Fe(d,Qe),Fe(d,Ve)),!0===C.mathMl)&&(Fe(f,Be),Fe(d,Ke),Fe(d,Ve)),e.ADD_TAGS&&Fe(f=f===te?qe(f):f,e.ADD_TAGS),e.ADD_ATTR&&Fe(d=d===ie?qe(d):d,e.ADD_ATTR),e.ADD_URI_SAFE_ATTR&&Fe(S,e.ADD_URI_SAFE_ATTR),E&&(f["#text"]=!0),j&&Fe(f,["html","head","body"]),f.table&&(Fe(f,["tbody"]),delete b.tbody),Ce&&Ce(e),D=e)},le=Fe({},["mi","mo","mn","ms","mtext"]),fe=Fe({},["foreignobject","desc","title","annotation-xml"]),N=Fe({},ze),de=(Fe(N,$e),Fe(N,Ge),Fe({},Be)),be=(Fe(de,Je),"http://www.w3.org/1998/Math/MathML"),ve="http://www.w3.org/2000/svg",I="http://www.w3.org/1999/xhtml",_e=function(t){Ne(r.removed,{element:t});try{t.parentNode.removeChild(t)}catch(e){try{t.outerHTML=Q}catch(e){t.remove()}}},je=function(e,t){try{Ne(r.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){Ne(r.removed,{attribute:null,from:t})}t.removeAttribute(e)},ye=function(e){var t,i=void 0,s=void 0,n=(w?e=""+e:s=(n=Oe(e,/^[\r\n\t ]+/))&&n[0],h?h.createHTML(e):e);try{i=(new z).parseFromString(n,"text/html")}catch(e){}return i&&i.documentElement||((t=(i=a.createHTMLDocument("")).body).parentNode.removeChild(t.parentNode.firstElementChild),t.outerHTML=n),e&&s&&i.body.insertBefore(o.createTextNode(s),i.body.childNodes[0]||null),V.call(i,j?"html":"body")[0]},we=function(e){return K.call(e.ownerDocument||e,e,t.SHOW_ELEMENT|t.SHOW_COMMENT|t.SHOW_TEXT,function(){return t.FILTER_ACCEPT},!1)},O=function(e){return"object"===(void 0===g?"undefined":Ye(g))?e instanceof g:e&&"object"===(void 0===e?"undefined":Ye(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},L=function(e,t,i){n[e]&&Se(n[e],function(e){e.call(r,t,i,D)})},ke=function(e){if(L("beforeSanitizeElements",e,null),((a=e)instanceof H||a instanceof M||"string"==typeof a.nodeName&&"string"==typeof a.textContent&&"function"==typeof a.removeChild&&a.attributes instanceof q&&"function"==typeof a.removeAttribute&&"function"==typeof a.setAttribute&&"string"==typeof a.namespaceURI&&"function"==typeof a.insertBefore)&&!Oe(e.nodeName,/[\u0080-\uFFFF]/)){var t,i,s,n,o,a=Ie(e.nodeName);if(L("uponSanitizeElement",e,{tagName:a,allowedTags:f}),O(e.firstElementChild)||O(e.content)&&O(e.content.firstElementChild)||!Re(/<[/\w]/g,e.innerHTML)||!Re(/<[/\w]/g,e.textContent)){if(f[a]&&!b[a])return(!(e instanceof F)||((s=U(t=e))&&s.tagName||(s={namespaceURI:I,tagName:"template"}),n=Ie(t.tagName),o=Ie(s.tagName),t.namespaceURI===ve?s.namespaceURI===I?"svg"===n:s.namespaceURI===be?"svg"===n&&("annotation-xml"===o||le[o]):Boolean(N[n]):t.namespaceURI===be?s.namespaceURI===I?"math"===n:s.namespaceURI===ve?"math"===n&&fe[o]:Boolean(de[n]):t.namespaceURI===I&&!(s.namespaceURI===ve&&!fe[o]||s.namespaceURI===be&&!le[o]||(i=Fe({},["title","style","font","a","script"]),de[n]))&&(i[n]||!N[n])))&&("noscript"!==a&&"noembed"!==a||!Re(/<\/no(script|embed)/i,e.innerHTML))?(_&&3===e.nodeType&&(t=e.textContent,t=Le(t,m," "),t=Le(t,p," "),e.textContent!==t)&&(Ne(r.removed,{element:e.cloneNode()}),e.textContent=t),L("afterSanitizeElements",e,null),!1):(_e(e),!0);if(E&&!ue[a])for(var c=U(e),u=W(e),g=u.length-1;0<=g;--g)c.insertBefore($(u[g],!0),B(e))}}return _e(e),!0},xe=function(e,t,i){if(ce&&("id"===t||"name"===t)&&(i in o||i in me))return!1;if(!(v&&Re(Z,t)||ne&&Re(G,t))){if(!d[t]||se[t])return!1;if(!S[t]&&!Re(l,Le(i,ee,""))&&("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==Pe(i,"data:")||!ge[e])&&(!oe||Re(J,Le(i,ee,"")))&&i)return!1}return!0},Te=function(e){var t=void 0,i=void 0,s=(L("beforeSanitizeAttributes",e,null),e.attributes);if(s){for(var n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:d},i=s.length;i--;){c=s[i];var o=c.name,a=c.namespaceURI,t=Ze(c.value),c=Ie(o);if(n.attrName=c,n.attrValue=t,n.keepAttr=!0,n.forceKeepAttr=void 0,L("uponSanitizeAttribute",e,n),t=n.attrValue,!n.forceKeepAttr&&(je(o,e),n.keepAttr))if(Re(/\/>/i,t))je(o,e);else{_&&(t=Le(t,m," "),t=Le(t,p," "));var u=e.nodeName.toLowerCase();if(xe(u,c,t))try{a?e.setAttributeNS(a,o,t):e.setAttribute(o,t),De(r.removed)}catch(e){}}}L("afterSanitizeAttributes",e,null)}},Ee=function e(t){var i,s=we(t);for(L("beforeSanitizeShadowDOM",t,null);i=s.nextNode();)L("uponSanitizeShadowNode",i,null),ke(i)||(i.content instanceof R&&e(i.content),Te(i));L("afterSanitizeShadowDOM",t,null)};r.sanitize=function(e,t){var i,s=void 0,n=void 0,o=void 0;if("string"!=typeof(e=e||"\x3c!--\x3e")&&!O(e)){if("function"!=typeof e.toString)throw Ae("toString is not a function");if("string"!=typeof(e=e.toString()))throw Ae("dirty is not a string, aborting")}if(!r.isSupported){if("object"===Ye(c.toStaticHTML)||"function"==typeof c.toStaticHTML){if("string"==typeof e)return c.toStaticHTML(e);if(O(e))return c.toStaticHTML(e.outerHTML)}return e}if(y||pe(t),r.removed=[],!(A="string"==typeof e?!1:A))if(e instanceof g)1===(t=(s=ye("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===t.nodeName||"HTML"===t.nodeName?s=t:s.appendChild(t);else{if(!k&&!_&&!j&&-1===e.indexOf("<"))return h&&T?h.createHTML(e):e;if(!(s=ye(e)))return k?null:Q}s&&w&&_e(s.firstChild);for(var a=we(A?e:s);i=a.nextNode();)3===i.nodeType&&i===n||ke(i)||(i.content instanceof R&&Ee(i.content),Te(i),n=i);if(n=null,A)return e;if(k){if(x)for(o=Y.call(s.ownerDocument);s.firstChild;)o.appendChild(s.firstChild);else o=s;return o=ae?X.call(u,o,!0):o}t=j?s.outerHTML:s.innerHTML;return _&&(t=Le(t,m," "),t=Le(t,p," ")),h&&T?h.createHTML(t):t},r.setConfig=function(e){pe(e),y=!0},r.clearConfig=function(){D=null,y=!1},r.isValidAttribute=function(e,t,i){D||pe({});e=Ie(e),t=Ie(t);return xe(e,t,i)},r.addHook=function(e,t){"function"==typeof t&&(n[e]=n[e]||[],Ne(n[e],t))},r.removeHook=function(e){n[e]&&De(n[e])},r.removeHooks=function(e){n[e]&&(n[e]=[])},r.removeAllHooks=function(){n={}}}else r.isSupported=!1;return r}()}),(()=>{function i(){this.fg=this.bg=null,this.bright=0}var e,t="undefined"!=typeof module,a=[[{color:"0, 0, 0",class:"ansi-black"},{color:"187, 0, 0",class:"ansi-red"},{color:"0, 187, 0",class:"ansi-green"},{color:"187, 187, 0",class:"ansi-yellow"},{color:"0, 0, 187",class:"ansi-blue"},{color:"187, 0, 187",class:"ansi-magenta"},{color:"0, 187, 187",class:"ansi-cyan"},{color:"255,255,255",class:"ansi-white"}],[{color:"85, 85, 85",class:"ansi-bright-black"},{color:"255, 85, 85",class:"ansi-bright-red"},{color:"0, 255, 0",class:"ansi-bright-green"},{color:"255, 255, 85",class:"ansi-bright-yellow"},{color:"85, 85, 255",class:"ansi-bright-blue"},{color:"255, 85, 255",class:"ansi-bright-magenta"},{color:"85, 255, 255",class:"ansi-bright-cyan"},{color:"255, 255, 255",class:"ansi-bright-white"}]];i.prototype.escape_for_html=function(e){return e.replace(/[&<>]/gm,function(e){return"&"==e?"&":"<"==e?"<":">"==e?">":void 0})},i.prototype.linkify=function(e){return e.replace(/(https?:\/\/[^\s]+)/gm,function(e){return'
    '+e+""})},i.prototype.ansi_to_html=function(e,t){var e=e.split(/\033\[/),i=e.shift(),s=this,e=e.map(function(e){return s.process_chunk(e,t)});return e.unshift(i),e.reduce(function(e,t){return Array.isArray(t)?e.concat(t):(e.push(t),e)},[]).join("")},i.prototype.process_chunk=function(e,t){var i,s,t=void 0!==(t=void 0===t?{}:t).use_classes&&t.use_classes,n=t?"class":"color",o=e.match(/([\d;]*)m([^]*)/m);return o?(i=o[2],o=o[1].split(";"),s=this,o.map(function(e){e=parseInt(e);isNaN(e)||0===e?(s.fg=s.bg=null,s.bright=0):1===e?s.bright=1:30<=e&&e<38?s.fg=a[s.bright][e%10][n]:40<=e&&e<48&&(s.bg=a[0][e%10][n])}),null===s.fg&&null===s.bg?i:(o=classes=[],s.fg&&(t?classes.push(s.fg+"-fg"):o.push("color:rgb("+s.fg+")")),s.bg&&(t?classes.push(s.bg+"-bg"):o.push("background-color:rgb("+s.bg+")")),t?['',i,""]:['',i,""])):e},e={escape_for_html:function(e){return(new i).escape_for_html(e)},linkify:function(e){return(new i).linkify(e)},ansi_to_html:function(e,t){return(new i).ansi_to_html(e,t)},ansi_to_html_obj:function(){return new i}},t&&(module.exports=e),"undefined"!=typeof window&&"undefined"==typeof ender&&(window.ansi_up=e),"function"==typeof define&&define.amd&&define("ansi_up",[],function(){return e})})(Date);var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=(u=>{var e,t,g=/\blang(?:uage)?-([\w-]+)\b/i,i=0,P={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof R?new R(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=a.reach);_+=v.value.length,v=v.next){var j=v.value;if(i.length>t.length)return;if(!(j instanceof R)){var y=1;if(l&&v!=i.tail.prev){if(b.lastIndex=_,!(E=b.exec(t)))break;var w=E.index+(p&&E[1]?E[1].length:0),k=E.index+E[0].length,x=_;for(x+=v.value.length;x<=w;)x+=(v=v.next).value.length;if(_=x-=v.value.length,v.value instanceof R)continue;for(var T=v;T!==i.tail&&(xa.reach&&(a.reach=j),v.prev),N=(C&&(D=F(i,D,C),_+=C.length),L=O=I=C=N=void 0,i),C=D,I=y,O=C.next,L=0;L"+o.content+""},u.document)?((e=P.util.currentScript())&&(P.filename=e.src,e.hasAttribute("data-manual"))&&(P.manual=!0),P.manual||("loading"===(t=document.readyState)||"interactive"===t&&e&&e.defer?document.addEventListener("DOMContentLoaded",s):window.requestAnimationFrame?window.requestAnimationFrame(s):window.setTimeout(s,16))):u.addEventListener&&!P.disableWorkerMessageHandler&&u.addEventListener("message",function(e){var e=JSON.parse(e.data),t=e.language,i=e.immediateClose;u.postMessage(P.highlight(e.code,P.languages[t],t)),i&&u.close()},!1),P;function s(){P.manual||P.highlightAll()}})(_self);function loadFilterPrefs(e){try{var t=localStorage.getItem(e);return t?JSON.parse(t):null}catch(e){return null}}function saveFilterPrefs(e,t){try{localStorage.setItem(e,JSON.stringify(t))}catch(e){}}function encodePathForUrl(e){return String(e||"").split("/").map(e=>encodeURIComponent(e)).join("/")}function humanFileSize(e,t=!1,i=1){var s=t?1e3:1024;if(e/=8,Math.abs(e)=s&&o/g,"%3E")}return e}"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/,name:/[^\s<>'"]+/}},cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var i={},i=(i["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[t]},i.cdata=/^$/i,{"included-cdata":{pattern://i,inside:i}}),t=(i["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]},{});t[e]={pattern:RegExp("(<__[^]*?>)(?:))*\\]\\]>|(?!)".replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:i},Prism.languages.insertBefore("markup","cdata",t)}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml,Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\s*[\[$\w\xA0-\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-flags":/[a-z]+$/,"regex-delimiter":/^\/|\/$/}},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.js=Prism.languages.javascript,Prism.languages.julia={comment:{pattern:/(^|[^\\])(?:#=(?:[^#=]|=(?!#)|#(?!=)|#=(?:[^#=]|=(?!#)|#(?!=))*=#)*=#|#.*)/,lookbehind:!0},regex:{pattern:/r"(?:\\.|[^"\\\r\n])*"[imsx]{0,4}/,greedy:!0},string:{pattern:/"""[\s\S]+?"""|\w*"(?:\\.|[^"\\\r\n])*"|(^|[^\w'])'(?:\\[^\r\n][^'\r\n]*|[^\\\r\n])'|`(?:[^\\`\r\n]|\\.)*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:abstract|baremodule|begin|bitstype|break|catch|ccall|const|continue|do|else|elseif|end|export|finally|for|function|global|if|immutable|import|importall|in|let|local|macro|module|print|println|quote|return|struct|try|type|typealias|using|while)\b/,boolean:/\b(?:true|false)\b/,number:/(?:\b(?=\d)|\B(?=\.))(?:0[box])?(?:[\da-f]+(?:_[\da-f]+)*\.?(?:\d+(?:_\d+)*)?|\.\d+(?:_\d+)*)(?:[efp][+-]?\d+(?:_\d+)*)?j?/i,operator:/&&|\|\||[-+*^%÷⊻&$\\]=?|\/[\/=]?|!=?=?|\|[=>]?|<(?:<=?|[=:|])?|>(?:=|>>?=?)?|==?=?|[~≠≤≥'√∛]/,punctuation:/::?|[{}[\]();,.?]/,constant:/\b(?:(?:NaN|Inf)(?:16|32|64)?|im|pi|e|catalan|eulergamma|golden)\b|[πℯγφ]/},Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},"string-interpolation":{pattern:/(?:f|rf|fr)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:{{)*){(?!{)(?:[^{}]|{(?!{)(?:[^{}]|{(?!{)(?:[^{}])+})+})+}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|rb|br)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|rb|br)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^\s*)@\w+(?:\.\w+)*/im,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:True|False|None)\b/,number:/(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python,Prism.languages.r={comment:/#.*/,string:{pattern:/(['"])(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},"percent-operator":{pattern:/%[^%\s]*%/,alias:"operator"},boolean:/\b(?:TRUE|FALSE)\b/,ellipsis:/\.\.(?:\.|\d+)/,number:[/\b(?:NaN|Inf)\b/,/(?:\b0x[\dA-Fa-f]+(?:\.\d*)?|\b\d+\.?\d*|\B\.\d+)(?:[EePp][+-]?\d+)?[iL]?/],keyword:/\b(?:if|else|repeat|while|function|for|in|next|break|NULL|NA|NA_integer_|NA_real_|NA_complex_|NA_character_)\b/,operator:/->?>?|<(?:=|=!]=?|::?|&&?|\|\|?|[+*\/^$@~]/,punctuation:/[(){}\[\],;]/},((e,t)=>{"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(e.document)return t(e);throw new Error("jQuery requires a window with a document")}:t(e)})("undefined"!=typeof window?window:this,function(w,R){function v(e){return"function"==typeof e&&"number"!=typeof e.nodeType}function f(e){return null!=e&&e===e.window}var e=[],k=w.document,F=Object.getPrototypeOf,c=e.slice,q=e.concat,H=e.push,M=e.indexOf,z={},$=z.toString,B=z.hasOwnProperty,W=B.toString,U=W.call(Object),d={},Q={type:!0,src:!0,nonce:!0,noModule:!0};function K(e,t,i){var s,n,o=(i=i||k).createElement("script");if(o.text=e,t)for(s in Q)(n=t[s]||t.getAttribute&&t.getAttribute(s))&&o.setAttribute(s,n);i.head.appendChild(o).parentNode.removeChild(o)}function l(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?z[$.call(e)]||"object":typeof e}var x=function(e,t){return new x.fn.init(e,t)},V=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function Y(e){var t=!!e&&"length"in e&&e.length,i=l(e);return!v(e)&&!f(e)&&("array"===i||0===t||"number"==typeof t&&0{function h(e,t,i){var s="0x"+t-65536;return s!=s||i?t:s<0?String.fromCharCode(65536+s):String.fromCharCode(s>>10|55296,1023&s|56320)}function F(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}function q(){w()}var e,m,j,o,H,p,M,z,y,u,g,w,k,i,x,l,s,n,f,T="sizzle"+ +new Date,d=R.document,E=0,$=0,B=N(),W=N(),U=N(),b=N(),Q=function(e,t){return e===t&&(g=!0),0},K={}.hasOwnProperty,t=[],V=t.pop,Y=t.push,A=t.push,X=t.slice,C=function(e,t){for(var i=0,s=e.length;i+~]|"+a+")"+a+"*"),se=new RegExp(a+"|>"),ne=new RegExp(J),oe=new RegExp("^"+c+"$"),v={ID:new RegExp("^#("+c+")"),CLASS:new RegExp("^\\.("+c+")"),TAG:new RegExp("^("+c+"|[*])"),ATTR:new RegExp("^"+G),PSEUDO:new RegExp("^"+J),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+a+"*(even|odd|(([+-]|)(\\d*)n|)"+a+"*(?:([+-]|)"+a+"*(\\d+)|))"+a+"*\\)|)","i"),bool:new RegExp("^(?:"+Z+")$","i"),needsContext:new RegExp("^"+a+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+a+"*((?:-\\d)?\\d*)"+a+"*\\)|)(?=[^-]|$)","i")},ae=/HTML$/i,ce=/^(?:input|select|textarea|button)$/i,ue=/^h\d$/i,r=/^[^{]+\{\s*\[native \w/,ge=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,re=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+a+"?|("+a+")|.)","ig"),he=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,me=ve(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{A.apply(t=X.call(d.childNodes),d.childNodes),t[d.childNodes.length].nodeType}catch(e){A={apply:t.length?function(e,t){Y.apply(e,X.call(t))}:function(e,t){for(var i=e.length,s=0;e[i++]=t[s++];);e.length=i-1}}}function D(e,t,i,s){var n,o,a,c,u,g,r=t&&t.ownerDocument,h=t?t.nodeType:9;if(i=i||[],"string"!=typeof e||!e||1!==h&&9!==h&&11!==h)return i;if(!s&&((t?t.ownerDocument||t:d)!==k&&w(t),t=t||k,x)){if(11!==h&&(c=ge.exec(e)))if(n=c[1]){if(9===h){if(!(g=t.getElementById(n)))return i;if(g.id===n)return i.push(g),i}else if(r&&(g=r.getElementById(n))&&f(t,g)&&g.id===n)return i.push(g),i}else{if(c[2])return A.apply(i,t.getElementsByTagName(e)),i;if((n=c[3])&&m.getElementsByClassName&&t.getElementsByClassName)return A.apply(i,t.getElementsByClassName(n)),i}if(m.qsa&&!b[e+" "]&&(!l||!l.test(e))&&(1!==h||"object"!==t.nodeName.toLowerCase())){if(g=e,r=t,1===h&&se.test(e)){for((a=t.getAttribute("id"))?a=a.replace(he,F):t.setAttribute("id",a=T),o=(u=p(e)).length;o--;)u[o]="#"+a+" "+P(u[o]);g=u.join(","),r=re.test(e)&&de(t.parentNode)||t}try{return A.apply(i,r.querySelectorAll(g)),i}catch(t){b(e,!0)}finally{a===T&&t.removeAttribute("id")}}}return z(e.replace(S,"$1"),t,i,s)}function N(){var s=[];return function e(t,i){return s.push(t+" ")>j.cacheLength&&delete e[s.shift()],e[t+" "]=i}}function I(e){return e[T]=!0,e}function O(e){var t=k.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t)}}function pe(e,t){for(var i=e.split("|"),s=i.length;s--;)j.attrHandle[i[s]]=t}function le(e,t){var i=t&&e,s=i&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(s)return s;if(i)for(;i=i.nextSibling;)if(i===t)return-1;return e?1:-1}function fe(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&me(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function L(a){return I(function(o){return o=+o,I(function(e,t){for(var i,s=a([],e.length,o),n=s.length;n--;)e[i=s[n]]&&(e[i]=!(t[i]=e[i]))})})}function de(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in m=D.support={},H=D.isXML=function(e){var t=e.namespaceURI,e=(e.ownerDocument||e).documentElement;return!ae.test(t||e&&e.nodeName||"HTML")},w=D.setDocument=function(e){var e=e?e.ownerDocument||e:d;return e!==k&&9===e.nodeType&&e.documentElement&&(i=(k=e).documentElement,x=!H(k),d!==k&&(e=k.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",q,!1):e.attachEvent&&e.attachEvent("onunload",q)),m.attributes=O(function(e){return e.className="i",!e.getAttribute("className")}),m.getElementsByTagName=O(function(e){return e.appendChild(k.createComment("")),!e.getElementsByTagName("*").length}),m.getElementsByClassName=r.test(k.getElementsByClassName),m.getById=O(function(e){return i.appendChild(e).id=T,!k.getElementsByName||!k.getElementsByName(T).length}),m.getById?(j.filter.ID=function(e){var t=e.replace(_,h);return function(e){return e.getAttribute("id")===t}},j.find.ID=function(e,t){if(void 0!==t.getElementById&&x)return(t=t.getElementById(e))?[t]:[]}):(j.filter.ID=function(e){var t=e.replace(_,h);return function(e){e=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return e&&e.value===t}},j.find.ID=function(e,t){if(void 0!==t.getElementById&&x){var i,s,n,o=t.getElementById(e);if(o){if((i=o.getAttributeNode("id"))&&i.value===e)return[o];for(n=t.getElementsByName(e),s=0;o=n[s++];)if((i=o.getAttributeNode("id"))&&i.value===e)return[o]}return[]}}),j.find.TAG=m.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):m.qsa?t.querySelectorAll(e):void 0}:function(e,t){var i,s=[],n=0,o=t.getElementsByTagName(e);if("*"!==e)return o;for(;i=o[n++];)1===i.nodeType&&s.push(i);return s},j.find.CLASS=m.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&x)return t.getElementsByClassName(e)},s=[],l=[],(m.qsa=r.test(k.querySelectorAll))&&(O(function(e){i.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&l.push("[*^$]="+a+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||l.push("\\["+a+"*(?:value|"+Z+")"),e.querySelectorAll("[id~="+T+"-]").length||l.push("~="),e.querySelectorAll(":checked").length||l.push(":checked"),e.querySelectorAll("a#"+T+"+*").length||l.push(".#.+[+~]")}),O(function(e){e.innerHTML="";var t=k.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&l.push("name"+a+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&l.push(":enabled",":disabled"),i.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&l.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),l.push(",.*:")})),(m.matchesSelector=r.test(n=i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.oMatchesSelector||i.msMatchesSelector))&&O(function(e){m.disconnectedMatch=n.call(e,"*"),n.call(e,"[s!='']:x"),s.push("!=",J)}),l=l.length&&new RegExp(l.join("|")),s=s.length&&new RegExp(s.join("|")),e=r.test(i.compareDocumentPosition),f=e||r.test(i.contains)?function(e,t){var i=9===e.nodeType?e.documentElement:e,t=t&&t.parentNode;return e===t||!(!t||1!==t.nodeType||!(i.contains?i.contains(t):e.compareDocumentPosition&&16&e.compareDocumentPosition(t)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},Q=e?function(e,t){var i;return e===t?(g=!0,0):!e.compareDocumentPosition-!t.compareDocumentPosition||(1&(i=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!m.sortDetached&&t.compareDocumentPosition(e)===i?e===k||e.ownerDocument===d&&f(d,e)?-1:t===k||t.ownerDocument===d&&f(d,t)?1:u?C(u,e)-C(u,t):0:4&i?-1:1)}:function(e,t){if(e===t)return g=!0,0;var i,s=0,n=e.parentNode,o=t.parentNode,a=[e],c=[t];if(!n||!o)return e===k?-1:t===k?1:n?-1:o?1:u?C(u,e)-C(u,t):0;if(n===o)return le(e,t);for(i=e;i=i.parentNode;)a.unshift(i);for(i=t;i=i.parentNode;)c.unshift(i);for(;a[s]===c[s];)s++;return s?le(a[s],c[s]):a[s]===d?-1:c[s]===d?1:0}),k},D.matches=function(e,t){return D(e,null,null,t)},D.matchesSelector=function(e,t){if((e.ownerDocument||e)!==k&&w(e),m.matchesSelector&&x&&!b[t+" "]&&(!s||!s.test(t))&&(!l||!l.test(t)))try{var i=n.call(e,t);if(i||m.disconnectedMatch||e.document&&11!==e.document.nodeType)return i}catch(e){b(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(_,h),e[3]=(e[3]||e[4]||e[5]||"").replace(_,h),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||D.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&D.error(e[0]),e},PSEUDO:function(e){var t,i=!e[6]&&e[2];return v.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":i&&ne.test(i)&&(t=(t=p(i,!0))&&i.indexOf(")",i.length-t)-i.length)&&(e[0]=e[0].slice(0,t),e[2]=i.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(_,h).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=B[e+" "];return t||(t=new RegExp("(^|"+a+")"+e+"("+a+"|$)"))&&B(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(t,i,s){return function(e){e=D.attr(e,t);return null==e?"!="===i:!i||(e+="","="===i?e===s:"!="===i?e!==s:"^="===i?s&&0===e.indexOf(s):"*="===i?s&&-1function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t})(e);for(e in{submit:!0,reset:!0})j.pseudos[e]=(i=>function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===i})(e);function be(){}function P(e){for(var t=0,i=e.length,s="";t{for(var s=0,n=t.length;s:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function J(e,i,s){return v(i)?x.grep(e,function(e,t){return!!i.call(e,t,e)!==s}):i.nodeType?x.grep(e,function(e){return e===i!==s}):"string"!=typeof i?x.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/,ie=((x.fn.init=function(e,t,i){if(e){if(i=i||ee,"string"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==i.ready?i.ready(e):e(x):x.makeArray(e,this);if(!(s="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:te.exec(e))||!s[1]&&t)return(!t||t.jquery?t||i:this.constructor(t)).find(e);if(s[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(s[1],t&&t.nodeType?t.ownerDocument||t:k,!0)),G.test(s[1])&&x.isPlainObject(t))for(var s in t)v(this[s])?this[s](t[s]):this.attr(s,t[s])}else(i=k.getElementById(s[2]))&&(this[0]=i,this.length=1)}return this}).prototype=x.fn,ee=x(k),/^(?:parents|prev(?:Until|All))/),se={children:!0,contents:!0,next:!0,prev:!0};function ne(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}x.fn.extend({has:function(e){var t=x(e,this),i=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,xe=/^$|^module$|\/(?:java|ecma)script/i,A={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function C(e,t){var i=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&u(e,t)?x.merge([e],i):i}function Te(e,t){for(var i=0,s=e.length;ix",d.noCloneChecked=!!I.cloneNode(!0).lastChild.defaultValue;var Ce=/^key/,Se=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,De=/^([^.]*)(?:\.(.+)|)/;function i(){return!0}function S(){return!1}function Ne(e,t){return e===(()=>{try{return k.activeElement}catch(e){}})()==("focus"===t)}function Ie(e,t,i,s,n,o){var a,c;if("object"==typeof t){for(c in"string"!=typeof i&&(s=s||i,i=void 0),t)Ie(e,c,i,s,t[c],o);return e}if(null==s&&null==n?(n=i,s=i=void 0):null==n&&("string"==typeof i?(n=s,s=void 0):(n=s,s=i,i=void 0)),!1===n)n=S;else if(!n)return e;return 1===o&&(a=n,(n=function(e){return x().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=x.guid++)),e.each(function(){x.event.add(this,t,n,s,i)})}function Oe(e,n,o){o?(b.set(e,n,!1),x.event.add(e,n,{namespace:!1,handler:function(e){var t,i,s=b.get(this,n);if(1&e.isTrigger&&this[n]){if(s.length)(x.event.special[n]||{}).delegateType&&e.stopPropagation();else if(s=c.call(arguments),b.set(this,n,s),t=o(this,n),this[n](),s!==(i=b.get(this,n))||t?b.set(this,n,!1):i={},s!==i)return e.stopImmediatePropagation(),e.preventDefault(),i.value}else s.length&&(b.set(this,n,{value:x.event.trigger(x.extend(s[0],x.Event.prototype),s.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===b.get(e,n)&&x.event.add(e,n,i)}x.event={global:{},add:function(t,e,i,s,n){var o,a,c,u,g,r,h,m,p,l=b.get(t);if(l)for(i.handler&&(i=(o=i).handler,n=o.selector),n&&x.find.matchesSelector(j,n),i.guid||(i.guid=x.guid++),c=(c=l.events)||(l.events={}),a=(a=l.handle)||(l.handle=function(e){return void 0!==x&&x.event.triggered!==e.type?x.event.dispatch.apply(t,arguments):void 0}),u=(e=(e||"").match(T)||[""]).length;u--;)h=p=(m=De.exec(e[u])||[])[1],m=(m[2]||"").split(".").sort(),h&&(g=x.event.special[h]||{},h=(n?g.delegateType:g.bindType)||h,g=x.event.special[h]||{},p=x.extend({type:h,origType:p,data:s,handler:i,guid:i.guid,selector:n,needsContext:n&&x.expr.match.needsContext.test(n),namespace:m.join(".")},o),(r=c[h])||((r=c[h]=[]).delegateCount=0,g.setup&&!1!==g.setup.call(t,s,m,a))||t.addEventListener&&t.addEventListener(h,a),g.add&&(g.add.call(t,p),p.handler.guid||(p.handler.guid=i.guid)),n?r.splice(r.delegateCount++,0,p):r.push(p),x.event.global[h]=!0)},remove:function(e,t,i,s,n){var o,a,c,u,g,r,h,m,p,l,f,d=b.hasData(e)&&b.get(e);if(d&&(u=d.events)){for(g=(t=(t||"").match(T)||[""]).length;g--;)if(p=f=(c=De.exec(t[g])||[])[1],l=(c[2]||"").split(".").sort(),p){for(h=x.event.special[p]||{},m=u[p=(s?h.delegateType:h.bindType)||p]||[],c=c[2]&&new RegExp("(^|\\.)"+l.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=m.length;o--;)r=m[o],!n&&f!==r.origType||i&&i.guid!==r.guid||c&&!c.test(r.namespace)||s&&s!==r.selector&&("**"!==s||!r.selector)||(m.splice(o,1),r.selector&&m.delegateCount--,h.remove&&h.remove.call(e,r));a&&!m.length&&(h.teardown&&!1!==h.teardown.call(e,l,d.handle)||x.removeEvent(e,p,d.handle),delete u[p])}else for(p in u)x.event.remove(e,p+t[g],i,s,!0);x.isEmptyObject(u)&&b.remove(e,"handle events")}},dispatch:function(e){var t,i,s,n,o,a=x.event.fix(e),c=new Array(arguments.length),e=(b.get(this,"events")||{})[a.type]||[],u=x.event.special[a.type]||{};for(c[0]=a,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,Pe=/\s*$/g;function qe(e,t){return u(e,"table")&&u(11!==t.nodeType?t:t.firstChild,"tr")&&x(e).children("tbody")[0]||e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Me(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function ze(e,t){var i,s,n,o,a,c;if(1===t.nodeType){if(b.hasData(e)&&(o=b.access(e),a=b.set(t,o),c=o.events))for(n in delete a.handle,a.events={},c)for(i=0,s=c[n].length;i")},clone:function(e,t,i){var s,n,o,a,c,u,g,r=e.cloneNode(!0),h=y(e);if(!(d.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(a=C(r),s=0,n=(o=C(e)).length;s{for(var t=e[0].toUpperCase()+e.slice(1),i=it.length;i--;)if((e=it[i]+t)in st)return e})(e)||e)}var at=/^(none|table(?!-c[ea]).+)/,ct=/^--/,ut={position:"absolute",visibility:"hidden",display:"block"},gt={letterSpacing:"0",fontWeight:"400"};function rt(e,t,i){var s=m.exec(t);return s?Math.max(0,s[2]-(i||0))+(s[3]||"px"):t}function ht(e,t,i,s,n,o){var a="width"===t?1:0,c=0,u=0;if(i===(s?"border":"content"))return 0;for(;a<4;a+=2)"margin"===i&&(u+=x.css(e,i+p[a],!0,n)),s?("content"===i&&(u-=x.css(e,"padding"+p[a],!0,n)),"margin"!==i&&(u-=x.css(e,"border"+p[a]+"Width",!0,n))):(u+=x.css(e,"padding"+p[a],!0,n),"padding"!==i?u+=x.css(e,"border"+p[a]+"Width",!0,n):c+=x.css(e,"border"+p[a]+"Width",!0,n));return!s&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-c-.5))||0),u}function mt(e,t,i){var s=Be(e),n=(!d.boxSizingReliable()||i)&&"border-box"===x.css(e,"boxSizing",!1,s),o=n,a=Ge(e,t,s),c="offset"+t[0].toUpperCase()+t.slice(1);if(Xe.test(a)){if(!i)return a;a="auto"}return(!d.boxSizingReliable()&&n||"auto"===a||!parseFloat(a)&&"inline"===x.css(e,"display",!1,s))&&e.getClientRects().length&&(n="border-box"===x.css(e,"boxSizing",!1,s),o=c in e)&&(a=e[c]),(a=parseFloat(a)||0)+ht(e,t,i||(n?"border":"content"),o,s,a)+"px"}function o(e,t,i,s,n){return new o.prototype.init(e,t,i,s,n)}x.extend({cssHooks:{opacity:{get:function(e,t){if(t)return""===(t=Ge(e,"opacity"))?"1":t}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,i,s){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var n,o,a,c=_(t),u=ct.test(t),g=e.style;if(u||(t=ot(c)),a=x.cssHooks[t]||x.cssHooks[c],void 0===i)return a&&"get"in a&&void 0!==(n=a.get(e,!1,s))?n:g[t];"string"==(o=typeof i)&&(n=m.exec(i))&&n[1]&&(i=je(e,t,n),o="number"),null!=i&&i==i&&("number"!==o||u||(i+=n&&n[3]||(x.cssNumber[c]?"":"px")),d.clearCloneStyle||""!==i||0!==t.indexOf("background")||(g[t]="inherit"),a&&"set"in a&&void 0===(i=a.set(e,i,s))||(u?g.setProperty(t,i):g[t]=i))}},css:function(e,t,i,s){var n,o=_(t);return ct.test(t)||(t=ot(o)),"normal"===(n=void 0===(n=(o=x.cssHooks[t]||x.cssHooks[o])&&"get"in o?o.get(e,!0,i):n)?Ge(e,t,s):n)&&t in gt&&(n=gt[t]),(""===i||i)&&(o=parseFloat(n),!0===i||isFinite(o))?o||0:n}}),x.each(["height","width"],function(e,a){x.cssHooks[a]={get:function(e,t,i){if(t)return!at.test(x.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?mt(e,a,i):ve(e,ut,function(){return mt(e,a,i)})},set:function(e,t,i){var s=Be(e),n=!d.scrollboxSize()&&"absolute"===s.position,o=(n||i)&&"border-box"===x.css(e,"boxSizing",!1,s),i=i?ht(e,a,i,o,s):0;return o&&n&&(i-=Math.ceil(e["offset"+a[0].toUpperCase()+a.slice(1)]-parseFloat(s[a])-ht(e,a,"border",!1,s)-.5)),i&&(o=m.exec(t))&&"px"!==(o[3]||"px")&&(e.style[a]=t,t=x.css(e,a)),rt(0,t,i)}}}),x.cssHooks.marginLeft=Je(d.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Ge(e,"marginLeft"))||e.getBoundingClientRect().left-ve(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),x.each({margin:"",padding:"",border:"Width"},function(n,o){x.cssHooks[n+o]={expand:function(e){for(var t=0,i={},s="string"==typeof e?e.split(" "):[e];t<4;t++)i[n+p[t]+o]=s[t]||s[t-2]||s[0];return i}},"margin"!==n&&(x.cssHooks[n+o].set=rt)}),x.fn.extend({css:function(e,t){return h(this,function(e,t,i){var s,n,o={},a=0;if(Array.isArray(t)){for(s=Be(e),n=t.length;a{for(var s,n,o,a,c=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===s&&(s=e.mimeType||t.getResponseHeader("Content-Type"));if(s)for(n in c)if(c[n]&&c[n].test(s)){u.unshift(n);break}if(u[0]in i)o=u[0];else{for(n in i){if(!u[0]||e.converters[n+" "+u[0]]){o=n;break}a=a||n}o=o||a}if(o)return o!==u[0]&&u.unshift(o),i[o]})(l,j,i)),a=((e,t,i,s)=>{var n,o,a,c,u,g={},r=e.dataTypes.slice();if(r[1])for(a in e.converters)g[a.toLowerCase()]=e.converters[a];for(o=r.shift();o;)if(e.responseFields[o]&&(i[e.responseFields[o]]=t),!u&&s&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=r.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=g[u+" "+o]||g["* "+o]))for(n in g)if((c=n.split(" "))[1]===o&&(a=g[u+" "+c[0]]||g["* "+c[0]])){!0===a?a=g[n]:!0!==g[n]&&(o=c[0],r.unshift(c[1]));break}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}})(l,a,j,s),s?(l.ifModified&&((i=j.getResponseHeader("Last-Modified"))&&(x.lastModified[g]=i),i=j.getResponseHeader("etag"))&&(x.etag[g]=i),204===e||"HEAD"===l.type?c="nocontent":304===e?c="notmodified":(c=a.state,n=a.data,s=!(o=a.error))):(o=c,!e&&c||(c="error",e<0&&(e=0))),j.status=e,j.statusText=(t||c)+"",s?b.resolveWith(f,[n,c,j]):b.rejectWith(f,[j,c,o]),j.statusCode(_),_=void 0,p&&d.trigger(s?"ajaxSuccess":"ajaxError",[j,l,s?n:o]),v.fireWith(f,[j,c]),p&&(d.trigger("ajaxComplete",[j,l]),--x.active||x.event.trigger("ajaxStop")))}},getJSON:function(e,t,i){return x.get(e,t,i,"json")},getScript:function(e,t){return x.get(e,void 0,t,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,t,i,s){return v(t)&&(s=s||i,i=t,t=void 0),x.ajax(x.extend({url:e,type:n,dataType:s,data:t,success:i},x.isPlainObject(e)&&e))}}),x._evalUrl=function(e,t){return x.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){x.globalEval(e,t)}})},x.fn.extend({wrapAll:function(e){return this[0]&&(v(e)&&(e=e.call(this[0])),e=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(i){return v(i)?this.each(function(e){x(this).wrapInner(i.call(this,e))}):this.each(function(){var e=x(this),t=e.contents();t.length?t.wrapAll(i):e.append(i)})},wrap:function(t){var i=v(t);return this.each(function(e){x(this).wrapAll(i?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){x(this).replaceWith(this.childNodes)}),this}}),x.expr.pseudos.hidden=function(e){return!x.expr.pseudos.visible(e)},x.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},x.ajaxSettings.xhr=function(){try{return new w.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Yt=x.ajaxSettings.xhr();d.cors=!!Yt&&"withCredentials"in Yt,d.ajax=Yt=!!Yt,x.ajaxTransport(function(n){var o,a;if(d.cors||Yt&&!n.crossDomain)return{send:function(e,t){var i,s=n.xhr();if(s.open(n.type,n.url,n.async,n.username,n.password),n.xhrFields)for(i in n.xhrFields)s[i]=n.xhrFields[i];for(i in n.mimeType&&s.overrideMimeType&&s.overrideMimeType(n.mimeType),n.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)s.setRequestHeader(i,e[i]);o=function(e){return function(){o&&(o=a=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?t(0,"error"):t(s.status,s.statusText):t(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=o(),a=s.onerror=s.ontimeout=o("error"),void 0!==s.onabort?s.onabort=a:s.onreadystatechange=function(){4===s.readyState&&w.setTimeout(function(){o&&a()})},o=o("abort");try{s.send(n.hasContent&&n.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),x.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),x.ajaxTransport("script",function(i){var s,n;if(i.crossDomain||i.scriptAttrs)return{send:function(e,t){s=x("`); + }; +} diff --git a/src/server/routes/admin.ts b/src/server/routes/admin.ts index 867a690..8e754d9 100644 --- a/src/server/routes/admin.ts +++ b/src/server/routes/admin.ts @@ -392,7 +392,16 @@ router.get("/queues", dashboardCache, async (req, res) => { ...QUEUE_STATES.map(async (state) => { const jobs = await targetQueue.getJobs([state], 0, 199); return jobs.map((j) => { - const json: Record = { ...j.asJSON(), _state: state }; + // asJSON() serializes these fields to strings; the dashboard needs + // the objects (payload repoId, stack lines, failure reason). + const json: Record = { + ...j.asJSON(), + data: j.data, + failedReason: j.failedReason, + stacktrace: j.stacktrace, + returnvalue: j.returnvalue, + _state: state, + }; if (state === "delayed" && j.delay > 0) { json.delayUntil = j.timestamp + j.delay; } diff --git a/src/server/routes/billing.ts b/src/server/routes/billing.ts new file mode 100644 index 0000000..f97407f --- /dev/null +++ b/src/server/routes/billing.ts @@ -0,0 +1,114 @@ +import * as express from "express"; +import { randomBytes } from "crypto"; +import { ensureAuthenticated } from "./connection"; +import { getUser, handleError } from "./route-utils"; +import config from "../../config"; +import { BillingAccount, BillingGrant } from "../../core/billing/models"; +import { billingOrigin, billingPlans, requireBilling } from "../../core/billing/config"; +import { createCheckout, stripeClient, fulfillCheckout, syncSubscription } from "../../core/billing/stripe"; +import { saveBillingDraft, getBillingDraft } from "../../core/billing/drafts"; +import { allowance } from "../../core/billing/quotas"; + +import { receiveBillingEvent, billingFailure } from "../../core/billing/events"; +import { createLogger } from "../../core/logger"; +import { rateLimit } from "express-rate-limit"; + +declare module "express-session" { interface SessionData { billingCSRF?: string } } +export const billingWebhook = express.Router(); +billingWebhook.post("/", express.raw({ type: "application/json", limit: "256kb" }), async (req, res) => { + if (!config.BILLING_ENABLED) { res.sendStatus(404); return; } + let event; + try { event = stripeClient().webhooks.constructEvent(req.body, req.headers["stripe-signature"] as string, config.STRIPE_WEBHOOK_SECRET); } + catch { res.status(400).json({ error: "invalid_billing_signature" }); return; } + if (event.account || (typeof event.livemode === "boolean" && event.livemode !== /^(sk|rk)_live_/.test(config.STRIPE_SECRET_KEY))) { + res.status(400).json({ error: "billing_environment_mismatch" }); return; + } + try { await receiveBillingEvent(event); res.json({ received: true }); } + catch (error) { createLogger("billing").error("billing event storage failed", { eventId: event.id, code: billingFailure(error) }); res.status(500).json({ error: "billing_webhook_failed" }); } +}); +const priceCache = new Map["prices"]["retrieve"]>> }>(); +async function displayedPrice(id: string) { + const cached = priceCache.get(id); + if (cached && cached.expires > Date.now()) return cached.value; + const value = await stripeClient().prices.retrieve(id); + priceCache.set(id, { expires: Date.now() + 60_000, value }); + return value; +} +const router = express.Router(); +router.use(ensureAuthenticated); +router.get("/", async (req, res) => { + try { + const user = await getUser(req); + req.session.billingCSRF ||= randomBytes(32).toString("hex"); + await new Promise((resolve, reject) => req.session.save(error => error ? reject(error) : resolve())); + const account = await BillingAccount.findById(user.id).lean(); + const grants = await BillingGrant.find({ owner: user.id }).select("planId scope status until cancelAtPeriodEnd revoked").lean(); + const plans = config.BILLING_ENABLED ? await Promise.all(billingPlans().filter(p => p.priceId).map(async p => { + const price = await displayedPrice(p.priceId); + return { id: p.id, name: p.name, description: p.description, repositories: p.repositories, storage: p.storage, + amount: price.unit_amount, currency: price.currency, interval: price.recurring?.interval, intervalCount: price.recurring?.interval_count }; + })) : []; + res.set("Cache-Control", "no-store").json({ enabled: config.BILLING_ENABLED, csrf: req.session.billingCSRF, plans, checkoutEnabled: config.BILLING_CHECKOUT_ENABLED, + grants: grants.map(g => ({ planId: g.planId, scope: g.scope, status: g.revoked ? "revoked" : g.status, until: g.until, cancelAtPeriodEnd: g.cancelAtPeriodEnd })), + portal: !!account?.customerId, allowance: config.BILLING_ENABLED ? await allowance(user.id) : null }); + } catch (error) { handleError(error, res, req); } +}); +router.get("/draft/:id", async (req, res) => { + try { + const user = await getUser(req); + const draft = await getBillingDraft(user.id, req.params.id); + res.set("Cache-Control", "no-store").json({ id: draft._id, path: draft.path, values: draft.values }); + } catch (error) { handleError(error, res, req); } +}); +router.use((req, res, next) => { + if (!req.session.billingCSRF || req.headers["x-csrf-token"] !== req.session.billingCSRF) { res.status(403).json({ error: "invalid_auth_state" }); return; } + next(); +}); +router.use(rateLimit({ windowMs: 60_000, limit: 30, standardHeaders: "draft-8", legacyHeaders: false })); +router.post("/draft", async (req, res) => { + try { + requireBilling(); + const user = await getUser(req); + const draft = await saveBillingDraft(user.id, req.body.path, req.body.values); + res.set("Cache-Control", "no-store").json({ id: draft._id }); + } catch (error) { handleError(error, res, req); } +}); +router.post("/checkout", async (req, res) => { + try { + requireBilling(); + const user = await getUser(req); + if (typeof req.body.planId !== "string" || (req.body.target != null && typeof req.body.target !== "string")) { res.sendStatus(400); return; } + let returnPath: string | undefined; + if (req.body.draftId != null) { + const draft = await getBillingDraft(user.id, req.body.draftId); + returnPath = `${draft.path}?billingDraft=${draft._id}`; + } + res.json({ url: await createCheckout(user.id, req.body.planId, req.body.target, undefined, returnPath) }); + } catch (error) { handleError(error, res, req); } +}); +router.post("/portal", async (req, res) => { + try { + const user = await getUser(req); + const account = await BillingAccount.findById(user.id).lean(); + if (!account?.customerId) { res.status(404).json({ error: "billing_customer_not_found" }); return; } + const portal = await stripeClient().billingPortal.sessions.create({ customer: account.customerId, return_url: `${billingOrigin()}/profile?billing=portal` }); + res.json({ url: portal.url }); + } catch (error) { handleError(error, res, req); } +}); +router.post("/refresh", async (req, res) => { + try { + requireBilling(); + const user = await getUser(req); + const account = await BillingAccount.findById(user.id).lean(); + let pending = !!account?.checkout?.key && !account.checkout.id; + if (account?.checkout?.id) { + await fulfillCheckout(account.checkout.id); + const checkout = await stripeClient().checkout.sessions.retrieve(account.checkout.id); + pending = checkout.status !== "expired" && checkout.payment_status === "unpaid"; + } + const grants = await BillingGrant.find({ owner: user.id, subscriptionId: { $exists: true } }).lean(); + for (const grant of grants) await syncSubscription(grant.subscriptionId!); + res.json({ ok: true, pending }); + } catch (error) { handleError(error, res, req); } +}); +export default router; diff --git a/src/server/routes/conference.ts b/src/server/routes/conference.ts index cf7c375..7bd7f28 100644 --- a/src/server/routes/conference.ts +++ b/src/server/routes/conference.ts @@ -1,3 +1,5 @@ +import config from "../../config"; +import { billingError } from "../../core/billing/config"; import * as express from "express"; import AnonymousError from "../../core/AnonymousError"; import Conference from "../../core/Conference"; @@ -12,37 +14,14 @@ const router = express.Router(); router.use(ensureAuthenticated); const plans = [ - { - id: "free_conference", - name: "Free", - pricePerRepo: 0, - storagePerRepo: -1, - description: `
  • Quota is deducted from user account
  • -
  • No-download
  • -
  • Conference dashboard
  • `, - }, - { - id: "premium_conference", - name: "Premium", - pricePerRepo: 0.5, - storagePerRepo: 500 * 8 * 1024, - description: `
  • 500Mo / repository
  • -
  • Repository download
  • -
  • Conference dashboard
  • `, - }, - { - id: "unlimited_conference", - name: "Unlimited", - pricePerRepo: 3, - storagePerRepo: 0, - description: `
  • Unlimited repository size
  • -
  • Repository download
  • -
  • Conference dashboard
  • `, - }, + { id: "free_conference", name: "Free", pricePerRepo: 0, storagePerRepo: -1, + description: "
  • Uses each author's allowance
  • Conference dashboard
  • " }, + { id: "conference", name: "Review package", pricePerRepo: 0, storagePerRepo: config.PAID_CONFERENCE_STORAGE, + description: `
  • Up to ${config.PAID_CONFERENCE_REPOSITORIES} repositories
  • Up to 180 days
  • Prepaid at checkout
  • ` }, ]; router.get("/plans", async (req: express.Request, res: express.Response) => { - res.json(plans); + res.json(config.BILLING_ENABLED && config.STRIPE_CONFERENCE_PRICE ? plans : plans.filter(p => p.id === "free_conference")); }); router.get("/", async (req: express.Request, res: express.Response) => { @@ -68,7 +47,7 @@ router.get("/", async (req: express.Request, res: express.Response) => { }); // eslint-disable-next-line @typescript-eslint/no-explicit-any -function validateConferenceForm(conf: any) { +function validateConferenceForm(conf: any, isNew: boolean) { if (!conf.name) throw new AnonymousError("conf_name_missing", { object: conf, @@ -99,11 +78,13 @@ function validateConferenceForm(conf: any) { object: conf, httpStatus: 400, }); - if (plans.filter((p) => p.id == conf.plan.planID).length != 1) + if (!conf.plan || plans.filter((p) => p.id == conf.plan.planID).length != 1) throw new AnonymousError("invalid_plan", { object: conf, httpStatus: 400, }); + if (!Number.isFinite(new Date(conf.startDate).getTime()) || !Number.isFinite(new Date(conf.endDate).getTime())) throw billingError("conf_start_date_invalid"); + if (conf.plan.planID === "conference" && ((isNew && (!config.BILLING_ENABLED || !config.STRIPE_CONFERENCE_PRICE)) || new Date(conf.endDate).getTime() - new Date(conf.startDate).getTime() > 180 * 86400_000)) throw billingError("billing_invalid_review_window"); const plan = plans.filter((p) => p.id == conf.plan.planID)[0]; if (plan.pricePerRepo > 0) { const billing = conf.billing; @@ -145,16 +126,16 @@ function validateConferenceForm(conf: any) { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function applyConferenceForm( model: IConferenceDocument, - body: any, + body: { name: string; startDate: string | Date; endDate: string | Date; url: string; + options: IConferenceDocument["options"]; plan?: { planID: string } }, isNew: boolean ) { model.name = body.name; model.startDate = new Date(body.startDate); model.endDate = new Date(body.endDate); - model.status = "ready"; + if (isNew) model.status = body.plan?.planID === "conference" ? "pending_payment" : "ready"; model.url = body.url; model.options = body.options; if (isNew) model.repositories = []; @@ -178,7 +159,13 @@ router.post( model = queryModel; isOwnerOrAdmin(model.owners, user); } - validateConferenceForm(req.body); + if (req.params.conferenceID) { + if (req.body.plan?.planID !== model.plan.planID) throw billingError("billing_plan_change_not_supported"); + if (model.plan.planID === "conference" && (new Date(req.body.startDate).getTime() !== model.startDate.getTime() || new Date(req.body.endDate).getTime() !== model.endDate.getTime())) throw billingError("billing_review_window_locked"); + } + // Historical conference plans remain editable without being offered for purchase. + const legacyPlan = req.params.conferenceID && !plans.some(p => p.id === model.plan.planID); + validateConferenceForm(legacyPlan ? { ...req.body, plan: { planID: "free_conference" } } : req.body, !req.params.conferenceID); applyConferenceForm(model, req.body, !req.params.conferenceID); if (!req.params.conferenceID) { @@ -194,7 +181,7 @@ router.post( size: plans.filter((p) => p.id == req.body.plan.planID)[0] .storagePerRepo, file: 0, - repository: 0, + repository: req.body.plan.planID === "conference" ? config.PAID_CONFERENCE_REPOSITORIES : 0, }, }; diff --git a/src/server/routes/connection.ts b/src/server/routes/connection.ts index 6c77db6..c7ac330 100644 --- a/src/server/routes/connection.ts +++ b/src/server/routes/connection.ts @@ -1,3 +1,5 @@ +import { resetOwnerAccessAlerts } from "../../core/owner-notifications"; +import { saveRegistrationEmail } from "../../core/registration-email"; import { createClient } from "redis"; import * as passport from "passport"; import * as session from "express-session"; @@ -99,12 +101,9 @@ export const verify = async ( externalIDs: { github: profile.id, }, - emails: profile.emails?.map((email) => { - return { email: email.value, default: false }; - }), + emails: [], photo, }); - if (user.emails?.length) user.emails[0].default = true; await user.save(); } } @@ -120,6 +119,8 @@ export const verify = async ( return; } await setCredential(String(user!._id), accessToken); + await saveRegistrationEmail(user!, accessToken); + if (flow.ownerId) await resetOwnerAccessAlerts(String(user!._id)); done(null, { username: user!.username, user }); } catch (error) { logger.error("verify failed", serializeError(error)); @@ -206,7 +207,7 @@ router.get( returnTo: safeAuthReturnTo(req.query.returnTo, current ? "/connections" : "/dashboard"), expires: Date.now() + 10 * 60000 }; next(); }, - passport.authenticate("github", { scope: ["repo"] }), // Note the scope here + passport.authenticate("github", { scope: ["repo", "user:email"] }), function (req: express.Request, res: express.Response) { res.redirect("/"); } diff --git a/src/server/routes/gist-private.ts b/src/server/routes/gist-private.ts index 59882a1..548ad3f 100644 --- a/src/server/routes/gist-private.ts +++ b/src/server/routes/gist-private.ts @@ -1,3 +1,4 @@ +import { resetOwnerAccessAlerts } from "../../core/owner-notifications"; import * as express from "express"; import { ensureAuthenticated } from "./connection"; @@ -29,6 +30,7 @@ router.post( const user = await getUser(req); isOwnerOrAdmin([gist.owner.id], user); + if (user.id === gist.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "gist", id: String(gist.model._id) }); await gist.updateIfNeeded({ force: true }); res.json({ status: gist.status }); } catch (error) { @@ -68,6 +70,7 @@ router.post( { $set: { "options.expirationDate": newExpiration } } ).exec(); + if (user.id === gist.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "gist", id: String(gist.model._id) }); await gist.updateIfNeeded({ force: true }); res.json({ status: gist.status, expirationDate: newExpiration }); } catch (error) { @@ -222,6 +225,7 @@ router.post( } ).exec(); await gist.updateStatus(RepositoryStatus.PREPARING); + if (user.id === gist.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "gist", id: String(gist.model._id) }); await gist.updateIfNeeded({ force: true }); res.json(gist.toJSON()); } catch (error) { diff --git a/src/server/routes/github-app.ts b/src/server/routes/github-app.ts index 30cb582..4c0a623 100644 --- a/src/server/routes/github-app.ts +++ b/src/server/routes/github-app.ts @@ -1,3 +1,5 @@ +import { notificationEmail, normalizeEmail, resetOwnerAccessAlerts } from "../../core/owner-notifications"; +import { saveRegistrationEmail } from "../../core/registration-email"; import * as express from "express"; import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto"; import config from "../../config"; @@ -16,7 +18,7 @@ import { isDisabledAccount, safeAuthReturnTo } from "./auth-utils"; type Flow = { state: string; expires: number; ownerId?: string; returnTo: string; repository?: string; install?: boolean }; declare module "express-session" { - interface SessionData { githubAppFlow?: Flow; githubInstallFlow?: Flow; githubConnectionCSRF?: string; } + interface SessionData { githubAppFlow?: Flow; githubInstallFlow?: Flow; githubConnectionCSRF?: string; emailPromptDismissed?: boolean; } } export function safeReturnTo(value: unknown): string { return safeAuthReturnTo(value, "/connections"); @@ -28,6 +30,12 @@ export function consumeFlow(flow: Flow | undefined, state: unknown): Flow { function newFlow(ownerId: string | undefined, returnTo: unknown): Flow { return { state: randomBytes(32).toString("hex"), expires: Date.now() + 10 * 60000, ownerId, returnTo: safeReturnTo(returnTo) }; } +// Parallel user/connections reads must initialize the same token even when +// both loaded the session before either request saved it. +export function connectionCSRF(req: express.Request): string { + return req.session.githubConnectionCSRF ||= createHmac("sha256", config.SESSION_SECRET) + .update(`github-connections:${req.sessionID}`).digest("hex"); +} function saveSession(req: express.Request) { return new Promise((resolve, reject) => req.session.save(err => err ? reject(err) : resolve())); } function enabled(req: express.Request, res: express.Response, next: express.NextFunction) { if (!config.GITHUB_APP_ENABLED || !config.GITHUB_APP_NEW_CONNECTIONS) return res.status(503).json({ error: "github_app_disabled" }); @@ -87,6 +95,8 @@ router.get("/app/callback", enabled, async (req, res) => { } if (isDisabledAccount(user.status)) throw appError("not_connected", 403); await saveAppGrant(user.id, tokens); + await saveRegistrationEmail(user, tokens.access_token); + if (flow.ownerId) await resetOwnerAccessAlerts(user.id); await new Promise((resolve, reject) => req.login({ username: user!.username, user }, err => err ? reject(err) : resolve())); res.redirect(flow.install ? `/github/app/install?returnTo=${encodeURIComponent(flow.returnTo)}&repository=${encodeURIComponent(flow.repository || "")}` : flow.returnTo); } catch (error) { handleError(error, res, req); } @@ -141,6 +151,7 @@ router.get("/app/setup", enabled, async (req, res) => { if (req.query.setup_action !== "request") { const installations = await userInstallations(user.id); if (!installations.some(i => String(i.id) === req.query.installation_id)) throw appError("github_app_access_required"); + await resetOwnerAccessAlerts(user.id); } await UserModel.updateOne({ _id: user.id }, { $set: { repositories: [] } }); res.redirect(flow.returnTo); @@ -150,7 +161,7 @@ router.get("/app/setup", enabled, async (req, res) => { router.get("/connections", async (req, res) => { try { const user = await getUser(req); - req.session.githubConnectionCSRF ||= randomBytes(32).toString("hex"); + connectionCSRF(req); await saveSession(req); const credentials = await CredentialModel.find({ ownerId: user.id }).select("provider revoked").lean(); const appConnected = credentials.some(c => c.provider === APP_PROVIDER && !c.revoked); @@ -167,7 +178,8 @@ router.get("/connections", async (req, res) => { const repos = await RepositoryModel.find({ owner: user.id, status: { $ne: "removed" } }).select("repoId source.repositoryName githubAccess status statusDate").lean(); const prs = await PullRequestModel.find({ owner: user.id, status: { $ne: "removed" } }).select("pullRequestId source.repositoryFullName githubAccess status statusDate").lean(); const gistCount = await GistModel.countDocuments({ owner: user.id, status: { $ne: "removed" } }); - res.json({ csrf: req.session.githubConnectionCSRF, appEnabled: config.GITHUB_APP_ENABLED && config.GITHUB_APP_NEW_CONNECTIONS, + res.json({ notificationEmail: notificationEmail(user.model.emails, user.model.notificationEmail), emailNotificationsEnabled: !!(config.RESEND_API_KEY && config.EMAIL_FROM), + csrf: req.session.githubConnectionCSRF, appEnabled: config.GITHUB_APP_ENABLED && config.GITHUB_APP_NEW_CONNECTIONS, gistConnection: config.GITHUB_APP_ENABLED && credentials.some(c => c.provider === APP_PROVIDER) ? "github-app" : "oauth", oauthEnabled: config.GITHUB_OAUTH_ENABLED, oauthConnected: !!(await getCredentialToken(user.id)), appConnected, appError: appErrorCode, installations, gistCount, resources: [ @@ -184,6 +196,33 @@ router.use("/connections", (req, res, next) => { next(); }); +router.get("/connections/email", async (req, res) => { + try { + const user = await getUser(req); + const csrf = connectionCSRF(req); + await saveSession(req); + res.json({ csrf, notificationEmail: notificationEmail(user.model.emails, user.model.notificationEmail) }); + } catch (error) { handleError(error, res, req); } +}); + +router.post("/connections/email", async (req, res) => { + try { + const user = await getUser(req); + const action = req.body?.action || "save"; + if (!["save", "later", "never"].includes(action)) return res.status(400).json({ error: "Invalid email preference." }); + if (action === "later" || action === "never") { + if (action === "never") await UserModel.updateOne({ _id: user.id }, { $set: { emailPromptNever: true } }); + req.session.emailPromptDismissed = true; + await saveSession(req); + return res.json({ ok: true }); + } + const email = normalizeEmail(req.body?.email); + if (!email) return res.status(400).json({ error: "Enter a valid email address." }); + await UserModel.updateOne({ _id: user.id }, { $set: { notificationEmail: email } }); + res.json({ notificationEmail: email }); + } catch (error) { handleError(error, res, req); } +}); + router.post("/connections/migrate", async (req, res) => { try { const user = await getUser(req); @@ -225,7 +264,8 @@ router.post("/connections/migrate", async (req, res) => { } }; const result = isRepo ? await RepositoryModel.updateOne(filter, change) : await PullRequestModel.updateOne(filter, change); if (!result.modifiedCount) throw appError("connection_changed", 409); - res.json({ connection }); + await resetOwnerAccessAlerts(user.id, { kind: isRepo ? "repository" : "pull-request", id: String(model._id) }); + res.json({ connection, fullName: selected.fullName || name }); } catch (error) { handleError(error, res, req); } }); diff --git a/src/server/routes/pullRequest-private.ts b/src/server/routes/pullRequest-private.ts index ae2deb1..af23be7 100644 --- a/src/server/routes/pullRequest-private.ts +++ b/src/server/routes/pullRequest-private.ts @@ -1,3 +1,4 @@ +import { resetOwnerAccessAlerts } from "../../core/owner-notifications"; import { selectRepositoryAccess } from "../../core/github-app"; import * as express from "express"; import { ensureAuthenticated } from "./connection"; @@ -30,6 +31,7 @@ router.post( const user = await getUser(req); isOwnerOrAdmin([pullRequest.owner.id], user); + if (user.id === pullRequest.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "pull-request", id: String(pullRequest.model._id) }); await pullRequest.updateIfNeeded({ force: true }); res.json({ status: pullRequest.status }); } catch (error) { @@ -71,6 +73,7 @@ router.post( { $set: { "options.expirationDate": newExpiration } } ).exec(); + if (user.id === pullRequest.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "pull-request", id: String(pullRequest.model._id) }); await pullRequest.updateIfNeeded({ force: true }); res.json({ status: pullRequest.status, expirationDate: newExpiration }); } catch (error) { @@ -242,6 +245,7 @@ router.post( } ).exec(); await pullRequest.updateStatus(RepositoryStatus.PREPARING); + if (user.id === pullRequest.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "pull-request", id: String(pullRequest.model._id) }); await pullRequest.updateIfNeeded({ force: true }); res.json({ ...pullRequest.toJSON(), connection: pullRequest.model.githubAccess?.kind || "oauth" }); } catch (error) { diff --git a/src/server/routes/repository-private.ts b/src/server/routes/repository-private.ts index 98b9e3d..3d06d2f 100644 --- a/src/server/routes/repository-private.ts +++ b/src/server/routes/repository-private.ts @@ -1,3 +1,9 @@ +import Repository from "../../core/Repository"; +import { sourceStorageSize } from "../../core/source/Source"; +import { neverExpirationDeadline, validateBillingExpiration } from "../../core/billing/expiration"; +import config from "../../config"; +import { assertConferenceAdmission, reserveRepository } from "../../core/billing/quotas"; +import { resetOwnerAccessAlerts } from "../../core/owner-notifications"; import { randomUUID } from "crypto"; import { githubQuotaKey } from "../../core/github-token-context"; import { selectRepositoryAccess, boundAppToken, appError } from "../../core/github-app"; @@ -132,7 +138,8 @@ router.post("/claim", async (req, res) => { await AnonymizedRepositoryModel.updateOne( { repoId: repoConfig.repoId }, - { $set: { owner: user.model.id, githubAccess: selectedAccess.binding } } + { $set: { owner: user.model.id, githubAccess: selectedAccess.binding, accessAlertGeneration: randomUUID() }, + $unset: { accessAlertClaimedAt: "" } } ).collation({ locale: "en", strength: 2 }); return res.send("Ok"); } catch (error) { @@ -157,13 +164,14 @@ router.post( repo.status == RepositoryStatus.PREPARING || repo.status == RepositoryStatus.QUEUE || (repo.status == RepositoryStatus.DOWNLOAD && - repo.model.statusDate > new Date(Date.now() - 5 * 60_000)) || + repo.statusActivityDate > new Date(Date.now() - 5 * 60_000)) || repo.status == RepositoryStatus.REMOVING || repo.status == RepositoryStatus.EXPIRING ) { throw new AnonymousError("invalid_status", { httpStatus: 409 }); } + if (user.id === repo.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "repository", id: String(repo.model._id) }); await repo.refresh(); res.json({ status: repo.status }); } catch (error) { @@ -199,7 +207,9 @@ router.post( const newExpiration = extendExpirationDate( repo.model.options.expirationDate ); + validateBillingExpiration({ ...repo.model.options, expirationDate: newExpiration }); const reactivating = repo.status === RepositoryStatus.EXPIRED; + if (reactivating) await assertConferenceAdmission(String(repo.model.owner), repo.repoId, repo.model.conference); const updates: Record = { "options.expirationDate": newExpiration, settingsSavedAt: new Date(), @@ -218,6 +228,7 @@ router.post( { $set: updates } ).exec(); if (saved && saved.matchedCount === 0) throw new AnonymousError("invalid_status", { httpStatus: 409 }); + if (user.id === repo.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "repository", id: String(repo.model._id) }); if (reactivating) { // Expiration removes the cached files. Rebuild the saved commit @@ -490,6 +501,7 @@ export function shouldReactivateInactiveRepository( router.post( "/:repoId/", async (req, res) => { + let recoverReactivation: (() => Promise) | undefined; try { const repo = await getRepo(req, res, { nocheck: true, @@ -525,11 +537,44 @@ router.post( const sourceChanged = reconnecting || hasRepositorySourceChanged(repo.model, repoUpdate); const previousAccessRevision = repo.model.githubAccess?.revision; - updateRepoModel(repo.model, repoUpdate); - const reactivating = shouldReactivateInactiveRepository(repo.model); + // Keep proposed settings separate until cleanup passes the persisted fences. + const proposed = new AnonymizedRepositoryModel(repo.model.toObject()); + if (repo.model.billingManaged) validateBillingExpiration(repoUpdate.options); + if (config.BILLING_ENABLED && repoUpdate.options.expirationMode === "never" && + (repo.model.options.expirationMode !== "never" || repo.model.billingNeverExpiresUntil)) { + proposed.billingNeverExpiresUntil = await neverExpirationDeadline(String(repo.model.owner), + repo.model.options.expirationMode === "never" ? repo.model.billingNeverExpiresUntil : undefined); + } else if (repoUpdate.options.expirationMode !== "never") { + proposed.billingNeverExpiresUntil = undefined; + } + updateRepoModel(proposed, repoUpdate); + const reactivating = shouldReactivateInactiveRepository(proposed); + if (reactivating) { - repo.model.anonymizeDate = new Date(); + proposed.anonymizeDate = new Date(); + } + + const needsAdmission = sourceChanged || reactivating || repoUpdate.conference !== repo.model.conference; + if (needsAdmission) { + await assertConferenceAdmission(String(repo.model.owner), repo.repoId, repoUpdate.conference); + if (reactivating && config.BILLING_ENABLED && repo.model.billingManaged) { + const previousStatus = repo.status; + const previousStatusDate = repo.model.statusDate; + const claimedAt = new Date(); + const claim = await AnonymizedRepositoryModel.updateOne({ _id: repo.model._id, + status: previousStatus, statusDate: previousStatusDate, + }, { $set: { status: RepositoryStatus.PREPARING, statusDate: claimedAt } }).exec(); + if (!claim.matchedCount) throw appError("invalid_status", 409); + repo.model.status = RepositoryStatus.PREPARING; + repo.model.statusDate = claimedAt; + recoverReactivation = async () => { + await AnonymizedRepositoryModel.updateOne({ _id: repo.model._id, + status: RepositoryStatus.PREPARING, statusDate: claimedAt, + }, { $set: { status: previousStatus, statusDate: previousStatusDate || new Date() } }).exec(); + }; + } + if (repo.model.billingManaged && !sourceChanged) await reserveRepository(String(repo.model.owner), repo.repoId, repo.model.size?.storage || 0, repoUpdate.conference); } if (sourceChanged) { @@ -566,15 +611,22 @@ router.post( const commit = await repository.getCommitInfo(repoUpdate.source.commit, { accessToken: sourceAccess.token, }); - if (reconnecting) repo.model.source.commitDate = commit.commit.committer?.date + if (reconnecting) proposed.source.commitDate = commit.commit.committer?.date ? new Date(commit.commit.committer.date) : undefined; - repo.model.githubAccess = { ...sourceAccess.binding, revision: randomUUID() }; - repo.model.source.repositoryId = repository.model.id; - repo.model.source.repositoryName = + proposed.githubAccess = { ...sourceAccess.binding, revision: randomUUID() }; + proposed.source.repositoryId = repository.model.id; + proposed.source.repositoryName = repository.fullName || repoUpdate.fullName; - repo.model.anonymizeDate = new Date(); - await repo.remove({ accessRevision: previousAccessRevision }); + proposed.anonymizeDate = new Date(); + if (repo.model.billingManaged) { + const storageBytes = await sourceStorageSize(new Repository(proposed).source); + await reserveRepository(String(repo.model.owner), repo.repoId, storageBytes, repoUpdate.conference); + } + await repo.remove({ accessRevision: previousAccessRevision, replacement: true }); } + repo.model.set({ options: proposed.options, source: proposed.source, + githubAccess: proposed.githubAccess, anonymizeDate: proposed.anonymizeDate, + billingNeverExpiresUntil: proposed.billingNeverExpiresUntil }); const removeRepoFromConference = async (conferenceID: string) => { const conf = await ConferenceModel.findOne({ @@ -633,6 +685,7 @@ router.post( "githubAccess.revision": previousAccessRevision || { $exists: false } }, { $set: { + ...(repo.model.billingNeverExpiresUntil ? { billingNeverExpiresUntil: repo.model.billingNeverExpiresUntil } : {}), settingsSavedAt: new Date(), options: repo.model.options, source: repo.model.source, @@ -641,9 +694,12 @@ router.post( anonymizeDate: repo.model.anonymizeDate, ...preparation, }, + ...(!repo.model.billingNeverExpiresUntil ? { $unset: { billingNeverExpiresUntil: 1 } } : {}), } ).exec(); if (!saved.matchedCount) throw appError("connection_changed", 409); + recoverReactivation = undefined; + if (user.id === repo.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "repository", id: String(repo.model._id) }); if (!sourceChanged && !reactivating) { return res.json({ status: repo.status }); } @@ -654,6 +710,7 @@ router.post( await enqueueSavedSnapshot(repo.model); res.json({ status: repo.status }); } catch (error) { + if (recoverReactivation) await recoverReactivation(); return handleError(error, res, req); } } @@ -712,6 +769,10 @@ router.post("/", async (req, res) => { repo.owner = user.id; repo.githubAccess = selectedAccess.binding; + validateBillingExpiration(repoUpdate.options); + if (repoUpdate.options.expirationMode === "never") { + repo.billingNeverExpiresUntil = await neverExpirationDeadline(user.id); + } updateRepoModel(repo, repoUpdate); repo.source.type = "GitHubStream"; repo.source.repositoryId = repository.model.id; @@ -719,6 +780,9 @@ router.post("/", async (req, res) => { repo.conference = repoUpdate.conference; + await assertConferenceAdmission(user.id, repo.repoId, repo.conference); + repo.billingManaged = config.BILLING_ENABLED; + await reserveRepository(user.id, repo.repoId, 0, repo.conference); await repo.save(); if (repoUpdate.conference) { diff --git a/src/server/routes/repository-public.ts b/src/server/routes/repository-public.ts index 9150bd3..90fbc40 100644 --- a/src/server/routes/repository-public.ts +++ b/src/server/routes/repository-public.ts @@ -1,3 +1,6 @@ +import { FILE_LISTING_LIMIT } from "../../core/file-tree"; +import { folderCounts as scopedFolderCounts } from "../../core/folder-counts"; +import { repositoryFailureStatus } from "../../core/repository-failure-status"; import { anonymizePath } from "../../core/anonymize-utils"; import * as express from "express"; import config from "../../config"; @@ -32,6 +35,7 @@ router.get( } const repo = await getRepo(req, res); if (!repo) return; + if (repo.model.billingManaged && repo.model.truncatedFolders?.length) throw new AnonymousError("repository_tree_incomplete", { httpStatus: 409 }); let user: User | undefined = undefined; try { @@ -56,6 +60,7 @@ router.get( res.attachment(`${repo.repoId}.zip`); if (req.method === "HEAD") { res.end(); return; } await repo.countView(); + const submodules = await repo.submoduleMounts(); if (config.STREAMER_ENTRYPOINT) { // use the streamer service @@ -74,6 +79,7 @@ router.get( branch: repo.model.source.branch, repoId: repo.repoId, anonymizerOptions: anonymizer.opt, + submodules, contentOptions: { image: repo.options.image, pdf: repo.options.pdf, @@ -168,6 +174,7 @@ router.get( commit: repo.model.source.commit || "HEAD", getToken: () => repo.getToken(), anonymizerOptions: anonymizer.opt, + submodules, contentOptions: { image: repo.options.image, pdf: repo.options.pdf, @@ -208,26 +215,37 @@ router.get( const repo = await getRepo(req, res); if (!repo) return; try { + if (req.query?.path !== undefined) return res.json(await scopedFolderCounts(repo, req.query.path)); + // Retain the guarded legacy response for clients requesting all counts. const repoId = repo.repoId; const results = await FileModel.aggregate([ { $match: { repoId, treeGeneration: repo.model.treeGeneration || { $exists: false }, size: { $ne: null } } }, { $project: { _id: 0, path: 1 } }, { $group: { _id: "$path", count: { $sum: 1 } } }, + { $limit: FILE_LISTING_LIMIT + 1 }, ]).exec(); + if (results.length > FILE_LISTING_LIMIT) throw new AnonymousError("file_listing_too_large", { httpStatus: 413 }); - const directCounts: Record = {}; + const directCounts: Record = Object.create(null); for (const r of results) { directCounts[r._id ?? ""] = r.count; } - const folderCounts: Record = {}; + const folderCounts: Record = Object.create(null); + let folders = 0; + const addCount = (folder: string, count: number) => { + if (!Object.hasOwn(folderCounts, folder) && ++folders > FILE_LISTING_LIMIT) { + throw new AnonymousError("file_listing_too_large", { httpStatus: 413 }); + } + folderCounts[folder] = (folderCounts[folder] || 0) + count; + }; for (const [folder, count] of Object.entries(directCounts)) { let p = folder; - folderCounts[p] = (folderCounts[p] || 0) + count; + addCount(p, count); while (p) { const idx = p.lastIndexOf("/"); p = idx >= 0 ? p.substring(0, idx) : ""; - folderCounts[p] = (folderCounts[p] || 0) + count; + addCount(p, count); if (!p) break; } } @@ -238,7 +256,7 @@ router.get( "../../core/anonymize-utils" ); const compiled = compileTerms(terms); - const anonymized: Record = {}; + const anonymized: Record = Object.create(null); for (const [folder, count] of Object.entries(folderCounts)) { const anonFolder = anonymizePathCompiled(folder, compiled); anonymized[anonFolder] = (anonymized[anonFolder] || 0) + count; @@ -319,7 +337,7 @@ router.get( if (repo.status != "ready") { if ( repo.status != RepositoryStatus.QUEUE && - repo.model.statusDate < fiveMinuteAgo + repo.statusActivityDate < fiveMinuteAgo ) { await repo.updateStatus(RepositoryStatus.PREPARING); await downloadQueue.add(repo.repoId, { repoId: repo.repoId }, { @@ -334,7 +352,7 @@ router.get( : "repository_not_accessible", { object: repo, - httpStatus: 500, + httpStatus: repositoryFailureStatus(repo.model.statusMessage), } ); } @@ -371,21 +389,26 @@ router.get( sourceCommitDate: repo.model.source.commitDate, settingsSavedAt: repo.model.settingsSavedAt, publishedAt: repo.model.publishedAt, + // Lets reviewers see how long the link stays available. + expirationDate: + repo.options.expirationMode !== "never" + ? repo.options.expirationDate || null + : null, isAdmin: user?.isAdmin === true, isOwner: user?.id == repo.model.owner, hasWebsite: !!repo.options.page && !!repo.options.pageSource, truncatedFolders: (repo.model.truncatedFolders || []).map((path) => anonymizePath(path, repo.options.terms || []) ), - // Submodule contents are not included in GitHub archives/trees, so - // they end up as empty folders in the anonymized repository. Surface - // a warning in the explorer when the repository uses submodules (#737). + // Legacy trees retain .gitmodules until their next tree refresh. hasSubmodules: (await FileModel.exists({ repoId: repo.repoId, treeGeneration: repo.model.treeGeneration || { $exists: false }, - name: ".gitmodules", - path: "", + $or: [ + { submodule: { $exists: true, $ne: "included" } }, + { name: ".gitmodules", path: "" }, + ], })) != null, }); } catch (error) { diff --git a/src/server/routes/route-utils.ts b/src/server/routes/route-utils.ts index 295bb01..cbed9e5 100644 --- a/src/server/routes/route-utils.ts +++ b/src/server/routes/route-utils.ts @@ -233,10 +233,10 @@ export function handleError( errorCode = String(error); } let status = 500; - if (error.httpStatus) { - status = error.httpStatus; - } else if (error.$metadata?.httpStatusCode) { - status = error.$metadata.httpStatusCode; + const explicitStatus = [error?.httpStatus, error?.status, error?.response?.statusCode, + error?.$metadata?.httpStatusCode].find(value => Number.isInteger(value) && value >= 400 && value <= 599); + if (explicitStatus !== undefined) { + status = explicitStatus; } else if ( errorCode && (errorCode.indexOf("not_found") > -1 || diff --git a/src/server/routes/user.ts b/src/server/routes/user.ts index 1dd96d9..c163ef6 100644 --- a/src/server/routes/user.ts +++ b/src/server/routes/user.ts @@ -1,6 +1,10 @@ +import { BillingQuota, BillingDraft } from "../../core/billing/models"; +import { cancelAccountBilling } from "../../core/billing/stripe"; +import { allowance } from "../../core/billing/quotas"; +import { notificationEmail } from "../../core/owner-notifications"; import DashboardName from "../../core/model/dashboard-name"; import { dashboardSummary } from "./dashboard-summary"; -import { revokeGrant } from "./github-app"; +import { connectionCSRF, revokeGrant } from "./github-app"; import CredentialModel from "../../core/model/credentials/credentials.model"; import * as express from "express"; import config from "../../config"; @@ -40,7 +44,18 @@ router.get("/logout", async (req, res) => { router.get("/", async (req, res) => { try { const user = await getUser(req); + const showEmailPrompt = !!(config.RESEND_API_KEY && config.EMAIL_FROM) && + !notificationEmail(user.model.emails, user.model.notificationEmail) && !user.model.emailPromptNever && !req.session.emailPromptDismissed; + if (showEmailPrompt) { + connectionCSRF(req); + await new Promise((resolve, reject) => req.session.save(error => error ? reject(error) : resolve())); + } + res.set("Cache-Control", "no-store"); res.json({ + showEmailPrompt, + emailPromptCSRF: showEmailPrompt ? req.session.githubConnectionCSRF : undefined, + notificationEmail: notificationEmail(user.model.emails, user.model.notificationEmail), + emailNotificationsEnabled: !!(config.RESEND_API_KEY && config.EMAIL_FROM), username: user.username, photo: user.photo, isAdmin: user.isAdmin, @@ -58,7 +73,15 @@ router.get("/dashboard", async (req, res) => { router.get("/quota", async (req, res) => { try { const user = await getUser(req); - const ready = await user.quotaRepositories(); + let ready = await user.quotaRepositories(); + const limits = config.BILLING_ENABLED ? await allowance(user.id) : null; + if (limits) { + const funded = []; + for (const repository of ready) { + if ((await allowance(user.id, repository.repoId, repository.conference)).scope === limits.scope) funded.push(repository); + } + ready = funded; + } let totalStorage = 0; let totalFiles = 0; @@ -112,18 +135,20 @@ router.get("/quota", async (req, res) => { } } + const reserved = limits ? await BillingQuota.findById(limits.scope).lean() : null; res.json({ storage: { - used: totalStorage, - total: config.DEFAULT_QUOTA, + unit: limits ? "bytes" : "legacy", + used: reserved ? reserved.allocations.reduce((sum, entry) => sum + (entry.bytes || 0), 0) : totalStorage, + total: limits?.storage ?? config.DEFAULT_QUOTA, }, file: { used: totalFiles, total: 0, }, repository: { - used: ready.length, - total: 20, + used: reserved ? reserved.allocations.length : ready.length, + total: limits?.repositories ?? 20, }, }); } catch (error) { @@ -175,6 +200,8 @@ router.post("/default", async (req, res) => { router.delete("/", async (req, res) => { try { const user = await getUser(req); + if (config.BILLING_ENABLED) await cancelAccountBilling(user.id); + if (isConnected) await BillingDraft.deleteMany({ owner: user.id }); const repositories = (await user.getRepositories()).filter( (r) => r.owner.id === user.model.id && r.status !== "removed" @@ -226,6 +253,9 @@ router.delete("/", async (req, res) => { repositories: [], }, $unset: { + notificationEmail: "", + emailPromptNever: "", + accessAlertAfter: "", accessTokens: "", accessTokenDates: "", externalIDs: "", diff --git a/src/server/schedule.ts b/src/server/schedule.ts index b500be4..af1f04d 100644 --- a/src/server/schedule.ts +++ b/src/server/schedule.ts @@ -1,3 +1,4 @@ +import { reconcileNeverExpiration } from "../core/billing/expiration"; import { Model, Document } from "mongoose"; import * as schedule from "node-schedule"; import AnonymizedGistModel from "../core/model/anonymizedGists/anonymizedGists.model"; @@ -62,6 +63,7 @@ export function repositoryMaintenanceQuery(now: Date) { } export async function runRepositoryStatusCheck(now = new Date()) { + await reconcileNeverExpiration(now); logger.info("checking repository status and unused repositories"); const fourMonthAgo = new Date(now); fourMonthAgo.setMonth(fourMonthAgo.getMonth() - 4); @@ -156,7 +158,7 @@ export async function runRepositoryStatusCheck(now = new Date()) { await expireContent(AnonymizedPullRequestModel, data => new PullRequest(data), now); const retiredCursor = AnonymizedRepositoryModel.find({ $or: [ - { retiredTreeGenerations: { $exists: true } }, { retiredContentPrefixes: { $exists: true } }, + { retiredTreeGenerations: { $exists: true } }, { retiredPathIndexKeys: { $exists: true } }, { retiredContentPrefixes: { $exists: true } }, { legacyContentCleanupPending: true, status: RepositoryStatus.READY }, { "stagedFileTrees.until": { $lte: now } }, ] }).cursor(); diff --git a/src/server/server-options.ts b/src/server/server-options.ts new file mode 100644 index 0000000..dc4f6c6 --- /dev/null +++ b/src/server/server-options.ts @@ -0,0 +1,9 @@ +import { RequestHandler } from "express"; + +/** The asterisk request target asks about the server, not a session or route. */ +export const serverOptions: RequestHandler = (req, res, next) => { + if (req.originalUrl !== "*") return next(); + if (req.method !== "OPTIONS") { res.status(400).json({ error: "invalid_request" }); return; } + res.setHeader("Allow", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"); + res.status(204).end(); +}; diff --git a/src/server/service/review-callbacks.ts b/src/server/service/review-callbacks.ts new file mode 100644 index 0000000..7c3dfe1 --- /dev/null +++ b/src/server/service/review-callbacks.ts @@ -0,0 +1,28 @@ +import { decodeReviewJSON } from "./review-json"; +const invalid = (): never => { throw new Error("Invalid review callback registry"); }; +export function createReviewCallbacks(raw: string) { + let value: unknown; + try { value = decodeReviewJSON(Buffer.from(raw, "utf8")); } catch { return invalid(); } + if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "callbacks,version") return invalid(); + const config = value as { version: unknown; callbacks: unknown }; + if (config.version !== 1 || !Array.isArray(config.callbacks) || !config.callbacks.length || config.callbacks.length > 32) return invalid(); + const callbacks = new Map(), destinations = new Set(); + for (const item of config.callbacks) { + if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).sort().join(",") !== "callbackId,clientId,url") return invalid(); + const { clientId, callbackId, url } = item; + if (![clientId, callbackId].every(id => typeof id === "string" && /^[a-f0-9]{32}$/.test(id)) || typeof url !== "string" || url.length > 2048) return invalid(); + let parsed: URL; + try { parsed = new URL(url); } catch { return invalid(); } + if (parsed.protocol !== "https:" || parsed.href !== url || parsed.username || parsed.password || parsed.search || parsed.hash || url.includes("?") || url.includes("#") || !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(parsed.pathname)) return invalid(); + const key = clientId + ":" + callbackId; + if (callbacks.has(key)) return invalid(); + callbacks.set(key, url); destinations.add(url); + } + const formAction = "'self' " + [...destinations].join(" "); + if (Buffer.byteLength(formAction) > 4096) return invalid(); + return Object.freeze({ + formAction, + resolve(clientId: string, callbackId: string): string | undefined { return callbacks.get(clientId + ":" + callbackId); }, + }); +} +export type ReviewCallbacks = ReturnType; diff --git a/src/server/service/review-capabilities.ts b/src/server/service/review-capabilities.ts new file mode 100644 index 0000000..173ef31 --- /dev/null +++ b/src/server/service/review-capabilities.ts @@ -0,0 +1,200 @@ +import { createHash, randomBytes, timingSafeEqual } from "crypto"; +import { RequestHandler } from "express"; + +const contract = "4open.artifacts/1"; +const clientPattern = /^[a-f0-9]{32}$/; +const keyPattern = /^[A-Za-z0-9_-]{1,64}$/; +const secretPattern = /^[a-f0-9]{64}$/; +type Key = { clientId: string; digest: Buffer; from: number; until: number }; + +function invalid(): never { + // Never include private configuration or request values in diagnostic errors. + throw new Error("Invalid REVIEW_SERVICE_KEYS configuration"); +} +function record(value: unknown, fields: string[]): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) invalid(); + const data = value as Record; + if ( + Object.keys(data).length !== fields.length || + fields.some((k) => !Object.prototype.hasOwnProperty.call(data, k)) + ) + invalid(); + return data; +} +function timestamp(value: unknown): number { + if ( + typeof value !== "string" || + !/^20[0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/.test(value) + ) + invalid(); + const time = Date.parse(value); + if ( + !Number.isFinite(time) || + new Date(time).toISOString() !== value.replace("Z", ".000Z") + ) + invalid(); + return time; +} +function registry(raw: string): Map { + if (Buffer.byteLength(raw, "utf8") > 65536) invalid(); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + invalid(); + } + const config = record(parsed, ["version", "keys"]); + if ( + config.version !== 1 || + !Array.isArray(config.keys) || + config.keys.length < 1 || + config.keys.length > 64 + ) + invalid(); + const keys = new Map(), + digests = new Set(), + clients = new Map(); + for (const entry of config.keys) { + const k = record(entry, [ + "clientId", + "keyId", + "tokenSHA256", + "notBefore", + "notAfter", + ]); + if ( + typeof k.clientId !== "string" || + !clientPattern.test(k.clientId) || + typeof k.keyId !== "string" || + !keyPattern.test(k.keyId) || + typeof k.tokenSHA256 !== "string" || + !secretPattern.test(k.tokenSHA256) + ) + invalid(); + const from = timestamp(k.notBefore), + until = timestamp(k.notAfter); + const id = k.clientId + ":" + k.keyId; + const count = (clients.get(k.clientId) || 0) + 1; + if ( + until <= from || + keys.has(id) || + digests.has(k.tokenSHA256) || + count > 16 + ) + invalid(); + clients.set(k.clientId, count); + if (clients.size > 32) invalid(); + digests.add(k.tokenSHA256); + keys.set(id, { + clientId: k.clientId, + digest: Buffer.from(k.tokenSHA256, "hex"), + from, + until, + }); + } + return keys; +} + +/** Optional server-to-server read-only endpoint. No browser, owner or repository authority is inferred. */ +export function createReviewCapabilities( + raw?: string, + now: () => number = Date.now, +): RequestHandler { + const keys = raw === undefined ? null : registry(raw); + // One bounded counter per registered client, shared by its rotation keys. + const rates = new Map(); + const dummyDigest = Buffer.alloc(32); + return (req, res) => { + res.setHeader("Cache-Control", "no-store"); + res.setHeader("Referrer-Policy", "no-referrer"); + res.setHeader("X-Content-Type-Options", "nosniff"); + const fail = (status: number, code: string, retry?: number) => { + // Do not keep a rejected request alive while Node drains an unread body. + res.setHeader("Connection", "close"); + if (retry) res.setHeader("Retry-After", String(retry)); + res.status(status).json({ + contract, + code, + requestId: randomBytes(16).toString("hex"), + ...(retry ? { retryAfterSeconds: retry } : {}), + }); + }; + if (!keys || req.originalUrl !== "/service/v1/capabilities") + return fail(404, "not-found"); + if (req.method !== "GET") return fail(405, "invalid-request"); + // Reject browser context and bodies before authentication; never read or log them. + if ( + [ + "cookie", + "origin", + "referer", + "sec-fetch-site", + "sec-fetch-mode", + "sec-fetch-dest", + "sec-fetch-user", + ].some((h) => req.headers[h] !== undefined) + ) + return fail(403, "forbidden"); + if ( + req.headers["transfer-encoding"] !== undefined || + req.headers["content-type"] !== undefined || + (req.headers["content-length"] !== undefined && + req.headers["content-length"] !== "0") + ) + return fail(400, "invalid-request"); + const one = (name: string): string | undefined => { + let count = 0; + for (let i = 0; i < req.rawHeaders.length; i += 2) + if (req.rawHeaders[i].toLowerCase() === name) count++; + const value = req.headers[name]; + return count === 1 && typeof value === "string" ? value : undefined; + }; + const client = one("x-4open-artifact-client-id"), + keyId = one("x-4open-artifact-service-key-id"), + authorization = one("authorization"); + if ( + !client || + !clientPattern.test(client) || + !keyId || + !keyPattern.test(keyId) || + !authorization || + !/^Bearer [a-f0-9]{64}$/i.test(authorization) || + !secretPattern.test(authorization.slice(7)) + ) + return fail(401, "unauthorized"); + const key = keys.get(client + ":" + keyId); + const digest = createHash("sha256") + .update(authorization.slice(7), "ascii") + .digest(); + const equal = timingSafeEqual(digest, key ? key.digest : dummyDigest); + const time = now(); + if ( + !key || + !equal || + !Number.isFinite(time) || + time < key.from || + time >= key.until + ) + return fail(401, "unauthorized"); + const minute = Math.floor(time / 60000); + let rate = rates.get(client); + if (!rate || rate.minute !== minute) { + rate = { minute, count: 0 }; + rates.set(client, rate); + } + if (rate.count >= 60) + return fail( + 429, + "rate-limited", + Math.max(1, Math.ceil((60000 - (time % 60000)) / 1000)), + ); + rate.count++; + // No link, snapshot, protected-delivery or retention support exists yet. + res.status(200).json({ + contract, + clientId: client, + contracts: [contract], + available: [], + }); + }; +} diff --git a/src/server/service/review-completion-http.ts b/src/server/service/review-completion-http.ts new file mode 100644 index 0000000..b33d751 --- /dev/null +++ b/src/server/service/review-completion-http.ts @@ -0,0 +1,262 @@ +import { randomBytes } from "crypto"; +import { RequestHandler } from "express"; +import { + createReviewOwnerConsent, + ReviewConsentError, +} from "./review-owner-consent"; +import { + createReviewServiceAuth, + singleReviewHeader, +} from "./review-service-auth"; +import { decodeReviewJSON } from "./review-json"; + +const contract = "4open.artifacts/1"; +const opaque = /^[a-f0-9]{32}$/; +function exact( + value: unknown, + keys: string[], +): value is Record { + return ( + !!value && + typeof value === "object" && + !Array.isArray(value) && + Object.keys(value).sort().join(",") === keys.sort().join(",") + ); +} +function receipt(value: unknown, clientId: string, intentId: string): unknown { + if ( + !exact(value, [ + "contract", + "clientId", + "intentId", + "bindingId", + "submissionRef", + "policy", + "entitlementId", + ]) || + value.contract !== contract || + value.clientId !== clientId || + value.intentId !== intentId || + ![value.bindingId, value.submissionRef, value.entitlementId].every( + (id) => typeof id === "string" && opaque.test(id), + ) || + !exact(value.policy, ["version", "access", "retainUntil"]) + ) + throw new Error("Invalid receipt"); + const policy = value.policy; + if ( + !Number.isSafeInteger(policy.version) || + (policy.version as number) < 1 || + !["anonymous-link", "restricted-review"].includes( + policy.access as string, + ) || + typeof policy.retainUntil !== "string" || + !/^2[01][0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/.test( + policy.retainUntil, + ) || + !Number.isFinite(Date.parse(policy.retainUntil)) || + new Date(policy.retainUntil).toISOString() !== + policy.retainUntil.replace("Z", ".000Z") + ) + throw new Error("Invalid receipt"); + return value; +} + +/** Mount before global JSON parsing and browser authentication. Disabled without + * its own scoped registry. This factory is intentionally not mounted at startup. */ +export function createReviewCompletionService( + raw: string | undefined, + backend: Pick< + ReturnType, + "exchange" | "receipt" + >, + now: () => number = Date.now, +): RequestHandler { + const auth = + raw === undefined ? undefined : createReviewServiceAuth(raw, now); + const rates = new Map(); + let active = 0; + return async (req, res) => { + res.set({ + "Cache-Control": "no-store", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + }); + res.removeHeader("Access-Control-Allow-Origin"); + const fail = (status: number, code: string) => { + if (res.headersSent || res.destroyed) return; + res.setHeader("Connection", "close"); + res + .status(status) + .json({ contract, code, requestId: randomBytes(16).toString("hex") }); + }; + const exchange = req.originalUrl === "/service/v1/completions/exchange"; + const read = /^\/service\/v1\/intents\/([a-f0-9]{32})\/receipt$/.exec( + req.originalUrl, + ); + if (!auth || (!exchange && !read)) return fail(404, "not-found"); + if (req.method !== (exchange ? "POST" : "GET")) + return fail(405, "invalid-request"); + if ( + !req.secure || + [ + "cookie", + "origin", + "referer", + "sec-fetch-site", + "sec-fetch-mode", + "sec-fetch-dest", + "sec-fetch-user", + ].some((name) => req.headers[name] !== undefined) + ) + return fail(403, "forbidden"); + if (req.body !== undefined) return fail(503, "unavailable"); + if (req.headers["content-encoding"] !== undefined) + return fail(400, "invalid-request"); + for (const name of ["content-type", "content-length", "transfer-encoding"]) + if ( + req.headers[name] !== undefined && + singleReviewHeader(req, name) === undefined + ) + return fail(400, "invalid-request"); + if (exchange) { + if ( + !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test( + singleReviewHeader(req, "content-type") || "", + ) + ) + return fail(400, "invalid-request"); + const length = req.headers["content-length"]; + if ( + length !== undefined && + (!/^\d+$/.test(String(length)) || Number(length) > 65536) + ) + return fail(400, "invalid-request"); + } else if ( + req.headers["transfer-encoding"] !== undefined || + req.headers["content-type"] !== undefined || + (req.headers["content-length"] !== undefined && + req.headers["content-length"] !== "0") + ) + return fail(400, "invalid-request"); + const principal = auth.authenticate( + req, + exchange ? "completion.exchange" : "receipt.read", + ); + if (!principal) return fail(401, "unauthorized"); + const minute = Math.floor(now() / 60000); + let rate = rates.get(principal.clientId); + if (!rate || rate.minute !== minute) { + rate = { minute, count: 0 }; + rates.set(principal.clientId, rate); + } + if (rate.count++ >= 60) return fail(429, "rate-limited"); + if (active >= 4) return fail(503, "unavailable"); + active++; + let cancelBody: (() => void) | undefined; + let ended = false; + const close = () => { + ended = true; + cancelBody?.(); + }; + res.once("close", close); + const timer = setTimeout(() => { + ended = true; + fail(503, "unavailable"); + cancelBody?.(); + }, 15000); + try { + let intentId = read?.[1] || ""; + let result: unknown; + if (exchange) { + const bytes = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let size = 0; + const cleanup = () => { + req.off("data", data); + req.off("end", end); + req.off("aborted", abort); + req.off("error", abort); + cancelBody = undefined; + }; + const abort = () => { + cleanup(); + reject(new Error("Request unavailable")); + }; + const data = (chunk: Buffer) => { + size += chunk.length; + if (size > 65536) { + fail(400, "invalid-request"); + abort(); + } else chunks.push(chunk); + }; + const end = () => { + cleanup(); + resolve(Buffer.concat(chunks)); + }; + cancelBody = abort; + req.on("data", data); + req.once("end", end); + req.once("aborted", abort); + req.once("error", abort); + }); + if (ended) return; + let command: unknown; + try { + command = decodeReviewJSON(bytes); + } catch { + return fail(400, "invalid-request"); + } + if ( + !exact(command, [ + "contract", + "clientId", + "intentId", + "code", + "requestId", + ]) || + typeof command.contract !== "string" || + ![command.clientId, command.intentId, command.requestId].every( + (id) => typeof id === "string" && opaque.test(id), + ) || + typeof command.code !== "string" || + !/^[a-f0-9]{64}$/.test(command.code) + ) + return fail(400, "invalid-request"); + if (command.contract !== contract) + return fail(422, "unsupported-version"); + if (command.clientId !== principal.clientId) + return fail(403, "forbidden"); + if (!principal.valid()) return fail(401, "unauthorized"); + intentId = command.intentId as string; + result = await backend.exchange( + principal.clientId, + command as Parameters[1], + ); + } else { + if (!principal.valid()) return fail(401, "unauthorized"); + result = await backend.receipt(principal.clientId, intentId); + } + if (ended) return; + if (!principal.valid()) return fail(401, "unauthorized"); + res.status(200).json(receipt(result, principal.clientId, intentId)); + } catch (error) { + if (ended) return; + if (error instanceof ReviewConsentError) { + const codes = { + invalid: [400, "invalid-request"], + forbidden: [403, "forbidden"], + expired: [410, "expired"], + conflict: [409, "conflict"], + unavailable: [503, "unavailable"], + } as const; + const [status, code] = codes[error.kind]; + fail(status, code); + } else fail(503, "unavailable"); + } finally { + clearTimeout(timer); + res.off("close", close); + active--; + } + }; +} diff --git a/src/server/service/review-consent-http.ts b/src/server/service/review-consent-http.ts new file mode 100644 index 0000000..8124494 --- /dev/null +++ b/src/server/service/review-consent-http.ts @@ -0,0 +1,336 @@ +import { ReviewCallbacks } from "./review-callbacks"; +import { createHmac, timingSafeEqual } from "crypto"; +import * as express from "express"; +import { Request, Response } from "express"; +import { createReviewOwnerConsent } from "./review-owner-consent"; + +type BrowserSession = { passport?: { user?: unknown } }; +type Context = { accountId: string; sessionId: string }; +const opaque = /^[a-f0-9]{32}$/; +const secret = /^[a-f0-9]{64}$/; +function fields( + value: unknown, + names: string[], +): value is Record { + return ( + !!value && + typeof value === "object" && + !Array.isArray(value) && + Object.keys(value).sort().join(",") === names.sort().join(",") + ); +} +function reject(res: Response, status: number, code: string) { + if (!res.headersSent && !res.destroyed) res.setHeader("Connection", "close"); + if (!res.headersSent && !res.destroyed) + res.status(status).json({ error: { code } }); +} +async function current(req: Request): Promise { + const id = (req.user as { user?: { _id?: unknown } } | undefined)?.user?._id; + const accountId = id === undefined ? "" : String(id); + if ( + !req.isAuthenticated?.() || + !/^[a-f0-9]{24}$/.test(accountId) || + !/^[A-Za-z0-9_-]{24,256}$/.test(req.sessionID || "") + ) + return undefined; + const session = await new Promise( + (resolve, fail) => + req.sessionStore.get(req.sessionID, (error, value) => + error + ? fail(new Error("Session unavailable")) + : resolve(value as BrowserSession | undefined), + ), + ); + if (session?.passport?.user !== accountId) return undefined; + return { accountId, sessionId: req.sessionID }; +} + +/** Mount after current Passport/session authentication and BEFORE any JSON + * parser. This factory is not registered by application startup. TLS must be + * established directly or identified through the configured trusted proxy. */ +export function createReviewConsentRouter( + origin: string, + backend: ReturnType, + csrfKey: Buffer, + callbacks?: ReviewCallbacks, +) { + try { + if (new URL(origin).origin !== origin || !origin.startsWith("https://")) + throw new Error(); + } catch { + throw new Error("Invalid review consent origin"); + } + if (!Buffer.isBuffer(csrfKey) || csrfKey.length !== 32) + throw new Error("Invalid review consent CSRF key"); + const key = Buffer.from(csrfKey); + const csrf = (actor: Context) => + createHmac("sha256", key) + .update( + "4open.review-consent-csrf/1." + + actor.accountId + + "." + + actor.sessionId, + ) + .digest(); + const router = express.Router({ strict: true, caseSensitive: true }); + const rates = new Map(); + router.use((req, res, next) => { + res.setHeader("Cache-Control", "no-store"); + res.setHeader("Referrer-Policy", "no-referrer"); + res.setHeader("X-Content-Type-Options", "nosniff"); + res.removeHeader("Access-Control-Allow-Origin"); + if (!["/csrf", "/preview", "/confirm", ...(callbacks ? ["/handoff"] : [])].includes(req.url)) + return reject(res, 404, "not-found"); + if (req.method !== (req.url === "/csrf" ? "GET" : "POST")) + return reject(res, 405, "invalid-request"); + if ( + req.url === "/csrf" && + (req.headers["transfer-encoding"] !== undefined || + (req.headers["content-length"] !== undefined && + req.headers["content-length"] !== "0")) + ) + return reject(res, 400, "invalid-request"); + if ( + !req.secure || + req.headers.authorization !== undefined || + req.headers["content-encoding"] !== undefined + ) + return reject(res, 403, "forbidden"); + for (const name of [ + "origin", + "content-type", + "x-review-csrf", + "sec-fetch-site", + ]) { + if ( + req.rawHeaders.filter((v, i) => i % 2 === 0 && v.toLowerCase() === name) + .length > 1 + ) + return reject(res, 403, "forbidden"); + } + if ( + (req.headers.origin !== undefined && req.headers.origin !== origin) || + (req.method === "POST" && req.headers.origin !== origin) || + (req.headers["sec-fetch-site"] !== undefined && + req.headers["sec-fetch-site"] !== "same-origin") + ) + return reject(res, 403, "forbidden"); + if ( + req.method === "POST" && + !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test( + req.headers["content-type"] || "", + ) + ) + return reject(res, 415, "invalid-request"); + // Fail closed if mounted after a body parser that bypassed our byte limit. + if (req.body !== undefined) return reject(res, 503, "unavailable"); + const controller = new AbortController(); + res.locals.consentSignal = controller.signal; + const timer = setTimeout(() => { + controller.abort(); + reject(res, 408, "expired"); + req.destroy(); + }, 15000); + const done = () => { + clearTimeout(timer); + controller.abort(); + }; + res.once("close", done); + res.once("finish", done); + next(); + }); + router.use(async (req, res, next) => { + try { + const actor = await current(req); + if (res.destroyed || res.headersSent || res.locals.consentSignal.aborted) + return; + if (!actor) return reject(res, 401, "unauthorized"); + if (req.method === "POST") { + const supplied = req.headers["x-review-csrf"]; + if ( + typeof supplied !== "string" || + !secret.test(supplied) || + !timingSafeEqual(Buffer.from(supplied, "hex"), csrf(actor)) + ) + return reject(res, 403, "forbidden"); + } + const now = Date.now(); + for (const [id, rate] of rates) if (rate.until <= now) rates.delete(id); + const rate = rates.get(actor.accountId) || { + until: now + 60000, + count: 0, + }; + if ( + rate.count >= 60 || + (!rates.has(actor.accountId) && rates.size >= 1024) + ) { + res.setHeader("Retry-After", "60"); + return reject(res, 429, "rate-limited"); + } + rate.count++; + rates.set(actor.accountId, rate); + res.locals.consentActor = actor; + next(); + } catch { + reject(res, 503, "unavailable"); + } + }); + router.get("/csrf", async (req, res) => { + try { + const fresh = await current(req); + if (!fresh || fresh.accountId !== res.locals.consentActor.accountId) + return reject(res, 401, "unauthorized"); + // Derive without saving the session: a late CSRF response must never + // resurrect a session that a concurrent logout has destroyed. + res.json({ csrf: csrf(fresh).toString("hex"), handoffEnabled: !!callbacks }); + } catch { + reject(res, 503, "unavailable"); + } + }); + router.use(express.json({ limit: 12288, strict: true, inflate: false })); + // Uploads can outlive the session checked before body parsing. Revalidate + // before any provider call or consent write, not only before the response. + router.use(async (req, res, next) => { + try { + const actor = res.locals.consentActor as Context; + const fresh = await current(req); + if (res.destroyed || res.headersSent || res.locals.consentSignal.aborted) + return; + if ( + !fresh || + fresh.accountId !== actor.accountId || + fresh.sessionId !== actor.sessionId + ) + return reject(res, 401, "unauthorized"); + next(); + } catch { + reject(res, 503, "unavailable"); + } + }); + router.post(["/preview", "/confirm", "/handoff"], async (req, res) => { + const actor = res.locals.consentActor as Context; + try { + let result: unknown; + if (req.url === "/handoff") { + if (!callbacks || !fields(req.body, ["ticket"]) || typeof req.body.ticket !== "string" || !req.body.ticket || req.body.ticket.length > 8192) return reject(res, 400, "invalid-request"); + const result = await backend.handoff(actor, req.body.ticket, callbacks.resolve); + if (res.destroyed || res.headersSent) return; + const fresh = await current(req); + if (!fresh || fresh.accountId !== actor.accountId || fresh.sessionId !== actor.sessionId) return reject(res, 401, "unauthorized"); + res.json({ completion: { contract: result.completion.contract, clientId: result.completion.clientId, intentId: result.completion.intentId, code: result.completion.code, expiresAt: result.completion.expiresAt }, callbackUrl: result.callbackUrl }); + } else if (req.url === "/preview") { + if ( + !fields(req.body, ["repositoryId", "intent"]) || + typeof req.body.repositoryId !== "string" || + !/^[A-Za-z0-9_-]{3,128}$/.test(req.body.repositoryId) || + !fields(req.body.intent, [ + "contract", + "clientId", + "intentId", + "token", + "requestId", + ]) + ) + return reject(res, 422, "invalid-request"); + const input = req.body.intent; + if ( + input.contract !== "4open.artifacts/1" || + ![input.clientId, input.intentId, input.requestId].every( + (v) => typeof v === "string" && opaque.test(v), + ) || + typeof input.token !== "string" || + !secret.test(input.token) + ) + return reject(res, 422, "invalid-request"); + const preview = await backend.preview( + actor, + req.body.repositoryId, + { + contract: input.contract, + clientId: input.clientId as string, + intentId: input.intentId as string, + requestId: input.requestId as string, + token: input.token, + }, + res.locals.consentSignal, + ); + result = { + ticket: preview.ticket, + repositoryName: preview.repositoryName, + policy: { + version: preview.policy.version, + access: preview.policy.access, + retainUntil: preview.policy.retainUntil, + }, + expiresAt: preview.expiresAt, + }; + } else { + if ( + !fields(req.body, [ + "ticket", + "requestId", + "acceptAccess", + "acceptRetention", + ]) || + typeof req.body.ticket !== "string" || + req.body.ticket.length > 8192 || + typeof req.body.requestId !== "string" || + !opaque.test(req.body.requestId) || + req.body.acceptAccess !== true || + req.body.acceptRetention !== true + ) + return reject(res, 422, "invalid-request"); + const saved = await backend.confirm(actor, req.body.ticket, { + requestId: req.body.requestId, + acceptAccess: true, + acceptRetention: true, + }); + result = { + requestId: saved.requestId, + policy: { + version: saved.policy.version, + access: saved.policy.access, + retainUntil: saved.policy.retainUntil, + }, + confirmedAt: saved.confirmedAt, + }; + } + const fresh = await current(req); + if ( + !fresh || + fresh.accountId !== actor.accountId || + fresh.sessionId !== actor.sessionId + ) + return reject(res, 401, "unauthorized"); + if (!res.headersSent && !res.destroyed) res.json(result); + } catch (error) { + const kind = (error as { kind?: string })?.kind; + const status = + kind === "invalid" || kind === "protocol" + ? 422 + : kind === "forbidden" || kind === "rejected" + ? 403 + : kind === "expired" + ? 410 + : kind === "conflict" + ? 409 + : 503; + reject( + res, + status, + status === 503 + ? "unavailable" + : status === 422 + ? "invalid-request" + : kind!, + ); + } + }); + router.use(((error, _req, res, _next) => + reject( + res, + error?.status === 413 ? 413 : 400, + "invalid-request", + )) as express.ErrorRequestHandler); + return router; +} diff --git a/src/server/service/review-intent-client.ts b/src/server/service/review-intent-client.ts new file mode 100644 index 0000000..17dcd5f --- /dev/null +++ b/src/server/service/review-intent-client.ts @@ -0,0 +1,357 @@ +import { decodeReviewJSON } from "./review-json"; +import * as https from "https"; +import { ClientRequest } from "http"; + +const contract = "4open.artifacts/1", + limit = 65536; +const opaque = /^[a-f0-9]{32}$/, + secret = /^[a-f0-9]{64}$/; +export type IntentClientFailure = + | "invalid" + | "busy" + | "unavailable" + | "rejected" + | "conflict" + | "expired" + | "not-found" + | "protocol"; +export class IntentClientError extends Error { + constructor(public readonly kind: IntentClientFailure) { + super("Review intent consumption " + kind); + this.name = "IntentClientError"; + } +} +export interface ReviewIntentClientConfig { + origin: string; + clientId: string; + keyId: string; + token: string; + notBefore: string; + notAfter: string; +} +export interface IntentConsumption { + contract: string; + clientId: string; + intentId: string; + token: string; + requestId: string; +} +export type ConsumedReviewIntent = Readonly<{ + contract: string; + clientId: string; + intentId: string; + submissionRef: string; + callbackId: string; + entitlementId: string; + expiresAt: string; + policy: Readonly<{ + version: number; + access: "anonymous-link" | "restricted-review"; + retainUntil: string; + }>; +}>; +function fail(kind: IntentClientFailure): never { + throw new IntentClientError(kind); +} +function object(value: unknown, fields: string[]): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) + fail("protocol"); + const out = value as Record; + if ( + Object.keys(out).length !== fields.length || + fields.some((k) => !Object.prototype.hasOwnProperty.call(out, k)) + ) + fail("protocol"); + return out; +} +function instant(value: unknown): number { + if ( + typeof value !== "string" || + !/^2[01][0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/.test(value) + ) + fail("protocol"); + const time = Date.parse(value); + if ( + !Number.isFinite(time) || + new Date(time).toISOString() !== value.replace("Z", ".000Z") + ) + fail("protocol"); + return time; +} +function id(value: unknown): value is string { + return typeof value === "string" && opaque.test(value); +} + +function decode(bytes: Buffer): unknown { + try { + return decodeReviewJSON(bytes); + } catch { + return fail("protocol"); + } +} + +function intent( + bytes: Buffer, + clientId: string, + intentId: string, +): ConsumedReviewIntent { + const p = object(decode(bytes), [ + "contract", + "clientId", + "intentId", + "submissionRef", + "callbackId", + "policy", + "expiresAt", + "entitlementId", + ]); + const policy = object(p.policy, ["version", "access", "retainUntil"]), + now = Date.now(); + if ( + p.contract !== contract || + p.clientId !== clientId || + p.intentId !== intentId || + !id(p.submissionRef) || + !id(p.callbackId) || + !id(p.entitlementId) + ) + fail("protocol"); + if ( + !Number.isSafeInteger(policy.version) || + (policy.version as number) < 1 || + (policy.access !== "anonymous-link" && + policy.access !== "restricted-review") || + instant(policy.retainUntil) <= now + ) + fail("protocol"); + const expires = instant(p.expiresAt); + if (expires <= now || expires > now + 600000) fail("protocol"); + return Object.freeze({ + contract, + clientId, + intentId, + submissionRef: p.submissionRef, + callbackId: p.callbackId, + entitlementId: p.entitlementId, + expiresAt: p.expiresAt as string, + policy: Object.freeze({ + version: policy.version as number, + access: policy.access as "anonymous-link" | "restricted-review", + retainUntil: policy.retainUntil as string, + }), + }); +} + +/** Trusted server configuration only. One attempt per call; the caller retains + * its request ID for an explicit retry. No browser callback or owner authority + * is established by consuming an intent. No credentials are exposed on the + * returned object, through serialization, or in transport errors. */ +export function createReviewIntentConsumer(config: ReviewIntentClientConfig) { + let origin: string, + clientId: string, + keyId: string, + token: string, + from: number, + until: number; + try { + const c = object(config, [ + "origin", + "clientId", + "keyId", + "token", + "notBefore", + "notAfter", + ]); + if ( + typeof c.origin !== "string" || + c.origin.length > 2048 || + !id(c.clientId) || + typeof c.keyId !== "string" || + !/^[A-Za-z0-9_-]{1,64}$/.test(c.keyId) || + typeof c.token !== "string" || + !secret.test(c.token) + ) + fail("invalid"); + const url = new URL(c.origin); + if ( + url.protocol !== "https:" || + url.origin !== c.origin || + url.username || + url.password + ) + fail("invalid"); + origin = c.origin; + clientId = c.clientId; + keyId = c.keyId; + token = c.token; + from = instant(c.notBefore); + until = instant(c.notAfter); + if (until <= from || Date.now() < from || Date.now() >= until) + fail("invalid"); + } catch { + return fail("invalid"); + } + let active = 0; + return Object.freeze({ + consume( + input: IntentConsumption, + signal?: AbortSignal, + ): Promise { + let body: Buffer, expectedIntent: string; + try { + const p = object(input, [ + "contract", + "clientId", + "intentId", + "token", + "requestId", + ]); + if ( + p.contract !== contract || + p.clientId !== clientId || + !id(p.intentId) || + !id(p.requestId) || + typeof p.token !== "string" || + !secret.test(p.token) || + Date.now() < from || + Date.now() >= until + ) + fail("invalid"); + expectedIntent = p.intentId; + body = Buffer.from( + JSON.stringify({ + contract, + clientId, + intentId: expectedIntent, + token: p.token, + requestId: p.requestId, + }), + ); + } catch { + return Promise.reject(new IntentClientError("invalid")); + } + if (signal?.aborted) + return Promise.reject(new IntentClientError("unavailable")); + if (active >= 4) return Promise.reject(new IntentClientError("busy")); + active++; + return new Promise((resolve, reject) => { + let request: ClientRequest | undefined, + settled = false; + const abort = () => finish("unavailable"); + const timer = setTimeout(abort, 10000); + function finish( + error?: IntentClientFailure, + value?: ConsumedReviewIntent, + ) { + if (settled) return; + settled = true; + clearTimeout(timer); + signal?.removeEventListener("abort", abort); + active--; + if (error) { + request?.destroy(); + reject(new IntentClientError(error)); + } else resolve(value!); + } + signal?.addEventListener("abort", abort, { once: true }); + try { + request = https.request( + origin + "/service/v1/artifact-intents/consume", + { + method: "POST", + agent: false, + rejectUnauthorized: true, + minVersion: "TLSv1.2", + maxHeaderSize: 16384, + headers: { + Authorization: "Bearer " + token, + "X-4open-Artifact-Client-Id": clientId, + "X-4open-Artifact-Service-Key-Id": keyId, + "Content-Type": "application/json", + Accept: "application/json", + "Accept-Encoding": "identity", + "Content-Length": body.length, + }, + }, + (response) => { + response.once("aborted", abort); + response.once("error", abort); + const status = response.statusCode || 0; + if (status !== 200) { + const kind: IntentClientFailure = + status === 404 + ? "not-found" + : status === 409 + ? "conflict" + : status === 410 + ? "expired" + : [400, 401, 403, 422].includes(status) + ? "rejected" + : [408, 429, 500, 502, 503, 504].includes(status) + ? "unavailable" + : "protocol"; + finish(kind); + return; + } + const types = response.rawHeaders.filter( + (_v, i) => + i % 2 === 0 && + response.rawHeaders[i].toLowerCase() === "content-type", + ).length; + const length = response.headers["content-length"]; + if ( + types !== 1 || + !/^application\/json(?:\s*;\s*charset=(?:utf-8|"utf-8"))?\s*$/i.test( + response.headers["content-type"] || "", + ) || + response.headers["content-encoding"] !== undefined || + response.headers["set-cookie"] !== undefined || + (length !== undefined && + (!/^[0-9]+$/.test(length) || Number(length) > limit)) + ) { + finish("protocol"); + return; + } + let size = 0; + const chunks: Buffer[] = []; + response.on("data", (chunk: Buffer) => { + size += chunk.length; + if (size > limit) finish("protocol"); + else if (!settled) chunks.push(chunk); + }); + response.once("end", () => { + if (settled) return; + if ( + signal?.aborted || + Date.now() < from || + Date.now() >= until + ) { + finish("unavailable"); + return; + } + try { + finish( + undefined, + intent( + Buffer.concat(chunks, size), + clientId, + expectedIntent, + ), + ); + } catch { + finish("protocol"); + } + }); + }, + ); + request.once("error", abort); + request.setTimeout(5000, abort); + if (signal?.aborted) abort(); + if (!settled) request.end(body); + } catch { + finish("unavailable"); + } + }); + }, + }); +} diff --git a/src/server/service/review-json.ts b/src/server/service/review-json.ts new file mode 100644 index 0000000..abc6cd0 --- /dev/null +++ b/src/server/service/review-json.ts @@ -0,0 +1,68 @@ +function invalid(): never { + throw new Error("Invalid review JSON"); +} + +// JSON.parse checks syntax; this bounded walk additionally rejects duplicate +// decoded keys, including escaped spellings, before typed field validation. +export function decodeReviewJSON(bytes: Buffer): unknown { + if (bytes.length > 65536) invalid(); + const text = bytes.toString("utf8"); + if (!Buffer.from(text, "utf8").equals(bytes)) invalid(); + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + invalid(); + } + let at = 0; + const space = () => { + while (/\s/.test(text[at] || "") && at < text.length) at++; + }; + const string = (): string => { + const start = at++; + while (at < text.length) { + if (text[at] === "\\") { + at += 2; + continue; + } + if (text[at++] === '"') return JSON.parse(text.slice(start, at)); + } + return invalid(); + }; + const walk = (depth: number): void => { + if (depth > 8) invalid(); + space(); + if (text[at] === '"') { + string(); + return; + } + if (text[at] === "{" || text[at] === "[") { + const isObject = text[at++] === "{", + end = isObject ? "}" : "]", + keys = new Set(); + space(); + if (text[at] === end) { + at++; + return; + } + for (;;) { + space(); + if (isObject) { + const key = string(); + if (keys.has(key)) invalid(); + keys.add(key); + space(); + at++; // colon; syntax already checked + } + walk(depth + 1); + space(); + if (text[at++] === end) return; + } + } + while (at < text.length && !/[\s,}\]]/.test(text[at])) at++; + }; + walk(0); + space(); + if (at !== text.length) invalid(); + return parsed; +} diff --git a/src/server/service/review-owner-consent.ts b/src/server/service/review-owner-consent.ts new file mode 100644 index 0000000..db4857f --- /dev/null +++ b/src/server/service/review-owner-consent.ts @@ -0,0 +1,602 @@ +import { createHash, createHmac, timingSafeEqual, randomBytes } from "crypto"; +import { Connection, ClientSession, Types } from "mongoose"; +import { + ConsumedReviewIntent, + IntentConsumption, + createReviewIntentConsumer, +} from "./review-intent-client"; + +type Principal = { accountId: string; sessionId: string }; +type Authority = { + accountId: string; + repositoryId: string; + repositoryName: string; + role: "owner" | "coauthor"; +}; +type Quote = { + version: 1; + accountId: string; + sessionHash: string; + repositoryId: string; + intent: ConsumedReviewIntent; +}; +export type ConsentReceipt = Readonly<{ + clientId: string; + intentId: string; + accountId: string; + repositoryId: string; + role: "owner" | "coauthor"; + policy: ConsumedReviewIntent["policy"]; + requestId: string; + confirmedAt: string; +}>; +type SavedConsent = { + _id: string; + accountId: string; + repositoryId: string; + requestId: string; + ticketHash: string; + receipt: ConsentReceipt; + intent: ConsumedReviewIntent; +}; +type BindingReceipt = { + contract: "4open.artifacts/1"; + clientId: string; + intentId: string; + bindingId: string; + submissionRef: string; + policy: ConsumedReviewIntent["policy"]; + entitlementId: string; +}; +type SavedCompletion = { + _id: string; + accountId: string; + repositoryId: string; + ticketHash: string; + intent: ConsumedReviewIntent; + nonce: string; + codeHash: string; + expiresAt: string; + exchange?: { requestId: string; receipt: BindingReceipt }; +}; +export class ReviewConsentError extends Error { + constructor( + public readonly kind: + "invalid" | "forbidden" | "expired" | "conflict" | "unavailable", + ) { + super("Review owner consent " + kind); + this.name = "ReviewConsentError"; + } +} +const deny = (kind: ReviewConsentError["kind"]): never => { + throw new ReviewConsentError(kind); +}; +const hash = (value: string) => + createHash("sha256").update(value).digest("hex"); +const objectId = (value: unknown): value is string => + typeof value === "string" && /^[a-f0-9]{24}$/.test(value); +function principal(value: Principal): { + accountId: string; + sessionHash: string; +} { + if ( + !value || + !objectId(value.accountId) || + typeof value.sessionId !== "string" || + !/^[A-Za-z0-9_-]{24,256}$/.test(value.sessionId) + ) + deny("invalid"); + return { accountId: value.accountId, sessionHash: hash(value.sessionId) }; +} + +/** Server-side consent storage. Principals must come from current authenticated + * browser sessions, never request JSON. A future HTTP adapter must apply its + * own same-origin/CSRF checks. No route or provider is enabled by this factory. */ +export function createReviewOwnerConsent( + connection: Connection, + consumer: ReturnType, + signingKey: Buffer, +) { + if (!Buffer.isBuffer(signingKey) || signingKey.length !== 32) deny("invalid"); + const key = Buffer.from(signingKey); + let activeTransactions = 0; + let activeReads = 0; + const signature = (body: string) => + createHmac("sha256", key) + .update("4open.review-consent/1." + body) + .digest(); + function decode(ticket: string, who: ReturnType): Quote { + if ( + typeof ticket !== "string" || + ticket.length > 8192 || + !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/.test(ticket) + ) + deny("invalid"); + const [body, encoded] = ticket.split("."); + const supplied = Buffer.from(encoded, "base64url"); + if ( + supplied.length !== 32 || + supplied.toString("base64url") !== encoded || + !timingSafeEqual(supplied, signature(body)) + ) + deny("invalid"); + let quote: Quote; + try { + quote = JSON.parse(Buffer.from(body, "base64url").toString("utf8")); + } catch { + return deny("invalid"); + } + if ( + quote.version !== 1 || + quote.accountId !== who.accountId || + quote.sessionHash !== who.sessionHash || + !objectId(quote.repositoryId) + ) + deny("forbidden"); + if ( + Date.parse(quote.intent.expiresAt) <= Date.now() || + Date.parse(quote.intent.policy.retainUntil) <= Date.now() + ) + deny("expired"); + return quote; + } + async function transaction( + who: ReturnType, + repository: { repoId: string } | { _id: Types.ObjectId }, + work: (authority: Authority, session: ClientSession) => Promise, + ): Promise { + if (connection.readyState !== 1 || activeTransactions >= 4) + return deny("unavailable"); + activeTransactions++; + const session = await connection.startSession().catch(() => { + activeTransactions--; + return deny("unavailable"); + }); + try { + session.startTransaction({ + readConcern: { level: "snapshot" }, + writeConcern: { w: "majority", wtimeoutMS: 3000 }, + maxCommitTimeMS: 5000, + }); + // Touch the authority documents in the same transaction as the receipt. + // Concurrent disablement, ownership changes and coauthor removal then + // conflict with this transaction instead of producing a stale consent. + const user = ( + await connection.db.collection("users").findOneAndUpdate( + { + _id: new Types.ObjectId(who.accountId), + $or: [{ status: "active" }, { status: { $exists: false } }], + }, + { $inc: { reviewConsentRevision: 1 } }, + { + session, + returnDocument: "after", + maxTimeMS: 3000, + projection: { externalIDs: 1 }, + }, + ) + ).value; + if (!user) return deny("forbidden"); + const githubId = user.externalIDs?.github; + const roles: Record[] = [ + { owner: new Types.ObjectId(who.accountId) }, + ]; + if (typeof githubId === "string" && /^[1-9][0-9]{0,19}$/.test(githubId)) + roles.push({ "coauthors.githubId": githubId }); + const repo = ( + await connection.db + .collection("anonymizedrepositories") + .findOneAndUpdate( + { ...repository, status: "ready", $or: roles }, + { $inc: { reviewConsentRevision: 1 } }, + { + session, + returnDocument: "after", + maxTimeMS: 3000, + projection: { repoId: 1, owner: 1, options: 1 }, + }, + ) + ).value; + if (!repo) return deny("forbidden"); + if ( + repo.options?.expirationMode !== "never" && + repo.options?.expirationDate && + (!Number.isFinite(new Date(repo.options.expirationDate).getTime()) || + new Date(repo.options.expirationDate).getTime() <= Date.now()) + ) + return deny("expired"); + const authority: Authority = { + accountId: who.accountId, + repositoryId: String(repo._id), + repositoryName: repo.repoId, + role: String(repo.owner) === who.accountId ? "owner" : "coauthor", + }; + const result = await work(authority, session); + await session.commitTransaction(); + return result; + } catch (error) { + if (session.inTransaction()) + await session.abortTransaction().catch(() => undefined); + if (error instanceof ReviewConsentError) throw error; + return deny("unavailable"); + } finally { + activeTransactions--; + await session.endSession().catch(() => undefined); + } + } + const completionCode = (id: string, nonce: string) => + createHmac("sha256", key) + .update("4open.review-completion/1." + id + "." + nonce) + .digest("hex"); + const api = { + async handoff(actor: Principal, ticket: string, resolveCallback: (clientId: string, callbackId: string) => string | undefined) { + const quote = decode(ticket, principal(actor)); + const callbackUrl = resolveCallback(quote.intent.clientId, quote.intent.callbackId); + if (!callbackUrl) return deny("forbidden"); + const completion = await api.completion(actor, ticket); + return Object.freeze({ completion, callbackUrl }); + }, + // Called only with the current authenticated browser principal. The HTTP + // adapter must retain its session reload, same-origin and CSRF checks. + async completion(actor: Principal, ticket: string) { + const who = principal(actor), + quote = decode(ticket, who); + return transaction( + who, + { _id: new Types.ObjectId(quote.repositoryId) }, + async (authority, session) => { + decode(ticket, who); + const id = quote.intent.clientId + ":" + quote.intent.intentId; + const consent = await connection.db + .collection("review_owner_consents") + .findOne({ _id: id }, { session, maxTimeMS: 3000 }); + if ( + !consent || + consent.ticketHash !== hash(ticket) || + consent.accountId !== who.accountId || + consent.repositoryId !== authority.repositoryId || + !consent.intent + ) + return deny("forbidden"); + const collection = + connection.db.collection("review_completions"); + let saved = await collection.findOne( + { _id: id }, + { session, maxTimeMS: 3000 }, + ); + if ( + saved && + (saved.ticketHash !== consent.ticketHash || + saved.accountId !== who.accountId || + saved.repositoryId !== authority.repositoryId) + ) + return deny("conflict"); + if (!saved) { + const nonce = randomBytes(32).toString("hex"); + saved = { + _id: id, + accountId: who.accountId, + repositoryId: authority.repositoryId, + ticketHash: consent.ticketHash, + intent: consent.intent, + nonce, + codeHash: hash(completionCode(id, nonce)), + expiresAt: new Date( + Math.floor( + Math.min( + Date.now() + 300000, + Date.parse(consent.intent.expiresAt), + Date.parse(consent.intent.policy.retainUntil), + ) / 1000, + ) * 1000, + ) + .toISOString() + .replace(/\.000Z$/, "Z"), + }; + await collection.insertOne(saved, { session, maxTimeMS: 3000 }); + } + if (Date.parse(saved.expiresAt) <= Date.now()) return deny("expired"); + const code = completionCode(id, saved.nonce); + // A key rotation must not silently issue a different code for a saved + // intent. Keep the issuing key available for the short code lifetime. + if (hash(code) !== saved.codeHash) return deny("conflict"); + return Object.freeze({ + contract: "4open.artifacts/1", + clientId: saved.intent.clientId, + intentId: saved.intent.intentId, + code, + expiresAt: saved.expiresAt, + }); + }, + ); + }, + // authenticatedClientId must come from service credential verification, + // never browser identity or the request body's clientId alone. + async exchange( + authenticatedClientId: string, + input: { + contract: string; + clientId: string; + intentId: string; + code: string; + requestId: string; + }, + ): Promise { + if ( + !input || + Object.keys(input).sort().join(",") !== + "clientId,code,contract,intentId,requestId" || + input.contract !== "4open.artifacts/1" || + ![input.clientId, input.intentId, input.requestId].every( + (value) => typeof value === "string" && /^[a-f0-9]{32}$/.test(value), + ) || + typeof input.code !== "string" || + !/^[a-f0-9]{64}$/.test(input.code) + ) + return deny("invalid"); + const command = { ...input }; + if (authenticatedClientId !== command.clientId) return deny("forbidden"); + if (connection.readyState !== 1 || activeReads >= 4) + return deny("unavailable"); + const id = command.clientId + ":" + command.intentId; + const collection = + connection.db.collection("review_completions"); + const read = async (session?: ClientSession) => { + const saved = await collection.findOne( + { _id: id }, + { session, maxTimeMS: 3000, ...(session ? {} : { readConcern: { level: "majority" as const } }) }, + ); + if (!saved) return deny("forbidden"); + if ( + !timingSafeEqual( + Buffer.from(saved.codeHash, "hex"), + Buffer.from(hash(command.code), "hex"), + ) + ) + return deny( + saved.exchange?.requestId === command.requestId + ? "conflict" + : "forbidden", + ); + return saved; + }; + activeReads++; + try { + const before = await read(); + if (before.exchange) { + if (before.exchange.requestId !== command.requestId) + return deny("conflict"); + return Object.freeze(before.exchange.receipt); + } + return await transaction( + { accountId: before.accountId, sessionHash: "" }, + { _id: new Types.ObjectId(before.repositoryId) }, + async (_authority, session) => { + const saved = await read(session); + if (saved.exchange) { + if (saved.exchange.requestId !== command.requestId) + return deny("conflict"); + return Object.freeze(saved.exchange.receipt); + } + if ( + Date.parse(saved.expiresAt) <= Date.now() || + Date.parse(saved.intent.policy.retainUntil) <= Date.now() + ) + return deny("expired"); + // The unique request document detects a reused key on another intent. + const requests = connection.db.collection<{ + _id: string; + intentId: string; + }>("review_completion_requests"); + const requestKey = command.clientId + ":" + command.requestId; + if ( + await requests.findOne( + { _id: requestKey }, + { session, maxTimeMS: 3000 }, + ) + ) + return deny("conflict"); + const receipt: BindingReceipt = { + contract: "4open.artifacts/1", + clientId: saved.intent.clientId, + intentId: saved.intent.intentId, + bindingId: randomBytes(16).toString("hex"), + submissionRef: saved.intent.submissionRef, + policy: { ...saved.intent.policy }, + entitlementId: saved.intent.entitlementId, + }; + await connection.db + .collection<{ + _id: string; + clientId: string; + intentId: string; + accountId: string; + repositoryId: string; + state: "pending"; + revision: number; + receipt: BindingReceipt; + }>("review_artifact_bindings") + .insertOne( + { + _id: receipt.bindingId, + clientId: command.clientId, + intentId: command.intentId, + accountId: saved.accountId, + repositoryId: saved.repositoryId, + state: "pending", + revision: 1, + receipt, + }, + { session, maxTimeMS: 3000 }, + ); + await requests.insertOne( + { _id: requestKey, intentId: command.intentId }, + { session, maxTimeMS: 3000 }, + ); + await collection.updateOne( + { _id: id }, + { $set: { exchange: { requestId: command.requestId, receipt } } }, + { session, maxTimeMS: 3000 }, + ); + return Object.freeze(receipt); + }, + ); + } catch (error) { + if (error instanceof ReviewConsentError) throw error; + return deny("unavailable"); + } finally { + activeReads--; + } + }, + async receipt( + authenticatedClientId: string, + intentId: string, + ): Promise { + if ( + ![authenticatedClientId, intentId].every( + (value) => typeof value === "string" && /^[a-f0-9]{32}$/.test(value), + ) + ) + return deny("invalid"); + if (connection.readyState !== 1 || activeReads >= 4) + return deny("unavailable"); + activeReads++; + try { + const saved = await connection.db + .collection("review_completions") + .findOne( + { _id: authenticatedClientId + ":" + intentId }, + { maxTimeMS: 3000, readConcern: { level: "majority" }, projection: { exchange: 1 } }, + ); + if (!saved?.exchange) return deny("forbidden"); + return Object.freeze(saved.exchange.receipt); + } catch (error) { + if (error instanceof ReviewConsentError) throw error; + return deny("unavailable"); + } finally { + activeReads--; + } + }, + async preview( + actor: Principal, + repositoryName: string, + input: IntentConsumption, + signal?: AbortSignal, + ) { + const who = principal(actor); + const command = { ...input }; + if ( + typeof repositoryName !== "string" || + !/^[A-Za-z0-9_-]{3,128}$/.test(repositoryName) + ) + return deny("invalid"); + const before = await transaction( + who, + { repoId: repositoryName }, + async (authority) => authority, + ); + // The provider call holds no database transaction or authority lock. + const intent = await consumer.consume(command, signal); + const current = await transaction( + who, + { _id: new Types.ObjectId(before.repositoryId) }, + async (authority) => authority, + ); + if ( + signal?.aborted || + Date.parse(intent.expiresAt) <= Date.now() || + Date.parse(intent.policy.retainUntil) <= Date.now() + ) + return deny("expired"); + const quote: Quote = { + version: 1, + accountId: who.accountId, + sessionHash: who.sessionHash, + repositoryId: current.repositoryId, + intent, + }; + const body = Buffer.from(JSON.stringify(quote)).toString("base64url"); + return Object.freeze({ + ticket: body + "." + signature(body).toString("base64url"), + repositoryName: current.repositoryName, + policy: intent.policy, + expiresAt: intent.expiresAt, + }); + }, + async confirm( + actor: Principal, + ticket: string, + input: { + requestId: string; + acceptAccess: boolean; + acceptRetention: boolean; + }, + ): Promise { + const who = principal(actor); + if ( + !input || + Object.keys(input).sort().join(",") !== + "acceptAccess,acceptRetention,requestId" || + input.acceptAccess !== true || + input.acceptRetention !== true || + typeof input.requestId !== "string" || + !/^[a-f0-9]{32}$/.test(input.requestId) + ) + return deny("invalid"); + const quote = decode(ticket, who), + requestId = input.requestId; + return transaction( + who, + { _id: new Types.ObjectId(quote.repositoryId) }, + async (authority, session) => { + decode(ticket, who); // Recheck expiry after waiting for authority locks. + const collection = connection.db.collection( + "review_owner_consents", + ); + const id = quote.intent.clientId + ":" + quote.intent.intentId; + const ticketHash = hash(ticket); + // A string _id binds each intent to at most one accepted repository and + // session. No raw session ID, intent token or service key is persisted. + const existing = await collection.findOne( + { _id: id }, + { session, maxTimeMS: 3000 }, + ); + if (existing) { + if ( + existing.ticketHash !== ticketHash || + existing.requestId !== requestId || + existing.accountId !== who.accountId || + existing.repositoryId !== authority.repositoryId + ) + return deny("conflict"); + return Object.freeze(existing.receipt as ConsentReceipt); + } + const receipt: ConsentReceipt = Object.freeze({ + clientId: quote.intent.clientId, + intentId: quote.intent.intentId, + accountId: who.accountId, + repositoryId: authority.repositoryId, + role: authority.role, + policy: quote.intent.policy, + requestId, + confirmedAt: new Date().toISOString(), + }); + await collection.insertOne( + { + _id: id, + accountId: who.accountId, + repositoryId: authority.repositoryId, + requestId, + ticketHash, + receipt, + intent: quote.intent, + }, + { session, maxTimeMS: 3000 }, + ); + return receipt; + }, + ); + }, + }; + return Object.freeze(api); +} diff --git a/src/server/service/review-service-auth.ts b/src/server/service/review-service-auth.ts new file mode 100644 index 0000000..d6bba33 --- /dev/null +++ b/src/server/service/review-service-auth.ts @@ -0,0 +1,160 @@ +import { createHash, timingSafeEqual } from "crypto"; +import { Request } from "express"; +import { decodeReviewJSON } from "./review-json"; + +type Scope = "completion.exchange" | "receipt.read"; +type Key = { + clientId: string; + digest: Buffer; + from: number; + until: number; + scopes: Scope[]; +}; +const opaque = /^[a-f0-9]{32}$/; +const secret = /^[a-f0-9]{64}$/; +const invalid = (): never => { + throw new Error("Invalid review exchange credentials"); +}; +function fields(value: unknown, names: string[]): Record { + if ( + !value || + typeof value !== "object" || + Array.isArray(value) || + Object.keys(value).sort().join(",") !== names.sort().join(",") + ) + return invalid(); + return value as Record; +} +function date(value: unknown): number { + if ( + typeof value !== "string" || + !/^20[0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/.test(value) + ) + return invalid(); + const number = Date.parse(value); + if ( + !Number.isFinite(number) || + new Date(number).toISOString() !== value.replace("Z", ".000Z") + ) + return invalid(); + return number; +} +export function singleReviewHeader( + req: Request, + name: string, +): string | undefined { + let count = 0; + for (let i = 0; i < req.rawHeaders.length; i += 2) + if (req.rawHeaders[i].toLowerCase() === name) count++; + const value = req.headers[name]; + return count === 1 && typeof value === "string" ? value : undefined; +} + +/** Separate scoped registry: the capabilities-only registry is not accepted. */ +export function createReviewServiceAuth( + raw: string, + now: () => number = Date.now, +) { + let parsed: unknown; + try { + parsed = decodeReviewJSON(Buffer.from(raw, "utf8")); + } catch { + return invalid(); + } + const config = fields(parsed, ["version", "keys"]); + if ( + config.version !== 1 || + !Array.isArray(config.keys) || + config.keys.length < 1 || + config.keys.length > 64 + ) + return invalid(); + const keys = new Map(), + digests = new Set(), + clients = new Map(); + for (const entry of config.keys) { + const k = fields(entry, [ + "clientId", + "keyId", + "tokenSHA256", + "notBefore", + "notAfter", + "scopes", + ]); + if ( + typeof k.clientId !== "string" || + !opaque.test(k.clientId) || + typeof k.keyId !== "string" || + !/^[A-Za-z0-9_-]{1,64}$/.test(k.keyId) || + typeof k.tokenSHA256 !== "string" || + !secret.test(k.tokenSHA256) || + !Array.isArray(k.scopes) || + k.scopes.length < 1 || + k.scopes.length > 2 || + k.scopes.some( + (scope) => scope !== "completion.exchange" && scope !== "receipt.read", + ) || + new Set(k.scopes).size !== k.scopes.length + ) + return invalid(); + const from = date(k.notBefore), + until = date(k.notAfter), + id = k.clientId + ":" + k.keyId; + const count = (clients.get(k.clientId) || 0) + 1; + if ( + until <= from || + keys.has(id) || + digests.has(k.tokenSHA256) || + count > 16 + ) + return invalid(); + clients.set(k.clientId, count); + if (clients.size > 32) return invalid(); + digests.add(k.tokenSHA256); + keys.set(id, { + clientId: k.clientId, + digest: Buffer.from(k.tokenSHA256, "hex"), + from, + until, + scopes: [...k.scopes] as Scope[], + }); + } + const dummy = Buffer.alloc(32); + return Object.freeze({ + authenticate( + req: Request, + scope: Scope, + ): { clientId: string; valid: () => boolean } | undefined { + const client = singleReviewHeader(req, "x-4open-artifact-client-id"), + keyId = singleReviewHeader(req, "x-4open-artifact-service-key-id"), + authorization = singleReviewHeader(req, "authorization"); + if ( + !client || + !opaque.test(client) || + !keyId || + !/^[A-Za-z0-9_-]{1,64}$/.test(keyId) || + !authorization || + !/^Bearer [a-f0-9]{64}$/i.test(authorization) || + !secret.test(authorization.slice(7)) + ) + return undefined; + const key = keys.get(client + ":" + keyId); + const matches = timingSafeEqual( + createHash("sha256").update(authorization.slice(7), "ascii").digest(), + key?.digest || dummy, + ); + const valid = () => { + const time = now(); + return ( + !!key && + matches && + key.scopes.includes(scope) && + Number.isFinite(time) && + time >= key.from && + time < key.until + ); + }; + return valid() ? Object.freeze({ clientId: client, valid }) : undefined; + }, + }); +} diff --git a/src/server/website-rate-limit.ts b/src/server/website-rate-limit.ts new file mode 100644 index 0000000..19203f3 --- /dev/null +++ b/src/server/website-rate-limit.ts @@ -0,0 +1,20 @@ +import rateLimit, { Store } from "express-rate-limit"; +import config from "../config"; +import { requestRateLimitKey } from "./rate-limit-key"; +import { sendRequestRateLimit } from "./request-rate-limit"; + +/** A page's images and scripts must not consume its visitor's API budget. */ +export function createWebsiteRateLimiter(store?: Store) { + if (!Number.isSafeInteger(config.WEBSITE_RATE_LIMIT) || config.WEBSITE_RATE_LIMIT <= 0) { + throw new Error("WEBSITE_RATE_LIMIT must be a positive integer"); + } + return rateLimit({ + windowMs: 15 * 60_000, + limit: config.WEBSITE_RATE_LIMIT, + keyGenerator: requestRateLimitKey, + standardHeaders: "draft-6", + legacyHeaders: false, + handler: sendRequestRateLimit, + ...(store ? { store } : {}), + }); +} diff --git a/src/streamer/index.ts b/src/streamer/index.ts index 28106b0..8e80c7f 100644 --- a/src/streamer/index.ts +++ b/src/streamer/index.ts @@ -1,3 +1,4 @@ +import { installFatalErrorLogging } from "../core/process-monitoring"; import { config as dotenv } from "dotenv"; dotenv(); @@ -16,6 +17,7 @@ import { startPerformanceMonitoring } from "../core/performance-monitoring"; import { getAnonymizationPoolStats } from "../core/anonymization-pool"; const logger = createLogger("streamer"); +installFatalErrorLogging("streamer"); const app = express(); app.use(monitorRequests("streamer")); diff --git a/src/streamer/route.ts b/src/streamer/route.ts index 68ea4cb..3a64bf0 100644 --- a/src/streamer/route.ts +++ b/src/streamer/route.ts @@ -35,6 +35,7 @@ router.post( getToken: () => token, anonymizerOptions: { ...anonymizerOptions, repoId }, contentOptions, + submodules: req.body.submodules, }, res ); @@ -90,7 +91,7 @@ router.post("/", async (req: express.Request, res: express.Response) => { const content = await source.getFileContentCache( filePath, repoId, - () => ({ sha: fileSha, size: fileSize }) + () => ({ sha: fileSha, size: fileSize, upstream: req.body.upstream }) ); if (res.destroyed || res.writableEnded) { content.destroy(); anonymizer.destroy(); return; } const mime = lookup(filePath); diff --git a/test/admin-queue-jobs.test.js b/test/admin-queue-jobs.test.js new file mode 100644 index 0000000..6df891e --- /dev/null +++ b/test/admin-queue-jobs.test.js @@ -0,0 +1,32 @@ +const { expect } = require("chai"); + +describe("admin queue jobs", function () { + it("returns job payloads, failure reasons and stacks as values, not JSON strings", async function () { + const queues = require("../src/queue"); + const metrics = require("../src/queue/queueMetrics"); + const router = require("../src/server/routes/admin").default; + const handler = router.stack.find(layer => layer.route?.path === "/queues").route.stack.at(-1).handle; + const originals = [queues.downloadQueue, queues.removeQueue, queues.cacheQueue, metrics.queryMetrics]; + const failed = { + data: { repoId: "paper-7F2A" }, failedReason: "branch_not_found", stacktrace: ["Error: branch_not_found\n at fetch"], returnvalue: null, + // BullMQ's asJSON() stringifies these four fields. + asJSON() { return { id: "paper-7F2A", name: "paper-7F2A", data: JSON.stringify(this.data), failedReason: JSON.stringify(this.failedReason), stacktrace: JSON.stringify(this.stacktrace), returnvalue: JSON.stringify(this.returnvalue) }; }, + }; + const queue = { getJobCounts: async () => ({}), getWorkers: async () => [], isPaused: async () => false, + getJobs: async ([state]) => state === "failed" ? [failed] : [] }; + queues.downloadQueue = queue; queues.removeQueue = queue; queues.cacheQueue = queue; + metrics.queryMetrics = async () => []; + try { + let body; + // The remove queue keeps this snapshot apart from other tests' cache entries. + await handler({ query: { queue: "remove" } }, { json: data => { body = data; } }); + const job = body.jobs.find(j => j.id === "paper-7F2A"); + expect(job.data).to.deep.equal({ repoId: "paper-7F2A" }); + expect(job.failedReason).to.equal("branch_not_found"); + expect(job.stacktrace).to.deep.equal(["Error: branch_not_found\n at fetch"]); + expect(job._state).to.equal("failed"); + } finally { + [queues.downloadQueue, queues.removeQueue, queues.cacheQueue, metrics.queryMetrics] = originals; + } + }); +}); diff --git a/test/anonymize-regex-prefix.test.js b/test/anonymize-regex-prefix.test.js new file mode 100644 index 0000000..7aa7fd9 --- /dev/null +++ b/test/anonymize-regex-prefix.test.js @@ -0,0 +1,52 @@ +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const { ContentAnonimizer } = require("../src/core/anonymize-utils"); + +function unfiltered(terms, text) { + const a = new ContentAnonimizer({ terms }); + a.termPresence = null; + for (const term of a.compiledTerms) delete term.requiredPrefix; + return a.anonymize(text); +} + +describe("regex required-prefix searches", function () { + it("keeps RE2 results for assertions, escaped punctuation, optional atoms and quantifiers", function () { + const terms = [String.raw`\b[Aa]uthor\b`, String.raw`\Bauthor\B`, String.raw`author\.example\.org`, + "Research[ _-]?Institute", "a?b?c?d?e?required", "a??bc", "a+b", "a*b", "a{0,2}b", "a{2}b", + String.raw`\(Author\)`, String.raw`\ba?\b`, "[a-z]Name", "[^x]Name", "a|b", "(a)?b", "^Author", "Author$", + String.raw`Author\s+Name`, String.raw`Author\d+`, String.raw`Author\x41`, "Σ?Author", "😀?Author"]; + const text = "Author author xauthorx author.example.org authorXexampleYorg Research-Institute Research Institute " + + "ResearchInstitute required aaaaab ab b aabc abc (Author) aName xName Author Name Author12 AuthorA ΣAuthor 😀Author"; + for (const term of terms) { + const a = new ContentAnonimizer({ terms: [term] }); + expect(a.anonymize(text), term).to.equal(unfiltered([term], text)); + } + }); + + it("preserves cascades, anchors and external boundaries after skipping unmatched prefixes", function () { + const cases = [ + [[String.raw`\bAuthor\b=>Research Institute`, "Research[ _-]?Institute=>Hidden"], "Author"], + [["a?bc=>Hidden"], "xbc abc bc abcx abc"], + [["a+b=>Hidden"], "xaaab aaab aabx ab"], + [[String.raw`\b[Aa]uthor\b=>Hidden`], "éAuthor Authoré Author xAuthor Author"], + [[String.raw`\bAuthor\b=>Hidden`], "😀Author\nAuthor😀"], + [["a?=>X"], "xyz"], + [["a{0}=>X"], "xyz"], + [[String.raw`\bAuthor\b=>First`, "^First=>Second", "Second$=>Last"], "Author"], + ]; + for (const [terms, text] of cases) { + expect(new ContentAnonimizer({ terms }).anonymize(text), JSON.stringify(terms)).to.equal(unfiltered(terms, text)); + } + }); + + it("processes sparse boundary and optional regex rules in a large file within the worker deadline", function () { + this.timeout(20000); + const body = "const value = [1,2,3,4,5]; // ordinary application data\n".repeat(290000); + const input = `Author Research Project\n${body}\nAuthor`; + const terms = [String.raw`\b[Aa]uthor\b=>Hidden`, "Research[ _-]?Institute=>Organization", + ...Array.from({ length: 24 }, (_, i) => String.raw`\bResearcher${i}\b=>Hidden`), + String.raw`research\.example\.org=>Website`]; + const a = new ContentAnonimizer({ terms }); + expect(a.anonymize(input, 10000)).to.equal(`Hidden Research Project\n${body}\nHidden`); + }); +}); diff --git a/test/app-reinstall-recovery.test.js b/test/app-reinstall-recovery.test.js index 55392a9..77f99e4 100644 --- a/test/app-reinstall-recovery.test.js +++ b/test/app-reinstall-recovery.test.js @@ -50,7 +50,7 @@ for (const type of ["repository", "pull-request"]) { expect(writes).to.have.length(0); expect(model.githubAccess.installationId).to.equal(100); }); it("replaces only the binding while preserving the URL, source and snapshot", async function () { - expect(await run()).to.deep.equal({ connection: "github-app" }); + expect(await run()).to.deep.equal({ connection: "github-app", fullName: "owner/private" }); expect(writes).to.have.length(1); expect(writes[0].update).to.deep.equal({ $set: { githubAccess: selected.binding } }); expect(writes[0].filter.githubAccess).to.deep.equal(model.githubAccess); diff --git a/test/billing-integration.test.js b/test/billing-integration.test.js new file mode 100644 index 0000000..96d6253 --- /dev/null +++ b/test/billing-integration.test.js @@ -0,0 +1,420 @@ +const { URL } = require("node:url"); +const { setTimeout } = require("node:timers"); +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const mongoose = require("mongoose"); +const config = require("../src/config").default; +const { BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft } = require("../src/core/billing/models"); +const Repo = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; +const Conference = require("../src/core/model/conference/conferences.model").default; +const { reserveRepository, allowance, assertConferenceAdmission } = require("../src/core/billing/quotas"); +const { fulfillCheckout, syncSubscription, createCheckout, processBillingEvent } = require("../src/core/billing/stripe"); +const uri = process.env.BILLING_TEST_MONGODB_URI; +const owner = "507f1f77bcf86cd799439011"; +const future = () => new Date(Date.now() + 86400000); +async function rejects(promise, message) { + try { await promise; throw new Error("Expected rejection"); } catch (error) { expect(error.message).to.equal(message); } +} +(uri ? describe : describe.skip)("billing with MongoDB", function () { + this.timeout(15000); + const original = { ...config }; + before(async () => { + if (!/^billing_test_/.test(new URL(uri).pathname.slice(1))) throw new Error("Use a disposable billing_test_ database"); + await mongoose.connect(uri); + await Promise.all([BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft, Repo, Conference].map(m => m.init())); + }); + beforeEach(async () => { + Object.assign(config, { BILLING_ENABLED: true, FREE_AUTHOR_REPOSITORIES: 3, FREE_AUTHOR_STORAGE: 100, + PAID_AUTHOR_STORAGE: 1000, PAID_AUTHOR_REPOSITORIES: 20, STRIPE_AUTHOR_MONTHLY_PRICE: "price_monthly", STRIPE_AUTHOR_PASS_PRICE: "price_pass", STRIPE_CONFERENCE_PRICE: "price_conf" }); + await Promise.all([BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft, Repo, Conference].map(m => m.deleteMany({}))); + await BillingAccount.create({ _id: owner, customerId: "cus_owner" }); + }); + after(async () => { Object.assign(config, original); await mongoose.disconnect(); }); + it("atomically admits only three concurrent repositories", async () => { + const results = await Promise.allSettled(Array.from({ length: 12 }, (_, i) => reserveRepository(owner, `repo-${i}`, 10))); + expect(results.filter(r => r.status === "fulfilled")).to.have.length(3); + expect((await BillingQuota.findById(`user:${owner}`)).allocations).to.have.length(3); + }); + it("atomically caps storage and never shrinks a concurrent reservation", async () => { + const results = await Promise.allSettled([reserveRepository(owner, "a", 60), reserveRepository(owner, "b", 60)]); + expect(results.filter(r => r.status === "fulfilled")).to.have.length(1); + const ledger = await BillingQuota.findById(`user:${owner}`); + const id = ledger.allocations[0].repoId; + await Promise.all([reserveRepository(owner, id, 90), reserveRepository(owner, id, 1)]); + expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(90); + }); + it("does not seed personal capacity with conference-funded or pass-funded repositories", async () => { + await BillingGrant.create([ + { _id: "conf-grant", owner, scope: "conference:funded", status: "paid", until: future(), planId: "conference" }, + { _id: "pass-grant", owner, scope: "repo:pass", status: "paid", until: future(), planId: "author_pass" }, + ]); + await Repo.create([...["a", "b", "c"].map(repoId => ({ repoId, owner, conference: "funded", status: "ready", size: { storage: 80 } })), + { repoId: "pass", owner, status: "ready", size: { storage: 80 } }]); + await reserveRepository(owner, "personal", 10); + const ledger = await BillingQuota.findById(`user:${owner}`); + expect(ledger.allocations.map(a => a.repoId)).to.deep.equal(["personal"]); + }); + it("accounts for concurrent resolved-file growth without double charging retries", async () => { + const { reserveFileExpansion } = require("../src/core/billing/quotas"); + await Repo.create({ repoId: "lfs", owner, billingManaged: true, status: "ready", size: { storage: 10 } }); + await reserveRepository(owner, "lfs", 10); + await Promise.all(Array.from({ length: 8 }, () => reserveFileExpansion("lfs", "file-a", 40))); + expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(50); + await reserveFileExpansion("lfs", "file-b", 40); + await rejects(reserveFileExpansion("lfs", "file-c", 20), "quota_exceeded"); + await reserveRepository(owner, "lfs", 15); + expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(95); + await reserveFileExpansion("lfs", "file-a", 1); + expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(95); + }); + it("shows only personally funded repositories before a user ledger exists", async () => { + const utils = require("../src/server/routes/route-utils"); + const originalUser = utils.getUser; + await BillingGrant.create({ _id: "conf-usage", owner, scope: "conference:funded", status: "paid", until: future(), planId: "conference" }); + utils.getUser = async () => ({ id: owner, quotaRepositories: async () => [ + { repoId: "conference-work", conference: "funded", size: { storage: 90, file: 1 } }, + { repoId: "personal-work", size: { storage: 10, file: 1 } }, + ] }); + try { + const router = require("../src/server/routes/user").default; + const handler = router.stack.find(layer => layer.route?.path === "/quota").route.stack[0].handle; + let body; + await handler({}, { json: value => { body = value; } }); + expect(body.repository.used).to.equal(1); + expect(body.storage.used).to.equal(10); + expect(body.file.used).to.equal(1); + } finally { utils.getUser = originalUser; } + }); + for (const replacement of [true, false]) { + it(`retains a reservation during retirement and ${replacement ? "replacement" : "releases it after permanent removal"}`, async () => { + const Repository = require("../src/core/Repository").default; + const db = require("../src/server/database"); + const wasConnected = db.isConnected; + config.FREE_AUTHOR_REPOSITORIES = 1; + const model = await Repo.create({ repoId: "published", owner, billingManaged: true, status: "ready", size: { storage: 10 } }); + await reserveRepository(owner, "published", 20); + const repo = new Repository(model); + repo.resetSate = async () => { + expect((await Repo.findOne({ repoId: "published" })).status).to.equal("removing"); + await rejects(reserveRepository(owner, "competitor", 1), "quota_exceeded"); + }; + db.isConnected = true; + try { await repo.remove({ replacement }); } + finally { db.isConnected = wasConnected; } + expect((await Repo.findOne({ repoId: "published" })).status).to.equal(replacement ? "preparing" : "removed"); + if (replacement) { + await rejects(reserveRepository(owner, "competitor", 1), "quota_exceeded"); + expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(20); + } else await reserveRepository(owner, "competitor", 1); + }); + } + for (const fails of [false, true]) { + it(`claims removed restoration before admission and ${fails ? "recovers a failed admission" : "protects it from a competitor"}`, async () => { + const Repository = require("../src/core/Repository").default; + const utils = require("../src/server/routes/route-utils"); + const quotas = require("../src/core/billing/quotas"); + const queue = require("../src/queue"); + const saved = { getRepo: utils.getRepo, getUser: utils.getUser, handleError: utils.handleError, reserve: quotas.reserveRepository, queue: queue.downloadQueue }; + config.FREE_AUTHOR_REPOSITORIES = 1; + const model = await Repo.create({ repoId: "restore", owner, billingManaged: true, status: "removed", statusDate: new Date(0), + source: { type: "GitHubStream", repositoryName: "owner/repo", commit: "abc123", branch: "main" }, size: { storage: 10 } }); + utils.getRepo = async () => new Repository(model); + utils.getUser = async () => ({ isAdmin: true }); + utils.handleError = error => { throw error; }; + let queued = false; + queue.downloadQueue = { add: async () => { queued = true; } }; + quotas.reserveRepository = async (...args) => { + expect((await Repo.findById(model._id)).status).to.equal("preparing"); + if (fails) throw Error("quota_exceeded"); + await saved.reserve(...args); + await rejects(saved.reserve(owner, "competitor", 1), "quota_exceeded"); + }; + try { + const router = require("../src/server/routes/repository-private").default; + const handler = router.stack.find(layer => layer.route?.path === "/:repoId/" && layer.route.methods.post).route.stack[0].handle; + const work = handler({ body: { repoId: "restore", fullName: "owner/repo", source: { commit: "abc123", branch: "main" }, terms: [], + options: { expirationMode: "remove", expirationDate: future() } } }, { json() {} }); + if (fails) await rejects(work, "quota_exceeded"); else await work; + expect(queued).to.equal(!fails); + expect((await Repo.findById(model._id)).status).to.equal(fails ? "removed" : "preparing"); + } finally { + utils.getRepo = saved.getRepo; utils.getUser = saved.getUser; utils.handleError = saved.handleError; + quotas.reserveRepository = saved.reserve; queue.downloadQueue = saved.queue; + } + }); + } + it("does not let stale removal cleanup delete a fresh reservation", async () => { + config.FREE_AUTHOR_REPOSITORIES = 1; + await Repo.create({ repoId: "restoring", owner, status: "ready", size: { storage: 10 } }); + await reserveRepository(owner, "restoring", 10); + await Repo.updateOne({ repoId: "restoring" }, { $set: { status: "removed" } }); + const update = BillingQuota.updateOne; + let interleaved = false; + BillingQuota.updateOne = async function (...args) { + if (args[1].$pull && !interleaved) { + interleaved = true; + await Repo.updateOne({ repoId: "restoring" }, { $set: { status: "preparing" } }); + await reserveRepository(owner, "restoring", 20); + } + return update.apply(this, args); + }; + try { await rejects(reserveRepository(owner, "competitor", 1), "quota_exceeded"); } + finally { BillingQuota.updateOne = update; } + expect(interleaved).to.equal(true); + expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(20); + }); + it("counts legacy repositories and releases removed repositories", async () => { + await Repo.create(["one", "two", "three"].map(repoId => ({ repoId, owner, status: "ready", size: { storage: 10 } }))); + await rejects(reserveRepository(owner, "four", 1), "quota_exceeded"); + await Repo.updateOne({ repoId: "one" }, { $set: { status: "removed" } }); + await reserveRepository(owner, "four", 1); + expect((await BillingQuota.findById(`user:${owner}`)).allocations.map(a => a.repoId)).not.to.include("one"); + }); + it("charges shared repositories only to their owner, regardless of coauthor subscriptions", async () => { + const collaborator = "507f1f77bcf86cd799439012"; + await Repo.create(["one", "two", "three"].map(repoId => ({ + repoId, owner, status: "ready", size: { storage: 10 }, + coauthors: [{ username: "collaborator", githubId: "42" }], + }))); + // A collaborator can create their own repository even when the owner is full. + await reserveRepository(collaborator, "own-work", 10); + expect((await BillingQuota.findById(`user:${collaborator}`)).allocations.map(a => a.repoId)).to.deep.equal(["own-work"]); + await BillingGrant.create({ _id: "coauthor-sub", owner: collaborator, + scope: `user:${collaborator}`, status: "active", until: future(), planId: "author_monthly" }); + expect((await allowance(owner, "one")).plan).to.equal("free"); + await rejects(reserveRepository(owner, "four", 1), "quota_exceeded"); + // Editing shared work at the owner's count cap is allowed if its bytes fit. + await reserveRepository(owner, "one", 20); + await rejects(reserveRepository(owner, "one", 90), "quota_exceeded"); + }); + it("uses a conference allowance before an author's subscription", async () => { + await BillingGrant.create([{ _id: "sub", owner, scope: `user:${owner}`, status: "active", until: future(), planId: "author_monthly" }, + { _id: "conf", owner, scope: "conference:CONF", status: "paid", until: future(), planId: "conference" }]); + expect((await allowance(owner, "repo", "CONF")).scope).to.equal("conference:CONF"); + await BillingGrant.updateOne({ _id: "conf" }, { $set: { revoked: true } }); + expect((await allowance(owner, "repo", "CONF")).scope).to.equal(`user:${owner}`); + }); + it("requires the owner's recurring subscription for never expiration", async () => { + const { neverExpirationDeadline, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration"); + await rejects(neverExpirationDeadline(owner), "subscription_required_for_never_expiration"); + await BillingGrant.create({ _id: "pass-only", owner, scope: "repo:repo", status: "paid", until: future(), planId: "author_pass" }); + await rejects(neverExpirationDeadline(owner), "subscription_required_for_never_expiration"); + const until = future(); + await BillingGrant.create({ _id: "author", owner, scope: `user:${owner}`, status: "active", until, planId: "author_annual" }); + expect((await neverExpirationDeadline(owner)).getTime()).to.equal(until.getTime() + NEVER_EXPIRATION_GRACE_MS); + }); + it("renews never expiration, honors 90 days of grace and leaves legacy links alone", async () => { + const { reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration"); + const until = future(); + const oldDeadline = new Date(Date.now() + 1000); + await Repo.create([{ repoId: "paid-never", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: oldDeadline }, + { repoId: "legacy-never", owner, status: "ready", options: { expirationMode: "never" } }]); + await BillingGrant.create({ _id: "author", owner, scope: `user:${owner}`, status: "active", until, planId: "author_monthly" }); + await reconcileNeverExpiration(); + const deadline = new Date(until.getTime() + NEVER_EXPIRATION_GRACE_MS); + expect((await Repo.findOne({ repoId: "paid-never" })).billingNeverExpiresUntil.getTime()).to.equal(deadline.getTime()); + await reconcileNeverExpiration(new Date(deadline.getTime() - 1)); + expect((await Repo.findOne({ repoId: "paid-never" })).options.expirationMode).to.equal("never"); + await reconcileNeverExpiration(deadline, async () => {}); + const expired = await Repo.findOne({ repoId: "paid-never" }); + expect(expired.options.expirationMode).to.equal("remove"); + expect(expired.options.expirationDate.getTime()).to.equal(deadline.getTime()); + expect((await Repo.findOne({ repoId: "legacy-never" })).options.expirationMode).to.equal("never"); + }); + function session(status = "paid") { + return { id: "cs_pass", metadata: { application: "anonymous-github", owner, planId: "author_pass", target: "repo" }, + customer: "cus_owner", status: "complete", payment_status: status, created: Math.floor(Date.now() / 1000), + line_items: { data: [{ price: { id: "price_pass" }, quantity: 1 }] }, payment_intent: { id: "pi_pass", latest_charge: { amount_refunded: 0, disputed: false } } }; + } + it("ignores an unfinished recurring Checkout before Stripe assigns a subscription", async () => { + const stripe = { checkout: { sessions: { retrieve: async () => ({ + id: "cs_open", metadata: { application: "anonymous-github", owner, planId: "author_monthly" }, + customer: "cus_owner", status: "open", payment_status: "unpaid", subscription: null, + }) } } }; + await fulfillCheckout("cs_open", stripe); + expect(await BillingGrant.countDocuments()).to.equal(0); + }); + it("ignores unpaid checkout and fulfills duplicate deliveries exactly once", async () => { + let value = session("unpaid"); + const stripe = { checkout: { sessions: { retrieve: async () => value } } }; + await fulfillCheckout("cs_pass", stripe); + expect(await BillingGrant.countDocuments()).to.equal(0); + value = session(); + await Promise.all([fulfillCheckout("cs_pass", stripe), fulfillCheckout("cs_pass", stripe)]); + const grant = await BillingGrant.findById("cs_pass"); + expect(await BillingGrant.countDocuments()).to.equal(1); + expect(grant.scope).to.equal("repo:repo"); + expect(grant.until.getTime()).to.equal(value.created * 1000 + 180 * 86400000); + }); + it("does not reactivate a refunded pass on a checkout replay", async () => { + const value = session(); + const stripe = { checkout: { sessions: { retrieve: async () => value } } }; + await fulfillCheckout("cs_pass", stripe); + await processBillingEvent({ type: "charge.refunded", data: { object: { payment_intent: "pi_pass" } } }, { ...stripe, invoicePayments: { list: async () => ({ data: [] }) } }); + await fulfillCheckout("cs_pass", stripe); + expect((await BillingGrant.findById("cs_pass")).revoked).to.equal(true); + }); + it("reconciles retired one-time prices and continues refund recovery", async () => { + const { reconcileBillingAccount } = require("../src/core/billing/reconciliation"); + const iterable = values => ({ async *[Symbol.asyncIterator]() { yield* values; } }); + const value = session(); + const stripe = { checkout: { sessions: { retrieve: async () => value, list: () => iterable([value]) } }, + subscriptions: { list: () => iterable([]) }, charges: { list: () => iterable([{ payment_intent: "pi_pass", refunded: true }]) }, + invoicePayments: { list: async () => ({ data: [] }) } }; + await fulfillCheckout(value.id, stripe); + config.STRIPE_AUTHOR_PASS_PRICE = ""; + await BillingGrant.deleteMany({}); + await reconcileBillingAccount(owner, stripe); + expect((await BillingGrant.findById(value.id)).revoked).to.equal(true); + expect((await BillingAccount.findById(owner)).reconciledAt).to.be.instanceOf(Date); + value.line_items.data[0].price.id = "price_unrecognized"; + await rejects(fulfillCheckout(value.id, stripe), "billing_invalid_purchase"); + }); + it("preserves previously accepted grants without historical price fields", async () => { + const value = session(); + await BillingGrant.create({ _id: value.id, owner, customerId: "cus_owner", scope: "repo:repo", planId: "author_pass", status: "paid" }); + config.STRIPE_AUTHOR_PASS_PRICE = ""; + await fulfillCheckout(value.id, { checkout: { sessions: { retrieve: async () => value } } }); + expect((await BillingPrice.findById("price_pass")).planId).to.equal("author_pass"); + }); + it("rejects a checkout for a different Stripe customer", async () => { + const value = session(); value.customer = "cus_other"; + await rejects(fulfillCheckout("cs_pass", { checkout: { sessions: { retrieve: async () => value } } }), "billing_customer_mismatch"); + }); + function subscription(overrides = {}) { + return { id: "sub_owner", metadata: { application: "anonymous-github", owner }, customer: "cus_owner", status: "active", + latest_invoice: { status: "paid" }, items: { data: [{ price: { id: "price_monthly" }, current_period_end: Math.floor(future().getTime() / 1000) }] }, cancel_at_period_end: false, ...overrides }; + } + it("recognizes portal cancellations scheduled with cancel_at", async () => { + await syncSubscription("sub_owner", { subscriptions: { retrieve: async () => subscription({ + cancel_at_period_end: false, cancel_at: Math.floor(future().getTime() / 1000), + }) } }); + expect((await BillingGrant.findById("sub_owner")).cancelAtPeriodEnd).to.equal(true); + expect((await allowance(owner)).plan).to.equal("author_monthly"); + }); + it("honors paid subscription periods, cancellation and renewal failure", async () => { + let value = subscription(); + const stripe = { subscriptions: { retrieve: async () => value } }; + await syncSubscription("sub_owner", stripe); + expect((await allowance(owner)).plan).to.equal("author_monthly"); + const paidThrough = (await BillingGrant.findById("sub_owner")).until; + value = subscription({ status: "past_due", latest_invoice: { status: "open" } }); + value.items.data[0].current_period_end += 86400 * 30; + await syncSubscription("sub_owner", stripe); + expect((await BillingGrant.findById("sub_owner")).until).to.deep.equal(paidThrough); + value = subscription({ cancel_at_period_end: true }); + await syncSubscription("sub_owner", stripe); + expect((await allowance(owner)).plan).to.equal("author_monthly"); + value = subscription({ status: "canceled" }); + await syncSubscription("sub_owner", stripe); + expect((await allowance(owner)).plan).to.equal("free"); + }); + it("protects a newer subscription update from an older in-flight fetch", async () => { + await BillingGrant.create({ _id: "sub_owner", owner, scope: `user:${owner}`, planId: "author_monthly", status: "active", until: future() }); + let release; const gate = new Promise(resolve => { release = resolve; }); let started; + const firstRead = new Promise(resolve => { started = resolve; }); let calls = 0; + const stripe = { subscriptions: { retrieve: async () => { + if (++calls === 1) { started(); await gate; return subscription(); } + return subscription({ status: "canceled" }); + } } }; + const old = syncSubscription("sub_owner", stripe); + await firstRead; + await syncSubscription("sub_owner", stripe); + release(); await old; + expect((await BillingGrant.findById("sub_owner")).status).to.equal("canceled"); + }); + it("keeps conference admission closed until verified payment", async () => { + await Conference.create({ conferenceID: "CONF", owners: [owner], status: "pending_payment", startDate: new Date(Date.now() - 1000), endDate: future(), plan: { planID: "conference" } }); + await rejects(assertConferenceAdmission(owner, "repo", "CONF"), "conf_not_activated"); + const value = session(); value.id = "cs_conf"; value.metadata.planId = "conference"; value.metadata.target = "CONF"; value.line_items.data[0].price.id = "price_conf"; + await fulfillCheckout("cs_conf", { checkout: { sessions: { retrieve: async () => value } } }); + expect((await Conference.findOne({ conferenceID: "CONF" })).status).to.equal("ready"); + await assertConferenceAdmission(owner, "repo", "CONF"); + await BillingGrant.updateOne({ _id: "cs_conf" }, { $set: { revoked: true } }); + await rejects(assertConferenceAdmission(owner, "repo", "CONF"), "conference_payment_required"); + }); + it("prevents two simultaneous checkout sessions and reuses an open session", async () => { + let creates = 0; + const stripe = { prices: { retrieve: async () => ({ active: true, currency: "eur", unit_amount: 500, recurring: { interval: "month", interval_count: 1 } }) }, subscriptions: { list: async () => ({ data: [] }) }, checkout: { sessions: { + create: async () => { creates++; await new Promise(r => setTimeout(r, 30)); return { id: "cs_new", url: "https://checkout.stripe.com/test" }; }, + retrieve: async () => ({ id: "cs_new", status: "open", url: "https://checkout.stripe.com/test" }), + } } }; + const results = await Promise.allSettled([createCheckout(owner, "author_monthly", "", stripe), createCheckout(owner, "author_monthly", "", stripe)]); + expect(results.filter(r => r.status === "fulfilled")).to.have.length(1); + await createCheckout(owner, "author_monthly", "", stripe); + expect(creates).to.equal(1); + }); + it("revokes only the refunded subscription, including refunds arriving before subscription sync", async () => { + await BillingGrant.create({ _id: "other-sub", owner, customerId: "cus_owner", subscriptionId: "other-sub", status: "active", until: future() }); + const stripe = { invoicePayments: { list: async () => ({ data: [{ invoice: "in_refunded" }] }) }, + invoices: { retrieve: async () => ({ parent: { subscription_details: { subscription: "sub_refunded" } } }) } }; + await processBillingEvent({ type: "charge.refunded", data: { object: { customer: "cus_owner", payment_intent: "pi_refunded" } } }, stripe); + expect((await BillingGrant.findById("sub_refunded")).revoked).to.equal(true); + expect((await BillingGrant.findById("other-sub")).revoked).to.equal(false); + await syncSubscription("sub_refunded", { subscriptions: { retrieve: async () => subscription({ id: "sub_refunded" }) } }); + expect((await BillingGrant.findById("sub_refunded")).revoked).to.equal(true); + }); + + it("keeps reviewer visits from refreshing snapshots after paid access expires", async () => { + const Repository = require("../src/core/Repository").default; + const model = new Repo({ repoId: "frozen", owner, billingManaged: true, status: "ready", lastView: new Date(0), options: { update: true, expirationMode: "never" }, source: { type: "GitHubStream", repositoryName: "owner/repo" } }); + const repo = new Repository(model); + repo.getToken = () => { throw new Error("Unexpected upstream refresh"); }; + await BillingGrant.create({ _id: "expired-sub", owner, scope: `user:${owner}`, status: "active", until: new Date(0), planId: "author_monthly" }); + await repo.updateIfNeeded(); + expect(repo.status).to.equal("ready"); + }); + + it("authenticates billing, checks CSRF, and verifies raw webhook signatures", async () => { + const express = require("express"); + const sessionMiddleware = require("express-session"); + const Stripe = require("stripe"); + const Users = require("../src/core/model/users/users.model").default; + const routes = require("../src/server/routes/billing"); + await Users.updateOne({ _id: owner }, { $set: { username: "billing-test-author", status: "active" } }, { upsert: true }); + Object.assign(config, { STRIPE_SECRET_KEY: "sk_test_example", STRIPE_WEBHOOK_SECRET: "whsec_test_example", STRIPE_AUTHOR_MONTHLY_PRICE: "", STRIPE_AUTHOR_ANNUAL_PRICE: "", STRIPE_AUTHOR_PASS_PRICE: "", STRIPE_CONFERENCE_PRICE: "" }); + const app = express(); + app.use("/webhook", routes.billingWebhook); + app.use(express.json()); + app.use(sessionMiddleware({ secret: "billing-test-session-secret", resave: false, saveUninitialized: false })); + app.use((req, _res, next) => { req.isAuthenticated = () => req.headers["x-test-auth"] === "yes"; req.user = { user: { _id: owner } }; next(); }); + app.use("/billing", routes.default); + const server = await new Promise(resolve => { const listening = app.listen(0, "127.0.0.1", () => resolve(listening)); }); + const base = `http://127.0.0.1:${server.address().port}`; + try { + expect((await globalThis.fetch(`${base}/billing`)).status).to.equal(401); + const get = await globalThis.fetch(`${base}/billing`, { headers: { "x-test-auth": "yes" } }); + const data = await get.json(); + expect(data.csrf).to.be.a("string"); + expect(JSON.stringify(data)).not.to.include("cus_owner"); + const headers = { "x-test-auth": "yes", cookie: get.headers.get("set-cookie").split(";")[0], "content-type": "application/json" }; + expect((await globalThis.fetch(`${base}/billing/checkout`, { method: "POST", headers, body: JSON.stringify({ planId: "arbitrary" }) })).status).to.equal(403); + expect((await globalThis.fetch(`${base}/billing/draft`, { method: "POST", headers, body: JSON.stringify({ path: "/anonymize", values: { terms: "Private Author" } }) })).status).to.equal(403); + headers["x-csrf-token"] = data.csrf; + const saved = await globalThis.fetch(`${base}/billing/draft`, { method: "POST", headers, body: JSON.stringify({ path: "/anonymize", values: { terms: "Private Author" } }) }); + expect(saved.status).to.equal(200); + const draft = await saved.json(); + expect((await globalThis.fetch(`${base}/billing/draft/${draft.id}`)).status).to.equal(401); + const restored = await globalThis.fetch(`${base}/billing/draft/${draft.id}`, { headers }); + expect(restored.headers.get("cache-control")).to.equal("no-store"); + expect((await restored.json()).values.terms).to.equal("Private Author"); + await BillingDraft.updateOne({ _id: draft.id }, { owner: "another-owner" }); + expect((await globalThis.fetch(`${base}/billing/draft/${draft.id}`, { headers })).status).to.equal(404); + + const rejected = await globalThis.fetch(`${base}/billing/checkout`, { method: "POST", headers, body: JSON.stringify({ planId: "arbitrary" }) }); + expect((await rejected.json()).error).to.equal("billing_plan_unavailable"); + const payload = JSON.stringify({ id: "evt_test", type: "customer.created", data: { object: { id: "cus_unused" } } }); + const stripe = new Stripe("sk_test_example"); + const signature = stripe.webhooks.generateTestHeaderString({ payload, secret: config.STRIPE_WEBHOOK_SECRET }); + expect((await globalThis.fetch(`${base}/webhook`, { method: "POST", headers: { "content-type": "application/json", "stripe-signature": signature }, body: payload })).status).to.equal(200); + expect((await globalThis.fetch(`${base}/webhook`, { method: "POST", headers: { "content-type": "application/json", "stripe-signature": signature }, body: payload + " " })).status).to.equal(400); + expect((await BillingEvent.findById("test:platform:evt_test")).state).to.equal("pending"); + const originalUpdate = BillingEvent.updateOne; + BillingEvent.updateOne = async () => { throw new Error("simulated database outage"); }; + try { + expect((await globalThis.fetch(`${base}/webhook`, { method: "POST", headers: { "content-type": "application/json", "stripe-signature": signature }, body: payload })).status).to.equal(500); + } finally { BillingEvent.updateOne = originalUpdate; } + } finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); await Users.deleteOne({ _id: owner }); } + }); + +}); diff --git a/test/billing-reliability.test.js b/test/billing-reliability.test.js new file mode 100644 index 0000000..bd1c8ad --- /dev/null +++ b/test/billing-reliability.test.js @@ -0,0 +1,249 @@ +const { URL } = require("url"); +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const mongoose = require("mongoose"); +const config = require("../src/config").default; +const { BillingPrice, BillingAccount, BillingGrant, BillingEvent, BillingRevocation, BillingLock, BillingDraft } = require("../src/core/billing/models"); +const { receiveBillingEvent, processBillingInbox, billingHealth } = require("../src/core/billing/events"); +const { reconcileBillingAccount } = require("../src/core/billing/reconciliation"); +const { createCheckout, processBillingEvent } = require("../src/core/billing/stripe"); +const { reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration"); +const Repo = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; +const uri = process.env.BILLING_TEST_MONGODB_URI; +const owner = "507f1f77bcf86cd799439011"; +const { saveBillingDraft, getBillingDraft } = require("../src/core/billing/drafts"); +const models = [BillingDraft, BillingPrice, BillingAccount, BillingGrant, BillingEvent, BillingRevocation, BillingLock, Repo]; +const iterable = values => ({ async *[Symbol.asyncIterator]() { yield* values; } }); +const event = id => ({ id, livemode: false, type: "customer.created", data: { object: { id: "cus_test", email: "private@example.org" } } }); +(uri ? describe : describe.skip)("billing failure recovery", function () { + this.timeout(20000); + const original = { ...config }; + before(async () => { + if (!/^billing_test_/.test(new URL(uri).pathname.slice(1))) throw new Error("Disposable database required"); + await mongoose.connect(uri); + await Promise.all(models.map(m => m.init())); + }); + beforeEach(async () => { + await Promise.all(models.map(m => m.deleteMany({}))); + Object.assign(config, { BILLING_ENABLED: true, BILLING_CHECKOUT_ENABLED: true, STRIPE_AUTHOR_MONTHLY_PRICE: "price_monthly" }); + await BillingAccount.create({ _id: owner, customerId: "cus_owner" }); + }); + after(async () => { Object.assign(config, original); await mongoose.disconnect(); }); + it("deduplicates concurrent receipt without retaining customer data", async () => { + await Promise.all(Array.from({ length: 8 }, () => receiveBillingEvent(event("evt_one")))); + expect(await BillingEvent.countDocuments()).to.equal(1); + expect(JSON.stringify(await BillingEvent.findOne())).not.to.include("private@example.org"); + let calls = 0; + await Promise.all([processBillingInbox(1, async () => { calls++; }), processBillingInbox(1, async () => { calls++; })]); + expect(calls).to.equal(1); + expect((await BillingEvent.findOne()).state).to.equal("complete"); + }); + it("recovers a crash after side effects but before acknowledgement", async () => { + await receiveBillingEvent(event("evt_crash")); + await BillingGrant.create({ _id: "already-applied", status: "paid" }); + await BillingEvent.updateOne({}, { $set: { state: "processing", token: "dead-worker", leaseUntil: new Date(0) } }); + await processBillingInbox(1, async () => { + await BillingGrant.updateOne({ _id: "already-applied" }, { $setOnInsert: { status: "paid" } }, { upsert: true }); + }); + expect(await BillingGrant.countDocuments()).to.equal(1); + expect((await BillingEvent.findOne()).state).to.equal("complete"); + }); + it("backs off failures, redacts errors and exposes exhausted retries", async () => { + await receiveBillingEvent(event("evt_fail")); + await processBillingInbox(1, async () => { throw new Error("secret payload"); }); + let record = await BillingEvent.findOne(); + expect(record.state).to.equal("pending"); + expect(record.nextAttemptAt.getTime()).to.be.greaterThan(record.receivedAt.getTime()); + expect(record.lastError).not.to.include("secret"); + await BillingEvent.updateOne({}, { $set: { attempts: 11, nextAttemptAt: new Date(0) } }); + await processBillingInbox(1, async () => { throw new Error("secret"); }); + record = await BillingEvent.findOne(); + expect(record.state).to.equal("failed"); + expect((await billingHealth()).failed).to.equal(1); + }); + it("recovers missing grants from provider history and applies missed refunds", async () => { + const sub = { id: "sub_lost", metadata: { application: "anonymous-github", owner }, customer: "cus_owner", status: "active", + latest_invoice: { status: "paid" }, items: { data: [{ price: { id: "price_monthly" }, current_period_end: Math.floor(Date.now() / 1000) + 86400 }] } }; + const stripe = { subscriptions: { list: () => iterable([sub]), retrieve: async () => sub }, + checkout: { sessions: { list: () => iterable([]) } }, + charges: { list: () => iterable([{ id: "ch_refunded", refunded: true, payment_intent: "pi_refunded" }]) }, + invoicePayments: { list: async () => ({ data: [{ invoice: "in_refunded" }] }) }, + invoices: { retrieve: async () => ({ parent: { subscription_details: { subscription: "sub_lost" } } }) } }; + await reconcileBillingAccount(owner, stripe); + expect((await BillingGrant.findById("sub_lost")).revoked).to.equal(true); + expect((await BillingAccount.findById(owner)).reconciledAt).to.be.instanceOf(Date); + // A restored database with no grants can reconstruct the same final state. + await BillingGrant.deleteMany({}); + await reconcileBillingAccount(owner, stripe); + expect((await BillingGrant.findById("sub_lost")).revoked).to.equal(true); + }); + it("recovers paid time when the latest invoice failed after a missed renewal", async () => { + const { syncSubscription } = require("../src/core/billing/stripe"); + const end = Math.floor(Date.now() / 1000); + await syncSubscription("sub_history", { + subscriptions: { retrieve: async () => ({ id: "sub_history", customer: "cus_owner", metadata: { application: "anonymous-github", owner }, + status: "past_due", latest_invoice: { status: "open" }, items: { data: [{ id: "si_owned", price: { id: "price_monthly" }, current_period_end: end + 86400 }] } }) }, + invoices: { list: () => iterable([{ id: "in_paid" }]), listLineItems: () => iterable([ + { amount: 500, parent: { subscription_item_details: { subscription_item: "si_owned" } }, period: { end } }, + { amount: 500, parent: { subscription_item_details: { subscription_item: "si_unrelated" } }, period: { end: end + 999999 } }, + ]) }, + }); + expect((await BillingGrant.findById("sub_history")).until.getTime()).to.equal(end * 1000); + }); + it("preserves a refund received before the first grant exists", async () => { + const { fulfillCheckout } = require("../src/core/billing/stripe"); + config.STRIPE_AUTHOR_PASS_PRICE = "price_pass"; + const stripe = { invoicePayments: { list: async () => ({ data: [] }) }, checkout: { sessions: { retrieve: async () => ({ + id: "cs_late", status: "complete", payment_status: "paid", customer: "cus_owner", created: Math.floor(Date.now() / 1000), + metadata: { application: "anonymous-github", owner, planId: "author_pass", target: "repo" }, + line_items: { data: [{ price: { id: "price_pass" }, quantity: 1 }] }, + payment_intent: { id: "pi_early", latest_charge: { refunded: false, disputed: false } }, + }) } } }; + await processBillingEvent({ type: "charge.refunded", data: { object: { payment_intent: "pi_early", refunded: true } } }, stripe); + await fulfillCheckout("cs_late", stripe); + expect((await BillingGrant.findById("cs_late")).revoked).to.equal(true); + }); + it("starts a fresh attempt only after the old immutable request cannot execute", async () => { + await BillingAccount.updateOne({ _id: owner }, { $set: { checkout: { + key: "old", planId: "author_monthly", target: "", createdAt: new Date(0), params: { expires_at: 1000 }, + } } }); + const stripe = checkoutStripe(); + stripe.checkout.sessions.create = async (_params, options) => { + expect(options.idempotencyKey).not.to.equal("old"); + return { id: "cs_fresh", url: "https://checkout.stripe.com/fresh" }; + }; + expect(await createCheckout(owner, "author_monthly", "", stripe)).to.equal("https://checkout.stripe.com/fresh"); + }); + it("keeps partial refunds active and revokes a full refund", async () => { + await BillingGrant.create({ _id: "pass", paymentIntentId: "pi_pass", status: "paid" }); + const stripe = { invoicePayments: { list: async () => ({ data: [] }) } }; + const refund = refunded => ({ type: "charge.refunded", data: { object: { payment_intent: "pi_pass", refunded } } }); + await processBillingEvent(refund(false), stripe); + expect((await BillingGrant.findById("pass")).revoked).to.equal(false); + await processBillingEvent(refund(true), stripe); + expect((await BillingGrant.findById("pass")).revocationReason).to.equal("full_refund"); + }); + it("requires review for a new dispute but ignores an older refund replay after review", async () => { + await BillingGrant.create({ _id: "reviewed-pass", paymentIntentId: "pi_reviewed", status: "paid" }); + const stripe = { invoicePayments: { list: async () => ({ data: [] }) }, charges: { retrieve: async () => ({ payment_intent: "pi_reviewed", disputed: true }) } }; + const refund = { type: "charge.refunded", data: { object: { payment_intent: "pi_reviewed", refunded: true } } }; + await processBillingEvent(refund, stripe); + await BillingRevocation.updateOne({ _id: "pi_reviewed" }, { $set: { reviewed: true } }); + await BillingGrant.updateOne({ _id: "reviewed-pass" }, { $set: { revoked: false } }); + await processBillingEvent({ type: "charge.dispute.created", data: { object: { charge: "ch_reviewed" } } }, stripe); + expect((await BillingGrant.findById("reviewed-pass")).revoked).to.equal(true); + await BillingRevocation.updateOne({ _id: "pi_reviewed" }, { $set: { reviewed: true } }); + await BillingGrant.updateOne({ _id: "reviewed-pass" }, { $set: { revoked: false } }); + await processBillingEvent(refund, stripe); + expect((await BillingGrant.findById("reviewed-pass")).revoked).to.equal(false); + }); + it("defers expiration during outage and discovers a renewal before removing content", async () => { + const deadline = new Date(Date.now() - 1000); + await Repo.create({ repoId: "protected", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: deadline }); + await reconcileNeverExpiration(new Date(), async () => { throw new Error("Stripe unavailable"); }); + expect((await Repo.findOne()).options.expirationMode).to.equal("never"); + const until = new Date(Date.now() + 86400_000); + await reconcileNeverExpiration(new Date(), async () => { + await BillingGrant.create({ _id: "renewal", owner, scope: `user:${owner}`, planId: "author_monthly", until, status: "active" }); + }); + const repo = await Repo.findOne(); + expect(repo.options.expirationMode).to.equal("never"); + expect(repo.billingNeverExpiresUntil.getTime()).to.equal(until.getTime() + NEVER_EXPIRATION_GRACE_MS); + }); + function checkoutStripe() { + return { prices: { retrieve: async () => ({ active: true, currency: "eur", unit_amount: 500, recurring: { interval: "month", interval_count: 1 } }) }, + subscriptions: { list: async () => ({ data: [] }) }, checkout: { sessions: { + create: async () => { throw new Error("connection lost"); }, + list: () => iterable([]), + retrieve: async () => ({ status: "open", url: "https://checkout.stripe.com/recovered" }), + } } }; + } + it("records an accepted one-time price before an uncertain checkout response", async () => { + config.STRIPE_AUTHOR_PASS_PRICE = "price_retiring"; + await Repo.create({ repoId: "pass-target", owner, status: "ready" }); + const stripe = checkoutStripe(); + stripe.prices.retrieve = async () => ({ active: true, currency: "eur", unit_amount: 1500 }); + try { await createCheckout(owner, "author_pass", "pass-target", stripe); throw Error("expected network failure"); } + catch (error) { expect(error.message).to.equal("connection lost"); } + expect((await BillingPrice.findById("price_retiring")).planId).to.equal("author_pass"); + config.STRIPE_AUTHOR_PASS_PRICE = ""; + const { fulfillCheckout } = require("../src/core/billing/stripe"); + stripe.checkout.sessions.retrieve = async () => ({ id: "cs_retired", status: "complete", payment_status: "paid", customer: "cus_owner", + created: Math.floor(Date.now() / 1000), metadata: { application: "anonymous-github", owner, planId: "author_pass", target: "pass-target" }, + line_items: { data: [{ price: { id: "price_retiring" }, quantity: 1 }] }, payment_intent: null }); + await fulfillCheckout("cs_retired", stripe); + expect((await BillingGrant.findById("cs_retired")).status).to.equal("paid"); + }); + it("keeps checkout drafts private, bounded and expired even before the TTL sweep", async () => { + const draft = await saveBillingDraft(owner, "/anonymize", { terms: "Private Author", options: { expirationMode: "never" }, customerId: "untrusted" }); + expect((await getBillingDraft(owner, draft.id)).values).not.to.have.property("customerId"); + for (const other of ["someone-else", owner]) { + if (other === owner) await BillingDraft.updateOne({ _id: draft.id }, { expiresAt: new Date(0) }); + try { await getBillingDraft(other, draft.id); throw new Error("expected rejection"); } + catch (error) { expect(error.message).to.equal("billing_draft_not_found"); } + } + for (const path of ["//evil.example", "/anonymize?next=https://evil.example", "/anonymize/../profile"]) { + try { await saveBillingDraft(owner, path, {}); throw new Error("expected rejection"); } + catch (error) { expect(error.message).to.equal("billing_invalid_draft"); } + } + try { await saveBillingDraft(owner, "/anonymize", { terms: "x".repeat(65537) }); throw new Error("expected rejection"); } + catch (error) { expect(error.message).to.equal("billing_invalid_draft"); } + }); + it("returns checkout to the saved task and refuses reuse for a different task", async () => { + const stripe = checkoutStripe(); + const draft = await saveBillingDraft(owner, "/anonymize", { terms: "Author" }); + const destination = `/anonymize?billingDraft=${draft.id}`; + let parameters; + stripe.checkout.sessions.create = async params => { parameters = params; return { id: "cs_task", url: "https://checkout.stripe.com/task" }; }; + await createCheckout(owner, "author_monthly", "", stripe, destination); + expect(parameters.success_url).to.include(destination + "&billing=success"); + expect(parameters.cancel_url).to.include(destination + "&billing=cancelled"); + await createCheckout(owner, "author_monthly", "", stripe, destination); + try { await createCheckout(owner, "author_monthly", "", stripe); throw new Error("expected rejection"); } + catch (error) { expect(error.message).to.equal("billing_checkout_in_progress"); } + try { await createCheckout(owner, "author_monthly", "", stripe, "https://evil.example"); throw new Error("expected rejection"); } + catch (error) { expect(error.message).to.equal("billing_invalid_draft"); } + }); + it("retries a lost response with identical parameters and the same key", async () => { + const stripe = checkoutStripe(); + let first; + stripe.checkout.sessions.create = async (params, options) => { first = { params, options }; throw new Error("connection lost"); }; + try { await createCheckout(owner, "author_monthly", "", stripe); } catch (e) { expect(e.message).to.equal("connection lost"); } + const saved = await BillingAccount.findById(owner); + expect(saved.checkout.id).to.equal(undefined); + stripe.checkout.sessions.create = async (params, options) => { + expect(params).to.deep.equal(first.params); + expect(options).to.deep.equal(first.options); + return { id: "cs_recovered", url: "https://checkout.stripe.com/recovered" }; + }; + await createCheckout(owner, "author_monthly", "", stripe); + expect((await BillingAccount.findById(owner)).checkout.id).to.equal("cs_recovered"); + }); + it("recovers an old attempt by metadata without creating another payment", async () => { + await BillingAccount.updateOne({ _id: owner }, { $set: { checkout: { key: "attempt", planId: "author_monthly", target: "", createdAt: new Date(0) } } }); + const stripe = checkoutStripe(); + stripe.checkout.sessions.list = () => iterable([{ id: "cs_recovered", metadata: { attempt: "attempt", owner } }]); + expect(await createCheckout(owner, "author_monthly", "", stripe)).to.equal("https://checkout.stripe.com/recovered"); + }); + it("blocks unresolved old requests instead of reusing a pruned key", async () => { + await BillingAccount.updateOne({ _id: owner }, { $set: { checkout: { key: "old", planId: "author_monthly", target: "", createdAt: new Date(0) } } }); + try { await createCheckout(owner, "author_monthly", "", checkoutStripe()); throw new Error("expected rejection"); } + catch (e) { expect(e.message).to.equal("billing_recovery_required"); } + }); + it("checks subsequent subscription pages before offering another subscription", async () => { + const stripe = checkoutStripe(); + stripe.subscriptions.list = async ({ starting_after }) => starting_after + ? { data: [{ id: "active", status: "active" }], has_more: false } + : { data: [{ id: "old", status: "canceled" }], has_more: true }; + try { await createCheckout(owner, "author_monthly", "", stripe); throw new Error("expected rejection"); } + catch (e) { expect(e.message).to.equal("billing_subscription_exists"); } + }); + it("stops new purchases independently of event processing", async () => { + config.BILLING_CHECKOUT_ENABLED = false; + try { await createCheckout(owner, "author_monthly", "", checkoutStripe()); throw new Error("expected rejection"); } + catch (e) { expect(e.message).to.equal("billing_checkout_disabled"); } + await receiveBillingEvent(event("evt_existing")); + await processBillingInbox(1, async () => {}); + expect((await BillingEvent.findOne()).state).to.equal("complete"); + }); +}); diff --git a/test/billing.test.js b/test/billing.test.js new file mode 100644 index 0000000..3c70a80 --- /dev/null +++ b/test/billing.test.js @@ -0,0 +1,102 @@ +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const { usableGrant } = require("../src/core/billing/quotas"); +const { billingOrigin } = require("../src/core/billing/config"); +const config = require("../src/config").default; +const { applyConferenceForm } = require("../src/server/routes/conference"); + +describe("paid access policy", () => { + it("requires paid time and rejects revoked, unpaid, cancelled and expired grants", () => { + const until = new Date(Date.now() + 60000); + for (const status of ["active", "paid", "past_due"]) expect(usableGrant({ status, until })).to.equal(true); + for (const status of ["incomplete", "unpaid", "canceled", "paused", "trialing"]) expect(usableGrant({ status, until })).to.equal(false); + expect(usableGrant({ status: "active", until: new Date(0) })).to.equal(false); + expect(usableGrant({ status: "active", until, revoked: true })).to.equal(false); + }); + it("does not activate a paid conference from a submitted form or an edit", () => { + const model = {}; + const body = { plan: { planID: "conference" }, startDate: "2026-11-01", endDate: "2027-01-01" }; + applyConferenceForm(model, body, true); + expect(model.status).to.equal("pending_payment"); + applyConferenceForm(model, body, false); + expect(model.status).to.equal("pending_payment"); + model.status = "removed"; + applyConferenceForm(model, body, false); + expect(model.status).to.equal("removed"); + }); + it("requires a finite dated expiration when billing is enabled", () => { + const { validateBillingExpiration } = require("../src/core/billing/expiration"); + const enabled = config.BILLING_ENABLED; + config.BILLING_ENABLED = true; + try { + for (const options of [{}, { expirationMode: "remove" }, { expirationMode: "remove", expirationDate: "invalid" }, + { expirationMode: "remove", expirationDate: "2099-01-01" }]) { + expect(() => validateBillingExpiration(options)).to.throw("invalid_billing_expiration"); + } + expect(() => validateBillingExpiration({ expirationMode: "never" })).not.to.throw(); + expect(() => validateBillingExpiration({ expirationMode: "remove", expirationDate: new Date() })).not.to.throw(); + } finally { config.BILLING_ENABLED = enabled; } + }); + it("rejects unsafe configured redirect origins", () => { + const original = config.BILLING_ORIGIN; + try { + for (const value of ["http://example.org", "https://user:pass@example.org", "https://example.org/path", "https://example.org?next=x"]) { + config.BILLING_ORIGIN = value; + expect(() => billingOrigin()).to.throw(); + } + config.BILLING_ORIGIN = "https://example.org"; + expect(billingOrigin()).to.equal("https://example.org"); + } finally { config.BILLING_ORIGIN = original; } + }); +}); + +describe("retired conference offers", () => { + const Model = require("../src/core/model/conference/conferences.model").default; + const utils = require("../src/server/routes/route-utils"); + const router = require("../src/server/routes/conference").default; + const save = router.stack.find(layer => layer.route?.path === "/{:conferenceID}" && layer.route.methods.post).route.stack[0].handle; + const originals = {}; + let model, saves, body; + beforeEach(() => { + originals.enabled = config.BILLING_ENABLED; originals.price = config.STRIPE_CONFERENCE_PRICE; + originals.find = Model.findOne; originals.user = utils.getUser; + originals.auth = utils.isOwnerOrAdmin; originals.error = utils.handleError; + config.BILLING_ENABLED = true; config.STRIPE_CONFERENCE_PRICE = ""; + const start = new Date(Date.now() + 86400000), end = new Date(Date.now() + 30 * 86400000); + saves = 0; + model = { conferenceID: "CONF", name: "Old", owners: [], status: "ready", + plan: { planID: "conference" }, startDate: start, endDate: end, + save: async () => { saves++; } }; + body = { conferenceID: "CONF", name: "New", plan: { planID: "conference" }, + startDate: start.toISOString(), endDate: end.toISOString(), options: { terms: ["Author"] } }; + Model.findOne = async () => model; + utils.getUser = async () => ({ model: { id: "owner" } }); + utils.isOwnerOrAdmin = () => {}; + utils.handleError = error => { throw error; }; + }); + afterEach(() => { + config.BILLING_ENABLED = originals.enabled; config.STRIPE_CONFERENCE_PRICE = originals.price; + Model.findOne = originals.find; utils.getUser = originals.user; + utils.isOwnerOrAdmin = originals.auth; utils.handleError = originals.error; + }); + it("saves existing settings without an advertised price", async () => { + await save({ params: { conferenceID: "CONF" }, body }, { send: () => {} }); + expect(saves).to.equal(1); + expect(model.name).to.equal("New"); + expect(model.options.terms).to.deep.equal(["Author"]); + expect(model.status).to.equal("ready"); + expect(model.plan.planID).to.equal("conference"); + }); + for (const change of ["new", "plan", "dates"]) { + it(`still rejects ${change} purchases or locked changes`, async () => { + const params = change === "new" ? {} : { conferenceID: "CONF" }; + if (change === "plan") body.plan.planID = "free_conference"; + if (change === "dates") body.endDate = new Date(Date.now() + 60 * 86400000).toISOString(); + let error; + try { await save({ params, body }, { send: () => {} }); } catch (e) { error = e; } + expect(error?.message).to.equal(change === "new" ? "billing_invalid_review_window" : + change === "plan" ? "billing_plan_change_not_supported" : "billing_review_window_locked"); + expect(saves).to.equal(0); + }); + } +}); diff --git a/test/credential-migration.test.js b/test/credential-migration.test.js index 18afd77..9947af6 100644 --- a/test/credential-migration.test.js +++ b/test/credential-migration.test.js @@ -179,26 +179,34 @@ describeMongo("credential access (MongoDB)", function () { await setCredential(owner.id, "old-token"); await Credential.updateOne({ ownerId: owner._id }, { $set: { updatedAt: new Date(0) } }); const originalFetch = global.fetch; + const oauthConfig = [config.GITHUB_OAUTH_ENABLED, config.CLIENT_ID, config.CLIENT_SECRET]; + config.GITHUB_OAUTH_ENABLED = true; config.CLIENT_ID = "fixture-client"; config.CLIENT_SECRET = "fixture-secret"; global.fetch = async (url, options) => { expect(url).to.include("api.github.com/applications/"); expect(JSON.parse(options.body).access_token).to.equal("old-token"); return { ok: true, json: async () => ({ token: "refreshed-token" }) }; }; try { expect(await repo.getToken()).to.equal("refreshed-token"); } - finally { global.fetch = originalFetch; } + finally { global.fetch = originalFetch; [config.GITHUB_OAUTH_ENABLED, config.CLIENT_ID, config.CLIENT_SECRET] = oauthConfig; } expect(await getCredentialToken(owner.id)).to.equal("refreshed-token"); expect((await RepoModel.collection.findOne({ _id: repo.model._id })).source.accessToken).to.equal(undefined); }); it("OAuth login writes a credential and returns a token-free session user", async () => { const passport = require("passport"); require("../src/server/routes/connection"); - const result = await new Promise((resolve, reject) => passport._strategy("github")._verify({ githubOAuthContext: { expires: Date.now() + 60000 } }, "oauth-secret", "refresh-secret", { - id: "external-test", username: owner.username, emails: [], photos: [], - }, (error, user) => error ? reject(error) : resolve(user))); + const originalFetch = global.fetch; + global.fetch = async () => ({ ok: true, json: async () => [{ email: "private@example.com", primary: true, verified: true }] }); + let result; + try { + result = await new Promise((resolve, reject) => passport._strategy("github")._verify({ githubOAuthContext: { expires: Date.now() + 60000 } }, "oauth-secret", "refresh-secret", { + id: "external-test", username: owner.username, emails: [], photos: [], + }, (error, user) => error ? reject(error) : resolve(user))); + } finally { global.fetch = originalFetch; } expect(JSON.stringify(result)).not.to.include("oauth-secret"); expect(JSON.stringify(result)).not.to.include("refresh-secret"); expect(await getCredentialToken(owner.id)).to.equal("oauth-secret"); expect((await UserModel.collection.findOne({ _id: owner._id })).accessTokens).to.equal(undefined); + expect((await UserModel.findById(owner.id)).emails[0].email).to.equal("private@example.com"); }); it("reads hidden legacy resource tokens only during compatibility mode", async () => { const resource = await GistModel.collection.insertOne({ gistId: "legacy-gist", owner: owner._id, source: { accessToken: "legacy-resource" } }); diff --git a/test/deploy-owner-alert-migration.test.js b/test/deploy-owner-alert-migration.test.js new file mode 100644 index 0000000..1e09e8f --- /dev/null +++ b/test/deploy-owner-alert-migration.test.js @@ -0,0 +1,124 @@ +const { expect } = require("chai"); +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +describe("owner alert migration deployment", function () { + this.timeout(10000); + function deploy(options = {}) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "owner-alert-deploy-")); + const statePath = path.join(directory, "state.json"), logPath = path.join(directory, "calls.jsonl"); + fs.mkdirSync(path.join(directory, "scripts")); + fs.copyFileSync(path.join(__dirname, "../scripts/deploy.sh"), path.join(directory, "scripts/deploy.sh")); + fs.writeFileSync(statePath, JSON.stringify({ ...options })); + fs.writeFileSync(path.join(directory, "docker"), `#!/usr/bin/env node +const fs = require("fs"); +const args = process.argv.slice(2), state = JSON.parse(fs.readFileSync(process.env.TASK_DOCKER_STATE)); +fs.appendFileSync(process.env.TASK_DOCKER_LOG, JSON.stringify(args) + "\\n"); +const finish = (output = "", code = 0) => { fs.writeFileSync(process.env.TASK_DOCKER_STATE, JSON.stringify(state)); process.stdout.write(output); process.exit(code); }; +if (args[0] === "image" && args[1] === "inspect") { + if (args.some(a => a.includes("Labels"))) finish(state.badProtocol ? "" : "recipient-v2"); + finish(state.built ? "sha:new" : "sha:old"); +} +if (args[0] === "inspect") { + if (args.some(a => a.includes("Config.Env"))) finish(state.sharedCache ? "SHARED_CACHE_ENABLED=true\\n" : ""); + if (args.some(a => a.includes("Labels"))) finish(state.oldProtocol || ""); + if (args.includes("{{.State.Running}}")) finish(state.stopped && !state.refuseStop ? "false" : "true"); + finish("healthy"); +} +if (args[0] === "compose") { + if (args[1] === "build") { if (state.buildFails) finish("", 1); state.built = true; finish(); } + if (args[1] === "stop") { state.stopped = true; finish(); } + if (args[1] === "run") { if (state.reconcileFails) finish("", 1); state.reconciled = true; finish(); } + if (args[1] === "up") { if (args.includes("streamer")) state.streamers = true; else state.api = true; finish(); } + if (args[1] === "ps") { + if (args.includes("-q")) finish(args.includes("streamer") ? "old-api\\nold-1\\nold-2\\nold-3\\nold-4\\n" : "new-api"); + if (args.includes("streamer")) finish(state.streamers ? "new-1\\nnew-2\\nnew-3\\nnew-4\\n" : "old-1\\nold-2\\nold-3\\nold-4\\n"); + finish(); + } +} +finish("unexpected docker call", 2); +`, { mode: 0o755 }); + try { + const result = spawnSync("bash", [path.join(directory, "scripts/deploy.sh")], { + encoding: "utf8", timeout: 8000, + env: { ...process.env, PATH: directory + path.delimiter + process.env.PATH, + COORDINATED_RESTART: String(options.coordinated ?? 0), FORCE: String(options.force ?? 1), STREAMER_REPLICAS: "4", TASK_DOCKER_STATE: statePath, TASK_DOCKER_LOG: logPath }, + }); + return { ...result, calls: fs.readFileSync(logPath, "utf8").trim().split("\n").map(line => JSON.parse(line)) }; + } finally { fs.rmSync(directory, { recursive: true, force: true }); } + } + + for (const sharedCache of [true, false]) it(`coordinates cache rollout with ${sharedCache ? "existing" : "new"} caching`, () => { + const result = deploy({ oldProtocol: "recipient-v2", sharedCache, coordinated: sharedCache ? 0 : 1 }); + expect(result.status, result.stderr).to.equal(0); + const stop = result.calls.findIndex(args => args[0] === "compose" && args[1] === "stop"); + const up = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up"); + expect(stop).to.be.greaterThan(-1); expect(up).to.be.greaterThan(stop); + expect(result.calls[up]).to.include.members(["--force-recreate", "streamer=4"]); + expect(result.calls.some(args => args.includes("streamer=8"))).to.equal(false); + }); + it("does not start new cache writers when an old writer fails to stop", () => { + const result = deploy({ oldProtocol: "recipient-v2", coordinated: 1, refuseStop: true }); + expect(result.status).not.to.equal(0); + expect(result.calls.some(args => args[0] === "compose" && args[1] === "up")).to.equal(false); + }); + it("stops and verifies every legacy writer before starting any migration-capable instance", () => { + const result = deploy(); + expect(result.status, result.stderr).to.equal(0); + const stop = result.calls.findIndex(args => args[0] === "compose" && args[1] === "stop"); + const up = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up"); + expect(stop).to.be.greaterThan(-1); expect(up).to.be.greaterThan(stop); + const reconcile = result.calls.findIndex(args => args[0] === "compose" && args[1] === "run"); + expect(reconcile).to.be.greaterThan(stop); expect(reconcile).to.be.lessThan(up); + expect(result.calls[reconcile]).to.include("./build/scripts/reconcile-legacy-owner-alerts.js"); + const checked = result.calls.slice(stop + 1, up).filter(args => args[0] === "inspect").map(args => args.at(-1)); + expect(checked).to.deep.equal(["old-api", "old-1", "old-2", "old-3", "old-4"]); + expect(result.calls[stop]).to.include.members(["anonymous_github", "streamer"]); + expect(result.calls[up]).to.include.members(["--force-recreate", "streamer=4"]); + const api = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up" && args.at(-1) === "anonymous_github"); + expect(api).to.be.greaterThan(up); + for (const name of ["new-1", "new-2", "new-3", "new-4"]) { + expect(result.calls.slice(up + 1, api).some(args => args[0] === "inspect" && args.at(-1) === name)).to.equal(true); + } + }); + it("stops recipient-v1 writers before restoring permanent recipient suppression", () => { + const result = deploy({ oldProtocol: "recipient-v1" }); + expect(result.status, result.stderr).to.equal(0); + const stop = result.calls.findIndex(args => args[0] === "compose" && args[1] === "stop"); + const reconcile = result.calls.findIndex(args => args[0] === "compose" && args[1] === "run"); + const up = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up"); + expect(stop).to.be.greaterThan(-1); expect(reconcile).to.be.greaterThan(stop); expect(up).to.be.greaterThan(reconcile); + }); + it("aborts before migration starts if a legacy writer remains running", () => { + const result = deploy({ refuseStop: true }); + expect(result.status).not.to.equal(0); + expect(result.stderr).to.include("still running"); + expect(result.calls.some(args => args[0] === "compose" && args[1] === "up")).to.equal(false); + }); + it("keeps old containers running if the image build fails", () => { + const result = deploy({ buildFails: true }); + expect(result.status).not.to.equal(0); + expect(result.calls.some(args => args[0] === "compose" && ["stop", "up"].includes(args[1]))).to.equal(false); + }); + it("keeps old containers running if the replacement lacks migration support", () => { + const result = deploy({ badProtocol: true }); + expect(result.status).not.to.equal(0); + expect(result.stderr).to.include("lacks recipient alert migration support"); + expect(result.calls.some(args => args[0] === "compose" && ["stop", "up"].includes(args[1]))).to.equal(false); + }); + it("starts no application instance if rollback reconciliation cannot be prepared", () => { + const result = deploy({ reconcileFails: true }); + expect(result.status).not.to.equal(0); + expect(result.calls.some(args => args[0] === "compose" && args[1] === "up")).to.equal(false); + }); + for (const stopped of [true, false]) it(`reconciles legacy ${stopped ? "stopped" : "running"} containers when the replacement was already built`, () => { + const result = deploy({ built: true, stopped, force: 0 }); + expect(result.status, result.stderr).to.equal(0); + const reconcile = result.calls.findIndex(args => args[0] === "compose" && args[1] === "run"); + const up = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up"); + expect(reconcile).to.be.greaterThan(-1); expect(up).to.be.greaterThan(reconcile); + expect(result.stdout).not.to.include("nothing to deploy"); + }); +}); diff --git a/test/download-header-performance.test.js b/test/download-header-performance.test.js index 15bb509..cc51757 100644 --- a/test/download-header-performance.test.js +++ b/test/download-header-performance.test.js @@ -43,17 +43,6 @@ describe("download headers and website revalidation", function () { expect(response.headers["etag"]).to.match(/^"f-/); expect(response.headers["content-type"]).to.include("text/plain"); expect(transfers).to.equal(0); }); - it("uses representation-safe validators for shared-cache HEAD and revalidation without reading bytes", async function () { - const previous = config.SHARED_CACHE_ENABLED; config.SHARED_CACHE_ENABLED = true; - try { - const path = "/api/repo/fixture/file/data.txt"; - const head = await fetch(path, "HEAD", { "accept-encoding": "gzip" }); - expect(head.status).to.equal(200); expect(head.headers.etag).to.match(/^W\/"f-/); - expect(head.headers.vary).to.include("Accept-Encoding"); expect(head.body).to.equal(""); - const fresh = await fetch(path, "GET", { "if-none-match": head.headers.etag, "accept-encoding": "identity" }); - expect(fresh.status).to.equal(304); expect(transfers).to.equal(0); - } finally { config.SHARED_CACHE_ENABLED = previous; } - }); for (const name of ["page.shtml", "drawing.svgz", "schema.xsd"]) { it(`sandboxes ${name} on GET, HEAD, and revalidation`, async function () { const path = `/api/repo/fixture/file/${name}`; @@ -83,6 +72,17 @@ describe("download headers and website revalidation", function () { expect(response.headers["content-disposition"]).to.include("attachment"); expect(response.headers["x-content-type-options"]).to.equal("nosniff"); }); + it("uses representation-safe validators for shared-cache HEAD and revalidation without reading bytes", async function () { + const previous = config.SHARED_CACHE_ENABLED; config.SHARED_CACHE_ENABLED = true; + try { + const path = "/api/repo/fixture/file/data.txt"; + const head = await fetch(path, "HEAD", { "accept-encoding": "gzip" }); + expect(head.status).to.equal(200); expect(head.headers.etag).to.match(/^W\/"f-/); + expect(head.headers.vary).to.include("Accept-Encoding"); expect(head.body).to.equal(""); + const fresh = await fetch(path, "GET", { "if-none-match": head.headers.etag, "accept-encoding": "identity" }); + expect(fresh.status).to.equal(304); expect(transfers).to.equal(0); + } finally { config.SHARED_CACHE_ENABLED = previous; } + }); it("preserves content gates for HEAD", async function () { repo.options.pdf = false; expect((await fetch("/api/repo/fixture/file/private.pdf", "HEAD")).status).to.equal(403); diff --git a/test/download-progress-fences.test.js b/test/download-progress-fences.test.js new file mode 100644 index 0000000..e5250ef --- /dev/null +++ b/test/download-progress-fences.test.js @@ -0,0 +1,78 @@ +const { expect } = require("chai"); +const { setImmediate } = require("timers"); +require("ts-node/register/transpile-only"); +const db = require("../src/server/database"); +const Model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; +const Repository = require("../src/core/Repository").default; +const gh = require("../src/core/GitHubUtils"); +const sift = require("sift").default; + +describe("download progress publication fences", function () { + const restores = []; + function stub(object, key, value) { const before = object[key]; restores.push(() => { object[key] = before; }); object[key] = value; } + afterEach(() => { while (restores.length) restores.pop()(); }); + function setup() { + const model = new Model({ repoId: "progress-test", status: "download", statusDate: new Date(1000), + owner: "507f1f77bcf86cd799439011", anonymizeDate: new Date(500), + source: { repositoryName: "owner/repo", commit: "abc", branch: "main" }, options: { terms: [], expirationMode: "never" } }); + const repo = new Repository(model); repo.protectLifecycle = true; + const stored = model.toObject(); + stub(db, "isConnected", true); + stub(Model, "updateOne", (filter, update) => ({ exec: async () => { + if (!sift(filter)(stored)) return { matchedCount: 0 }; + Object.assign(stored, update.$set); return { matchedCount: 1 }; + } })); + return { repo, stored }; + } + + it("keeps a captured publication fence valid when a queue progress tick runs", async function () { + const { repo, stored } = setup(); let tick; + stub(global, "setInterval", callback => { tick = callback; return 1; }); + stub(global, "clearInterval", () => {}); + stub(db, "connect", async () => {}); + stub(db, "getRepository", async () => repo); + stub(gh, "getToken", async () => "test-token"); + stub(gh, "getRedisGateResetAt", async () => 0); + stub(repo, "resetSate", async () => {}); + stub(repo, "anonymize", async progress => { + const fence = { status: stored.status, statusDate: stored.statusDate }; + progress("Writing files"); tick(); + await new Promise(resolve => setImmediate(resolve)); + expect(sift(fence)(stored), "progress invalidated the publication fence").to.equal(true); + expect(stored.statusMessage).to.equal("Writing files"); + }); + await require("../src/queue/processes/downloadRepository").default({ data: { repoId: repo.repoId }, updateProgress: async () => {} }); + expect(stored.status).to.equal("ready"); + }); + + it("keeps fresh long-running downloads busy without changing statusDate", async function () { + const { repo, stored } = setup(); const before = repo.model.statusDate; + await repo.updateProgressMessage("Writing files"); + expect(stored.statusDate).to.deep.equal(before); + expect(repo.model.statusDate).to.deep.equal(before); + expect(repo.statusActivityDate.getTime()).to.be.greaterThan(Date.now() - 1000); + try { await repo.check(); throw new Error("expected not ready"); } + catch (error) { expect(error.message).to.equal("repository_not_ready"); } + }); + + for (const replacement of ["ready", "removed", "download"]) { + it(`does not overwrite progress for a newer ${replacement} state`, async function () { + const { repo, stored } = setup(); + stored.status = replacement; stored.statusDate = new Date(2000); stored.statusMessage = "new operation"; + await repo.updateProgressMessage("old progress"); + expect(stored.statusMessage).to.equal("new operation"); + expect(repo.model.statusMessage).not.to.equal("old progress"); + expect(stored).not.to.have.property("progressDate"); + }); + } + + it("does not apply a completed progress write to a locally advanced status", async function () { + const { repo } = setup(); + stub(Model, "updateOne", () => ({ exec: async () => { + repo.model.status = "ready"; repo.model.statusDate = new Date(2000); repo.model.statusMessage = "done"; + return { matchedCount: 1 }; + } })); + await repo.updateProgressMessage("old progress"); + expect(repo.model.statusMessage).to.equal("done"); + }); +}); diff --git a/test/error-codes.test.js b/test/error-codes.test.js index ea1f52a..006617f 100644 --- a/test/error-codes.test.js +++ b/test/error-codes.test.js @@ -68,7 +68,9 @@ function extractBackendErrorCodes(files) { // Multi-line matching for AnonymousError calls that span lines // (e.g. ternary expressions where the string is on the next line) const multiLinePattern = - /AnonymousError\([\s\S]*?["']([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)["']/g; + // Stop at a closing parenthesis so a dynamic code cannot consume an + // unrelated string in a later statement, such as a monitoring label. + /AnonymousError\([^)]*?["']([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)["']/g; let m; while ((m = multiLinePattern.exec(content)) !== null) { const code = m[1]; diff --git a/test/fixtures/github-gate-cancellation.js b/test/fixtures/github-gate-cancellation.js new file mode 100644 index 0000000..2800643 --- /dev/null +++ b/test/fixtures/github-gate-cancellation.js @@ -0,0 +1,61 @@ +const assert = require("node:assert/strict"); +const { Readable } = require("node:stream"); +const { setImmediate: nextTurn, setTimeout: delay } = require("node:timers/promises"); +const { setTimeout, clearTimeout } = require("node:timers"); +const redis = require("redis"); +let reads = 0; +const resetAt = Date.now() + 10 * 60 * 1000; +let readGate = async () => String(resetAt); +const client = { isOpen: true, on() { return this; }, async connect() {}, + async get() { reads++; return readGate(); }, destroy() { this.isOpen = false; } }; +redis.createClient = () => client; +require("ts-node/register/transpile-only"); +const github = require("../../src/core/GitHubUtils"); +const GitHubStream = require("../../src/core/source/GitHubStream").default; +const got = require("got"); + +async function main() { + const preAborted = new AbortController(); preAborted.abort(); + await assert.rejects(github.waitForTokenGate("token", preAborted.signal), { name: "AbortError" }); + assert.equal(reads, 0); + + const firstController = new AbortController(), secondController = new AbortController(); + const first = github.waitForTokenGate("token", firstController.signal); + let secondSettled = false; + const second = github.waitForTokenGate("token", secondController.signal).finally(() => { secondSettled = true; }); + await nextTurn(); firstController.abort(); + await assert.rejects(first, { name: "AbortError" }); + await delay(20); assert.equal(secondSettled, false); + secondController.abort(); await assert.rejects(second, { name: "AbortError" }); + + // A shared Redis lookup may remain pending. Cancelling one caller must + // release that caller without closing the shared client. + readGate = () => new Promise(() => {}); + const lookupController = new AbortController(); + const lookup = github.waitForTokenGate("token", lookupController.signal); + await nextTurn(); lookupController.abort(); + await assert.rejects(lookup, { name: "AbortError" }); assert.equal(client.isOpen, true); + + let entered; + const gateEntered = new Promise(resolve => { entered = resolve; }); + readGate = async () => { entered(); return String(resetAt); }; + let metadataCalls = 0; + github.octokit = () => ({ repos: { getContent: async () => { metadataCalls++; throw Error("unexpected metadata request"); } } }); + got.stream = () => new Readable({ read() { + this.destroy(Object.assign(Error("private web 404"), { response: { statusCode: 404 } })); + } }); + const source = new GitHubStream({ repoId: "gate-cancel", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "token" }); + const input = source.deferredRawDownload("token", "file.bin"); + const collected = (async () => { for await (const chunk of input) void chunk; })().catch(() => {}); + await gateEntered; await nextTurn(); + const closed = new Promise(resolve => input.once("close", resolve)); input.destroy(); + await closed; await collected; + assert.equal(metadataCalls, 0); assert.equal(input.closed, true); +} + +const watchdog = setTimeout(() => { console.error("Cancellation did not release the gate wait"); process.exit(1); }, 5000); +watchdog.unref(); +main().then(() => { clearTimeout(watchdog); console.log("gate cancellations released"); }, + error => { clearTimeout(watchdog); console.error(error); process.exit(1); }); +// Do not force a successful exit: any retained ten-minute timer must fail +// the parent test's process timeout. diff --git a/test/fixtures/github-stream-failure.js b/test/fixtures/github-stream-failure.js new file mode 100644 index 0000000..faa4b28 --- /dev/null +++ b/test/fixtures/github-stream-failure.js @@ -0,0 +1,44 @@ +const assert = require("node:assert/strict"); +const { Readable } = require("node:stream"); +const { setImmediate } = require("node:timers"); +require("ts-node/register/transpile-only"); +const GitHubStream = require("../../src/core/source/GitHubStream").default; +const failure = Object.assign(new Error("upstream failed"), process.argv[2] === "reset" ? { code: "ECONNRESET" } : { response: { statusCode: 401 } }); +const source = new GitHubStream({ repoId: "fixture", organization: "owner", repoName: "repo", commit: "abc", getToken: () => "token" }); +async function main() { + if (process.argv[2] === "fatal") { + require("../../src/core/process-monitoring").installFatalErrorLogging("streamer"); + await new Promise(() => setImmediate(() => { throw new Error("fixture fatal"); })); + } + if (process.argv[2] === "fallback" || process.argv[2] === "public-raw") { + source.downloadFile = () => { + const input = new Readable({ read() {} }); + process.nextTick(() => input.destroy(Object.assign(new Error("blob unavailable"), { response: { statusCode: 404 } }))); + return input; + }; + source.downloadFileViaRaw = () => { + const input = new Readable({ read() {} }); + process.nextTick(() => input.destroy(failure)); + return input; + }; + const content = await source.downloadWithFallback(process.argv[2] === "public-raw" ? "" : "token", "blob", "file"); + await new Promise(resolve => setImmediate(resolve)); + await assert.rejects(async () => { for await (const chunk of content) void chunk; }, error => error === failure); + } else if (process.argv[2] === "lfs") { + const pointer = "version https://git-lfs.github.com/spec/v1\noid sha256:" + "a".repeat(64) + "\nsize 10\n"; + source.downloadFileViaRaw = () => new Readable({ read() { this.destroy(failure); } }); + const content = source.resolveLfsPointer(Readable.from([pointer]), "token", "file"); + // A producer must not start emitting errors while its consumer is still awaiting. + await new Promise(resolve => setImmediate(resolve)); + await assert.rejects(async () => { for await (const chunk of content) void chunk; }, error => error === failure); + } else { + source.downloadFile = () => { + const content = new Readable({ read() {} }); + process.nextTick(() => content.destroy(failure)); + return content; + }; + await assert.rejects(source.downloadWithFallback("token", "blob", "file"), error => error === failure); + await new Promise(resolve => setImmediate(resolve)); + } +} +main().then(() => { process.stdout.write("failure contained\n"); process.exit(0); }, error => { console.error(error); process.exit(1); }); diff --git a/test/fixtures/large-repository-tree.js b/test/fixtures/large-repository-tree.js new file mode 100644 index 0000000..b181367 --- /dev/null +++ b/test/fixtures/large-repository-tree.js @@ -0,0 +1,27 @@ +// Synthetic metadata only. No network or database is used by this process. +const assert = require("node:assert/strict"); +require("ts-node/register/transpile-only"); +const GitHubStream = require("../../src/core/source/GitHubStream").default; +const source = new GitHubStream({ repoId: "heap-fixture", organization: "fixture", repoName: "large", + commit: "head", getToken: () => "fixture-token" }); +const pageSize = 10000, pages = Number(process.env.TREE_FIXTURE_PAGES || 200); +source.getGHTree = async (_oct, _token, sha, _count, options) => { + if (sha === "head") return options.recursive + ? { truncated: true, tree: [] } + : { truncated: false, tree: Array.from({ length: pages }, (_, i) => ({ path: `folder${i}`, type: "tree", sha: String(i) })) }; + return { truncated: false, tree: Array.from({ length: pageSize }, (_, i) => ({ + path: `file${i}.txt`, type: "blob", sha: "a".repeat(40), size: i, + })) }; +}; +(async () => { + let count = 0, bytes = 0; + const files = source.iterateFiles ? source.iterateFiles() : await source.getFiles(); + for await (const file of files) { + assert.equal(file.repoId, "heap-fixture"); + if (file.size !== undefined) { bytes += file.size; assert.ok(file.path.startsWith("folder")); } + count++; + } + assert.equal(count, pages * (pageSize + 1)); + assert.equal(bytes, pages * pageSize * (pageSize - 1) / 2); + console.log(JSON.stringify({ count, bytes, peakRssMiB: process.resourceUsage().maxRSS / 1024 })); +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/test/fixtures/performance-streamer.js b/test/fixtures/performance-streamer.js index 3949f34..ee03454 100644 --- a/test/fixtures/performance-streamer.js +++ b/test/fixtures/performance-streamer.js @@ -9,7 +9,18 @@ const maintenance = require(path.join(base, "core/temporary-storage")); const startMaintenance = maintenance.startTemporaryStorageMaintenance; // Exercise the real startup/timer wiring without waiting a production minute. maintenance.startTemporaryStorageMaintenance = () => startMaintenance(25); -GitHubStream.prototype.downloadWithFallback = async function () { +const downloadWithFallback = GitHubStream.prototype.downloadWithFallback; +GitHubStream.prototype.downloadWithFallback = async function (token, sha, filePath) { + if (filePath.startsWith("fail-")) { + this.downloadFile = () => { + const input = new Readable({ read() {} }); + const failure = Object.assign(new Error("fixture upstream failure"), filePath === "fail-reset.txt" + ? { code: "ECONNRESET" } : { response: { statusCode: 401 } }); + process.nextTick(() => input.destroy(failure)); + return input; + }; + return downloadWithFallback.call(this, token, sha, filePath); + } process.send({ download: true }); return Readable.from([Buffer.alloc(300000, "x")]); }; diff --git a/test/fixtures/review-intent-client.js b/test/fixtures/review-intent-client.js new file mode 100644 index 0000000..d15216b --- /dev/null +++ b/test/fixtures/review-intent-client.js @@ -0,0 +1,46 @@ +require("ts-node/register/transpile-only"); +const process = require("process"); +const { setTimeout } = require("timers"); +const { AbortController } = globalThis; +const { + createReviewIntentConsumer, +} = require("../../src/server/service/review-intent-client"); + +process.on("message", async ({ id, config, input, mode }) => { + try { + const client = createReviewIntentConsumer(config); + const controller = new AbortController(); + if (mode === "preabort") controller.abort(); + if (mode === "abort") setTimeout(() => controller.abort(), 100); + if (mode === "mutate") { + config.token = "invalid"; + config.origin = "https://invalid.example"; + } + if (mode === "parallel") { + const pending = Array.from({ length: 5 }, () => + client.consume(input, controller.signal), + ); + const results = await Promise.allSettled(pending); + const followup = await client.consume(input); + process.send({ + id, + results: results.map((r) => + r.status === "fulfilled" ? "ok" : r.reason.kind, + ), + followup, + }); + return; + } + const started = Date.now(); + const result = await client.consume(input, controller.signal); + process.send({ + id, + result, + elapsedMs: Date.now() - started, + frozen: Object.isFrozen(result) && Object.isFrozen(result.policy), + serialized: JSON.stringify(client), + }); + } catch (error) { + process.send({ id, kind: error.kind, message: error.message }); + } +}); diff --git a/test/fixtures/shared-cache-worker.js b/test/fixtures/shared-cache-worker.js index 7f6d3d0..84f1194 100644 --- a/test/fixtures/shared-cache-worker.js +++ b/test/fixtures/shared-cache-worker.js @@ -12,12 +12,14 @@ if (process.env.BLOB_TEST_CRASH_AFTER_RENAME) { await new Promise(() => {}); // Parent kills the process before index publication. }; } +if (process.env.BLOB_TEST_DISK_BYTES) require("node:fs").statfsSync = () => ({ bavail: Number(process.env.BLOB_TEST_DISK_BYTES), bsize: 1 }); process.send("ready"); process.once("message", async () => { try { const stream = await cache.getOrFill("multiprocess", async () => { process.send("fill"); - await new Promise(resolve => setTimeout(resolve, 100)); + if (process.env.BLOB_TEST_HOLD_FILL) await new Promise(resolve => process.once("message", resolve)); + else await new Promise(resolve => setTimeout(resolve, 100)); return Readable.from("shared across processes"); }); let body = ""; for await (const chunk of stream) body += chunk; diff --git a/test/folder-counts.test.js b/test/folder-counts.test.js new file mode 100644 index 0000000..2f1f1ec --- /dev/null +++ b/test/folder-counts.test.js @@ -0,0 +1,107 @@ +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const mongoose = require("mongoose"); +const db = require("../src/server/database"); +const Repository = require("../src/core/Repository").default; +const Repo = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; +const File = require("../src/core/model/files/files.model").default; +const Path = require("../src/core/model/anonymized-path").default; +const { folderCounts } = require("../src/core/folder-counts"); +const { FILE_LISTING_LIMIT } = require("../src/core/file-tree"); +const uri = process.env.PERFORMANCE_MONGO_URI; + +describe("folder count request validation", () => { + for (const value of [[], {}, 1, "../private", "a/../b", "/src", "src/", "x".repeat(4097)]) { + it(`rejects invalid scope ${JSON.stringify(value).slice(0, 30)}`, async () => { + const error = await folderCounts({}, value).then(() => null, error => error); + expect(error.message).to.equal("invalid_path"); expect(error.httpStatus).to.equal(400); + }); + } +}); + +(uri ? describe : describe.skip)("scoped folder counts with MongoDB", function () { + this.timeout(60000); + let repo; + before(async () => { + if (!/\/perf_test_[A-Za-z0-9_-]+(?:\?|$)/.test(uri)) throw Error("An isolated perf_test_ database is required"); + await mongoose.connect(uri); db.isConnected = true; + await Promise.all([Repo, File, Path].map(model => model.createIndexes())); + }); + after(async () => { db.isConnected = false; await mongoose.connection.dropDatabase(); await mongoose.disconnect(); }); + beforeEach(async () => { + await Promise.all([Repo, File, Path].map(model => model.deleteMany({}))); + repo = new Repository(await Repo.create({ repoId: "counts", owner: new mongoose.Types.ObjectId(), status: "ready", + statusDate: new Date(), anonymizeDate: new Date(), treeGeneration: "active", source: { type: "GitHubStream", repositoryName: "fixture/repo" }, options: { terms: [] } })); + }); + const insert = rows => File.insertMany(rows.map(row => ({ repoId: "counts", treeGeneration: "active", ...row }))); + + it("returns only the current directory and immediate children across a large deep tree", async () => { + await insert([{ name: "src", path: "" }, { name: "root.txt", path: "", size: 1 }]); + for (let offset = 0; offset < 12000; offset += 500) await insert(Array.from({ length: 500 }, (_, i) => ({ + name: "file.txt", path: `src/deep/folder${offset + i}`, size: 1, + }))); + await insert([{ name: "ignored.txt", path: "staged", size: 1, treeGeneration: "staged" }, + { name: "ignored.txt", path: "old", size: 1, treeGeneration: "old" }]); + expect(await folderCounts(repo, "")).to.deep.equal({ "": 12001, src: 12000 }); + expect(await folderCounts(repo, "src")).to.deep.equal({ src: 12000, "src/deep": 12000 }); + }); + + it("escapes regex punctuation and handles Unicode scopes and empty directories", async () => { + await insert([{ path: "", name: "猫.[x]" }, { path: "猫.[x]", name: "empty" }, + { path: "猫.[x]/child", name: "a", size: 0 }, { path: "猫.[x]", name: "b", size: 2 }, + { path: "猫Xx/not-this-folder", name: "no", size: 3 }]); + expect(await folderCounts(repo, "猫.[x]")).to.deep.equal({ "猫.[x]": 2, "猫.[x]/child": 1 }); + expect(await folderCounts(repo, "猫.[x]/empty")).to.deep.equal({ "猫.[x]/empty": 0 }); + }); + + it("anonymizes child keys, sums collisions and resolves the same directory as browsing", async () => { + repo.model.options.terms = ["Alice|Bob"]; + await repo.model.save(); + await insert([{ name: "Alice", path: "" }, { name: "Bob", path: "" }, + { name: "one", path: "Alice/sub", size: 1 }, { name: "two", path: "Bob/sub", size: 1 }]); + const root = await folderCounts(repo, ""); + expect(root).to.deep.equal({ "": 2, "XXXX-1": 2 }); + const scoped = await folderCounts(repo, "XXXX-1"); + expect(scoped).to.deep.equal({ "XXXX-1": 1, "XXXX-1/sub": 1 }); + expect(JSON.stringify(scoped)).not.to.match(/Alice|Bob/); + }); + + for (const name of ["$data", "$$ROOT", "$$CURRENT"]) it(`treats directory ${name} as a literal in aggregation expressions`, async () => { + await insert([{ name, path: "" }, { name: "direct.txt", path: name, size: 1 }, + { name: "$nested", path: name }, { name: "child.txt", path: `${name}/$nested`, size: 1 }]); + expect(await folderCounts(repo, name)).to.deep.equal({ [name]: 2, [`${name}/$nested`]: 1 }); + expect(await folderCounts(repo, `${name}/$nested`)).to.deep.equal({ [`${name}/$nested`]: 1 }); + }); + + it("uses the active generation on each request without reusing obsolete counts", async () => { + await insert([{ path: "old", name: "a", size: 1 }]); + expect(await folderCounts(repo, "")).to.deep.equal({ "": 1, old: 1 }); + await insert([{ path: "new", name: "b", size: 1, treeGeneration: "replacement" }, + { path: "new", name: "c", size: 1, treeGeneration: "replacement" }]); + repo.model.treeGeneration = "replacement"; await repo.model.save(); + expect(await folderCounts(new Repository(await Repo.findById(repo.model._id)), "")).to.deep.equal({ "": 2, new: 2 }); + }); + + it("keeps a hard bound for over-wide responses", async () => { + for (let offset = 0; offset <= FILE_LISTING_LIMIT; offset += 500) await insert(Array.from({ + length: Math.min(500, FILE_LISTING_LIMIT + 1 - offset), + }, (_, i) => ({ name: "a", path: `folder${offset + i}`, size: 1 }))); + const error = await folderCounts(repo, "").then(() => null, error => error); + expect(error.message).to.equal("file_listing_too_large"); expect(error.httpStatus).to.equal(413); + }); + + it("runs the scoped route only after authorization and rejects object scopes", async () => { + const utils = require("../src/server/routes/route-utils"); + const original = { getRepo: utils.getRepo, handleError: utils.handleError }; + const router = require("../src/server/routes/repository-public").default; + const handler = router.stack.find(layer => layer.route?.path === "/:repoId/files/counts").route.stack.at(-1).handle; + const res = { header() {}, json() { throw Error("must not return counts"); } }; + try { + utils.getRepo = async () => null; + await handler({ query: { path: {} } }, res); + utils.getRepo = async () => repo; utils.handleError = error => { throw error; }; + const error = await handler({ query: { path: {} } }, res).then(() => null, error => error); + expect(error.httpStatus).to.equal(400); + } finally { Object.assign(utils, original); } + }); +}); diff --git a/test/frontend-regressions.test.js b/test/frontend-regressions.test.js index 2537738..534f807 100644 --- a/test/frontend-regressions.test.js +++ b/test/frontend-regressions.test.js @@ -3,6 +3,7 @@ const fs = require("fs"); const vm = require("vm"); const path = require("path"); const { setImmediate } = require("timers"); +const { URLSearchParams } = require("node:url"); const source = fs.readFileSync(path.join(__dirname, "../public/script/app.js"), "utf8"); function harness(date) { @@ -11,7 +12,7 @@ function harness(date) { const context = { reactive: value => value, console, Map, Set, Date: date || Date, formDraft: state => ({ restore: () => false, clear() {}, discard() {}, checkpoint: () => ({ terms: state.terms, options: state.options }), accept() {} }), navigator: { platform: "Linux" }, document: { location: { pathname: "/r/repo" }, addEventListener() {}, querySelector() {} }, - window: { addEventListener() {}, removeEventListener() {} }, Prism: { highlightAll() {} }, encodeURIComponent, + URLSearchParams, window: { location: { search: "" }, addEventListener() {}, removeEventListener() {} }, Prism: { highlightAll() {} }, encodeURIComponent, encodePathForUrl: p => p.split("/").map(encodeURIComponent).join("/"), humanFileSize: x => String(x), loadEditor: async () => {}, parseGithubUrl: () => ({ owner: "owner", repo: "repo" }), $: () => ({ on() {}, tooltip() {} }), @@ -20,6 +21,7 @@ function harness(date) { }; context.createTimers = () => ({ timeout: Object.assign(context.setTimeout, { cancel: context.clearTimeout }), interval: Object.assign(context.setInterval, { cancel: context.clearInterval }) }); context.createListeners = () => (target, name, callback) => target.addEventListener(name, callback); + vm.runInNewContext(fs.readFileSync(path.join(__dirname, "../public/script/billing.js"), "utf8").replaceAll("export function", "function"), context); const names = [...source.matchAll(/export const (\w+)/g)].map(match => match[1]); vm.runInNewContext(source.replace(/^import .*;$/gm, "").replace(/export const/g, "var") + "\nthis.pageSetups = {" + names.join(",") + "};", context); Object.assign(defs, context.pageSetups); @@ -59,7 +61,7 @@ describe("frontend production regressions", function () { for (const id of ["repoId", "pullRequestId", "gistId"]) { it(`preserves saved options when reopening a conference-linked ${id}`, async function () { const h = anonymize({ [id]: "saved" }); - h.requests[0].resolve({ data: { options: { update: true } } }); await h.flush(); + h.requests.find(r => r.url === "/api/user/default").resolve({ data: { options: { update: true } } }); await h.flush(); h.requests.at(-1).resolve({ data: { source: { fullName: "owner/repo", repositoryFullName: "owner/repo", branch: "main", commit: "abcdef", gistId: "123", pullRequestId: 1 }, conference: "conf", @@ -226,3 +228,41 @@ describe("frontend production regressions", function () { expect(h.scope.options.endDate.getTime()).to.be.greaterThan(h.scope.options.startDate.getTime()); }); }); + +describe("redaction highlighting", () => { + const { JSDOM } = require("jsdom"); + function helpers() { + const { window } = new JSDOM(""); + const h = harness(); + Object.assign(h.context, { DOMParser: window.DOMParser, NodeFilter: window.NodeFilter }); + return h.defs; + } + it("matches the mask with and without a counter", () => { + const { redactionPattern } = helpers(); + expect("by XXXX-1 and XXXX-12 at XXXX.".match(redactionPattern("XXXX"))).to.deep.equal(["XXXX-1", "XXXX-12", "XXXX"]); + expect("a.b-3 axb-3".match(redactionPattern("a.b"))).to.deep.equal(["a.b-3"]); + }); + it("marks redactions in text without touching attributes", () => { + const { highlightRedactions } = helpers(); + const html = highlightRedactions('

    By XXXX-1 link

    ', "XXXX"); + expect(html).to.include('By XXXX-1'); + expect(html).to.include('href="https://x.test/XXXX-2"'); + expect(highlightRedactions("

    nothing here

    ", "XXXX")).to.equal("

    nothing here

    "); + }); + it("leaves Mermaid sources and Prism code blocks for their renderers", () => { + const { highlightRedactions } = helpers(); + const html = highlightRedactions('
    graph TD; XXXX-1-->B
    print("XXXX-2")
    XXXX-3', "XXXX"); + expect(html).to.include('
    graph TD; XXXX-1-->B
    '); + expect(html).to.include('print("XXXX-2")'); + expect(html).to.include('XXXX-3'); + }); + it("marks a highlighted code block when Prism hands it over, using the configured mask", () => { + const { window } = new (require("jsdom").JSDOM)('
    "MASK-4"
    '); + const h = harness(); + Object.assign(h.context, { DOMParser: window.DOMParser, NodeFilter: window.NodeFilter }); + h.defs.setRedactionMask("MASK"); + const code = window.document.querySelector("code"); + h.defs.markRedactionsIn(code); + expect(code.querySelector("mark.redaction").textContent).to.equal("MASK-4"); + }); +}); diff --git a/test/github-app.test.js b/test/github-app.test.js index 6b92ea6..d984da1 100644 --- a/test/github-app.test.js +++ b/test/github-app.test.js @@ -95,7 +95,11 @@ describe("GitHub App protocol boundaries", () => { const GitHubStream = require("../src/core/source/GitHubStream").default; const source = Object.create(GitHubStream.prototype); source.data = { organization: "other", repoName: "public", commit: "abc" }; - await source.downloadWithFallback(handle, "blob", "README.md"); + const content = await source.downloadWithFallback(handle, "blob", "README.md"); + expect(captured).to.equal(undefined); + let body = ""; + for await (const chunk of content) body += chunk; + expect(body).to.equal("file"); expect(captured.url).to.include("other/public"); expect(captured.url).to.include("README.md"); expect(captured.options.headers.authorization).to.equal(undefined); @@ -534,7 +538,34 @@ describeMongo("GitHub App credential and repository integration", function () { expect(session.githubAppFlow).to.equal(undefined); const replay = await request("/github/app/callback?state=state&code=code"); expect(replay.status).to.equal(400); - expect(calls).to.have.length(2); + expect(calls).to.have.length(3); + }); + it("saves a verified private email during App registration", async () => { + authenticated = false; + session.githubAppFlow = { state: "state", expires: Date.now() + 60000, returnTo: "/dashboard" }; + mock(url => url.includes("/login/oauth/access_token") ? data() : url.includes("/user/emails?") + ? [{ email: "private@example.com", primary: true, verified: true }] + : { id: 20, login: "new-owner" }); + expect((await request("/github/app/callback?state=state&code=code")).location).to.equal("/dashboard"); + const created = await Users.findOne({ "externalIDs.github": "20" }); + expect(created.emails[0].email).to.equal("private@example.com"); + expect(created.emails[0].default).to.equal(true); + }); + it("preserves an email preference saved while App sign-in retrieves emails", async () => { + authenticated = false; + session.githubAppFlow = { state: "state", expires: Date.now() + 60000, returnTo: "/dashboard" }; + mock(async url => { + if (url.includes("/login/oauth/access_token")) return data(); + if (url.includes("/user/emails?")) { + await Users.updateOne({ _id: owner._id }, { $set: { notificationEmail: "chosen@example.com" } }); + return [{ email: "github@example.com", primary: true, verified: true }]; + } + return { id: 10, login: "owner" }; + }); + expect((await request("/github/app/callback?state=state&code=code")).location).to.equal("/dashboard"); + const current = await Users.findById(owner.id); + expect(current.notificationEmail).to.equal("chosen@example.com"); + expect(current.emails).to.have.length(0); }); it("signs an existing user in through the App without changing legacy access", async () => { authenticated = false; @@ -622,12 +653,54 @@ describeMongo("GitHub App credential and repository integration", function () { const body = { type: "repository", id: resource.repoId, connection: "github-app" }; expect((await request("/github/connections/migrate", { ...body, preview: true })).status).to.equal(200); expect((await CachedRepos.findById(cached.id)).name).to.equal("other/old-public"); - expect((await request("/github/connections/migrate", body)).status).to.equal(200); + const repaired = await request("/github/connections/migrate", body); + expect(repaired.status).to.equal(200); + expect(repaired.data.fullName).to.equal("other/public"); expect((await Repos.findById(resource.id)).githubAccess.publicRead).to.equal(true); expect((await Repos.findById(resource.id)).source.repositoryName).to.equal("other/public"); expect((await CachedRepos.findById(cached.id)).name).to.equal("other/public"); }); + for (const permission of ["granted", "missing", "replacement", "missing-commit"]) { + it(`repairs a public binding after the source becomes private (${permission})`, async () => { + const Repos = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; + await app.saveAppGrant(owner.id, data()); + const resource = await Repos.create({ repoId: "visibility-" + permission, owner: owner.id, status: "ready", + source: { type: "GitHubStream", repositoryName: "owner/repo", commit: "pinned", branch: "main" }, + options: { terms: ["author"], update: false }, + githubAccess: { kind: "github-app", repositoryId: 42, publicRead: true, revision: "original" } }); + const before = resource.toObject(); + mock(url => { + if (url.includes("/user/installations?")) return { installations: [{ id: 4, app_id: 123, account: { id: 10, login: "owner", type: "User" } }] }; + if (url.includes("/user/installations/4/repositories")) return { repositories: permission === "missing" ? [] : [ + { id: permission === "replacement" ? 99 : 42, full_name: "owner/repo", private: true, visibility: "private" }, + ] }; + if (url.endsWith("/repos/owner/repo") || url.endsWith("/repositories/42")) return { id: 42, full_name: "owner/repo", private: true, visibility: "private" }; + if (url.endsWith("/app/installations/4")) return { app_id: 123, permissions: { contents: "read", metadata: "read" } }; + if (url.endsWith("/access_tokens")) return { token: "ghs_private", expires_at: new Date(Date.now() + 3600000).toISOString() }; + if (url.endsWith("/repos/owner/repo/commits/pinned")) return permission === "missing-commit" ? { status: 404 } : { sha: "pinned" }; + throw new Error("Unexpected request " + url); + }); + await rejects(app.boundAppToken(owner.id, resource.githubAccess, "owner/repo"), "github_app_access_required"); + const result = await request("/github/connections/migrate", { type: "repository", id: resource.repoId, connection: "github-app" }); + const after = await Repos.findById(resource.id); + expect(after.repoId).to.equal(before.repoId); + expect(after.toObject().source).to.deep.equal(before.source); + expect(after.toObject().options).to.deep.equal(before.options); + expect(after.status).to.equal("ready"); + if (permission === "granted") { + expect(result.status).to.equal(200); + expect(after.githubAccess.publicRead).not.to.equal(true); + expect(after.githubAccess.installationId).to.equal(4); + expect(after.githubAccess.repositoryId).to.equal(42); + expect(await app.boundAppToken(owner.id, after.githubAccess, "owner/repo")).to.equal("ghs_private"); + } else { + expect(result.status).to.equal(403); + expect(after.toObject().githubAccess).to.deep.equal(before.githubAccess); + } + }); + } + it("recovers a renamed private pull request through its stable repository ID", async () => { const PRs = require("../src/core/model/anonymizedPullRequests/anonymizedPullRequests.model").default; await app.saveAppGrant(owner.id, data()); diff --git a/test/github-submodules.test.js b/test/github-submodules.test.js new file mode 100644 index 0000000..4afbf79 --- /dev/null +++ b/test/github-submodules.test.js @@ -0,0 +1,342 @@ +const { expect } = require("chai"); +const { Readable, PassThrough } = require("node:stream"); +const { once } = require("node:events"); +const { setImmediate } = require("node:timers"); +const archiver = require("archiver"); +const got = require("got"); +require("ts-node/register/transpile-only"); +const utils = require("../src/core/GitHubUtils"); +const { expandSubmodules, submoduleRepository, validateMounts, MAX_SUBMODULES, MAX_SUBMODULE_ENTRIES } = require("../src/core/source/github-submodules"); +const GitHubStream = require("../src/core/source/GitHubStream").default; +const FileModel = require("../src/core/model/files/files.model").default; +const storage = require("../src/core/storage").default; +const { streamAnonymizedZip } = require("../src/core/zipStream"); +const config = require("../src/config").default; +const sha = "a".repeat(40), blob = "b".repeat(40); +const parent = { repoId: "submodule-test", organization: "parent", repoName: "repo", commit: "c".repeat(40), getToken: () => "parent-private-token" }; +const upstream = { organization: "child", repoName: "public", commit: sha, path: "src/Alice.txt" }; +const mount = () => new FileModel({ repoId: parent.repoId, name: "dependency", path: "vendor", sha, submodule: "unsupported" }); + +async function zip(entries) { + const archive = archiver("zip"); const chunks = []; + archive.on("data", chunk => chunks.push(chunk)); const done = once(archive, "end"); + for (const [name, content] of Object.entries(entries)) archive.append(content, { name }); + await archive.finalize(); await done; return Buffer.concat(chunks); +} + +async function readZip(options) { + const response = new PassThrough(); const parser = require("unzip-stream").Parse(); + const entries = [], files = {}; + parser.on("entry", entry => { + const chunks = []; entry.on("data", chunk => chunks.push(chunk)); + entries.push(once(entry, "end").then(() => { files[entry.path] = Buffer.concat(chunks).toString(); })); + }); + response.pipe(parser); const done = once(parser, "finish"); + await streamAnonymizedZip(options, response); await done; await Promise.all(entries); + return files; +} + +const zipOptions = { ...parent, getToken: () => "", anonymizerOptions: { terms: ["Alice"], image: true, link: true }, + submodules: [{ organization: "child", repoName: "public", commit: sha, path: "vendor/dependency" }] }; + +describe("public GitHub submodules", function () { + this.timeout(10000); + const restore = []; + function stub(object, key, value) { const previous = object[key]; object[key] = value; restore.push(() => { object[key] = previous; }); } + afterEach(() => restore.splice(0).reverse().forEach(fn => fn())); + beforeEach(() => stub(utils, "waitForTokenGate", async () => {})); + + function api(tree = [{ path: "src/Alice.txt", type: "blob", sha: blob, size: 5 }], options = {}) { + const calls = []; + stub(utils, "octokit", token => ({ + repos: { getContent: async args => { + calls.push({ method: "contents", token, ...args }); + return { data: { submodule_git_url: options.url || "https://github.com/child/public.git", sha } }; + } }, + git: { getTree: async args => { + calls.push({ method: "tree", token, ...args }); + if (options.error) throw options.error; + return { data: { tree, truncated: options.truncated || false } }; + } }, + })); + return calls; + } + + it("mounts a pinned child tree without forwarding parent credentials or exposing .gitmodules", async () => { + const calls = api([ + { path: ".gitmodules", type: "blob", sha: blob, size: 100 }, + { path: "src", type: "tree", sha: blob }, + { path: "src/Alice.txt", type: "blob", sha: blob, size: 5 }, + { path: "nested", type: "commit", sha }, + ]); + const files = await expandSubmodules([mount(), { name: ".gitmodules", path: "" }], parent); + expect(files.map(file => file.name)).to.deep.equal(["dependency", "src", "Alice.txt", "nested"]); + expect(files[0].submodule).to.equal("included"); + expect(files[2]).to.include({ path: "vendor/dependency/src", sha: blob, size: 5 }); + expect(files[2].upstream).to.deep.equal(upstream); + expect(files[3].submodule).to.equal("nested"); + expect(calls).to.deep.equal([ + { method: "contents", token: "parent-private-token", owner: "parent", repo: "repo", path: "vendor/dependency", ref: parent.commit }, + { method: "tree", token: "", owner: "child", repo: "public", tree_sha: sha, recursive: "1" }, + ]); + expect(new FileModel(files[2]).toObject().upstream).to.deep.equal(upstream); + }); + + it("discovers submodules through scoped public-read contexts without authorizing child or write requests", async () => { + const { registerGitHubToken } = require("../src/core/github-token-context"); + const token = "public-read:submodule-discovery"; + registerGitHubToken(token, { publicRepository: "parent/repo", quotaKey: "submodule-test", + renew: async () => "parent-user-token" }); + const calls = []; + stub(globalThis, "fetch", async (url, options) => { + calls.push({ url: String(url), authorization: new globalThis.Headers(options.headers).get("authorization") }); + const result = String(url).includes("/contents/") + ? { submodule_git_url: "https://github.com/child/public.git", sha } + : { tree: [{ path: "src/Alice.txt", type: "blob", sha: blob, size: 5 }], truncated: false }; + return new globalThis.Response(JSON.stringify(result), { headers: { "content-type": "application/json" } }); + }); + const files = await expandSubmodules([mount()], { ...parent, getToken: () => token }); + expect(files[0].submodule).to.equal("included"); + expect(calls).to.deep.equal([ + { url: `https://api.github.com/repos/parent/repo/contents/vendor%2Fdependency?ref=${parent.commit}`, authorization: "token parent-user-token" }, + { url: `https://api.github.com/repos/child/public/git/trees/${sha}?recursive=1`, authorization: null }, + ]); + for (const request of [ + "GET /repos/child/public/contents/file.txt", "GET /repos/parent/repo-other/contents/file.txt", + "PUT /repos/parent/repo/contents/file.txt", "DELETE /repos/parent/repo/contents/file.txt", + ]) { + let caught; try { await utils.octokit(token).request(request); } catch (error) { caught = error; } + expect(caught?.message).to.equal("github_app_access_required"); + } + expect(calls).to.have.length(2); + }); + + it("recognizes gitlinks from the parent tree and leaves ordinary repositories at one request", async () => { + const source = new GitHubStream(parent); let calls = 0; + source.getGHTree = async () => { calls++; return { tree: [{ path: "README.md", type: "blob", sha: blob, size: 5 }] }; }; + expect((await source.getFiles()).map(file => file.name)).to.deep.equal(["README.md"]); expect(calls).to.equal(1); + api(); + source.getGHTree = async () => ({ tree: [{ path: "vendor/dependency", type: "commit", mode: "160000", sha }] }); + expect((await source.getFiles())[0].submodule).to.equal("included"); + }); + + it("supports relative and SSH GitHub URLs and rejects other hosts and unsafe paths", () => { + for (const url of ["../public.git", "git@github.com:parent/public.git", "ssh://git@github.com/parent/public.git", "https://github.com/parent/public.git"]) { + expect(submoduleRepository(url, parent)).to.deep.equal({ organization: "parent", repoName: "public" }); + } + for (const url of ["https://evil.test/child/repo", "file:///tmp/repo", "https://github.com@evil.test/a/b", "https://github.com/a/b?token=secret", "https://github.com/a%2fb/c"]) expect(submoduleRepository(url, parent)).to.equal(null); + for (const path of ["../escape", "/absolute", "a/../b", "a\\b", "a//b", "a\0b"]) expect(() => validateMounts([{ ...upstream, path }])).to.throw("invalid_request"); + expect(() => validateMounts([{ ...upstream, path: "lib" }, { ...upstream, path: "lib/nested" }])).to.throw("invalid_request"); + }); + + it("leaves private, missing and external children unsupported without blocking parent files", async () => { + api([], { error: Object.assign(new Error("not found"), { status: 404 }) }); + const result = await expandSubmodules([mount(), { name: "README.md", path: "", size: 3 }], parent); + expect(result[0].submodule).to.equal("unavailable"); expect(result[1].name).to.equal("README.md"); + const calls = api([], { url: "https://gitlab.com/child/repo.git" }); + expect((await expandSubmodules([mount()], parent))[0].submodule).to.equal("unsupported"); + expect(calls).to.have.length(1); + }); + + it("keeps parent files when a child gitlink points to an invalid Git object", async () => { + const error = Object.assign(new Error("invalid object"), { status: 422, response: { data: { + message: "Invalid object requested. SHA must identify a commit or a tree.", + } } }); + api([], { error }); + const source = new GitHubStream(parent); + source.getGHTree = async () => ({ tree: [ + { path: "vendor/dependency", type: "commit", mode: "160000", sha }, + { path: "README.md", type: "blob", sha: blob, size: 5 }, + ] }); + const files = await source.getFiles(); + expect(files.find(file => file.name === "dependency").submodule).to.equal("unavailable"); + expect(files.find(file => file.name === "dependency").upstream).not.to.exist; + expect(files.find(file => file.name === "README.md").sha).to.equal(blob); + }); + + it("does not suppress invalid-object errors from parent metadata", async () => { + const error = Object.assign(new Error("invalid parent"), { status: 422, response: { data: { + message: "Invalid object requested. SHA must identify a commit or a tree.", + } } }); + stub(utils, "octokit", () => ({ repos: { getContent: async () => { throw error; } } })); + let caught; try { await expandSubmodules([mount()], parent); } catch (err) { caught = err; } + expect(caught).to.equal(error); + }); + + for (const [status, message] of [[422, "Validation Failed"], [422, ""], [429, "rate limited"], [500, "server error"]]) { + it(`preserves child failure ${status}: ${message}`, async () => { + const error = Object.assign(new Error(message), { status, response: { data: { message } } }); + api([], { error }); + let caught; try { await expandSubmodules([mount()], parent); } catch (err) { caught = err; } + expect(caught).to.equal(error); + }); + } + + it("does not persist transient failures as permanently missing dependencies", async () => { + const error = Object.assign(new Error("rate limited"), { status: 403 }); api([], { error }); + let caught; try { await expandSubmodules([mount()], parent); } catch (err) { caught = err; } + expect(caught).to.equal(error); + }); + + it("bounds module count and rejects oversized or truncated child trees", async () => { + const calls = api(); + const mounts = Array.from({ length: MAX_SUBMODULES + 1 }, (_, i) => ({ ...mount().toObject(), name: `dependency${i}` })); + const result = await expandSubmodules(mounts, parent); + expect(result[MAX_SUBMODULES].submodule).to.equal("limit"); expect(calls).to.have.length(MAX_SUBMODULES * 2); + api(Array.from({ length: MAX_SUBMODULE_ENTRIES + 1 }, (_, i) => ({ path: `file${i}`, type: "blob", sha: blob, size: 1 }))); + expect((await expandSubmodules([mount()], parent))[0].submodule).to.equal("limit"); + api([], { truncated: true }); expect((await expandSubmodules([mount()], parent))[0].submodule).to.equal("limit"); + }); + + for (const hydrated of [false, true]) it(`fetches child content from ${hydrated ? "hydrated" : "plain"} metadata and caches under its parent mount`, async () => { + const fileOrigin = hydrated ? new FileModel({ upstream }).upstream : upstream; + const source = new GitHubStream({ ...parent, cacheGeneration: "tree:date" }); const cache = new Map(), requests = []; + stub(storage, "fileInfo", async (_id, path) => { if (!cache.has(path)) throw Object.assign(Error("missing"), { code: "ENOENT" }); return { size: cache.get(path).length }; }); + stub(storage, "write", async (id, path, input) => { expect(id).to.equal(parent.repoId); const chunks = []; for await (const chunk of input) chunks.push(Buffer.from(chunk)); cache.set(path, Buffer.concat(chunks)); }); + stub(storage, "read", async (_id, path) => Readable.from([cache.get(path)])); + stub(got, "stream", (url, options) => { requests.push({ url, options }); return Readable.from([Buffer.from("Alice")]); }); + for (let i = 0; i < 2; i++) { + const stream = await source.getFileContentCache("vendor/dependency/src/Alice.txt", parent.repoId, () => ({ sha: blob, size: 5, upstream: fileOrigin })); + for await (const chunk of stream) expect(chunk.toString()).to.equal("Alice"); + } + expect(requests).to.have.length(1); + expect(requests[0].url).to.equal(`https://github.com/child/public/raw/${sha}/src/Alice.txt`); + expect(requests[0].options.headers).to.deep.equal({}); + expect([...cache.keys()][0]).to.match(/\/vendor\/dependency\/src\/Alice\.txt$/); + const changed = { ...upstream, commit: "d".repeat(40) }; + const stream = await source.getFileContentCache("vendor/dependency/src/Alice.txt", parent.repoId, () => ({ sha: blob, size: 5, upstream: changed })); + stream.resume(); expect(requests).to.have.length(2); + }); + + it("does not report an unavailable public child as lost parent access", async () => { + const { isAccessFailure } = require("../src/core/owner-notifications"); + const source = new GitHubStream(parent); + stub(storage, "fileInfo", async () => { throw Object.assign(Error("missing"), { code: "ENOENT" }); }); + stub(storage, "write", async (_id, _path, input) => { for await (const _chunk of input) { /* consume */ } }); + stub(got, "stream", () => Readable.from((async function* () { + yield* []; + throw Object.assign(Error("not found"), { response: { statusCode: 404 } }); + })())); + let error; + try { await source.getFileContentCache("vendor/lib/file.txt", parent.repoId, () => ({ sha: blob, size: 5, upstream })); } + catch (caught) { error = caught; } + expect(error?.message).to.equal("submodule_not_available"); + expect(isAccessFailure(error)).to.equal(false); + }); + + it("includes pinned child archives with anonymized names and content, omitting manifests and disabled images", async () => { + const main = await zip({ "parent/README.md": "Alice parent", "parent/.gitmodules": "https://github.com/private/identity" }); + const child = await zip({ "child/src/Alice.txt": "Alice child", "child/.gitmodules": "identity", "child/photo.png": "image" }); + const calls = []; + stub(got, "stream", (url, options) => { calls.push({ url, options }); return Readable.from([calls.length === 1 ? main : child]); }); + expect(await readZip({ ...zipOptions, contentOptions: { image: false } })).to.deep.equal({ + "README.md": "XXXX-1 parent", "vendor/dependency/src/XXXX-1.txt": "XXXX-1 child", + }); + expect(calls.map(call => call.url)).to.deep.equal([ + `https://codeload.github.com/parent/repo/zip/${parent.commit}`, `https://codeload.github.com/child/public/zip/${sha}`, + ]); + expect(calls.every(call => call.options === undefined)).to.equal(true); + }); + + it("aborts the ZIP if a child exceeds the per-file limit", async () => { + const main = await zip({ "parent/README.md": "ok" }), child = await zip({ "child/large.txt": "too much data" }); + let calls = 0; stub(got, "stream", () => Readable.from([++calls === 1 ? main : child])); stub(config, "MAX_FILE_SIZE", 4); + const response = new PassThrough(); response.resume(); const errors = []; + response.on("error", error => errors.push(error)); const done = new Promise(resolve => response.once("close", resolve)); + await streamAnonymizedZip(zipOptions, response); await done; + expect(errors.map(error => error.message)).to.deep.equal(["file_too_big"]); + }); +}); + +describe("submodule transport and public metadata", function () { + this.timeout(10000); + it("does not expose upstream repository coordinates in the public tree", async () => { + const Repository = require("../src/core/Repository").default; + const files = await Repository.prototype.anonymizedFiles.call({ + _model: { options: { terms: ["Alice"] } }, + files: async () => [{ name: "Alice.txt", path: "vendor/dependency/src", sha: blob, size: 5, upstream, submodule: "included" }], + }, { includeSha: false }); + expect(files[0]).to.have.all.keys("name", "path", "sha", "size"); + expect(files[0].name).to.equal("XXXX-1.txt"); + expect(JSON.stringify(files)).not.to.include("public"); + }); + + for (const mode of ["send", "anonymizedContent"]) { + it(`preserves child origin through ${mode} and the real streamer route`, async () => { + const express = require("express"); + const File = require("../src/core/AnonymizedFile").default; + const { AnonymizeTransformer } = require("../src/core/anonymize-utils"); + const router = require("../src/streamer/route").router; + const previous = { endpoint: config.STREAMER_ENTRYPOINT, stream: got.stream, + info: storage.fileInfo, write: storage.write, read: storage.read }; + let server, payload; const cache = new Map(), requests = []; + try { + storage.fileInfo = async (_id, path) => { if (!cache.has(path)) throw Object.assign(Error("missing"), { code: "ENOENT" }); return { size: cache.get(path).length }; }; + storage.write = async (_id, path, stream) => { const chunks = []; for await (const chunk of stream) chunks.push(Buffer.from(chunk)); cache.set(path, Buffer.concat(chunks)); }; + storage.read = async (_id, path) => Readable.from([cache.get(path)]); + got.stream = (url, options) => { + if (!String(url).startsWith("https://github.com/")) return previous.stream(url, options); + requests.push({ url, options }); return Readable.from([Buffer.from("Alice")]); + }; + const app = express(); app.use(express.json()); + app.use((req, _res, next) => { payload = req.body; next(); }); app.use("/api", router); + server = await new Promise(resolve => { const listening = app.listen(0, "127.0.0.1", () => resolve(listening)); }); + config.STREAMER_ENTRYPOINT = `http://127.0.0.1:${server.address().port}/`; + const file = new File({ anonymizedPath: "vendor/dependency/src/XXXX-1.txt", repository: { + repoId: parent.repoId, options: { terms: ["Alice"] }, + model: { source: { repositoryName: "parent/repo", commit: parent.commit } }, + getToken: parent.getToken, owner: { id: "owner" }, + generateAnonymizeTransformer: filePath => new AnonymizeTransformer({ terms: ["Alice"], filePath, image: true, link: true }), + } }); + file._file = { name: "Alice.txt", path: "vendor/dependency/src", sha: blob, size: 5, upstream }; + let content = ""; + if (mode === "send") { + const response = new PassThrough(); response.setHeader = () => {}; + response.on("data", chunk => { content += chunk; }); + await file.send(response); + } else { + for await (const chunk of await file.anonymizedContent()) content += chunk; + } + expect(content).to.equal("XXXX-1"); + expect(payload.upstream).to.deep.equal(upstream); + expect(payload.filePath).to.equal("vendor/dependency/src/Alice.txt"); + expect(requests).to.have.length(1); + expect(requests[0].url).to.equal(`https://github.com/child/public/raw/${sha}/src/Alice.txt`); + expect(requests[0].options.headers).to.deep.equal({}); + } finally { + config.STREAMER_ENTRYPOINT = previous.endpoint; got.stream = previous.stream; + storage.fileInfo = previous.info; storage.write = previous.write; storage.read = previous.read; + if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } + } + }); + } + + it("cancels the current child archive when the reader disconnects", async () => { + const main = await zip({ "parent/README.md": "ok" }); const child = new PassThrough(); + const previous = got.stream; let calls = 0, started; + const childStarted = new Promise(resolve => { started = resolve; }); + const response = new PassThrough(); response.resume(); + try { + got.stream = () => { if (++calls === 1) return Readable.from([main]); started(); return child; }; + await streamAnonymizedZip({ ...zipOptions, submodules: [...zipOptions.submodules, + { ...zipOptions.submodules[0], path: "other/dependency" }] }, response); + await childStarted; response.destroy(); + await new Promise(resolve => setImmediate(resolve)); + expect(child.destroyed).to.equal(true); expect(calls).to.equal(2); + } finally { got.stream = previous; child.destroy(); response.destroy(); } + }); +}); + +describe("legacy submodule manifests", function () { + it("rejects a cached .gitmodules file even when its public name was anonymized", async () => { + const File = require("../src/core/AnonymizedFile").default; + const file = new File({ anonymizedPath: ".XXXX-1", repository: { + options: { terms: ["gitmodules"] }, model: { source: { type: "GitHubStream" } }, + } }); + file._file = { name: ".gitmodules", path: "", sha: blob, size: 5 }; + for (const read of [() => file.getFileInfo(), () => file.originalPath()]) { + let error; try { await read(); } catch (caught) { error = caught; } + expect(error?.message).to.equal("file_not_found"); + } + }); +}); diff --git a/test/large-repository-tree.test.js b/test/large-repository-tree.test.js new file mode 100644 index 0000000..8d6e379 --- /dev/null +++ b/test/large-repository-tree.test.js @@ -0,0 +1,479 @@ +const { expect } = require("chai"); +const { execFile } = require("node:child_process"); +const { promisify } = require("node:util"); +const fs = require("node:fs/promises"), path = require("node:path"), os = require("node:os"); +require("ts-node/register/transpile-only"); +const mongoose = require("mongoose"); +const db = require("../src/server/database"); +const Repository = require("../src/core/Repository").default; +const Repo = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; +const File = require("../src/core/model/files/files.model").default; +const Path = require("../src/core/model/anonymized-path").default; +const { FILE_TREE_BATCH_SIZE, FILE_LISTING_LIMIT, deleteRowsInBatches } = require("../src/core/file-tree"); +const GitHubStream = require("../src/core/source/GitHubStream").default; +const { expandSubmodules, MAX_SUBMODULES } = require("../src/core/source/github-submodules"); +const github = require("../src/core/GitHubUtils"); +const config = require("../src/config").default; + +describe("large repository tree allocation", function () { + this.timeout(120000); + it("reads two million entries within a 256 MiB V8 heap", async () => { + const { stdout } = await promisify(execFile)(process.execPath, + ["--max-old-space-size=256", path.join(__dirname, "fixtures/large-repository-tree.js")], + { timeout: 110000, maxBuffer: 262144, env: { ...process.env, NODE_ENV: "test", SESSION_SECRET: "large-tree-test" } }); + expect(JSON.parse(stdout).count).to.equal(2000200); + }); + + it("sums billing admission sizes using the streaming interface", async () => { + const { sourceStorageSize } = require("../src/core/source/Source"); + const size = await sourceStorageSize({ + getFiles: () => { throw Error("unbounded admission read"); }, + async *iterateFiles() { for (let i = 0; i < 10001; i++) yield { size: 3 }; }, + }); + expect(size).to.equal(30003); + expect(await sourceStorageSize({ getFiles: async () => [{ size: 2 }, {}] })).to.equal(2); + }); + + it("does not fetch child trees before the consumer advances past the parent page", async () => { + const source = new GitHubStream({ repoId: "backpressure", organization: "fixture", repoName: "repo", commit: "head", getToken: () => "fixture" }); + let calls = 0; + source.getGHTree = async (_oct, _token, _sha, _count, options) => { + calls++; + return options.recursive ? { truncated: true, tree: [] } + : { tree: [{ path: "folder", type: "tree", sha: "a".repeat(40) }] }; + }; + const iterator = source.iterateFiles(); + expect((await iterator.next()).value.name).to.equal("folder"); + expect(calls).to.equal(2); + await iterator.return(); + expect(calls).to.equal(2); + }); + + it("rejects an oversized pending-folder queue before fetching its children", async () => { + const source = new GitHubStream({ repoId: "wide", organization: "fixture", repoName: "repo", commit: "head", getToken: () => "fixture" }); + let calls = 0; + source.getGHTree = async (_oct, _token, _sha, _count, options) => { + calls++; + return options.recursive ? { truncated: true, tree: [] } + : { tree: Array.from({ length: 100001 }, (_, i) => ({ path: `folder${i}`, type: "tree", sha: String(i) })) }; + }; + const error = await (async () => { for await (const file of source.iterateFiles()) void file; })().then(() => null, error => error); + expect(error.message).to.equal("repository_tree_too_large"); expect(error.httpStatus).to.equal(413); expect(calls).to.equal(2); + }); + + it("applies the submodule budget across separate tree pages", async () => { + const octokit = github.octokit, gate = github.waitForTokenGate; + const budget = { modules: 0, remaining: 10000 }, sha = "a".repeat(40); + let calls = 0; + github.waitForTokenGate = async () => {}; + github.octokit = () => ({ repos: { getContent: async () => { + calls++; return { data: { submodule_git_url: "https://github.com/child/public.git", sha } }; + } }, git: { getTree: async () => ({ data: { tree: [], truncated: false } }) } }); + const data = { repoId: "modules", organization: "fixture", repoName: "repo", commit: "head", getToken: () => "fixture" }; + try { + for (let i = 0; i <= MAX_SUBMODULES; i++) { + const [mount] = await expandSubmodules([{ name: `child${i}`, path: "vendor", sha, submodule: "unsupported" }], data, budget); + expect(mount.submodule).to.equal(i < MAX_SUBMODULES ? "included" : "limit"); + } + expect(calls).to.equal(MAX_SUBMODULES); + } finally { github.octokit = octokit; github.waitForTokenGate = gate; } + }); +}); + +const uri = process.env.PERFORMANCE_MONGO_URI; +(uri ? describe : describe.skip)("large repository persistence and cleanup", function () { + this.timeout(60000); + let previousFolder, folder; + before(async () => { + if (!/\/perf_test_[A-Za-z0-9_-]+(?:\?|$)/.test(uri)) throw Error("An isolated perf_test_ database is required"); + await mongoose.connect(uri); + db.isConnected = true; + previousFolder = config.FOLDER; + folder = await fs.mkdtemp(path.join(os.tmpdir(), "large-tree-tests-")); config.FOLDER = folder; + await Promise.all([Repo, File, Path].map(model => model.createIndexes())); + }); + after(async () => { + db.isConnected = false; + await mongoose.connection.dropDatabase(); await mongoose.disconnect(); + config.FOLDER = previousFolder; await fs.rm(folder, { recursive: true, force: true }); + }); + beforeEach(async () => { await Promise.all([Repo, File, Path].map(model => model.deleteMany({}))); }); + const owner = new mongoose.Types.ObjectId(); + const repository = (extra = {}) => Repo.create({ repoId: "large", owner, status: "preparing", statusDate: new Date(), + source: { type: "GitHubStream", repositoryName: "fixture/repo", commit: "a".repeat(40) }, ...extra }); + async function rows(count, extra = {}) { + for (let offset = 0; offset < count; offset += FILE_TREE_BATCH_SIZE) { + await File.collection.insertMany(Array.from({ length: Math.min(count - offset, FILE_TREE_BATCH_SIZE) }, (_, i) => ({ + repoId: "large", treeGeneration: "old", name: `file${offset + i}.txt`, path: "", size: 1, sha: "b".repeat(40), + ...(typeof extra === "function" ? extra(offset + i) : extra), + }))); + } + } + + it("persists streamed input in bounded batches without reading a listing after preparation", async () => { + const row = await repository(), repo = new Repository(row), insert = File.insertMany; + let produced = 0, persisted = 0, calls = 0; + Object.defineProperty(repo, "source", { get: () => ({ + getFiles: () => { throw Error("unbounded source read"); }, + async *iterateFiles() { + for (let i = 0; i < 1503; i++) { produced++; yield { name: `file${i}.txt`, path: "", size: i, sha: "b".repeat(40) }; } + }, + }) }); + repo.files = () => { throw Error("preparation reloaded the tree"); }; + File.insertMany = async function (batch, ...args) { + expect(batch.length).to.be.at.most(FILE_TREE_BATCH_SIZE); + expect(produced).to.equal(persisted + batch.length); + const model = await Repo.findById(row._id); + expect(model.stagedFileTrees.some(stage => stage.generation === batch[0].treeGeneration)).to.equal(true); + const result = await insert.call(this, batch, ...args); persisted += batch.length; calls++; return result; + }; + try { await repo.anonymize(); } finally { File.insertMany = insert; } + expect(calls).to.equal(4); expect(persisted).to.equal(1503); + expect(await File.countDocuments({ repoId: row.repoId, treeGeneration: repo.model.treeGeneration })).to.equal(1503); + expect((await Repo.findById(row._id)).status).to.equal("ready"); + expect(await File.countDocuments({ treeStaged: true })).to.equal(0); + }); + + it("rejects billed streamed trees before publishing their generation", async () => { + const quotas = require("../src/core/billing/quotas"); + const reserve = quotas.reserveRepository; + const row = await repository({ treeGeneration: "old", billingManaged: true }); + const repo = new Repository(row); + await rows(1); + Object.defineProperty(repo, "source", { get: () => ({ + getFiles: () => { throw Error("unbounded source read"); }, + async *iterateFiles() { yield { name: "replacement", path: "", size: 100, sha: "b".repeat(40) }; }, + }) }); + quotas.reserveRepository = async (_owner, _repoId, bytes) => { + expect(bytes).to.equal(100); throw Error("quota_exceeded"); + }; + try { + const error = await repo.ensureFileTree({ force: true }).then(() => null, error => error); + expect(error?.message).to.equal("quota_exceeded"); + expect((await Repo.findById(row._id)).treeGeneration).to.equal("old"); + expect(await File.countDocuments({ treeGeneration: "old" })).to.equal(1); + } finally { quotas.reserveRepository = reserve; } + }); + + it("retires preparation metadata without deleting it in the request, and resumes interrupted cleanup", async () => { + const row = await repository({ treeGeneration: "old", pathIndexKey: "old-key" }), repo = new Repository(row); + await rows(1201); + await File.create({ repoId: "unrelated", treeGeneration: "old", name: "keep.txt", path: "" }); + await Path.collection.insertOne({ repoId: row.repoId, key: "old-key", anonymousPath: "old.txt" }); + const fileDelete = File.collection.deleteMany, pathDelete = Path.collection.deleteMany; + File.collection.deleteMany = Path.collection.deleteMany = () => { throw Error("foreground metadata deletion"); }; + try { await repo.resetSate("preparing"); } + finally { File.collection.deleteMany = fileDelete; Path.collection.deleteMany = pathDelete; } + let current = await Repo.findById(row._id); + expect(current.treeGeneration).not.to.equal("old"); expect(current.retiredTreeGenerations).to.deep.equal(["old"]); + expect(current.retiredPathIndexKeys).to.deep.equal(["old-key"]); + expect(await File.countDocuments({ repoId: row.repoId })).to.equal(1201); + await File.create({ repoId: row.repoId, treeGeneration: current.treeGeneration, name: "active.txt", path: "" }); + await Path.collection.insertOne({ repoId: row.repoId, key: "active-key", anonymousPath: "active.txt" }); + await Repo.updateOne({ _id: row._id }, { $set: { pathIndexKey: "active-key" } }); + let batches = 0; + File.collection.deleteMany = async function (filter, ...args) { + expect(filter._id.$in.length).to.be.at.most(FILE_TREE_BATCH_SIZE); + if (++batches === 2) throw Error("interrupted cleanup"); + return fileDelete.call(this, filter, ...args); + }; + try { await repo.cleanupRetiredFileTrees().then(() => { throw Error("expected interruption"); }, error => expect(error.message).to.equal("interrupted cleanup")); } + finally { File.collection.deleteMany = fileDelete; } + expect((await Repo.findById(row._id)).retiredTreeGenerations).to.deep.equal(["old"]); + await repo.cleanupRetiredFileTrees(); + expect((await File.find({ repoId: row.repoId })).map(file => file.name)).to.deep.equal(["active.txt"]); + expect((await Path.find({ repoId: row.repoId })).map(file => file.key)).to.deep.equal(["active-key"]); + expect(await File.countDocuments({ repoId: "unrelated" })).to.equal(1); + current = await Repo.findById(row._id); + expect(current.retiredTreeGenerations).to.equal(undefined); expect(current.retiredPathIndexKeys).to.equal(undefined); + }); + + it("stops ingestion between batches when another tree replaces its build", async () => { + const row = await repository({ treeGeneration: "old" }), repo = new Repository(row), insert = File.insertMany; + let produced = 0, calls = 0, closed = false; + Object.defineProperty(repo, "source", { get: () => ({ getFiles: () => { throw Error("unbounded read"); }, + async *iterateFiles() { + try { for (let i = 0; i < 1500; i++) { produced++; yield { name: `file${i}.txt`, path: "", size: i }; } } + finally { closed = true; } + }, + }) }); + File.insertMany = async function (...args) { + const result = await insert.apply(this, args); calls++; + await Repo.updateOne({ _id: row._id }, { $set: { treeGeneration: "replacement" } }); + await File.create({ repoId: row.repoId, treeGeneration: "replacement", name: "keep.txt", path: "" }); + return result; + }; + let error; + try { error = await repo.anonymize().then(() => null, error => error); } finally { File.insertMany = insert; } + expect(error.message).to.equal("repository_changed"); expect(calls).to.equal(1); expect(produced).to.equal(1000); expect(closed).to.equal(true); + expect(await File.countDocuments({ repoId: row.repoId, treeGeneration: "replacement" })).to.equal(1); + expect(await File.countDocuments({ repoId: row.repoId, treeStaged: true })).to.equal(500); + }); + + it("retires an index published between reading and clearing the persisted key", async () => { + const row = await repository({ status: "ready", treeGeneration: "old" }), resetter = new Repository(row); + await rows(2); + const searcher = new Repository(await Repo.findById(row._id)), updateOne = Repo.updateOne; + let attempts = 0, publishedKey; + Repo.updateOne = function (filter, update, ...args) { + const query = updateOne.call(this, filter, update, ...args); + if (update.$unset && Object.hasOwn(update.$unset, "pathIndexKey") && Object.hasOwn(filter, "pathIndexKey")) { + const exec = query.exec; + query.exec = async function (...execArgs) { + if (++attempts === 1) { + expect(await searcher.searchFiles("file")).to.have.length(2); + publishedKey = (await Repo.findById(row._id)).pathIndexKey; + } + return exec.apply(this, execArgs); + }; + } + return query; + }; + try { await resetter.resetSate("preparing"); } finally { Repo.updateOne = updateOne; } + expect(attempts).to.equal(2); + expect((await Repo.findById(row._id)).retiredPathIndexKeys).to.deep.equal([publishedKey]); + await resetter.cleanupRetiredFileTrees(); + expect(await Path.countDocuments({ repoId: row.repoId })).to.equal(0); + }); + + it("retires a path index published after the activating worker loaded its model", async () => { + const row = await repository({ status: "ready", treeGeneration: "old" }), worker = new Repository(row); + await rows(2); + const searcher = new Repository(await Repo.findById(row._id)); + Object.defineProperty(worker, "source", { get: () => ({ getFiles: async () => { + expect(await searcher.searchFiles("file")).to.have.length(2); + expect((await Repo.findById(row._id)).pathIndexKey).to.be.a("string"); + return [{ name: "replacement.txt", path: "", size: 1, sha: "c".repeat(40) }]; + } }) }); + await worker.files({ force: true }); + expect(await Path.countDocuments({ repoId: row.repoId })).to.equal(0); + expect((await File.find({ repoId: row.repoId })).map(file => file.name)).to.deep.equal(["replacement.txt"]); + }); + + const changedSnapshots = [ + ...["archived", "removing", "removed", "expiring", "expired", "ready"].map(status => [status, { status, statusDate: new Date(1) }]), + ["status timestamp", { statusDate: new Date(1) }], + ["source commit", { "source.commit": "c".repeat(40) }], + ["source repository", { "source.repositoryName": "other/repo" }], + ["source branch", { "source.branch": "replacement" }], + ["source type", { "source.type": "Zip" }], + ["anonymization snapshot", { anonymizeDate: new Date(1) }], + ["metadata revision", { fileMetadataRevision: "replacement" }], + ["access revision", { "githubAccess.revision": "replacement" }], + ]; + for (const [label, replacement] of changedSnapshots) it(`cancels preparation before retiring metadata after a changed ${label}`, async () => { + const row = await repository({ status: "download", treeGeneration: "old", emptyTreeGeneration: "old", pathIndexKey: "old-key", fileMetadataRevision: "before" }); + const worker = new Repository(row); worker.protectLifecycle = true; + worker.removeCache = () => { throw Error("cancelled reset touched storage"); }; + await rows(1); await Path.collection.insertOne({ repoId: row.repoId, key: "old-key", anonymousPath: "keep.txt" }); + await Repo.updateOne({ _id: row._id }, { $set: replacement }); + const before = await Repo.findById(row._id).lean(); + const error = await worker.resetSate("preparing").then(() => null, error => error); + expect(error.message).to.equal("repository_job_cancelled"); + const after = await Repo.findById(row._id).lean(); + for (const field of ["status", "statusDate", "treeGeneration", "emptyTreeGeneration", "fileMetadataRevision", "pathIndexKey", "retiredTreeGenerations", "retiredPathIndexKeys"]) { + expect(after[field], field).to.deep.equal(before[field]); + } + expect(worker.model.treeGeneration).to.equal("old"); expect(worker.model.emptyTreeGeneration).to.equal("old"); + await new Repository(await Repo.findById(row._id)).cleanupRetiredFileTrees(); + expect(await File.countDocuments({ repoId: row.repoId, treeGeneration: "old" })).to.equal(1); + expect(await Path.countDocuments({ repoId: row.repoId, key: "old-key" })).to.equal(1); + }); + + for (const replaced of [false, true]) it(`rejects an ${replaced ? "replaced" : "expired"} refresh lease before retirement`, async () => { + const row = await repository({ status: "download", treeGeneration: "old" }), worker = new Repository(row); + worker.protectLifecycle = true; worker.refreshToken = "original-lease"; + await Repo.updateOne({ _id: row._id }, { $set: { refreshToken: replaced ? "new-lease" : "original-lease", + refreshUntil: new Date(Date.now() + (replaced ? 60000 : -60000)) } }); + const error = await worker.resetSate("preparing").then(() => null, error => error); + expect(error.message).to.equal("invalid_status"); + const current = await Repo.findById(row._id); + expect(current.treeGeneration).to.equal("old"); expect(current.retiredTreeGenerations).to.equal(undefined); + }); + + it("rejects archival between reading the path-index key and committing retirement", async () => { + const row = await repository({ status: "download", treeGeneration: "old", pathIndexKey: "preserved" }), worker = new Repository(row); + worker.protectLifecycle = true; + const updateOne = Repo.updateOne; let attempted = false; + Repo.updateOne = function (filter, update, ...args) { + const query = updateOne.call(this, filter, update, ...args); + if (update.$unset && Object.hasOwn(update.$unset, "pathIndexKey")) { + const exec = query.exec; + query.exec = async function (...execArgs) { + attempted = true; + await updateOne.call(Repo, { _id: row._id }, { $set: { status: "archived" } }).exec(); + return exec.apply(this, execArgs); + }; + } + return query; + }; + let error; + try { error = await worker.resetSate("preparing").then(() => null, error => error); } finally { Repo.updateOne = updateOne; } + expect(attempted).to.equal(true); expect(error.message).to.equal("repository_job_cancelled"); + const current = await Repo.findById(row._id); + expect(current.status).to.equal("archived"); expect(current.treeGeneration).to.equal("old"); + expect(current.pathIndexKey).to.equal("preserved"); expect(current.retiredTreeGenerations).to.equal(undefined); + }); + + it("can remove a repository using the timestamp from its guarded access claim", async () => { + const row = await repository({ status: "ready", treeGeneration: "old" }), repo = new Repository(row); + await rows(1); + await repo.remove({}); + expect((await Repo.findById(row._id)).status).to.equal("removed"); + expect(await File.countDocuments({ repoId: row.repoId })).to.equal(0); + }); + + for (const reconnecting of [false, true]) it(`saves a ${reconnecting ? "reconnected repository" : "changed source"} through real removal`, async () => { + const row = await repository({ status: "ready", treeGeneration: "old", pathIndexKey: "old-key", + githubAccess: { kind: "oauth", revision: "old-access" }, anonymizeDate: new Date(1), + options: { terms: ["before"], expirationMode: "never" } }), repo = new Repository(row); + await rows(2); await Path.collection.insertOne({ repoId: row.repoId, key: "old-key", anonymousPath: "old.txt" }); + const utils = require("../src/server/routes/route-utils"), app = require("../src/core/github-app"); + const source = require("../src/core/source/GitHubRepository"), queue = require("../src/queue"); + const router = require("../src/server/routes/repository-private").default; + const handler = router.stack.find(layer => layer.route?.path === "/:repoId/" && layer.route.methods.post).route.stack[0].handle; + const restores = [], jobs = []; let response, cleanupSnapshot, removals = 0; + const stub = (object, key, value) => { const original = object[key]; restores.push(() => { object[key] = original; }); object[key] = value; }; + stub(utils, "getRepo", async () => repo); + stub(utils, "getUser", async () => ({ id: repo.owner.id, model: { id: repo.owner.id } })); + stub(utils, "handleError", error => { throw error; }); + stub(app, "selectRepositoryAccess", async () => ({ token: "fixture", binding: { kind: "oauth" } })); + stub(source, "getRepositoryFromGitHub", async () => ({ id: "gh_123", model: { id: "new-source" }, fullName: "fixture/repo", + branches: async () => [{ name: "replacement" }], getCommitInfo: async () => ({ commit: { committer: { date: "2026-11-01" } } }) })); + stub(queue, "downloadQueue", { add: async (...args) => { jobs.push(args); } }); + const remove = repo.remove; + stub(repo, "remove", async function (...args) { + removals++; + cleanupSnapshot = { commit: this.model.source.commit, accessRevision: this.model.githubAccess.revision, + anonymizeDate: this.model.anonymizeDate }; + return remove.apply(this, args); + }); + try { + await handler({ body: { repoId: row.repoId, fullName: "fixture/repo", terms: ["after"], + source: { branch: "replacement", commit: "b".repeat(40) }, options: { expirationMode: "never" }, + ...(reconnecting ? { reconnectRepositoryId: "gh_123" } : {}) } }, { json: value => { response = value; } }); + } finally { while (restores.length) restores.pop()(); } + expect(removals).to.equal(1); expect(response).to.deep.equal({ status: "preparing" }); + expect(cleanupSnapshot).to.deep.equal({ commit: "a".repeat(40), accessRevision: "old-access", anonymizeDate: new Date(1) }); + const saved = await Repo.findById(row._id); + expect(saved.status).to.equal("preparing"); expect(saved.source.commit).to.equal("b".repeat(40)); + expect(saved.source.branch).to.equal("replacement"); expect(saved.source.repositoryId).to.equal("new-source"); + expect(saved.githubAccess.revision).not.to.equal("old-access"); expect(saved.anonymizeDate.getTime()).to.be.greaterThan(1); + expect(saved.options.terms).to.deep.equal(["after"]); + expect(await File.countDocuments({ repoId: row.repoId })).to.equal(0); expect(await Path.countDocuments({ repoId: row.repoId })).to.equal(0); + expect(jobs).to.have.length(1); expect(jobs[0][1]).to.deep.equal({ repoId: row.repoId }); + }); + + for (const [label, change] of [...changedSnapshots, ["settings revision", { settingsSavedAt: new Date(1) }]] ) { + it(`rejects removal before claiming the lifecycle after a changed ${label}`, async () => { + const row = await repository({ status: "ready", treeGeneration: "old", pathIndexKey: "old-key" }), stale = new Repository(row); + await rows(1); await Repo.updateOne({ _id: row._id }, { $set: change }); + const before = await Repo.findById(row._id).lean(); + stale.removeCache = () => { throw Error("stale removal touched storage"); }; + const error = await stale.remove({}).then(() => null, error => error); + expect(error.message).to.equal("connection_changed"); + expect(await Repo.findById(row._id).lean()).to.deep.equal(before); + expect(await File.countDocuments({ repoId: row.repoId })).to.equal(1); + }); + } + + it("finishes preparation after an accepted options-only save with the same source", async () => { + const row = await repository({ status: "preparing", treeGeneration: "old", anonymizeDate: new Date(1), + source: { type: "GitHubStream", repositoryName: "fixture/repo", branch: "main", commit: "a".repeat(40) }, + options: { terms: ["before"], expirationMode: "never" } }); + const worker = new Repository(row), editor = new Repository(await Repo.findById(row._id)); + await rows(1); + Object.defineProperty(worker, "source", { get: () => ({ async *iterateFiles() { + yield { name: "replacement.txt", path: "", size: 2, sha: "b".repeat(40) }; + } }) }); + const utils = require("../src/server/routes/route-utils"), queue = require("../src/queue"); + const router = require("../src/server/routes/repository-private").default; + const handler = router.stack.find(layer => layer.route?.path === "/:repoId/" && layer.route.methods.post).route.stack[0].handle; + const restores = [], jobs = []; let savedSettingsAt; + const stub = (object, key, value) => { const original = object[key]; restores.push(() => { object[key] = original; }); object[key] = value; }; + stub(utils, "getRepo", async () => editor); + stub(utils, "getUser", async () => ({ id: editor.owner.id, model: { id: editor.owner.id } })); + stub(utils, "handleError", error => { throw error; }); + stub(queue, "downloadQueue", { add: async (...args) => { jobs.push(args); } }); + stub(db, "connect", async () => {}); + stub(db, "getRepository", async () => { + // The worker snapshot was loaded before this accepted owner edit. + await handler({ body: { repoId: row.repoId, fullName: "fixture/repo", terms: ["after"], + source: { branch: "main", commit: "a".repeat(40) }, options: { expirationMode: "never" } } }, { json() {} }); + savedSettingsAt = (await Repo.findById(row._id)).settingsSavedAt; + return worker; + }); + stub(github, "getToken", async () => "fixture"); stub(github, "getRedisGateResetAt", async () => 0); + try { + await require("../src/queue/processes/downloadRepository").default({ data: { repoId: row.repoId }, updateProgress: async () => {} }); + } finally { while (restores.length) restores.pop()(); } + const saved = await Repo.findById(row._id); + expect(saved.status).to.equal("ready"); expect(saved.options.terms).to.deep.equal(["after"]); + expect(saved.settingsSavedAt).to.deep.equal(savedSettingsAt); expect(savedSettingsAt).to.be.instanceOf(Date); + expect(jobs).to.have.length(0); + expect((await File.find({ repoId: row.repoId })).map(file => file.name)).to.deep.equal(["replacement.txt"]); + }); + + it("retains retirement markers when foreground cleanup reaches its batch budget", async () => { + const row = await repository({ treeGeneration: "active", retiredTreeGenerations: ["old"] }), repo = new Repository(row); + await rows(501); + await repo.cleanupRetiredFileTrees(new Date(), 1); + expect(await File.countDocuments({ repoId: row.repoId })).to.equal(1); + expect((await Repo.findById(row._id)).retiredTreeGenerations).to.deep.equal(["old"]); + await repo.cleanupRetiredFileTrees(); + expect(await File.countDocuments({ repoId: row.repoId })).to.equal(0); + }); + + it("stops expiration deletion when its lifecycle claim changes between batches", async () => { + const row = await repository({ status: "expiring", treeGeneration: "old" }), repo = new Repository(row); + await rows(1001); + const original = File.collection.deleteMany; let calls = 0; + File.collection.deleteMany = async function (...args) { + const result = await original.apply(this, args); calls++; + await Repo.updateOne({ _id: row._id }, { $set: { status: "ready", treeGeneration: "replacement", fileMetadataRevision: "replacement" } }); + await File.create({ repoId: row.repoId, treeGeneration: "replacement", name: "keep.txt", path: "" }); + return result; + }; + try { await repo.expire(); } finally { File.collection.deleteMany = original; } + expect(calls).to.equal(1); expect((await Repo.findById(row._id)).status).to.equal("ready"); + expect(await File.countDocuments({ repoId: row.repoId, treeGeneration: "replacement" })).to.equal(1); + expect(await File.countDocuments({ repoId: row.repoId, treeGeneration: "old" })).to.equal(501); + }); + + it("rejects oversized listings before hydration while allowing narrower folder reads", async () => { + const row = await repository({ status: "ready", treeGeneration: "old" }), repo = new Repository(row); + await rows(FILE_LISTING_LIMIT + 1); + await File.create({ repoId: row.repoId, treeGeneration: "old", name: "small.txt", path: "specific", size: 2, sha: "c".repeat(40) }); + const error = await repo.files({ recursive: false }).then(() => null, error => error); + expect(error.message).to.equal("file_listing_too_large"); expect(error.httpStatus).to.equal(413); + const files = await repo.files({ recursive: false, path: "specific" }); + expect(files).to.have.length(1); expect(files[0].name).to.equal("small.txt"); + expect(files[0]).not.to.be.instanceOf(mongoose.Document); + }); + + it("preserves rows that stop matching the deletion filter after selection", async () => { + await rows(2); + let checks = 0; + const complete = await deleteRowsInBatches(File.collection, { repoId: "large", treeGeneration: "old" }, { + current: async () => { + if (++checks === 2) await File.updateMany({ repoId: "large" }, { $set: { treeGeneration: "replacement" } }); + return true; + }, + }); + expect(complete).to.equal(true); expect(await File.countDocuments({ repoId: "large" })).to.equal(2); + }); + + for (const ancestors of [false, true]) it(`bounds ${ancestors ? "ancestor expansion" : "direct folders"} in folder-count responses`, async () => { + const row = await repository({ status: "ready", treeGeneration: "old" }), repo = new Repository(row); + await rows(ancestors ? 3000 : FILE_LISTING_LIMIT + 1, i => ({ path: ancestors ? `folder${i}/a/b/c` : `folder${i}` })); + const utils = require("../src/server/routes/route-utils"), getRepo = utils.getRepo, handleError = utils.handleError; + const router = require("../src/server/routes/repository-public").default; + const handler = router.stack.find(layer => layer.route?.path === "/:repoId/files/counts").route.stack.at(-1).handle; + utils.getRepo = async () => repo; utils.handleError = error => { throw error; }; + try { + const error = await handler({ params: { repoId: row.repoId } }, { header() {}, json() { throw Error("unbounded counts returned"); } }) + .then(() => null, error => error); + expect(error.message).to.equal("file_listing_too_large"); expect(error.httpStatus).to.equal(413); + } finally { utils.getRepo = getRepo; utils.handleError = handleError; } + }); +}); diff --git a/test/markdown-compatibility.test.js b/test/markdown-compatibility.test.js index ed409c5..f6df8b9 100644 --- a/test/markdown-compatibility.test.js +++ b/test/markdown-compatibility.test.js @@ -1,3 +1,4 @@ +const normalizeFootnotes = html => html.replace(/footnote-(?:[a-f0-9]+-){4}/g, "footnote-"); const { expect } = require("chai"); const { JSDOM } = require("jsdom"); const fs = require("node:fs"); @@ -88,6 +89,23 @@ describe("GitHub Markdown compatibility", function () { expect(dom.querySelectorAll("[data-footnote-backref]")).to.have.length(2); expect(dom.querySelector(".footnotes").textContent).to.include("Second paragraph."); }); + it("keeps footnote navigation and accessibility targets within each document", function () { + const source = "One[^a].\n\n[^a]: Details"; + const dom = JSDOM.fragment(`
    ${renderMarkdown(source)}
    ${renderMarkdown(source)}
    `); + const ids = [...dom.querySelectorAll("[id]")].map(node => node.id); + expect(new Set(ids).size).to.equal(ids.length); + for (const article of dom.querySelectorAll("article")) { + for (const node of article.querySelectorAll("[href], [aria-describedby]")) { + const target = node.getAttribute("aria-describedby") || node.getAttribute("href").slice(1); + expect(article.querySelector(`[id="${target}"]`)).not.to.equal(null); + } + } + }); + for (const source of ["Costs $5 and $10 per run.", "Budget: $100-$200."]) it(`preserves currency prose: ${source}`, function () { + const dom = fragment(source); + expect(dom.querySelector(".katex")).to.equal(null); + expect(dom.textContent.trim()).to.equal(source); + }); it("uses unique GitHub heading slugs including suffixed collisions and Unicode", function () { const dom = fragment("# Hello\n# Hello\n# Hello-1\n# Hello\n# 中文 *标题*\n# constructor"); expect([...dom.querySelectorAll("h1")].map(h => h.id)).to.deep.equal(["hello", "hello-1", "hello-1-1", "hello-2", "中文-标题", "constructor"]); @@ -136,6 +154,7 @@ describe("GitHub Markdown compatibility", function () { }); it("serves the shared renderer through the project-page route", async function () { + this.timeout(10000); // Includes cold TypeScript compilation of the route dependencies. const { Readable } = require("node:stream"); const utils = require("../src/server/routes/route-utils"); const File = require("../src/core/AnonymizedFile").default; @@ -181,7 +200,7 @@ describe("GitHub Markdown compatibility", function () { after(function () { dom.window.close(); }); it("matches the server renderer across the feature matrix", function () { for (const [name, source] of cases) { - expect(dom.window.renderMD(source, "/r/repo/"), name).to.equal(renderMarkdown(source, "/r/repo/")); + expect(normalizeFootnotes(dom.window.renderMD(source, "/r/repo/")), name).to.equal(normalizeFootnotes(renderMarkdown(source, "/r/repo/"))); } }); it("enhances Mermaid inserted while another diagram is rendering", async function () { diff --git a/test/markdown-fonts.test.js b/test/markdown-fonts.test.js new file mode 100644 index 0000000..03ad026 --- /dev/null +++ b/test/markdown-fonts.test.js @@ -0,0 +1,35 @@ +const { expect } = require("chai"); +const express = require("express"); +const fs = require("node:fs"); +const path = require("node:path"); +const ts = require("typescript"); + +// Exercise the production static middleware without starting databases/workers. +function staticApp() { + const app = express(); + const source = fs.readFileSync(path.join(__dirname, "../src/server/index.ts"), "utf8"); + const start = source.indexOf(" app.use(\n express.static"); + const end = source.indexOf("\n\n app", start); + new Function("app", "express", "join", ts.transpileModule(source.slice(start, end), {}).outputText)(app, express, (...parts) => path.join(__dirname, "..", ...parts)); + return app; +} +module.exports = { staticApp }; + +if (typeof describe === "function") describe("sandboxed Markdown font requests", function () { + let server; + before(async function () { server = await new Promise(resolve => { const listener = staticApp().listen(0, "127.0.0.1", () => resolve(listener)); }); }); + after(async function () { if (server) await new Promise(resolve => server.close(resolve)); }); + it("allows anonymous opaque-origin font requests", async function () { + const response = await globalThis.fetch(`http://127.0.0.1:${server.address().port}/css/fonts/KaTeX_Main-Regular.woff2`, { headers: { Origin: "null" } }); + expect(response.status).to.equal(200); + expect(response.headers.get("access-control-allow-origin")).to.equal("*"); + expect(response.headers.get("access-control-allow-credentials")).to.equal(null); + await response.arrayBuffer(); + }); + it("does not enable CORS for unrelated assets", async function () { + const response = await globalThis.fetch(`http://127.0.0.1:${server.address().port}/script/markdown.min.js`, { headers: { Origin: "null" } }); + expect(response.status).to.equal(200); + expect(response.headers.get("access-control-allow-origin")).to.equal(null); + await response.arrayBuffer(); + }); +}); diff --git a/test/markdown-security.test.js b/test/markdown-security.test.js new file mode 100644 index 0000000..6c99898 --- /dev/null +++ b/test/markdown-security.test.js @@ -0,0 +1,111 @@ +const normalizeFootnotes = html => html.replace(/footnote-(?:[a-f0-9]+-){4}/g, "footnote-"); +const { expect } = require("chai"); +const fs = require("node:fs"); +const path = require("node:path"); +const vm = require("node:vm"); +const { JSDOM } = require("jsdom"); +require("ts-node/register/transpile-only"); +const { renderMarkdown } = require("../src/markdown"); +const { renderDiagram } = require("../src/markdown-diagrams"); +const root = path.resolve(__dirname, ".."); + +function checkHtml(html, allowMediaData = false) { + const dom = JSDOM.fragment(html); + expect(dom.querySelector("script,iframe,object,embed,form,base,meta,link")).to.equal(null); + for (const element of dom.querySelectorAll("*")) { + for (const attr of element.attributes) { + expect(attr.name).not.to.match(/^on/i); + if (["href", "xlink:href", "src", "srcset", "poster"].includes(attr.name)) { + // Strip URL control characters before checking executable schemes. + // eslint-disable-next-line no-control-regex + expect(attr.value.replace(/[\u0000-\u0020]/g, "")).not.to.match(allowMediaData && ["src", "srcset", "poster"].includes(attr.name) ? /(?:javascript|vbscript):/i : /(?:javascript|vbscript|data):/i); + } + } + } +} + +describe("Markdown security boundaries", function () { + const attacks = [ + '', + 'link', + '', + '', + '

    ', + '
    More
    ', + '', + 'Text[^a" onclick="alert(1)]\n\n[^a" onclick="alert(1)]: Note', + '$\\href{javascript:alert(1)}{click}$', + '$\\url{javascript:alert(1)}$', + '$\\includegraphics{https://evil.test/tracker.svg}$', + '$\\htmlData{evil=1 onmouseover=alert(1)}{x}$', + '$\\htmlStyle{background:url(https://evil.test/tracker)}{x}$', + '', + '```javascript\n\n```', + '```mermaid\ngraph TD; A[""]\n```', + ]; + let browser; + before(function () { + browser = new JSDOM("", { runScripts: "outside-only", url: "https://viewer.test/" }); + vm.runInContext(fs.readFileSync(path.join(root, "public/script/markdown.min.js"), "utf8"), browser.getInternalVMContext()); + }); + after(function () { browser.window.close(); }); + for (const [index, source] of attacks.entries()) it(`rejects active content payload ${index + 1} on server and browser`, function () { + const server = renderMarkdown(source); + const client = browser.window.MarkdownRenderer.render(source); + checkHtml(server); checkHtml(client); + expect(normalizeFootnotes(client)).to.equal(normalizeFootnotes(server)); + }); + + it("sanitizes mutation payloads with the shipped DOMPurify", function () { + const dom = new JSDOM("", { runScripts: "outside-only" }); + try { + vm.runInContext(fs.readFileSync(path.join(root, "public/script/external/purify.min.js"), "utf8"), dom.getInternalVMContext()); + for (const source of attacks.slice(0, 7)) checkHtml(dom.window.DOMPurify.sanitize(source), true); + } finally { dom.window.close(); } + }); + + it("enforces strict Mermaid mode even when source HTML supplies an initialization flag", async function () { + const dom = new JSDOM('
    graph TD; A-->B
    ', { runScripts: "outside-only" }); + let configuration; + try { + dom.window.mermaid = { + initialize(value) { configuration = value; }, + async run({ nodes }) { nodes.forEach(node => { node.dataset.processed = "true"; }); }, + }; + vm.runInContext(fs.readFileSync(path.join(root, "public/script/markdown.min.js"), "utf8"), dom.getInternalVMContext()); + await new Promise(resolve => require("node:timers").setTimeout(resolve, 0)); + expect(configuration.securityLevel).to.equal("strict"); + } finally { dom.window.close(); } + }); + + for (const [library, source, publicFile] of [ + ["Mermaid", "node_modules/mermaid/dist/mermaid.min.js", "public/script/external/mermaid.min.js"], + ["DOMPurify", "node_modules/dompurify/dist/purify.min.js", "public/script/external/purify.min.js"], + ["KaTeX", "node_modules/katex/dist/katex.min.js", "public/script/external/katex.min.js"], + ["KaTeX CSS", "node_modules/katex/dist/katex.min.css", "public/css/katex.min.css"], + ]) it(`ships the audited ${library} version without an outdated vendored copy`, function () { + expect(fs.readFileSync(path.join(root, publicFile)).equals(fs.readFileSync(path.join(root, source)))).to.equal(true); + }); + + it("rejects array-like objects before TopoJSON loops over their forged length", function () { + const topology = { type: "Topology", arcs: [{ 0: [0, 0], length: 1e12 }], objects: { line: { type: "LineString", arcs: [0] } } }; + expect(() => renderDiagram(JSON.stringify(topology), "topojson")).to.throw("array"); + }); + it("bounds repeated TopoJSON arc expansion before allocating coordinates", function () { + const topology = { type: "Topology", arcs: [Array.from({ length: 1000 }, (_, i) => [i / 100, 0])], objects: { line: { type: "LineString", arcs: Array(1000).fill(0) } } }; + expect(() => renderDiagram(JSON.stringify(topology), "topojson")).to.throw("expansion limit"); + }); + it("rejects deeply nested geometry before recursive library calls", function () { + let geometry = { type: "Point", coordinates: [0, 0] }; + for (let i = 0; i < 40; i++) geometry = { type: "GeometryCollection", geometries: [geometry] }; + expect(() => renderDiagram(JSON.stringify(geometry), "geojson")).to.throw("complexity limit"); + }); + it("rejects nonnumeric coordinate objects without invoking D3", function () { + expect(() => renderDiagram('{"type":"Point","coordinates":[{"valueOf":"attack"},0]}', "geojson")).to.throw("position"); + }); + it("bounds diagram input size and keeps rejected source readable", function () { + expect(() => renderDiagram(" ".repeat(1_000_001), "stl")).to.throw("source limit"); + const source = '```geojson\n{"type":"Point","coordinates":[null,0]}\n```'; + expect(JSDOM.fragment(renderMarkdown(source)).querySelector("pre code").textContent).to.include('"coordinates":[null,0]'); + }); +}); diff --git a/test/owner-alert-actions.test.js b/test/owner-alert-actions.test.js new file mode 100644 index 0000000..59e0fae --- /dev/null +++ b/test/owner-alert-actions.test.js @@ -0,0 +1,65 @@ +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const notifications = require("../src/core/owner-notifications"); +const utils = require("../src/server/routes/route-utils"); +const app = require("../src/core/github-app"); +const db = require("../src/server/database"); +const github = require("../src/core/source/GitHubRepository"); +const Resources = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; +const Sources = require("../src/core/model/repositories/repositories.model").default; +const Users = require("../src/core/model/users/users.model").default; +const repositoryRouter = require("../src/server/routes/repository-private").default; +const appRouter = require("../src/server/routes/github-app").githubAppRouter; +const handler = (router, path) => router.stack.find(layer => layer.route?.path === path).route.stack.at(-1).handle; + +describe("owner actions rearm access alerts", () => { + let restores, resets; + const owner = "507f1f77bcf86cd799439011"; + function stub(object, key, value) { + const old = object[key]; restores.push(() => { object[key] = old; }); object[key] = value; + } + beforeEach(() => { + restores = []; resets = []; + stub(utils, "getUser", async () => ({ id: owner, model: { id: owner } })); + stub(utils, "handleError", error => { throw error; }); + stub(notifications, "resetOwnerAccessAlerts", async (...args) => { resets.push(args); }); + }); + afterEach(() => { while (restores.length) restores.pop()(); }); + + for (const isOwner of [true, false]) it(`extension ${isOwner ? "rearms for its owner" : "does not rearm for an admin"}`, async () => { + const id = isOwner ? owner : "admin"; + stub(utils, "getUser", async () => ({ id, model: { id }, isAdmin: !isOwner })); + const repo = { owner: { id: owner }, status: "ready", model: { _id: "resource", options: {} } }; + stub(utils, "getRepo", async () => repo); + stub(Resources, "updateOne", () => ({ exec: async () => ({ matchedCount: 1 }) })); + await handler(repositoryRouter, "/:repoId/extend")({}, { json() {} }); + expect(resets).to.deep.equal(isOwner ? [[owner, { kind: "repository", id: "resource" }]] : []); + }); + + it("clears the previous owner's claim atomically when a repository is claimed", async () => { + stub(db, "getRepository", async () => ({ repoId: "resource", model: { source: { repositoryId: "source" } } })); + stub(app, "selectRepositoryAccess", async () => ({ token: "token", binding: { kind: "oauth" } })); + stub(github, "getRepositoryFromGitHub", async () => ({ id: 42 })); + stub(Sources, "findById", async () => ({ externalId: 42 })); + let write; + stub(Resources, "updateOne", (_filter, update) => ({ collation: async () => { write = update; } })); + await handler(repositoryRouter, "/claim")({ body: { repoId: "resource", repoUrl: "https://github.com/owner/repo" }, query: {} }, { send() {} }); + expect(write.$set.owner).to.equal(owner); + expect(write.$set.accessAlertGeneration).to.be.a("string"); + expect(write.$unset).to.have.property("accessAlertClaimedAt"); + }); + + for (const action of ["install", "request", "unverified"]) it(`handles ${action} installation completion without premature resets`, async () => { + stub(app, "userInstallations", async () => [{ id: 42 }]); + stub(Users, "updateOne", async () => ({})); + const req = { session: { + githubInstallFlow: { ownerId: owner, state: "state", expires: Date.now() + 60000, returnTo: "/connections" }, + save: callback => callback(), + }, isAuthenticated: () => true, query: { state: "state", setup_action: action, installation_id: action === "unverified" ? "99" : "42" } }; + let failure; + try { await handler(appRouter, "/app/setup")(req, { redirect() {} }); } catch (error) { failure = error; } + expect(resets).to.deep.equal(action === "install" ? [[owner]] : []); + if (action === "unverified") expect(failure?.message).to.equal("github_app_access_required"); + else expect(failure).to.equal(undefined); + }); +}); diff --git a/test/owner-notifications.test.js b/test/owner-notifications.test.js new file mode 100644 index 0000000..49756cc --- /dev/null +++ b/test/owner-notifications.test.js @@ -0,0 +1,635 @@ +const { expect } = require("chai"); +require("ts-node/register/transpile-only"); +const config = require("../src/config").default; +const User = require("../src/core/model/users/users.model").default; +const notifications = require("../src/core/owner-notifications"); +const { normalizeEmail, notificationEmail, notifyOwnerAccessProblem } = notifications; + +describe("owner access notifications", () => { + const restores = []; + const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; }; + const ownerId = "507f1f77bcf86cd799439011"; + const resource = { kind: "repository", id: "507f1f77bcf86cd799439012" }; + const other = { kind: "repository", id: "507f1f77bcf86cd799439013" }; + let owner, ownerState, sent, claims, rows, generations, resourceRevisions, attemptIds, recipients, events; + beforeEach(() => { + owner = { status: "active", emails: [{ email: "owner@example.com", default: true }] }; + ownerState = {}; + sent = []; claims = []; rows = new Map(); generations = new Map(); recipients = new Map(); events = []; + resourceRevisions = new Map(); attemptIds = new Map(); + stub(config, "RESEND_API_KEY", "test-resend-key"); + stub(config, "EMAIL_FROM", "alerts@example.com"); + stub(config, "APP_HOSTNAME", "anonymous.example.com"); + stub(User, "findOne", () => ({ lean: async () => ["removed", "banned"].includes(owner?.status) ? null : owner })); + stub(User, "findById", () => ({ select: () => ({ lean: async () => owner }) })); + Object.defineProperty(User.db, "readyState", { configurable: true, value: 1 }); + restores.push(() => { delete User.db.readyState; }); + stub(User.db, "collection", name => { + if (name === "users") return { + findOne: async filter => String(filter._id) === ownerId ? { ...ownerState } : null, + findOneAndUpdate: async (filter, update) => { + if (String(filter._id) !== ownerId) return { value: null }; + ownerState.accessAlertResetRevision = (ownerState.accessAlertResetRevision || 0) + update.$inc.accessAlertResetRevision; + Object.assign(ownerState, update.$set); return { value: { ...ownerState } }; + }, + updateOne: async (filter, update) => { + if (String(filter._id) !== ownerId) return { matchedCount: 0 }; + if ("accessAlertResetRevision" in filter && ownerState.accessAlertResetRevision !== (typeof filter.accessAlertResetRevision === "number" ? filter.accessAlertResetRevision : undefined)) return { matchedCount: 0 }; + if (filter.$or && ownerState.accessAlertResetUntil?.getTime() > Date.now()) return { matchedCount: 0 }; + const get = path => path.split(".").reduce((value, key) => value?.[key], ownerState); + const set = (path, value) => { + const keys = path.split("."), last = keys.pop(); let target = ownerState; + for (const key of keys) target = target[key] ||= {}; + if (value === undefined) delete target[last]; else target[last] = value; + }; + for (const [key, value] of Object.entries(filter)) if (key.startsWith("accessAlertDispatches.") && get(key) !== value) return { matchedCount: 0 }; + if (Array.isArray(update)) { + const cutoff = update[0].$set.accessAlertDispatches.$arrayToObject.$filter.cond.$or[1].$gt[1]; + ownerState.accessAlertDispatches = Object.fromEntries(Object.entries(ownerState.accessAlertDispatches || {}).filter(([, value]) => value.state === "committed" || value.claimedAt > cutoff)); + for (const [key, value] of Object.entries(update[1].$set)) set(key, value); + } else { + if (update.$inc) for (const [key, value] of Object.entries(update.$inc)) set(key, (get(key) || 0) + value); + if (update.$set) for (const [key, value] of Object.entries(update.$set)) set(key, value); + if (update.$unset) for (const key of Object.keys(update.$unset)) set(key, undefined); + } + return { matchedCount: 1 }; + }, + }; + if (name === "owner_access_alert_events") return { insertOne: async record => events.push(record), findOne: async filter => events.find(event => event._id === filter._id) }; + if (name === notifications.RECIPIENT_ALERTS) return { + findOne: async filter => recipients.get(filter._id), + find: filter => { + const matching = () => [...recipients.values()].filter(r => { + if (filter.deferredRecoveries) return r.deferredRecoveries?.some(recovery => String(recovery.owner) === String(filter.deferredRecoveries.$elemMatch.owner)); + const claim = filter.$or[0]; + return String(r.owner) === String(claim.owner) && r.resourceKind === claim.resourceKind + && String(r.resourceId) === String(claim.resourceId) && r.generation !== claim.generation.$ne && filter.state.$in.includes(r.state); + }).map(r => ({ ...r })); + return { toArray: async () => matching(), async *[Symbol.asyncIterator]() { yield* matching(); } }; + }, + updateMany: async filter => { if (filter.legacyClaims?.$elemMatch) return; const claim = filter; for (const r of recipients.values()) if (String(r.owner) === String(claim.owner) + && (!claim.resourceId || String(r.resourceId) === String(claim.resourceId))) r.resetRevision = (r.resetRevision || 0) + 1; }, + insertOne: async record => { if (recipients.has(record._id)) throw Object.assign(new Error("duplicate"), { code: 11000 }); recipients.set(record._id, { ...record }); }, + updateOne: async (filter, update) => { const record = recipients.get(filter._id); + const stateMatches = !filter.state || (typeof filter.state === "string" ? record?.state === filter.state : filter.state.$in.includes(record?.state)); + const revisionMatches = !("resetRevision" in filter) || record?.resetRevision === (typeof filter.resetRevision === "number" ? filter.resetRevision : undefined); + if (!record || record.attemptId !== filter.attemptId || !stateMatches || !revisionMatches) return { matchedCount: 0 }; + if (Array.isArray(update)) { + const recovery = update[0].$set.deferredRecoveries.$concatArrays[1][0]; + record.deferredRecoveries = [recovery]; + } else Object.assign(record, update.$set); + return { matchedCount: 1 }; + }, + deleteOne: async filter => { const r = recipients.get(filter._id); + const stateMatches = typeof filter.state === "string" ? r?.state === filter.state : filter.state.$in.includes(r?.state); + const revisionMatches = !("resetRevision" in filter) || r?.resetRevision === (typeof filter.resetRevision === "number" ? filter.resetRevision : undefined); + if (r?.attemptId === filter.attemptId && stateMatches && revisionMatches) { recipients.delete(filter._id); return { deletedCount: 1 }; } + return { deletedCount: 0 }; + }, + deleteMany: async filter => { let deletedCount = 0; for (const [key, r] of recipients) { + if (String(r.owner) === String(filter.owner) && r.resourceKind === filter.resourceKind && String(r.resourceId) === String(filter.resourceId) + && r.generation !== filter.generation.$ne && filter.state.$in.includes(r.state)) { recipients.delete(key); deletedCount++; } + } return { deletedCount }; }, + }; + return { + findOne: async filter => { + const key = `${name}:${filter.owner}:${filter._id}`; + if (!generations.has(key)) generations.set(key, undefined); + if ("accessAlertGeneration" in filter && generations.get(key) !== (typeof filter.accessAlertGeneration === "string" ? filter.accessAlertGeneration : undefined)) return null; + if (filter.accessAlertClaimedAt?.$exists && !rows.has(key)) return null; + if ("accessAlertClaimRevision" in filter && (resourceRevisions.get(key) || 0) !== (typeof filter.accessAlertClaimRevision === "number" ? filter.accessAlertClaimRevision : 0)) return null; + if (filter.accessAlertAttemptId && attemptIds.get(key) !== filter.accessAlertAttemptId) return null; + return { accessAlertGeneration: generations.get(key), accessAlertClaimedAt: rows.get(key), accessAlertClaimRevision: resourceRevisions.get(key), status: "ready" }; + }, + updateOne: async (filter, update) => { + claims.push(filter); const key = `${name}:${filter.owner}:${filter._id}`; + if (generations.get(key) !== (typeof filter.accessAlertGeneration === "string" ? filter.accessAlertGeneration : undefined)) return { matchedCount: 0 }; + const revision = resourceRevisions.get(key) || 0; + if (Array.isArray(update)) { + const first = filter.$or[0].accessAlertClaimRevision; + const expected = typeof first === "number" ? first : 0; + const attemptId = update[0].$set.accessAlertAttemptId.$cond[0].$eq[1]; + if (revision !== expected && !(revision === expected + 1 && attemptIds.get(key) === attemptId)) return { matchedCount: 0 }; + resourceRevisions.set(key, revision + 1); + if (attemptIds.get(key) === attemptId) { rows.delete(key); attemptIds.delete(key); } + } else { + if ("accessAlertClaimRevision" in filter && revision !== (typeof filter.accessAlertClaimRevision === "number" ? filter.accessAlertClaimRevision : 0)) return { matchedCount: 0 }; + rows.set(key, update.$set.accessAlertClaimedAt); attemptIds.set(key, update.$set.accessAlertAttemptId); + resourceRevisions.set(key, revision + update.$inc.accessAlertClaimRevision); + } + return { matchedCount: 1 }; + }, + updateMany: async (filter, update) => { + for (const key of generations.keys()) if (key.startsWith(`${name}:${filter.owner}:`) && (!filter._id || key.endsWith(`:${filter._id}`))) { + rows.delete(key); attemptIds.delete(key); generations.set(key, update.$set.accessAlertGeneration); + } + }, + }; + }); + stub(global, "fetch", async (url, options) => { sent.push({ url, options, body: JSON.parse(options.body) }); return { ok: true, status: 200 }; }); + }); + afterEach(() => { while (restores.length) restores.pop()(); }); + + it("accepts one mailbox and rejects recipient lists and header injection", () => { + expect(normalizeEmail(" Owner+alerts@example.com ")).to.equal("Owner+alerts@example.com"); + for (const value of [undefined, {}, "a@b.com,b@b.com", "Person ", "a@b.com\r\nBcc: x@y.com", ".a@b.com", "a..b@b.com", "a".repeat(65) + "@example.com"]) { + expect(normalizeEmail(value)).to.equal(null); + } + expect(notificationEmail([{ email: "first@example.com", default: false }, { email: "primary@example.com", default: true }])).to.equal("primary@example.com"); + }); + + it("sends once per recipient across resources and concurrent readers, with no time expiry", async () => { + await Promise.all(Array.from({ length: 10 }, () => notifyOwnerAccessProblem(ownerId, "token_expired", resource))); + expect(sent).to.have.length(1); + for (const key of rows.keys()) rows.set(key, new Date("2000-01-01")); + await notifyOwnerAccessProblem(ownerId, "repo_not_found", resource); + expect(sent).to.have.length(1); + await notifyOwnerAccessProblem(ownerId, "repo_not_found", other); + expect(sent).to.have.length(1); + expect(sent[0].url).to.equal("https://api.resend.com/emails"); + expect(sent[0].body.to).to.deep.equal(["owner@example.com"]); + expect(sent[0].body.text).to.include("https://anonymous.example.com/connections"); + expect(sent[0].options.headers["Idempotency-Key"]).to.match(/^owner-access-alert\//); + expect(events[0].state).to.equal("prepared"); + expect(events[1].state).to.equal("accepted"); + }); + + it("keeps the recipient suppressed after owner action and successful access", async () => { + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + await notifyOwnerAccessProblem(ownerId, "token_expired", other); + expect(sent).to.have.length(1); + await notifications.resetOwnerAccessAlerts(ownerId, resource); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(sent).to.have.length(1); + await notifications.withOwnerAccessRecovery(ownerId, other, async () => {}); + expect(recipients.size).to.equal(1); + await notifications.withOwnerAccessRecovery(ownerId, resource, async () => {}); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(sent).to.have.length(1); + expect([...recipients.values()][0].state).to.equal("recovered"); + expect(events).to.have.length(2); + }); + + it("cannot clear another owner's recipient guard", async () => { + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + await notifications.resetOwnerAccessAlerts("507f1f77bcf86cd799439099", resource); + await notifications.withOwnerAccessRecovery("507f1f77bcf86cd799439099", resource, async () => {}); + expect(recipients.size).to.equal(1); + }); + + it("does not reopen on background success without owner action", async () => { + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + await notifications.withOwnerAccessRecovery(ownerId, resource, async () => {}); + expect(recipients.size).to.equal(1); + }); + + it("retains the guard when access recovery fails", async () => { + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + await notifications.resetOwnerAccessAlerts(ownerId, resource); + try { await notifications.withOwnerAccessRecovery(ownerId, resource, async () => { throw new Error("still forbidden"); }); } catch { /* Expected upstream failure. */ } + expect(recipients.size).to.equal(1); + }); + + it("rejects a pre-action failure paused during owner lookup", async () => { + let resume, entered; + const waiting = new Promise(resolve => { entered = resolve; }); + stub(User, "findOne", () => ({ lean: async () => { + entered(); + await new Promise(resolve => { resume = resolve; }); + return owner; + } })); + const oldFailure = notifyOwnerAccessProblem(ownerId, "token_expired", resource); + await waiting; + await notifications.resetOwnerAccessAlerts(ownerId, resource); + resume(); + await oldFailure; + expect(sent).to.have.length(0); + stub(User, "findOne", () => ({ lean: async () => owner })); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(sent).to.have.length(1); + }); + + it("reconnecting leaves the recipient blocked across all resource types", async () => { + const resources = [resource, { ...resource, kind: "pull-request" }, { ...resource, kind: "gist" }]; + for (const item of resources) await notifyOwnerAccessProblem(ownerId, "token_expired", item); + expect(sent).to.have.length(1); + await notifications.resetOwnerAccessAlerts(ownerId); + for (const item of resources) await notifyOwnerAccessProblem(ownerId, "token_expired", item); + expect(sent).to.have.length(1); + }); + + for (const failure of ["rejected", "timeout"]) it(`retains the claim after ${failure} including confirmed recovery`, async () => { + let attempts = 0; + stub(global, "fetch", async () => { attempts++; if (failure === "timeout") throw new Error("timeout"); return { ok: false }; }); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(attempts).to.equal(1); + await notifications.resetOwnerAccessAlerts(ownerId, resource); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(attempts).to.equal(1); + await notifications.withOwnerAccessRecovery(ownerId, resource, async () => {}); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(attempts).to.equal(1); + }); + + it("does not claim an alert without configuration, an active owner, and an address", async () => { + config.RESEND_API_KEY = ""; + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + config.RESEND_API_KEY = "test-key"; + owner.status = "banned"; + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + owner.status = "active"; owner.emails = []; + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(claims).to.have.length(0); + owner.notificationEmail = "alerts@example.com"; + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(sent[0].body.to).to.deep.equal(["alerts@example.com"]); + }); + + it("ignores transient failures and contains database failures", async () => { + await notifyOwnerAccessProblem(ownerId, new Error("github_rate_limit_exceeded"), resource); + await notifyOwnerAccessProblem(ownerId, Object.assign(new Error("repo_not_found"), { cause: { status: 503 } }), resource); + expect(claims).to.have.length(0); + stub(User, "findOne", () => { throw new Error("database down"); }); + await notifyOwnerAccessProblem(ownerId, "token_expired", resource); + expect(sent).to.have.length(0); + }); + + it("recognizes raw authentication and missing-resource errors without alerting for outages", () => { + for (const status of [401, 403, 404]) expect(notifications.isAccessFailure(Object.assign(new Error("GitHub rejected access"), { status }))).to.equal(true); + for (const status of [429, 500, 503]) expect(notifications.isAccessFailure(Object.assign(new Error("upstream error"), { status }))).to.equal(false); + }); + + it("suppresses primary and secondary rate limits including wrapped streaming errors", () => { + const AnonymousError = require("../src/core/AnonymousError").default; + for (const error of [ + Object.assign(new Error("API rate limit exceeded"), { status: 403 }), + Object.assign(new Error("abuse detection"), { status: 403 }), + Object.assign(new Error("Forbidden"), { status: 403, response: { headers: { "x-ratelimit-remaining": "0" } } }), + Object.assign(new Error("secondary rate limit"), { response: { statusCode: 403 } }), + ]) { + expect(notifications.isAccessFailure(error)).to.equal(false); + expect(notifications.isAccessFailure(new AnonymousError("file_not_accessible", { httpStatus: 403, cause: error }))).to.equal(false); + } + const forbidden = Object.assign(new Error("SAML enforcement"), { response: { statusCode: 403 } }); + expect(notifications.isAccessFailure(new AnonymousError("file_not_accessible", { httpStatus: 403, cause: forbidden }))).to.equal(true); + }); + +}); + +describe("owner email settings", () => { + const utils = require("../src/server/routes/route-utils"); + const router = require("../src/server/routes/github-app").githubAppRouter; + const endpoint = router.stack.find(layer => layer.route?.path === "/connections/email" && layer.route.methods.post).route.stack[0].handle; + const readEmail = router.stack.find(layer => layer.route?.path === "/connections/email" && layer.route.methods.get).route.stack[0].handle; + const csrf = router.stack.find(layer => !layer.route && layer.handle.toString().includes("x-csrf-token")).handle; + let oldUser, oldUpdate, writes; + beforeEach(() => { + writes = []; + oldUser = utils.getUser; oldUpdate = User.updateOne; + utils.getUser = async () => ({ id: "signed-in-owner" }); + User.updateOne = async (...args) => { writes.push(args); }; + }); + afterEach(() => { utils.getUser = oldUser; User.updateOne = oldUpdate; }); + const response = () => ({ statusCode: 200, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; } }); + for (const email of ["", "primary@example.com"]) it(`loads the account email and persists its save token (${email || "no address"})`, async () => { + utils.getUser = async () => ({ id: "signed-in-owner", model: { emails: [], notificationEmail: email } }); + let saved = false; + const req = { sessionID: "profile-session", session: { save: done => { saved = true; done(); } } }; + const res = response(); + await readEmail(req, res); + expect(saved).to.equal(true); + expect(res.body).to.deep.equal({ notificationEmail: email, csrf: req.session.githubConnectionCSRF }); + expect(res.body.csrf).to.be.a("string").with.length(64); + expect(writes).to.have.length(0); + }); + it("updates only the signed-in owner's address", async () => { + const res = response(); + await endpoint({ body: { email: " owner@example.com ", ownerId: "someone-else" } }, res); + expect(writes).to.deep.equal([[{ _id: "signed-in-owner" }, { $set: { notificationEmail: "owner@example.com" } }]]); + expect(res.body.notificationEmail).to.equal("owner@example.com"); + }); + it("rejects malformed input without a database write", async () => { + const res = response(); + await endpoint({ body: { email: ["one@example.com", "two@example.com"] } }, res); + expect(res.statusCode).to.equal(400); + expect(writes).to.have.length(0); + }); + for (const action of ["later", "never"]) it(`persists ${action} at the right scope`, async () => { + const req = { body: { action }, session: { save: callback => callback() } }; + const res = response(); + await endpoint(req, res); + expect(req.session.emailPromptDismissed).to.equal(true); + expect(res.body.ok).to.equal(true); + expect(writes).to.deep.equal(action === "never" ? [[{ _id: "signed-in-owner" }, { $set: { emailPromptNever: true } }]] : []); + }); + it("rejects unknown preference actions", async () => { + const res = response(); + await endpoint({ body: { action: "invalid" } }, res); + expect(res.statusCode).to.equal(400); + expect(writes).to.have.length(0); + }); + it("initializes the same CSRF token for parallel session snapshots", () => { + const { connectionCSRF } = require("../src/server/routes/github-app"); + const userRequest = { sessionID: "same-login-session", session: {} }; + const connectionsRequest = { sessionID: "same-login-session", session: {} }; + const popupToken = connectionCSRF(userRequest); + const pageToken = connectionCSRF(connectionsRequest); + expect(popupToken).to.equal(pageToken); + expect(connectionCSRF({ sessionID: "another-login", session: {} })).not.to.equal(pageToken); + for (const token of [popupToken, pageToken]) { + let accepted = false; + csrf({ method: "POST", session: connectionsRequest.session, headers: { "x-csrf-token": token } }, response(), () => { accepted = true; }); + expect(accepted).to.equal(true); + } + }); + it("requires the session's CSRF token before changing email", () => { + const res = response(); let next = false; + csrf({ method: "POST", session: { githubConnectionCSRF: "secret" }, headers: {} }, res, () => { next = true; }); + expect(res.statusCode).to.equal(403); expect(next).to.equal(false); + csrf({ method: "POST", session: { githubConnectionCSRF: "secret" }, headers: { "x-csrf-token": "secret" } }, response(), () => { next = true; }); + expect(next).to.equal(true); + }); +}); + +describe("email popup eligibility", () => { + const utils = require("../src/server/routes/route-utils"); + const router = require("../src/server/routes/user").default; + const endpoint = router.stack.find(layer => layer.route?.path === "/" && layer.route.methods.get).route.stack[0].handle; + let oldUser, oldKey, oldFrom; + beforeEach(() => { + oldUser = utils.getUser; oldKey = config.RESEND_API_KEY; oldFrom = config.EMAIL_FROM; + config.RESEND_API_KEY = "test"; config.EMAIL_FROM = "alerts@example.com"; + }); + afterEach(() => { utils.getUser = oldUser; config.RESEND_API_KEY = oldKey; config.EMAIL_FROM = oldFrom; }); + for (const reason of ["eligible", "email", "alert-email", "never", "later", "no-key", "no-sender"]) { + it(`checks ${reason} before showing the form`, async () => { + utils.getUser = async () => ({ username: "owner", model: { + emails: reason === "email" ? [{ email: "owner@example.com", default: true }] : [], + notificationEmail: reason === "alert-email" ? "alerts@example.com" : undefined, + emailPromptNever: reason === "never", + } }); + if (reason === "no-key") config.RESEND_API_KEY = ""; + if (reason === "no-sender") config.EMAIL_FROM = ""; + const req = { session: { emailPromptDismissed: reason === "later", save: cb => cb() } }; + const res = { set() {}, json(body) { this.body = body; } }; + await endpoint(req, res); + expect(res.body.showEmailPrompt).to.equal(reason === "eligible"); + expect(!!res.body.emailPromptCSRF).to.equal(reason === "eligible"); + }); + } +}); + +describe("repository notification hooks", () => { + const Repository = require("../src/core/Repository").default; + const RepositoryModel = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; + const Files = require("../src/core/model/files/files.model").default; + const github = require("../src/core/source/GitHubRepository"); + const tokens = require("../src/core/GitHubUtils"); + const app = require("../src/core/github-app"); + const restores = []; + const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; }; + let repo, sent; + beforeEach(() => { + sent = []; + repo = new Repository(new RepositoryModel({ repoId: "anonymous-id", owner: "507f1f77bcf86cd799439011", status: "ready", + source: { repositoryName: "private/source", branch: "main" }, options: { expirationMode: "never", update: true } })); + stub(notifications, "notifyOwnerAccessProblem", async (...args) => { sent.push(args); }); + }); + afterEach(() => { while (restores.length) restores.pop()(); }); + async function failsWith(work, error) { + try { await work(); } catch (caught) { expect(caught).to.equal(error); return; } + throw new Error("Expected original failure"); + } + it("notifies the saved owner when an App repository token cannot be resolved", async () => { + repo.model.githubAccess = { kind: "github-app", revision: "test" }; + const error = new Error("github_app_access_required"); + stub(app, "boundAppToken", async () => { throw error; }); + await failsWith(() => tokens.getToken(repo), error); + expect(sent).to.deep.equal([[repo.owner.id, error, { kind: "repository", id: String(repo.model._id) }]]); + }); + it("notifies the saved owner when reading the repository tree fails", async () => { + const error = new Error("repo_not_found"); + stub(Files, "exists", () => ({ exec: async () => null })); + Object.defineProperty(repo, "source", { value: { getFiles: async () => { throw error; } } }); + await failsWith(() => repo.files(), error); + expect(sent).to.deep.equal([[repo.owner.id, error, { kind: "repository", id: String(repo.model._id) }]]); + }); + for (const step of ["branches", "commit"]) it(`notifies on ${step} access errors during refresh`, async () => { + const error = Object.assign(new Error("Forbidden"), { status: 403 }); + repo.getToken = async () => "token"; + stub(github, "getRepositoryFromGitHub", async () => ({ + fullName: repo.model.source.repositoryName, model: { defaultBranch: "main" }, + branches: async () => { if (step === "branches") throw error; return [{ name: "main", commit: "new-sha" }]; }, + getCommitInfo: async () => { throw error; }, + })); + await failsWith(() => repo.updateIfNeeded({ force: true }), error); + expect(sent).to.deep.equal([[repo.owner.id, error, { kind: "repository", id: String(repo.model._id) }]]); + }); + it("notifies the saved owner when a repository refresh cannot read GitHub", async () => { + const error = new Error("repo_not_found"); + repo.getToken = async () => "token"; + stub(github, "getRepositoryFromGitHub", async () => { throw error; }); + await failsWith(() => repo.updateIfNeeded({ force: true }), error); + expect(sent).to.deep.equal([[repo.owner.id, error, { kind: "repository", id: String(repo.model._id) }]]); + }); +}); + +describe("gist and pull-request notification hooks", () => { + const Gist = require("../src/core/Gist").default; + const GistModel = require("../src/core/model/anonymizedGists/anonymizedGists.model").default; + const PullRequest = require("../src/core/PullRequest").default; + const PullRequestModel = require("../src/core/model/anonymizedPullRequests/anonymizedPullRequests.model").default; + const github = require("../src/core/GitHubUtils"); + const restores = []; + const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; }; + let sent; + beforeEach(() => { sent = []; stub(notifications, "notifyOwnerAccessProblem", async (...args) => { sent.push(args); }); }); + afterEach(() => { while (restores.length) restores.pop()(); }); + for (const type of ["gist", "pull-request"]) for (const failure of ["oauth", "app-access"]) { + it(`alerts the owner on ${type} ${failure} failure and preserves the error`, async () => { + const data = { owner: "507f1f77bcf86cd799439011", source: { gistId: "gist", repositoryFullName: "private/source", pullRequestId: 1 }, options: { expirationMode: "never" } }; + const resource = type === "gist" ? new Gist(new GistModel(data)) : new PullRequest(new PullRequestModel(data)); + const error = failure === "oauth" ? Object.assign(new Error("Bad credentials"), { status: 401 }) : new Error("github_app_access_required"); + if (type === "gist") { + resource.getAccess = async () => { + if (failure === "app-access") throw error; + return { token: "token", connection: "oauth" }; + }; + resource.downloadContent = async () => { throw error; }; + } else { + resource.getToken = async () => { if (failure === "app-access") throw error; return "token"; }; + stub(github, "octokit", () => ({ rest: { pulls: { get: async () => { throw error; } } } })); + } + let caught; + try { await resource.download(); } catch (err) { caught = err; } + expect(caught).to.be.instanceOf(Error); + expect(caught.message).to.equal(type === "gist" && failure === "oauth" ? "github_oauth_required" : error.message); + expect(sent).to.deep.equal([[resource.owner.id, caught, { kind: type, id: String(resource.model._id) }]]); + }); + } +}); + +describe("streamed file access alerts", () => { + const File = require("../src/core/AnonymizedFile").default; + const AnonymousError = require("../src/core/AnonymousError").default; + const { PassThrough } = require("stream"); + const got = require("got"); + const restores = []; + const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; }; + let sent, repository, file; + beforeEach(() => { + sent = []; + stub(notifications, "notifyOwnerAccessProblem", async (owner, error) => { if (notifications.isAccessFailure(error)) sent.push({ owner, error }); }); + repository = { owner: { id: "saved-owner" }, options: { terms: [] }, model: { source: {} }, repoId: "anon-id", status: "ready", source: {}, + getToken: async () => "token", generateAnonymizeTransformer: () => new PassThrough() }; + file = new File({ repository, anonymizedPath: "README.md" }); + }); + afterEach(() => { while (restores.length) restores.pop()(); }); + for (const status of [403, 404, 503]) it(`handles local file download failure ${status}`, async () => { + const cause = Object.assign(new Error("upstream"), { response: { statusCode: status } }); + const error = new AnonymousError(status === 403 ? "file_not_accessible" : status === 404 ? "file_not_found" : "upstream_error", { httpStatus: status, cause }); + repository.source.getFileContent = async () => { throw error; }; + let caught; + try { await file.content(); } catch (err) { caught = err; } + expect(caught).to.equal(error); + expect(sent).to.have.length(status === 503 ? 0 : 1); + if (sent.length) expect(sent[0].owner).to.equal("saved-owner"); + }); + for (const status of [403, 404, 503]) it(`handles remote streamer error ${status}`, async () => { + stub(config, "STREAMER_ENTRYPOINT", "http://streamer/"); + const stream = new PassThrough(); + stub(got, "stream", () => stream); + file._file = { name: "README.md", path: "", sha: "sha", size: 10 }; + const returned = await file.anonymizedContent(); + expect(returned).to.equal(stream); + const error = Object.assign(new Error("streamer failure"), { response: { statusCode: status, body: JSON.stringify({ error: status === 503 ? "upstream_error" : "file_not_accessible" }) } }); + stream.emit("error", error); + expect(sent).to.have.length(status === 503 ? 0 : 1); + stream.destroy(); + }); +}); + +describe("token validation preserves upstream failures", () => { + const tokens = require("../src/core/GitHubUtils"); + const { registerGitHubToken } = require("../src/core/github-token-context"); + for (const status of [undefined, 403, 429, 500, 503]) it(`propagates token-check failure ${status}`, async () => { + const error = Object.assign(new Error("upstream check failed"), { status }); + const token = `test-token-check-${status}`; + registerGitHubToken(token, { quotaKey: token, publicRepository: "owner/repo", renew: async () => { throw error; } }); + let caught; + try { await tokens.checkToken(token); } catch (err) { caught = err; } + expect(caught).to.equal(error); + }); + it("returns false only for a confirmed invalid token", async () => { + const token = "test-token-check-invalid"; + registerGitHubToken(token, { quotaKey: token, publicRepository: "owner/repo", renew: async () => { throw Object.assign(new Error("Bad credentials"), { status: 401 }); } }); + expect(await tokens.checkToken(token)).to.equal(false); + }); +}); + +describe("repository probes and truncated file recovery", () => { + const github = require("../src/core/GitHubUtils"); + const db = require("../src/server/database"); + const Model = require("../src/core/model/repositories/repositories.model").default; + const Files = require("../src/core/model/files/files.model").default; + const File = require("../src/core/AnonymizedFile").default; + const GitHubStream = require("../src/core/source/GitHubStream").default; + const AnonymizedRepositories = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; + const storage = require("../src/core/storage").default; + const { Readable } = require("node:stream"); + const { classifyGitHubMissError } = require("../src/core/source/GitHubBase"); + const restores = []; + const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; }; + const source = { getToken: async () => "probe-test", repoId: "anon", organization: "owner", repoName: "repo", commit: "sha" }; + const missing = () => Object.assign(new Error("Not found"), { status: 404 }); + afterEach(() => { while (restores.length) restores.pop()(); }); + for (const failed of [false, true]) it(`continues truncated metadata reads after recovery lookup failure: ${failed ? "upstream failure" : "success"}`, async () => { + const upstreamError = Object.assign(new Error("GitHub unavailable"), { status: 503 }); + let reads = 0; + stub(db, "isConnected", true); + stub(AnonymizedRepositories, "findOne", () => ({ select: () => ({ lean: () => ({ exec: async () => { throw new Error("recovery database unavailable"); } }) }) })); + stub(notifications, "withOwnerAccessRecovery", () => { throw new Error("recovery must be skipped without metadata"); }); + stub(github, "octokit", () => ({ repos: { getContent: async () => { + reads++; if (failed) throw upstreamError; + return { data: { type: "file", sha: "blob", size: 11 } }; + } } })); + const result = await new GitHubStream(source).fetchFileInfoFromPath("large/file.txt").then(value => ({ value }), error => ({ error })); + expect(reads).to.equal(1); + if (failed) expect(result.error).to.equal(upstreamError); + else expect(result.value).to.include({ name: "file.txt", path: "large", sha: "blob", size: 11 }); + }); + for (const failed of [false, true]) it(`continues uncached file reads after recovery lookup failure: ${failed ? "upstream failure" : "success"}`, async () => { + const upstreamError = Object.assign(new Error("GitHub unavailable"), { status: 503 }); + const data = { ...source, repoId: `lookup-failure-${failed}` }; + let cached, downloads = 0, recoveryLookups = 0; + stub(db, "isConnected", true); + stub(AnonymizedRepositories, "findOne", () => ({ select: fields => ({ lean: () => ({ exec: async () => { + if (fields === "owner source githubAccess") { recoveryLookups++; throw new Error("recovery database unavailable"); } + return { status: "ready" }; + } }) }) })); + stub(notifications, "withOwnerAccessRecovery", () => { throw new Error("recovery must be skipped without metadata"); }); + stub(storage, "fileInfo", async () => { if (!cached) throw Object.assign(new Error("missing"), { code: "ENOENT" }); return { size: cached.length }; }); + stub(storage, "write", async (_repo, _path, input) => { const chunks = []; for await (const chunk of input) chunks.push(chunk); cached = Buffer.concat(chunks); }); + stub(storage, "read", async () => Readable.from([cached])); + const stream = new GitHubStream(data); + stream.downloadWithFallback = async () => { downloads++; if (failed) throw upstreamError; return Readable.from([Buffer.from("fresh-bytes")]); }; + const result = await stream.getFileContentCache("file.txt", data.repoId, () => ({ sha: "blob", size: 11 })) + .then(value => ({ value }), error => ({ error })); + expect(downloads).to.equal(1); expect(recoveryLookups).to.equal(1); + if (failed) { expect(result.error.message).to.equal("upstream_error"); expect(result.error.cause).to.equal(upstreamError); } + else { + const chunks = []; for await (const chunk of result.value) chunks.push(chunk); + expect(Buffer.concat(chunks).toString()).to.equal("fresh-bytes"); + } + }); + for (const stage of ["name", "id"]) for (const status of [undefined, 429, 503]) { + it(`propagates inconclusive ${stage} probe ${status}`, async () => { + const error = Object.assign(new Error("probe failed"), { status }); + stub(db, "isConnected", true); + stub(Model, "findOne", async () => ({ name: "owner/repo", externalId: "gh_123" })); + stub(github, "octokit", () => ({ repos: { get: async () => { throw stage === "name" ? error : missing(); } }, request: async () => { throw error; } })); + let caught; + try { await classifyGitHubMissError(missing(), source); } catch (err) { caught = err; } + expect(caught).to.equal(error); + expect(notifications.isAccessFailure(caught)).to.equal(false); + }); + } + it("preserves an inconclusive repository probe during commit refresh", async () => { + const { GitHubRepository } = require("../src/core/source/GitHubRepository"); + const error = Object.assign(new Error("probe unavailable"), { status: 503 }); + stub(github, "octokit", () => ({ repos: { + getCommit: async () => { throw missing(); }, get: async () => { throw error; }, + } })); + let caught; + try { await new GitHubRepository({ name: "owner/repo" }).getCommitInfo("sha", { accessToken: "token" }); } catch (err) { caught = err; } + expect(caught).to.equal(error); + expect(notifications.isAccessFailure(caught)).to.equal(false); + }); + for (const status of [403, 503]) it(`preserves truncated recovery error ${status} before file content is read`, async () => { + const error = Object.assign(new Error("contents lookup failed"), { status }); + stub(github, "octokit", () => ({ repos: { getContent: async () => { throw error; } } })); + stub(Files, "findOne", async () => null); + const sent = []; + stub(notifications, "notifyOwnerAccessProblem", async (owner, err) => { if (notifications.isAccessFailure(err)) sent.push(owner); }); + const file = new File({ anonymizedPath: "large/missing.txt", repository: { + owner: { id: "saved-owner" }, repoId: "anon", options: { terms: [] }, + model: { truncatedFolders: ["large"] }, source: new GitHubStream(source), + } }); + let caught; + try { await file.originalPath(); } catch (err) { caught = err; } + expect(caught).to.equal(error); + expect(sent).to.deep.equal(status === 403 ? ["saved-owner"] : []); + }); + for (const repoMissing of [false, true]) it(`distinguishes a missing truncated file from lost repository access: ${repoMissing}`, async () => { + stub(db, "isConnected", false); + stub(github, "octokit", () => ({ repos: { + getContent: async () => { throw missing(); }, + get: async () => { if (repoMissing) throw missing(); return {}; }, + } })); + let result, caught; + try { result = await new GitHubStream(source).fetchFileInfoFromPath("large/missing.txt"); } catch (err) { caught = err; } + if (repoMissing) expect(caught.message).to.equal("repo_not_found"); + else { expect(caught).to.equal(undefined); expect(result).to.equal(null); } + }); +}); diff --git a/test/performance-integration.test.js b/test/performance-integration.test.js index 994079b..b71ac06 100644 --- a/test/performance-integration.test.js +++ b/test/performance-integration.test.js @@ -40,6 +40,19 @@ const uri = process.env.PERFORMANCE_MONGO_URI; }); beforeEach(async () => { await Promise.all([Repo, PR, Gist, File, Path, Name, ConferenceModel].map(model => model.deleteMany({}))); }); + it("preserves submodule origin through the persisted anonymized path index", async () => { + const row = await Repo.create({ repoId: "submodule-index", owner: user.model._id, status: "ready", treeGeneration: "generation", + source: { repositoryName: "parent/repo", commit: "a".repeat(40) }, options: { terms: ["Alice"] } }); + const upstream = { organization: "child", repoName: "public", commit: "b".repeat(40), path: "Alice.txt" }; + await File.create({ repoId: row.repoId, treeGeneration: "generation", name: "Alice.txt", path: "vendor/lib", sha: "c".repeat(40), size: 5, upstream }); + const repository = new Repository(row); + const file = new AnonymizedFile({ repository, anonymizedPath: "vendor/lib/XXXX-1.txt" }); + expect((await file.getFileInfo()).upstream).to.deep.equal(upstream); + const publicTree = await repository.anonymizedFiles({ includeSha: false }); + expect(publicTree[0]).not.to.have.property("upstream"); + expect(JSON.stringify(publicTree)).not.to.include("child"); + }); + it("uses membership indexes for owner, stable GitHub ID, and legacy username", async () => { const other = new mongoose.Types.ObjectId(); await Repo.insertMany(Array.from({ length: 2000 }, (_, i) => ({ repoId: `filler-${i}`, owner: other, coauthors: [{ username: `other-${i}`, githubId: String(i + 50000) }] }))); @@ -115,6 +128,7 @@ const uri = process.env.PERFORMANCE_MONGO_URI; await Gist.collection.createIndex({ statusMessage: 1 }, { name: "keep_existing_index" }); for (const model of [Repo, Gist, PR]) await model.collection.dropIndex("status_1_options.expirationDate_1"); await Repo.collection.dropIndex("retiredTreeGenerations_1"); + await Repo.collection.dropIndex("retiredPathIndexKeys_1"); await Repo.collection.dropIndex("stagedFileTrees.until_1"); await File.collection.dropIndex("treeStaged_1_repoId_1_treeGeneration_1"); await File.collection.dropIndex("metadataPending_1_repoId_1_treeGeneration_1"); @@ -130,6 +144,7 @@ const uri = process.env.PERFORMANCE_MONGO_URI; } expect((await Gist.collection.indexes()).map(index => index.name)).to.include("keep_existing_index"); expect((await Repo.collection.indexes()).find(index => index.name === "retiredTreeGenerations_1").sparse).to.equal(true); + expect((await Repo.collection.indexes()).find(index => index.name === "retiredPathIndexKeys_1").sparse).to.equal(true); expect((await Repo.collection.indexes()).find(index => index.name === "stagedFileTrees.until_1").sparse).to.equal(true); expect((await File.collection.indexes()).find(index => index.name === "treeStaged_1_repoId_1_treeGeneration_1").partialFilterExpression).to.deep.equal({ treeStaged: true }); expect((await File.collection.indexes()).find(index => index.name === "metadataPending_1_repoId_1_treeGeneration_1").partialFilterExpression).to.deep.equal({ metadataPending: true }); @@ -389,7 +404,7 @@ const uri = process.env.PERFORMANCE_MONGO_URI; expect(response).to.deep.equal({ "": 1, src: 1 }); await File.deleteMany({ repoId: model.repoId, treeGeneration: "active" }); let refreshed = false; - repo.updateIfNeeded = async ({ force }) => { refreshed = force; }; + repo.refresh = async () => { refreshed = true; }; expect(await repo.isReady()).to.equal(false); expect(refreshed).to.equal(true); } finally { utils.getRepo = getRepo; } }); @@ -716,7 +731,7 @@ const uri = process.env.PERFORMANCE_MONGO_URI; expect((await Repo.findById(model._id)).emptyTreeGeneration).to.equal(undefined); await File.deleteMany({ repoId: model.repoId }); let refreshed = false; - fresh.updateIfNeeded = async () => { refreshed = true; }; + fresh.refresh = async () => { refreshed = true; }; expect(await fresh.isReady()).to.equal(false); expect(refreshed).to.equal(true); }); @@ -1000,10 +1015,10 @@ const uri = process.env.PERFORMANCE_MONGO_URI; treeGeneration: previousGeneration, source: { type: "GitHubStream" }, options: { expirationMode: "never" } }); await File.create({ repoId: model.repoId, treeGeneration: previousGeneration, path: "private", name: "old.txt", size: 1 }); await File.create({ repoId: "unrelated", treeGeneration: previousGeneration, path: "", name: "keep.txt", size: 1 }); - const repo = new Repository(model), deleteMany = File.deleteMany; + const repo = new Repository(model), deleteMany = File.collection.deleteMany; Object.defineProperty(repo, "source", { get: () => ({ getFiles: async () => [{ path: "", name: "active.txt", size: 2 }] }) }); - File.deleteMany = function (filter, ...args) { - if (filter.repoId === model.repoId && filter.$or) return { exec: async () => { throw Error("cleanup unavailable"); } }; + File.collection.deleteMany = async function (filter, ...args) { + if (filter.repoId === model.repoId && filter.$or) throw Error("cleanup unavailable"); return deleteMany.call(this, filter, ...args); }; try { @@ -1014,7 +1029,7 @@ const uri = process.env.PERFORMANCE_MONGO_URI; await require("../src/server/schedule").runRepositoryStatusCheck(); expect((await Repo.findById(model._id)).retiredTreeGenerations).to.have.length(1); expect(await File.countDocuments({ repoId: model.repoId })).to.equal(2); - } finally { File.deleteMany = deleteMany; } + } finally { File.collection.deleteMany = deleteMany; } await require("../src/server/schedule").runRepositoryStatusCheck(); expect((await Repo.findById(model._id)).retiredTreeGenerations).to.equal(undefined); expect((await File.find({ repoId: model.repoId })).map(file => file.name)).to.deep.equal(["active.txt"]); @@ -1026,13 +1041,14 @@ const uri = process.env.PERFORMANCE_MONGO_URI; await Repo.insertMany(Array.from({ length: 2000 }, (_, i) => ({ repoId: `clean-${i}` }))); await File.insertMany(Array.from({ length: 2000 }, (_, i) => ({ repoId: `clean-${i}`, path: "", name: "file.txt" }))); await Repo.create({ repoId: "retired", treeGeneration: "active", retiredTreeGenerations: ["old"] }); + await Repo.create({ repoId: "retired-paths", retiredPathIndexKeys: ["old-key"] }); await Repo.create({ repoId: "retired-content", retiredContentPrefixes: ["__content/old"] }); await Repo.create({ repoId: "legacy-content", legacyContentCleanupPending: true, status: "ready" }); await Repo.create({ repoId: "staged", stagedFileTrees: [{ generation: "abandoned", until: new Date(0) }] }); await File.create({ repoId: "pending", path: "", name: "pending.txt", metadataPending: true }); await File.create({ repoId: "staged", treeGeneration: "abandoned", treeStaged: true, path: "private", name: "staged.txt" }); for (const [model, query] of [[Repo, { $or: [ - { retiredTreeGenerations: { $exists: true } }, { retiredContentPrefixes: { $exists: true } }, + { retiredTreeGenerations: { $exists: true } }, { retiredPathIndexKeys: { $exists: true } }, { retiredContentPrefixes: { $exists: true } }, { legacyContentCleanupPending: true, status: "ready" }, { "stagedFileTrees.until": { $lte: new Date() } }, ] }], [File, { metadataPending: true }], [File, { treeStaged: true }]]) { diff --git a/test/performance-regressions.test.js b/test/performance-regressions.test.js index 6c1fab7..73198dd 100644 --- a/test/performance-regressions.test.js +++ b/test/performance-regressions.test.js @@ -117,26 +117,28 @@ describe("performance regressions", function () { calls.push({ sha, recursive: options.recursive }); return { truncated: false, tree: Array.from({ length: 20 }, (_, i) => ({ path: `folder${i}/file.ts`, type: "blob", sha: "abc" })) }; }; - const files = await source.getTruncatedTree("head"); + const files = await source.getFiles(); expect(files).to.have.length(20); expect(calls).to.deep.equal([{ sha: "head", recursive: true }]); }); - for (const [upstream, code, status] of [ + for (const [caseId, [upstream, code, status]] of [ [Object.assign(new Error("forbidden"), { response: { statusCode: 403 } }), "file_not_accessible", 403], [Object.assign(new Error("missing"), { response: { statusCode: 404 } }), "file_not_found", 404], [Object.assign(new Error("large"), { status: 422 }), "file_too_big", 422], [Object.assign(new Error("missing"), { httpStatus: 404 }), "file_not_found", 404], [Object.assign(new Error("reset"), { code: "ECONNRESET" }), "upstream_error", 502], + [new (require("../src/core/GitHubUtils").RateLimitDelayError)(Date.now() + 60000, "quota-key"), "github_rate_limit_exceeded", 429], [new (require("../src/core/AnonymousError").default)("file_not_accessible", { httpStatus: 403 }), "file_not_accessible", 403], - ]) { + ].entries()) { it(`preserves ${code}/${status} for failed downloads (${upstream.message})`, async () => { - const source = new GitHubStream({ repoId: "failed-download", organization: "owner", repoName: "repo", getToken: () => "token" }); + const source = new GitHubStream({ repoId: `failed-download-${caseId}`, organization: "owner", repoName: "repo", getToken: () => "token" }); const input = new PassThrough(); stub(storage, "fileInfo", async () => { throw Error("cache miss"); }); let committed = false; stub(storage, "write", async (_repo, _path, stream) => { for await (const chunk of stream) void chunk; committed = true; }); source.downloadWithFallback = async () => { setImmediate(() => input.destroy(upstream)); return input; }; - try { await source.getFileContentCache("file.txt", "failed-download", () => ({ sha: "blob", size: 1 })); throw Error("expected failure"); } + stub(require("../src/core/source/github-content-miss"), "classifyContentMiss", async () => "file_not_found"); + try { await source.getFileContentCache("file.txt", source.data.repoId, () => ({ sha: "blob", size: 1 })); throw Error("expected failure"); } catch (error) { expect(error.message).to.equal(code); expect(error.httpStatus).to.equal(status); if (upstream instanceof require("../src/core/AnonymousError").default) expect(error).to.equal(upstream); @@ -155,7 +157,7 @@ describe("performance regressions", function () { if (sha === "head") return { truncated: false, tree: [{ path: "src", type: "tree", sha: "src" }] }; return { truncated: false, tree: [{ path: "file.ts", type: "blob", sha: "abc" }] }; }; - const files = await source.getTruncatedTree("head"); + const files = await source.getFiles(); expect(files.map(file => `${file.path}/${file.name}`)).to.deep.equal(["/src", "src/file.ts"]); expect(calls).to.deep.equal(["head:true", "head:false", "src:true"]); }); diff --git a/test/performance-streamer-integration.test.js b/test/performance-streamer-integration.test.js index 58d9299..ccfc38b 100644 --- a/test/performance-streamer-integration.test.js +++ b/test/performance-streamer-integration.test.js @@ -75,6 +75,22 @@ const uri = process.env.PERFORMANCE_MONGO_URI; repoId: row.repoId, repoFullName: "owner/repo", commit: "abc", ...(legacy ? {} : { cacheGeneration: options.cacheGeneration }), sha: "blob", size: 300000, filePath, anonymizerOptions: options }) }); } + for (const [filePath, expectedStatus] of [["fail-401.txt", 401], ["fail-reset.txt", 502]]) { + it(`contains ${filePath} errors in the actual streamer and serves another request`, async () => { + const runtime = await launch(); + try { + await runtime.ready; + const row = await createRow(filePath); + const failure = await request(runtime, row, filePath); + expect(failure.status).to.equal(expectedStatus); await failure.text(); + expect((await fetch(runtime.url + "/healthcheck")).status).to.equal(200); + const recovered = await request(runtime, row, "working.txt"); + expect(recovered.status).to.equal(200); expect((await recovered.text()).length).to.equal(300000); + expect(runtime.child.exitCode).to.equal(null); + } finally { await stop(runtime); } + }); + } + it("connects before serving and rejects a fill published after expiration in the actual streamer entrypoint", async () => { const runtime = await launch(false, true); let pending; try { diff --git a/test/production-failure-fixes.test.js b/test/production-failure-fixes.test.js new file mode 100644 index 0000000..48846ba --- /dev/null +++ b/test/production-failure-fixes.test.js @@ -0,0 +1,160 @@ +const { expect } = require("chai"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); +require("ts-node/register/transpile-only"); +const config = require("../src/config").default; +const credentials = require("../src/core/credentials"); +const { refreshLegacyToken } = require("../src/core/legacy-token-refresh"); +const { ExpiringMap } = require("../src/core/expiring-map"); +const { sendRequestRateLimit } = require("../src/server/request-rate-limit"); +const GitHubStream = require("../src/core/source/GitHubStream").default; +const storage = require("../src/core/storage").default; +const { logSummary, observeLine, containerSummary } = require("../scripts/check-production-logs"); + +describe("production failure fixes", function () { + this.timeout(20000); + const restores = []; + function stub(object, key, value) { const old = object[key]; object[key] = value; restores.push(() => { object[key] = old; }); } + afterEach(() => restores.splice(0).reverse().forEach(restore => restore())); + for (const mode of ["401", "reset", "lfs", "fallback", "public-raw"]) { + it(`contains ${mode} upstream stream errors in a process with no recovery handler`, function () { + const result = spawnSync(process.execPath, [path.join(__dirname, "fixtures/github-stream-failure.js"), mode], { + timeout: 15000, encoding: "utf8", env: { ...process.env, NODE_ENV: "test", GITHUB_APP_ENABLED: "false", REDIS_HOSTNAME: "127.0.0.1", REDIS_PORT: "1" }, + }); + expect(result.status, result.stderr).to.equal(0); + expect(result.stdout).to.include("failure contained"); + }); + } + it("records a fatal event without keeping a broken process running", function () { + const result = spawnSync(process.execPath, [path.join(__dirname, "fixtures/github-stream-failure.js"), "fatal"], { + timeout: 15000, encoding: "utf8", env: { ...process.env, NODE_ENV: "test", GITHUB_APP_ENABLED: "false", REDIS_PORT: "1" }, + }); + expect(result.status).to.equal(1); + expect(result.stderr + result.stdout).to.include("process_fatal_error"); + }); + function oauth() { + stub(config, "GITHUB_OAUTH_ENABLED", true); stub(config, "CLIENT_ID", "test-client"); stub(config, "CLIENT_SECRET", "test-secret"); + } + it("coalesces token refreshes and backs off on permanent 404 responses without replacing credentials", async () => { + oauth(); let calls = 0, replacements = 0; + stub(globalThis, "fetch", async () => { calls++; return { ok: false, status: 404, body: { cancel: async () => {} } }; }); + stub(credentials, "replaceCredential", async () => { replacements++; return true; }); + const first = await Promise.all(Array.from({ length: 8 }, () => refreshLegacyToken("owner-failure", "gho_first"))); + expect(first).to.deep.equal(Array(8).fill(null)); + expect(await refreshLegacyToken("owner-failure", "gho_first")).to.equal(null); + expect(calls).to.equal(1); expect(replacements).to.equal(0); + await refreshLegacyToken("owner-failure", "gho_reconnected"); expect(calls).to.equal(2); + config.CLIENT_SECRET = "rotated-secret"; + await refreshLegacyToken("owner-failure", "gho_first"); expect(calls).to.equal(3); + }); + it("skips PAT and GitHub App tokens and disabled OAuth", async () => { + oauth(); stub(globalThis, "fetch", () => { throw Error("unexpected refresh"); }); + for (const token of ["ghp_token", "ghs_token", "ghu_token", "ghr_token", "github_pat_token"]) expect(await refreshLegacyToken("owner", token)).to.equal(null); + config.GITHUB_OAUTH_ENABLED = false; expect(await refreshLegacyToken("owner", "gho_token")).to.equal(null); + }); + it("keeps a newer login when the refresh compare-and-swap loses", async () => { + oauth(); stub(globalThis, "fetch", async () => ({ ok: true, status: 200, json: async () => ({ token: "refreshed" }) })); + stub(credentials, "replaceCredential", async () => false); + stub(credentials, "getCredentialToken", async () => "new-login"); + expect(await refreshLegacyToken("owner-race", "gho_old")).to.equal("new-login"); + }); + it("backs off on network failures while leaving the current token intact", async () => { + oauth(); let calls = 0; + stub(globalThis, "fetch", async () => { calls++; throw Error("network failure"); }); + expect(await refreshLegacyToken("owner-network", "gho_old")).to.equal(null); + expect(await refreshLegacyToken("owner-network", "gho_old")).to.equal(null); + expect(calls).to.equal(1); + }); + it("bounds failure caches and expires entries so repaired content can be retried", () => { + let now = 0; const cache = new ExpiringMap(2, () => now); + cache.set("a", true, 30); cache.set("b", true, 30); cache.set("c", true, 30); + expect(cache.get("a")).to.equal(undefined); expect(cache.get("b")).to.equal(true); + now = 30; expect(cache.get("b")).to.equal(undefined); expect(cache.get("c")).to.equal(undefined); + }); + it("caches confirmed file misses by credential and commit, without caching permission failures", async () => { + stub(require("../src/core/source/github-content-miss"), "classifyContentMiss", async () => "file_not_found"); + let token = "credential-a", attempts = 0, now = Date.now(); + stub(Date, "now", () => now); + stub(storage, "fileInfo", async () => { throw Object.assign(Error("missing"), { code: "ENOENT" }); }); + stub(storage, "write", async (_repo, _path, input) => { for await (const chunk of input) void chunk; }); + const source = new GitHubStream({ repoId: "missing-fixture", organization: "owner", repoName: "repo", commit: "first", getToken: () => token }); + source.downloadWithFallback = async () => { attempts++; throw Object.assign(Error("missing"), { response: { statusCode: 404 } }); }; + const read = () => source.getFileContentCache("missing.js", "missing-fixture", () => ({ sha: "sha" })); + for (let i = 0; i < 3; i++) { try { await read(); } catch (error) { expect(error.httpStatus || error.response?.statusCode).to.equal(404); } } + expect(attempts).to.equal(1); + token = "credential-b"; await read().catch(() => {}); expect(attempts).to.equal(2); + source.data.commit = "second"; await read().catch(() => {}); expect(attempts).to.equal(3); + now += 30001; await read().catch(() => {}); expect(attempts).to.equal(4); + source.downloadWithFallback = async () => { attempts++; throw Object.assign(Error("forbidden"), { response: { statusCode: 403 } }); }; + now += 30001; await read().catch(() => {}); await read().catch(() => {}); expect(attempts).to.equal(6); + }); + it("blocks resolved LFS bytes before they reach the source cache when capacity is full", async () => { + const config = require("../src/config").default; + const quotas = require("../src/core/billing/quotas"); + stub(config, "BILLING_ENABLED", true); + stub(storage, "fileInfo", async () => { throw Object.assign(Error("missing"), { code: "ENOENT" }); }); + let written = 0, requested; + stub(storage, "write", async (_repo, _path, input) => { for await (const chunk of input) written += chunk.length; }); + stub(quotas, "reserveFileExpansion", async (_id, _key, bytes) => { requested = bytes; throw Error("quota_exceeded"); }); + const source = new GitHubStream({ repoId: "lfs-quota", organization: "owner", repoName: "repo", commit: "first", getToken: () => "token" }); + source.downloadWithFallback = async () => require("node:stream").Readable.from([Buffer.alloc(1024)]); + let error; + try { await source.getFileContentCache("large.bin", source.data.repoId, () => ({ sha: "sha", size: 130 })); } + catch (caught) { error = caught; } + expect(error?.message).to.equal("quota_exceeded"); + expect(requested).to.equal(1024 ** 2 - 130); + expect(written).to.equal(0); + }); + it("does not cache a storage 404 as a missing upstream file", async () => { + let attempts = 0; + stub(storage, "fileInfo", async () => { throw Object.assign(Error("missing"), { code: "ENOENT" }); }); + stub(storage, "write", async (_repo, _path, input) => { + for await (const chunk of input) void chunk; + throw Object.assign(Error("storage unavailable"), { httpStatus: 404 }); + }); + const source = new GitHubStream({ repoId: "storage-failure-fixture", organization: "owner", repoName: "repo", commit: "first", getToken: () => "token" }); + source.downloadWithFallback = async () => { attempts++; return require("node:stream").Readable.from(["content"]); }; + for (let i = 0; i < 2; i++) { + try { await source.getFileContentCache("file.js", source.data.repoId, () => ({ sha: "sha" })); throw Error("expected storage failure"); } + catch (error) { expect(error.message).to.equal("storage unavailable"); } + } + expect(attempts).to.equal(2); + }); + it("returns a client error from the real options route for an inaccessible source", async () => { + const utils = require("../src/server/routes/route-utils"); + const repo = { assertNotArchived() {}, status: "error", options: {}, + model: { statusDate: new Date(), statusMessage: "repo_not_found", source: {} } }; + stub(utils, "getRepo", async () => repo); stub(utils, "getUser", async () => { throw Error("anonymous"); }); + const router = require("../src/server/routes/repository-public").default; + const handler = router.stack.find(layer => layer.route?.path === "/:repoId/options").route.stack[0].handle; + const res = { header() {}, status(value) { this.statusCode = value; return this; }, json(value) { this.body = value; } }; + await handler({ params: { repoId: "inaccessible" } }, res); + expect(res.statusCode).to.equal(404); expect(res.body).to.deep.equal({ error: "repo_not_found" }); + repo.model.statusMessage = "unexpected_bug"; + await handler({ params: { repoId: "inaccessible" } }, res); expect(res.statusCode).to.equal(500); + }); + it("returns a structured 429 and the actual store retry time", () => { + const headers = {}, res = { setHeader: (key, value) => { headers[key] = value; }, status(value) { this.statusCode = value; return this; }, json(value) { this.body = value; } }; + const resetAt = Date.now() + 10000; + sendRequestRateLimit({ rateLimit: { resetTime: new Date(resetAt) } }, res); + expect(res.statusCode).to.equal(429); expect(res.body).to.deep.equal({ error: "rate_limited", resetAt }); + expect(Number(headers["Retry-After"])).to.be.within(9, 10); + }); + it("counts native and structured connection reset lines once each", () => { + const summary = logSummary(); + observeLine(summary, "Error: read ECONNRESET"); + observeLine(summary, "Error: socket hang up"); + observeLine(summary, '2026-10-09T00:00:00Z ERROR [process] process fatal error {"code":"process_fatal_error","message":"read ECONNRESET"}'); + observeLine(summary, "ordinary native output"); + expect(summary.connectionResets).to.equal(3); + expect(summary.fatalErrors).to.equal(1); + }); + it("flags native unhandled stream errors and restarts without reporting private log fields", () => { + const summary = logSummary(); + observeLine(summary, "throw er; // Unhandled 'error' event"); + observeLine(summary, '2026-10-09T00:00:00Z ERROR [process] process fatal error {"code":"process_fatal_error","message":"private data"}'); + observeLine(summary, '2026-10-09T00:00:00Z INFO [requests] request {"status":502,"outcome":"completed","ms":100,"url":"private"}'); + expect(summary.fatalErrors).to.equal(2); expect(summary.completed5xx).to.equal(1); expect(JSON.stringify(summary)).not.to.include("private"); + expect(containerSummary({ Name: "/streamer", RestartCount: 3, State: { Running: true, Health: { Status: "healthy" }, OOMKilled: false, StartedAt: "now" } }).restartCount).to.equal(3); + }); +}); diff --git a/test/production-followup-fixes.test.js b/test/production-followup-fixes.test.js new file mode 100644 index 0000000..af62acd --- /dev/null +++ b/test/production-followup-fixes.test.js @@ -0,0 +1,379 @@ +const { expect } = require("chai"); +const express = require("express"); +const session = require("express-session"); +const { Passport } = require("passport"); +const http = require("node:http"); +const { Readable } = require("node:stream"); +const { once } = require("node:events"); +const { randomBytes } = require("node:crypto"); +const { URL } = require("node:url"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); +const archiver = require("archiver"); +const got = require("got"); +require("ts-node/register/transpile-only"); +const { serverOptions } = require("../src/server/server-options"); +const { serializeError } = require("../src/core/logger"); +const { repositoryFailureStatus } = require("../src/core/repository-failure-status"); +const AnonymousError = require("../src/core/AnonymousError").default; +const github = require("../src/core/GitHubUtils"); +const GitHubStream = require("../src/core/source/GitHubStream").default; +const FileSystem = require("../src/core/storage/FileSystem").default; +const S3 = require("../src/core/storage/S3").default; +const config = require("../src/config").default; +const { AnonymizeTransformer } = require("../src/core/anonymize-utils"); +const { streamAnonymizedZip } = require("../src/core/zipStream"); +const { classifyContentMiss } = require("../src/core/source/github-content-miss"); +const storage = require("../src/core/storage").default; + +function request(server, path, method) { + return new Promise((resolve, reject) => { + http.request({ host: "127.0.0.1", port: server.address().port, path, method }, res => { + res.resume(); res.once("end", () => resolve({ status: res.statusCode, headers: res.headers })); + res.once("error", reject); + }).once("error", reject).end(); + }); +} + +describe("production follow-up fixes", function () { + this.timeout(10000); + const restores = []; + function stub(object, key, value) { + const previous = object[key]; object[key] = value; restores.push(() => { object[key] = previous; }); + } + afterEach(() => restores.splice(0).reverse().forEach(restore => restore())); + + it("answers OPTIONS * before sessions while preserving route OPTIONS and authentication", async () => { + const app = express(), passport = new Passport(); + let sessionRequests = 0; + app.use(serverOptions); + app.use((_req, _res, next) => { sessionRequests++; next(); }); + app.use(session({ secret: "test-secret", resave: false, saveUninitialized: false })); + app.use(passport.initialize()); app.use(passport.session()); + app.get("/protected", (req, res) => res.sendStatus(req.isAuthenticated() ? 200 : 401)); + const server = await new Promise(resolve => { const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); + try { + const global = await request(server, "*", "OPTIONS"); + expect(global.status).to.equal(204); expect(global.headers.allow).to.include("OPTIONS"); + expect(global.headers["set-cookie"]).to.equal(undefined); expect(sessionRequests).to.equal(0); + expect((await request(server, "*", "GET")).status).to.equal(400); expect(sessionRequests).to.equal(0); + const route = await request(server, "/protected", "OPTIONS"); + expect(route.status).to.equal(200); expect(route.headers.allow).to.equal("GET, HEAD"); + expect((await request(server, "/protected", "GET")).status).to.equal(401); + expect(sessionRequests).to.equal(2); + } finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } + }); + + function source(getTree, getRepo = async () => ({})) { + stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ git: { getTree }, repos: { get: getRepo } })); + return new GitHubStream({ repoId: "followup", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "test-token" }); + } + const invalidObject = () => Object.assign(Error("Invalid object requested"), { + status: 422, response: { data: { message: "Invalid object requested. SHA must identify a commit or a tree." } }, + }); + for (const [status, code, responseStatus] of [[401, "token_expired", 401], [403, "repo_not_accessible", 403], + [404, "commit_not_found", 404], [409, "repo_empty", 409], [500, "github_unavailable", 502], + [503, "github_unavailable", 502], [422, "github_unavailable", 502], [undefined, "github_unavailable", 502]]) { + it(`classifies tree failure ${status} without calling it a missing repository`, async () => { + const upstream = Object.assign(Error("upstream failure"), { status }); + const s = source(async () => { throw upstream; }); + const error = await s.getFiles().catch(error => error); + expect(error.message).to.equal(code); expect(error.httpStatus).to.equal(responseStatus); + expect(error.cause).to.equal(upstream); expect(s.data.commit).to.equal("pinned"); + }); + } + it("reports an invalid tree revision as a missing commit without changing the pinned snapshot", async () => { + const s = source(async () => { throw invalidObject(); }); + const error = await s.getFiles().catch(error => error); + expect(error.message).to.equal("commit_not_found"); expect(error.httpStatus).to.equal(404); + expect(s.data.commit).to.equal("pinned"); expect(repositoryFailureStatus(error.message)).to.equal(404); + }); + it("preserves quota errors from both the gate and tree request", async () => { + const quota = new AnonymousError("github_rate_limit_exceeded", { httpStatus: 429 }); + const s = source(async () => { throw quota; }); + expect(await s.getFiles().catch(error => error)).to.equal(quota); + github.waitForTokenGate = async () => { throw quota; }; + expect(await s.getFiles().catch(error => error)).to.equal(quota); + const delay = new github.RateLimitDelayError(Date.now() + 60000, "quota-key"); + github.waitForTokenGate = async () => { throw delay; }; + expect(await s.getFiles().catch(error => error)).to.equal(delay); + }); + it("classifies failures while traversing a truncated tree too", async () => { + const s = source(async ({ tree_sha }) => { + if (tree_sha === "pinned") return { data: { truncated: true, tree: [{ type: "tree", path: "folder", sha: "child" }] } }; + throw Object.assign(Error("GitHub offline"), { status: 503 }); + }); + const error = await s.getFiles().catch(error => error); + expect(error.message).to.equal("github_unavailable"); expect(error.httpStatus).to.equal(502); + }); + it("maps an empty repository consistently after its failure is persisted", () => { + expect(repositoryFailureStatus("repo_empty")).to.equal(409); + }); + it("serializes HTTP errors without invoking a deprecated code getter", () => { + let reads = 0; + const error = Object.assign(Error("GitHub error"), { status: 422 }); + Object.defineProperty(error, "code", { get() { reads++; throw Error("deprecated getter invoked"); } }); + expect(serializeError(error)).to.include({ status: 422, message: "GitHub error" }); + expect(reads).to.equal(0); + expect(serializeError(Object.assign(Error("reset"), { code: "ECONNRESET" }))).to.include({ code: "ECONNRESET" }); + }); + + it("coalesces content-miss probes across paths and expires them so restored access is retried", async () => { + let now = Date.now(), repoCalls = 0, commitCalls = 0; + stub(Date, "now", () => now); stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { + get: async () => { repoCalls++; }, getCommit: async options => { expect(options.mediaType.format).to.equal("sha"); commitCalls++; }, + } })); + const data = { repoId: "miss-coalescing", organization: "owner", repoName: "repo", commit: "pinned" }; + expect(await Promise.all(Array.from({ length: 20 }, () => classifyContentMiss(data, "a")))) + .to.deep.equal(Array(20).fill("file_not_found")); + expect(await classifyContentMiss(data, "a")).to.equal("file_not_found"); + expect(repoCalls).to.equal(1); expect(commitCalls).to.equal(1); + await classifyContentMiss(data, "b"); expect(repoCalls).to.equal(2); + await classifyContentMiss({ ...data, commit: "new" }, "a"); expect(repoCalls).to.equal(3); + await classifyContentMiss({ ...data, cacheRevision: "new" }, "a"); expect(repoCalls).to.equal(4); + now += 30001; await classifyContentMiss(data, "a"); expect(repoCalls).to.equal(5); + }); + for (const stage of ["repository", "commit"]) { + it(`reports a missing ${stage} instead of a missing file`, async () => { + let commits = 0; + stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { + get: async () => { if (stage === "repository") throw Object.assign(Error("missing"), { status: 404 }); }, + getCommit: async () => { commits++; throw Object.assign(Error("missing"), { status: 404 }); }, + } })); + const data = { repoId: `missing-${stage}`, organization: "owner", repoName: "repo", commit: "pinned" }; + expect(await classifyContentMiss(data, "a")).to.equal(stage === "repository" ? "repo_not_found" : "commit_not_found"); + expect(commits).to.equal(stage === "repository" ? 0 : 1); + }); + } + for (const status of [401, 403, 422, 503]) { + it(`does not cache a content-probe ${status} failure as a missing file`, async () => { + let calls = 0; + stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { get: async () => { calls++; throw Object.assign(Error("failed"), { status }); } } })); + const data = { repoId: `probe-failure-${status}`, organization: "owner", repoName: "repo", commit: "pinned" }; + for (let i = 0; i < 2; i++) { + const error = await classifyContentMiss(data, "a").catch(error => error); + expect(error.httpStatus).to.equal(status === 401 || status === 403 ? status : 502); + } + expect(calls).to.equal(2); + }); + } + it("recognizes the GitHub invalid-commit response without retrying with HEAD", async () => { + let ref; + stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { + get: async () => {}, getCommit: async options => { + ref = options.ref; throw Object.assign(Error("missing"), { status: 422, response: { data: { message: "No commit found for SHA: pinned" } } }); + }, + } })); + expect(await classifyContentMiss({ repoId: "invalid-commit-probe", organization: "owner", repoName: "repo", commit: "pinned" }, "a")) + .to.equal("commit_not_found"); + expect(ref).to.equal("pinned"); + }); + + function rawFallback(downloadUrl, content) { + const calls = []; + stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { getContent: async options => { + expect(options).to.include({ owner: "owner", repo: "repo", path: "file.bin", ref: "pinned" }); + return { data: { type: "file", download_url: downloadUrl } }; + } } })); + stub(got, "stream", (url, options) => { + calls.push({ url, options }); + if (calls.length === 1) return new Readable({ read() { this.destroy(Object.assign(Error("private web 404"), { response: { statusCode: 404 } })); } }); + return Readable.from(Array.isArray(content) ? content : [content]); + }); + const s = new GitHubStream({ repoId: "private-fallback", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "owner-secret" }); + return { stream: s.downloadFileViaRaw("owner-secret", "file.bin"), calls }; + } + it("recovers an accessible private file from the API download URL without forwarding the owner token", async () => { + const url = "https://raw.githubusercontent.com/owner/repo/pinned/file.bin?token=short-lived"; + const { stream, calls } = rawFallback(url, Buffer.from("actual content")); + expect((await collect(stream)).toString()).to.equal("actual content"); + expect(calls).to.have.length(2); expect(calls[1].url).to.equal(url); + expect(calls[1].options.headers).to.equal(undefined); + expect(() => calls[1].options.hooks.beforeRedirect[0]({ url: new URL("https://attacker.test/file") })).to.throw("upstream_error"); + expect(serializeError(Object.assign(Error("failed"), { response: { url } })).url).not.to.include("short-lived"); + }); + for (const url of ["http://raw.githubusercontent.com/file", "https://attacker.test/file", "https://owner-secret@raw.githubusercontent.com/file"]) { + it(`rejects an unsafe contents download URL ${new URL(url).hostname}`, async () => { + const { stream, calls } = rawFallback(url, Buffer.from("should never be read")); + const error = await collect(stream).catch(error => error); + expect(error.message).to.equal("upstream_error"); expect(error.httpStatus).to.equal(502); expect(calls).to.have.length(1); + }); + } + for (const mode of ["short", "long", "fragmented"]) { + it(`does not serve a ${mode} unresolved LFS pointer as file content`, async () => { + const extension = mode === "short" ? "" : "ext-0-test sha256:" + "b".repeat(64) + "\n"; + const pointer = Buffer.from("version https://git-lfs.github.com/spec/v1\n" + extension + "oid sha256:" + "a".repeat(64) + "\nsize 10\n"); + const chunks = mode === "fragmented" ? [pointer.subarray(0, 20), pointer.subarray(20, 150), pointer.subarray(150)] : pointer; + const { stream } = rawFallback("https://raw.githubusercontent.com/owner/repo/pinned/file.bin", chunks); + const error = await collect(stream).catch(error => error); + expect(error.message).to.equal("upstream_error"); expect(error.httpStatus).to.equal(502); + }); + } + for (const mode of ["single", "fragmented"]) { + it(`resolves a long blob LFS pointer with ${mode} delivery instead of forwarding its text`, async () => { + const pointer = Buffer.from("version https://git-lfs.github.com/spec/v1\next-0-test sha256:" + "b".repeat(64) + "\noid sha256:" + "a".repeat(64) + "\nsize 10\n"); + const s = new GitHubStream({ repoId: "long-lfs", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "token" }); + let downloads = 0; + s.downloadFileViaRaw = () => { downloads++; return Readable.from(["resolved file"]); }; + const chunks = mode === "fragmented" ? [pointer.subarray(0, 20), pointer.subarray(20, 150), pointer.subarray(150)] : [pointer]; + expect((await collect(s.resolveLfsPointer(Readable.from(chunks), "token", "file.bin"))).toString()).to.equal("resolved file"); + expect(downloads).to.equal(1); + }); + } + it("releases cancelled gate timers and private streams without retaining process handles", function () { + this.timeout(20000); + const result = spawnSync(process.execPath, [path.join(__dirname, "fixtures/github-gate-cancellation.js")], { + timeout: 15000, encoding: "utf8", env: { ...process.env, NODE_ENV: "test", GITHUB_APP_ENABLED: "false", REDIS_HOSTNAME: "127.0.0.1", REDIS_PORT: "1" }, + }); + expect(result.error, result.stderr).to.equal(undefined); + expect(result.status, result.stderr + result.stdout).to.equal(0); + expect(result.stdout).to.include("gate cancellations released"); + }); + it("keeps public raw misses off the contents API", async () => { + stub(github, "octokit", () => { throw Error("unexpected anonymous REST request"); }); + stub(got, "stream", () => new Readable({ read() { this.destroy(Object.assign(Error("missing"), { response: { statusCode: 404 } })); } })); + const s = new GitHubStream({ repoId: "public-raw", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "" }); + const error = await collect(s.downloadFileViaRaw("", "file.bin")).catch(error => error); + expect(error.response.statusCode).to.equal(404); + }); + it("does not start a private download after cancellation during the contents lookup", async () => { + let resolveMetadata, started, calls = 0; + const metadata = new Promise(resolve => { resolveMetadata = resolve; }); + const lookupStarted = new Promise(resolve => { started = resolve; }); + stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { getContent: async () => { started(); return metadata; } } })); + stub(got, "stream", () => { + calls++; return new Readable({ read() { this.destroy(Object.assign(Error("missing"), { response: { statusCode: 404 } })); } }); + }); + const s = new GitHubStream({ repoId: "cancel-private", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "token" }); + const input = s.downloadFileViaRaw("token", "file.bin"); + const collected = collect(input).catch(error => error); + await lookupStarted; input.destroy(); + resolveMetadata({ data: { type: "file", download_url: "https://raw.githubusercontent.com/owner/repo/pinned/file.bin" } }); + await collected; expect(calls).to.equal(1); + }); + for (const mode of ["raw", "lfs"]) { + it(`destroys a pending ${mode} upstream immediately when its consumer cancels`, async () => { + const { PassThrough } = require("node:stream"); + const upstream = new PassThrough(); + let started; + const ready = new Promise(resolve => { started = resolve; }); + stub(got, "stream", () => { started(); return upstream; }); + const s = new GitHubStream({ repoId: "pending-raw", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "token" }); + const pointer = "version https://git-lfs.github.com/spec/v1\noid sha256:" + "a".repeat(64) + "\nsize 10\n"; + const input = mode === "raw" ? s.deferredRawDownload("token", "file.bin") + : s.resolveLfsPointer(Readable.from([pointer]), "token", "file.bin"); + const collected = collect(input).catch(error => error); + await ready; input.destroy(); + expect(upstream.destroyed).to.equal(true); + await collected; + }); + } + it("closes a real upstream HTTP socket when a nested raw stream is cancelled", async () => { + let respond, closed; + const responseStarted = new Promise(resolve => { respond = resolve; }); + const socketClosed = new Promise(resolve => { closed = resolve; }); + const server = http.createServer((req, res) => { + req.socket.once("close", closed); res.write("partial content"); + }); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const originalStream = got.stream; + stub(got, "stream", () => { + const input = originalStream(`http://127.0.0.1:${server.address().port}`, { retry: { limit: 0 } }); + input.once("response", respond); return input; + }); + const s = new GitHubStream({ repoId: "cancel-socket", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "token" }); + const input = s.deferredRawDownload("token", "file.bin"); + const collected = collect(input).catch(error => error); + try { + await responseStarted; input.destroy(); + await socketClosed; await collected; + } finally { input.destroy(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } + }); + it("avoids file downloads for a known unavailable source and retries after the short TTL", async () => { + let now = Date.now(), attempts = 0; + stub(Date, "now", () => now); stub(github, "waitForTokenGate", async () => {}); + stub(github, "octokit", () => ({ repos: { get: async () => { throw Object.assign(Error("missing repository"), { status: 404 }); } } })); + stub(storage, "fileInfo", async () => { throw Object.assign(Error("missing cache"), { code: "ENOENT" }); }); + const s = new GitHubStream({ repoId: "source-miss-shortcut", organization: "owner", repoName: "repo", commit: "pinned", getToken: () => "token" }); + s.downloadWithFallback = async () => { attempts++; throw Object.assign(Error("missing"), { response: { statusCode: 404 } }); }; + for (const path of ["first", "second", "third"]) { + const error = await s.getFileContentCache(path, s.data.repoId, () => ({ sha: path })).catch(error => error); + expect(error.message).to.equal("repo_not_found"); expect(error.httpStatus).to.equal(404); + } + expect(attempts).to.equal(1); + now += 30001; + await s.getFileContentCache("fourth", s.data.repoId, () => ({ sha: "fourth" })).catch(() => {}); + expect(attempts).to.equal(2); + }); + + // Read the central directory to check the actual compression methods, + // rather than just asserting the options passed to archiver. + function compressionMethods(zip) { + const end = zip.length - 22; + expect(zip.readUInt32LE(end)).to.equal(0x06054b50); + const methods = {}; + let offset = zip.readUInt32LE(end + 16); + for (let i = 0; i < zip.readUInt16LE(end + 10); i++) { + expect(zip.readUInt32LE(offset)).to.equal(0x02014b50); + const nameLength = zip.readUInt16LE(offset + 28); + const name = zip.subarray(offset + 46, offset + 46 + nameLength).toString(); + methods[name] = zip.readUInt16LE(offset + 10); + offset += 46 + nameLength + zip.readUInt16LE(offset + 30) + zip.readUInt16LE(offset + 32); + } + return methods; + } + async function collect(input) { + const chunks = []; for await (const chunk of input) chunks.push(chunk); return Buffer.concat(chunks); + } + async function unpack(zip) { + const files = {}, entries = [], parser = require("unzip-stream").Parse(); + parser.on("entry", entry => { entries.push(collect(entry).then(body => { files[entry.path] = body; })); }); + const done = once(parser, "finish"); Readable.from([zip]).pipe(parser); + await done; await Promise.all(entries); return files; + } + const binary = Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), randomBytes(128 * 1024)]); + const contents = { "private.PNG": binary, "README.md": Buffer.from("private source content"), + "figure.svg": Buffer.from('private') }; + const options = { terms: ["private"], image: true, pdf: true, link: true }; + for (const backend of ["filesystem", "s3", "github"]) { + it(`avoids recompressing binary assets in ${backend} ZIPs and still anonymizes text`, async () => { + let zip; + if (backend === "github") { + const fixture = archiver("zip"); + const collected = collect(fixture); + for (const [name, body] of Object.entries(contents)) fixture.append(body, { name: `repo/${name}` }); + await fixture.finalize(); const input = await collected; + stub(got, "stream", () => Readable.from([input])); + const { PassThrough } = require("node:stream"); + const response = new PassThrough(); const output = collect(response); + await streamAnonymizedZip({ repoId: "test", organization: "owner", repoName: "public", commit: "pinned", + getToken: () => "", anonymizerOptions: options }, response); + zip = await output; + } else { + if (backend === "s3") stub(config, "S3_BUCKET", "test-bucket"); + const storage = backend === "filesystem" ? new FileSystem() : new S3(); + const names = Object.keys(contents); + storage.read = async (_repo, name) => Readable.from([contents[name]]); + if (backend === "filesystem") storage.listFiles = async () => names.map(name => ({ name, path: "", size: contents[name].length })); + else { + storage.client = () => ({ listObjectsV2: async () => ({ Contents: names.map(name => ({ Key: storage.repoPath("test") + name })) }) }); + } + zip = await collect(await storage.archive("test", "", { + fileTransformer: name => new AnonymizeTransformer({ ...options, filePath: name }), + })); + } + const methods = compressionMethods(zip), files = await unpack(zip); + const binaryName = backend === "github" ? "XXXX-1.PNG" : "private.PNG"; + expect(methods[binaryName]).to.equal(0); expect(files[binaryName]).to.deep.equal(binary); + expect(methods["README.md"]).to.equal(8); expect(files["README.md"].toString()).to.equal("XXXX-1 source content"); + expect(methods["figure.svg"]).to.equal(8); expect(files["figure.svg"].toString()).to.include("XXXX-1"); + }); + } +}); diff --git a/test/production-regressions.test.js b/test/production-regressions.test.js index c58664c..d5b1c5c 100644 --- a/test/production-regressions.test.js +++ b/test/production-regressions.test.js @@ -206,7 +206,8 @@ describe("production regressions", function () { it("retains truncation warnings from the source instance that fetched the tree", async function () { const FileModel = require("../src/core/model/files/files.model").default; stub(db, "isConnected", false); - for (const method of ["exists", "deleteMany", "find"]) stub(FileModel, method, () => ({ exec: async () => method === "find" ? [] : null })); + for (const method of ["exists", "deleteMany"]) stub(FileModel, method, () => ({ exec: async () => null })); + stub(FileModel, "find", () => ({ select() { return this; }, limit() { return this; }, lean() { return this; }, exec: async () => [] })); stub(FileModel, "insertMany", async () => []); const repo = new Repository(new RepoModel({ repoId: "repo", options: {} })); repo.computeSize = async () => {}; diff --git a/test/recipient-alerts-mongo.test.js b/test/recipient-alerts-mongo.test.js new file mode 100644 index 0000000..0e9056b --- /dev/null +++ b/test/recipient-alerts-mongo.test.js @@ -0,0 +1,930 @@ +const { expect } = require("chai"); +const mongoose = require("mongoose"); +require("ts-node/register/transpile-only"); +const Users = require("../src/core/model/users/users.model").default; +const config = require("../src/config").default; +const alerts = require("../src/core/owner-notifications"); +const { notifyOwnerAccessProblem: notify, resetOwnerAccessAlerts: reset, withOwnerAccessRecovery: recover } = alerts; +const describeMongo = process.env.TEST_MONGODB_URI ? describe : describe.skip; + +describeMongo("recipient access alerts in MongoDB", function () { + this.timeout(15000); + let owner, otherOwner, resource, other, sent, nativeFetch, originalConfig; + const coll = kind => mongoose.connection.collection({ repository: "anonymizedrepositories", gist: "anonymizedgists", "pull-request": "anonymizedpullrequests" }[kind]); + const guards = () => mongoose.connection.collection(alerts.RECIPIENT_ALERTS); + const recipient = () => alerts.recipientAlertKey("owner@example.com"); + async function create(ownerId, kind = "repository") { + const id = new mongoose.Types.ObjectId(); await coll(kind).insertOne({ _id: id, owner: ownerId, [{ repository: "repoId", gist: "gistId", "pull-request": "pullRequestId" }[kind]]: String(id), status: "ready" }); return { kind, id: String(id) }; + } + before(async () => { + nativeFetch = global.fetch; originalConfig = { ...config }; + await mongoose.connect(process.env.TEST_MONGODB_URI, { dbName: "recipient_alerts_" + Date.now() }); + Object.assign(config, { RESEND_API_KEY: "test-key", EMAIL_FROM: "alerts@example.com", APP_HOSTNAME: "anonymous.example.com" }); + }); + after(async () => { global.fetch = nativeFetch; Object.assign(config, originalConfig); await mongoose.connection.dropDatabase(); await mongoose.disconnect(); }); + beforeEach(async () => { + for (const c of Object.values(mongoose.connection.collections)) await c.deleteMany({}); + owner = await Users.create({ username: "owner", emails: [{ email: "owner@example.com", default: true }] }); + otherOwner = await Users.create({ username: "other-owner", notificationEmail: "OWNER@example.com" }); + resource = await create(owner._id); other = await create(otherOwner._id, "gist"); sent = []; + global.fetch = async (_url, options) => { sent.push(options); return { ok: true, status: 200 }; }; + }); + + it("allows only one sender across owners, resources and case variations under concurrent failures", async () => { + await Promise.all(Array.from({ length: 40 }, (_, i) => notify(i % 2 ? owner.id : otherOwner.id, "token_expired", i % 2 ? resource : other))); + expect(sent).to.have.length(1); expect(await guards().countDocuments({ _id: recipient() })).to.equal(1); + expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments({ state: "accepted" })).to.equal(1); + }); + for (const kind of ["repository", "gist", "pull-request"]) { + for (const status of ["archived", "expired", "expiring", "removed", "removing"]) it(`does not notify for ${status} ${kind}`, async () => { + const inactive = await create(owner._id, kind); + await coll(kind).updateOne({ _id: new mongoose.Types.ObjectId(inactive.id) }, { $set: { status } }); + await notify(owner.id, "token_expired", inactive); + expect(sent).to.have.length(0); expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments()).to.equal(0); + }); + } + it("cancels a send when the source is removed during audit preparation", async () => { + const events = mongoose.connection.collection("owner_access_alert_events"), insert = events.insertOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + events.insertOne = async function (...args) { entered(); await new Promise(resolve => { release = resolve; }); return insert.apply(this, args); }; + const pending = notify(owner.id, "token_expired", resource); + try { + await gate; + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } }); + } finally { events.insertOne = insert; release(); await pending; } + expect(sent).to.have.length(0); expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("records the triggering resource and categorized failure without its address or source name", async () => { + await notify(owner.id, "token_expired", resource); + const records = await mongoose.connection.collection("owner_access_alert_events").find().toArray(); + expect(records).to.have.length(2); + for (const record of records) { + expect(String(record.owner)).to.equal(owner.id); expect(String(record.resourceId)).to.equal(resource.id); + expect(record.resourceKind).to.equal(resource.kind); expect(record.generation).to.equal(null); + expect(JSON.stringify(record)).not.to.include("owner@example.com"); + } + expect(records.find(record => record.state === "prepared").failureCode).to.equal("token_expired"); + }); + for (const state of ["accepted", "failed"]) it(`restores a missing guard from ${state} provider history before serving traffic`, async () => { + await mongoose.connection.collection("owner_access_alert_events").insertOne({ _id: "old/completion", attemptId: "old", + recipientKey: recipient(), state, completedAt: new Date(1) }); + await Promise.all(Array.from({ length: 5 }, () => alerts.initializeOwnerAccessAlerts())); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recipient_notified"); + await notify(owner.id, "token_expired", resource); await notify(otherOwner.id, "token_expired", other); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0); + await alerts.initializeOwnerAccessAlerts(); expect(sent).to.have.length(0); + }); + it("does not restore a delivery guard from an uncompleted preparation event", async () => { + await mongoose.connection.collection("owner_access_alert_events").insertOne({ _id: "prepared", recipientKey: recipient(), state: "prepared", startedAt: new Date(1) }); + await alerts.initializeOwnerAccessAlerts(); expect(await guards().findOne({ _id: recipient() })).to.equal(null); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("resumes provider history restoration after a checkpointed startup failure", async () => { + const events = mongoose.connection.collection("owner_access_alert_events"); + const keys = Array.from({ length: 105 }, (_, i) => alerts.recipientAlertKey(`old${i}@example.com`)); + await events.insertMany(keys.map((recipientKey, i) => ({ _id: String(i).padStart(3, "0") + "/completion", + recipientKey, attemptId: String(i), state: "accepted", completedAt: new Date(1) }))); + const collection = guards(), update = collection.updateOne; let restored = 0; + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$setOnInsert?.state === "recipient_notified" && ++restored === 103) throw new Error("interrupted history backfill"); + return update.call(this, filter, operation, ...args); + }; + try { await alerts.initializeOwnerAccessAlerts(); throw new Error("Expected migration failure"); } + catch (error) { expect(error.message).to.equal("interrupted history backfill"); } + finally { collection.updateOne = update; } + expect((await guards().findOne({ _id: "recipient-alerts-v2" })).deliveryProgress.id).to.equal("099/completion"); + await guards().updateOne({ _id: "recipient-alerts-v2" }, { $set: { leaseUntil: new Date(0) } }); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().countDocuments({ state: "recipient_notified" })).to.equal(105); + expect((await guards().findOne({ _id: "recipient-alerts-v2" })).deliveryProgress.id).to.equal("104/completion"); + expect((await events.indexes()).some(index => index.name === "owner_access_alert_delivery_history" && index.partialFilterExpression)).to.equal(true); + expect(sent).to.have.length(0); + }); + it("runs the v2 history migration on a cold start with only a ready v1 marker", async () => { + await guards().insertOne({ _id: "recipient-alerts-v1", state: "ready" }); + await mongoose.connection.collection("owner_access_alert_events").insertOne({ _id: "v1/completion", attemptId: "v1", + recipientKey: recipient(), state: "accepted", completedAt: new Date(1) }); + await alerts.initializeOwnerAccessAlerts(); + expect((await guards().findOne({ _id: "recipient-alerts-v2" })).state).to.equal("ready"); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recipient_notified"); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0); + }); + it("cancels a fresh unsent v1 repeat before restoring its delivery suppression", async () => { + const attemptId = "v1-unsent-repeat"; + await guards().insertMany([{ _id: "recipient-alerts-v1", state: "ready" }, { + _id: recipient(), owner: owner._id, resourceKind: resource.kind, resourceId: new mongoose.Types.ObjectId(resource.id), + attemptId, state: "pending", dispatchPrepared: true, resourceRevision: 0, claimedAt: new Date(), + }]); + await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: { + [`accessAlertDispatches.${attemptId}`]: { state: "prepared", claimedAt: new Date() }, + } }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { + accessAlertClaimedAt: new Date(), accessAlertAttemptId: attemptId, accessAlertClaimRevision: 1, + } }); + await mongoose.connection.collection("owner_access_alert_events").insertOne({ _id: "v1-delivered/completion", attemptId: "v1-delivered", + recipientKey: recipient(), state: "accepted", completedAt: new Date(1) }); + await alerts.initializeOwnerAccessAlerts(); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recipient_notified"); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + expect((await mongoose.connection.collection("users").findOne({ _id: owner._id })).accessAlertDispatches[attemptId]).to.equal(undefined); + await alerts.initializeOwnerAccessAlerts(); await notify(owner.id, "token_expired", resource); + expect(sent).to.have.length(0); + }); + const models = () => ({ + repository: require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default, + gist: require("../src/core/model/anonymizedGists/anonymizedGists.model").default, + "pull-request": require("../src/core/model/anonymizedPullRequests/anonymizedPullRequests.model").default, + }); + for (const kind of ["repository", "gist", "pull-request"]) { + it(`fences ${kind} deletion after the sender's last resource check`, async () => { + const source = await create(owner._id, kind); + const collection = guards(), update = collection.updateOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$set?.state === "sending") { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const pending = notify(owner.id, "token_expired", source); + try { + await gate; + await models()[kind].updateOne({ _id: source.id }, { $set: { status: "removing" } }).exec(); + } finally { collection.updateOne = update; release(); await pending; } + expect(sent).to.have.length(0); expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect((await mongoose.connection.collection("users").findOne({ _id: owner._id })).accessAlertDeactivations).to.deep.equal([]); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + } + it("does not deactivate a resource reassigned after the lifecycle owner snapshot", async () => { + const Model = models().repository, users = mongoose.connection.collection("users"), update = users.updateMany; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + users.updateMany = async function (filter, operation, ...args) { + if (operation.$addToSet?.accessAlertDeactivations) { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const pending = Model.updateOne({ _id: resource.id }, { $set: { status: "removing" } }).exec(); + let result; + try { + await gate; + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { owner: otherOwner._id } }); + } finally { users.updateMany = update; release(); result = await pending; } + expect(result.matchedCount).to.equal(0); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).status).to.equal("ready"); + await notify(otherOwner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("blocks new senders until all concurrent lifecycle mutations finish", async () => { + const second = await create(owner._id), active = await create(owner._id); + const Model = models().repository, collection = Model.collection, update = collection.updateOne; + const releases = new Map(); let entered; const gate = new Promise(resolve => { entered = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$set?.status === "removing") { + await new Promise(resolve => { releases.set(String(filter.$and[0]._id), resolve); if (releases.size === 2) entered(); }); + } + return update.call(this, filter, operation, ...args); + }; + const first = Model.updateOne({ _id: resource.id }, { $set: { status: "removing" } }).exec(); + const last = Model.updateOne({ _id: second.id }, { $set: { status: "removing" } }).exec(); + try { + await gate; + expect((await mongoose.connection.collection("users").findOne({ _id: owner._id })).accessAlertDeactivations).to.have.length(2); + await notify(owner.id, "token_expired", active); expect(sent).to.have.length(0); + releases.get(second.id)(); await last; + await notify(owner.id, "token_expired", active); expect(sent).to.have.length(0); + } finally { collection.updateOne = update; for (const release of releases.values()) release(); await Promise.all([first, last]); } + await notify(owner.id, "token_expired", active); expect(sent).to.have.length(1); + }); + it("retains an ambiguous lifecycle reservation until offline reconciliation", async () => { + const Model = models().repository, collection = Model.collection, update = collection.updateOne; + collection.updateOne = async () => { throw new Error("lost database acknowledgement"); }; + try { await Model.updateOne({ _id: resource.id }, { $set: { status: "removing" } }).exec(); throw new Error("Expected mutation failure"); } + catch (error) { expect(error.message).to.equal("lost database acknowledgement"); } + finally { collection.updateOne = update; } + await alerts.initializeOwnerAccessAlerts(); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0); + await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts(); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + for (const operation of ["updateMany", "findOneAndUpdate", "save"]) it(`fences inactive lifecycle status via ${operation}`, async () => { + const Model = models().repository; + const before = await mongoose.connection.collection("users").findOne({ _id: owner._id }); + if (operation === "save") { + const doc = await Model.findById(resource.id); doc.status = "expired"; await doc.save(); + } else await Model[operation]({ _id: resource.id }, { $set: { status: "expired" } }).exec(); + const after = await mongoose.connection.collection("users").findOne({ _id: owner._id }); + expect(after.accessAlertResetRevision).to.equal((before.accessAlertResetRevision || 0) + 1); + expect(after.accessAlertDeactivations).to.deep.equal([]); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0); + }); + it("does not reopen after reconnect alone, unrelated resource success, or failed recovery", async () => { + await notify(owner.id, "token_expired", resource); await reset(owner.id); + await recover(otherOwner.id, other, async () => {}); + try { await recover(owner.id, resource, async () => { throw new Error("Forbidden"); }); } catch { /* Expected. */ } + await notify(owner.id, "token_expired", resource); await notify(otherOwner.id, "token_expired", other); + expect(sent).to.have.length(1); + }); + it("keeps suppression after recovery but allows a first alert to a new address", async () => { + await notify(owner.id, "token_expired", resource); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + await Users.updateOne({ _id: owner._id }, { $set: { notificationEmail: "new@example.com" } }); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(2); + expect(sent[0].headers["Idempotency-Key"]).not.to.equal(sent[1].headers["Idempotency-Key"]); + expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments()).to.equal(4); + await Users.updateOne({ _id: owner._id }, { $set: { notificationEmail: "OWNER@example.com" } }); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(2); + }); + it("does not release a pending send when recovery races with provider delivery", async () => { + let release, entered; const gate = new Promise(resolve => { entered = resolve; }); + global.fetch = async (_url, options) => { sent.push(options); entered(); await new Promise(resolve => { release = resolve; }); return { ok: true, status: 200 }; }; + const pending = notify(owner.id, "token_expired", resource); await gate; + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + release(); await pending; + }); + it("rejects stale recovery after another owner action changes the generation", async () => { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + await recover(owner.id, resource, async () => { await reset(owner.id, resource); }); + expect(await guards().findOne({ _id: recipient() })).not.to.equal(null); + }); + it("cancels an unsent claim before a paused sender can start dispatch", async () => { + const collection = mongoose.connection.collection("owner_access_alert_events"), insert = collection.insertOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + collection.insertOne = async function (...args) { entered(); await new Promise(resolve => { release = resolve; }); return insert.apply(this, args); }; + try { + const pending = notify(owner.id, "token_expired", resource); await gate; + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + release(); await pending; expect(sent).to.have.length(0); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + } finally { collection.insertOne = insert; } + }); + it("fences a second owner action that races after recovery's final resource check", async () => { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + const collection = guards(), update = collection.updateOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$set?.state === "recovered") { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + try { + const pending = recover(owner.id, resource, async () => {}); await gate; + await reset(owner.id, resource); release(); await pending; + expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); + } finally { collection.updateOne = update; } + }); + it("does not accept a successful read of a stale source binding", async () => { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { "source.repositoryName": "new/source" } }); + await recover(owner.id, resource, async () => {}, { "source.repositoryName": "old/source" }); + expect(await guards().findOne({ _id: recipient() })).not.to.equal(null); + }); + for (const failure of ["rejection", "timeout"]) it(`retains the recipient guard after ${failure}`, async () => { + global.fetch = async (_url, options) => { sent.push(options); if (failure === "timeout") throw new Error("timeout"); return { ok: false, status: 429 }; }; + await notify(owner.id, "token_expired", resource); await reset(owner.id); + await notify(otherOwner.id, "repo_not_found", other); expect(sent).to.have.length(1); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("failed"); + }); + it("keeps a removed resource's recipient guard and delivery history", async () => { + await notify(owner.id, "token_expired", resource); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments()).to.equal(2); + }); + for (const step of ["resource update", "resource verification", "audit insertion", "dispatch transition"]) it(`releases an unsent guard after ${step} fails`, async () => { + const collection = step.startsWith("resource") ? coll(resource.kind) : step === "audit insertion" ? mongoose.connection.collection("owner_access_alert_events") : guards(); + const method = step === "resource verification" ? "findOne" : step === "audit insertion" ? "insertOne" : "updateOne"; + const original = collection[method]; let calls = 0; + collection[method] = async function (...args) { + calls++; + if ((step === "resource verification" && calls === 3) + || (step === "dispatch transition" && args[1].$set?.state === "sending") + || (step === "resource update" && args[1].$set?.accessAlertClaimedAt) + || step === "audit insertion") throw new Error("transient database error"); + return original.apply(this, args); + }; + try { await notify(owner.id, "token_expired", resource); } + finally { collection[method] = original; } + expect(sent).to.have.length(0); expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("retains audit evidence while provider delivery is still in flight", async () => { + let release, entered; const gate = new Promise(resolve => { entered = resolve; }); + global.fetch = async () => { entered(); await new Promise(resolve => { release = resolve; }); return { ok: true, status: 200 }; }; + const pending = notify(owner.id, "token_expired", resource); await gate; + try { + const record = await guards().findOne({ _id: recipient() }); + const event = await mongoose.connection.collection("owner_access_alert_events").findOne({ _id: record.attemptId }); + expect(event.state).to.equal("prepared"); expect(record.state).to.equal("sending"); + } finally { release(); await pending; } + }); + it("reclaims a crashed pre-dispatch attempt without owner action under concurrent failures", async () => { + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "crashed-pending", state: "pending", claimedAt: new Date(0) }); + await Promise.all(Array.from({ length: 40 }, (_, i) => notify(i % 2 ? owner.id : otherOwner.id, "token_expired", i % 2 ? resource : other))); + expect(sent).to.have.length(1); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); + }); + it("refreshes the resource fence when an old claim is reclaimed before guard insertion", async () => { + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "old-pending", state: "pending", claimedAt: new Date(0) }); + const users = mongoose.connection.collection("users"), update = users.updateOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + users.updateOne = async function (filter, operation, ...args) { + if (Array.isArray(operation)) { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const pending = notify(owner.id, "token_expired", resource); + try { await gate; await alerts.initializeOwnerAccessAlerts(); } + finally { users.updateOne = update; release(); await pending; } + expect(sent).to.have.length(1); + expect((await guards().findOne({ _id: recipient() })).resourceRevision).to.equal(1); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimRevision).to.equal(2); + }); + it("keeps recent pending claims and old sending claims blocked", async () => { + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "active-pending", state: "pending", claimedAt: new Date() }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + await guards().updateOne({ _id: recipient() }, { $set: { state: "sending", claimedAt: new Date(0) } }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + await alerts.initializeOwnerAccessAlerts(); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending"); + }); + it("reaps only expired unsent claims during startup and never reseeds them", async () => { + await guards().insertOne({ _id: "recipient-alerts-v2", state: "ready" }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(0), accessAlertAttemptId: recipient() } }); + for (const [key, state, date] of [[recipient(), "pending", new Date(0)], ["recent", "pending", new Date()], + ["sending", "sending", new Date(0)], ["accepted", "accepted", new Date(0)], ["failed", "failed", new Date(0)]]) { + await guards().insertOne({ _id: key, owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: key, state, claimedAt: date }); + } + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + for (const key of ["recent", "sending", "accepted", "failed"]) expect(await guards().findOne({ _id: key })).not.to.equal(null); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("fences a paused sender after another reader reclaims its expired pending claim", async () => { + const collection = mongoose.connection.collection("owner_access_alert_events"), insert = collection.insertOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + let pausedOnce = false; + collection.insertOne = async function (...args) { + if (!pausedOnce) { pausedOnce = true; entered(); await new Promise(resolve => { release = resolve; }); } + return insert.apply(this, args); + }; + const paused = notify(owner.id, "token_expired", resource); + try { + await gate; + await guards().updateOne({ _id: recipient() }, { $set: { claimedAt: new Date(0) } }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + } finally { collection.insertOne = insert; release(); await paused; } + expect(sent).to.have.length(1); + expect((await guards().findOne({ _id: recipient() })).owner.toString()).to.equal(otherOwner.id); + }); + it("cannot reclaim a pending attempt that starts dispatch before cancellation", async () => { + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "racing-dispatch", state: "pending", claimedAt: new Date(0) }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(0), accessAlertAttemptId: "racing-dispatch" } }); + const collection = guards(), update = collection.updateOne; + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$set?.state === "cancelled") await update.call(this, { _id: recipient(), state: "pending", attemptId: "racing-dispatch" }, { $set: { state: "sending" } }); + return update.call(this, filter, operation, ...args); + }; + try { await notify(otherOwner.id, "token_expired", other); } + finally { collection.updateOne = update; } + expect(sent).to.have.length(0); expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending"); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertAttemptId).to.equal("racing-dispatch"); + }); + for (const state of ["pending", "cancelled"]) it(`clears a crashed unsent ${state} attempt's legacy fields before migration`, async () => { + const attemptId = `crashed-${state}`; + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), generation: "old-generation", attemptId, state, + claimedAt: state === "pending" ? new Date(0) : new Date() }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { + accessAlertGeneration: "old-generation", accessAlertClaimedAt: new Date(0), accessAlertAttemptId: attemptId, + } }); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + const row = await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) }); + expect(row.accessAlertClaimedAt).to.equal(undefined); expect(row.accessAlertAttemptId).to.equal(undefined); + expect(sent).to.have.length(0); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + for (const changed of ["generation", "attempt"]) it(`preserves newer legacy fields when the ${changed} no longer matches the cancelled attempt`, async () => { + await guards().insertOne({ _id: "recipient-alerts-v2", state: "ready" }); + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), generation: "old-generation", attemptId: "old-attempt", state: "pending", claimedAt: new Date(0) }); + const generation = changed === "generation" ? "new-generation" : "old-generation"; + const attemptId = changed === "attempt" ? "new-attempt" : "old-attempt"; + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { + accessAlertGeneration: generation, accessAlertClaimedAt: new Date(1234), accessAlertAttemptId: attemptId, + } }); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + const row = await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) }); + expect(row.accessAlertClaimedAt.getTime()).to.equal(1234); expect(row.accessAlertAttemptId).to.equal(attemptId); + }); + it("resumes cancelled metadata cleanup after a database failure without creating false legacy evidence", async () => { + const collection = coll(resource.kind), update = collection.updateOne, read = collection.findOne; + let reads = 0; + collection.findOne = async function (...args) { if (++reads === 3) throw new Error("verification failed"); return read.apply(this, args); }; + collection.updateOne = async function (filter, operation, ...args) { + if (Array.isArray(operation)) throw new Error("cleanup unavailable"); + return update.call(this, filter, operation, ...args); + }; + try { await notify(owner.id, "token_expired", resource); } + finally { collection.findOne = read; collection.updateOne = update; } + expect(sent).to.have.length(0); expect((await guards().findOne({ _id: recipient() })).state).to.equal("cancelled"); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect((await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("cleans recent pending legacy fields once deployment has stopped every writer", async () => { + await guards().insertOne({ _id: "recipient-alerts-v2", state: "ready" }); + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "recent-unsent", state: "pending", claimedAt: new Date() }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { + accessAlertClaimedAt: new Date(), accessAlertAttemptId: "recent-unsent", + } }); + await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + }); + it("preserves an untagged legacy delivery written during rollback while cancelling a native pending attempt", async () => { + await guards().insertOne({ _id: "recipient-alerts-v2", state: "ready" }); + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "native-unsent", state: "pending", claimedAt: new Date() }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date() } }); + await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts(); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("legacy"); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0); + }); + it("blocks dispatch while an owner reset has advanced the guard revision but not the resource generation", async () => { + const events = mongoose.connection.collection("owner_access_alert_events"), insert = events.insertOne; + const collection = coll(resource.kind), updateMany = collection.updateMany; + let prepared, releasePrepared, resetting, releaseReset; + const atPrepared = new Promise(resolve => { prepared = resolve; }); + const atReset = new Promise(resolve => { resetting = resolve; }); + events.insertOne = async function (...args) { prepared(); await new Promise(resolve => { releasePrepared = resolve; }); return insert.apply(this, args); }; + collection.updateMany = async function (...args) { resetting(); await new Promise(resolve => { releaseReset = resolve; }); return updateMany.apply(this, args); }; + const pending = notify(owner.id, "token_expired", resource); + let action; + try { + await atPrepared; action = reset(owner.id, resource); await atReset; + expect((await guards().findOne({ _id: recipient() })).resetRevision).to.equal(1); + releasePrepared(); await pending; + expect(sent).to.have.length(0); + const row = await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) }); + expect(row.accessAlertGeneration).to.equal(undefined); expect(row.accessAlertClaimedAt).to.equal(undefined); + } finally { + events.insertOne = insert; collection.updateMany = updateMany; + if (releasePrepared) releasePrepared(); if (releaseReset) releaseReset(); + await pending; if (action) await action; + } + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("invalidates an old failure when reset starts before its recipient guard exists", async () => { + const lookup = Users.findOne, collection = coll(resource.kind), updateMany = collection.updateMany; + let entered, release, resetting, releaseReset; + const atLookup = new Promise(resolve => { entered = resolve; }); + const atReset = new Promise(resolve => { resetting = resolve; }); + Users.findOne = function (...args) { return { lean: async () => { + entered(); await new Promise(resolve => { release = resolve; }); return lookup.apply(Users, args).lean(); + } }; }; + collection.updateMany = async function (...args) { resetting(); await new Promise(resolve => { releaseReset = resolve; }); return updateMany.apply(this, args); }; + const pending = notify(owner.id, "token_expired", resource); let action; + try { + await atLookup; action = reset(owner.id, resource); await atReset; + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + release(); await pending; expect(sent).to.have.length(0); + expect((await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + } finally { + Users.findOne = lookup; collection.updateMany = updateMany; + if (release) release(); if (releaseReset) releaseReset(); await pending; if (action) await action; + } + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("prevents a cancelled sender from recreating legacy fields after its database write resumes", async () => { + const collection = coll(resource.kind), update = collection.updateOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$set?.accessAlertClaimedAt) { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const pending = notify(owner.id, "token_expired", resource); + try { + await gate; await guards().updateOne({ _id: recipient() }, { $set: { claimedAt: new Date(0) } }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + } finally { collection.updateOne = update; release(); await pending; } + const row = await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) }); + expect(row.accessAlertClaimedAt).to.equal(undefined); expect(row.accessAlertClaimRevision).to.equal(1); + expect(sent).to.have.length(1); + }); + it("reaps a crashed pre-provider attempt after legacy preparation completed", async () => { + await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind, + resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "prepared-not-sent", state: "pending", resourceRevision: 0, claimedAt: new Date(0) }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { + accessAlertClaimedAt: new Date(0), accessAlertAttemptId: "prepared-not-sent", accessAlertClaimRevision: 1, + } }); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); expect(sent).to.have.length(0); + const row = await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) }); + expect(row.accessAlertClaimedAt).to.equal(undefined); expect(row.accessAlertClaimRevision).to.equal(2); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + for (const accepted of [true, false]) it(`applies recovery recorded during an in-flight ${accepted ? "accepted" : "rejected"} delivery`, async () => { + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + global.fetch = async (_url, options) => { + sent.push(options); if (sent.length === 1) { entered(); await new Promise(resolve => { release = resolve; }); } + return { ok: accepted, status: accepted ? 200 : 403 }; + }; + const pending = notify(owner.id, "token_expired", resource); + try { + await gate; await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + const record = await guards().findOne({ _id: recipient() }); expect(record.state).to.equal("sending"); + expect(record.deferredRecoveries).to.have.length(1); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + } finally { release(); await pending; } + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("invalidates in-flight recovery evidence after a newer reset", async () => { + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + global.fetch = async (_url, options) => { + sent.push(options); if (sent.length === 1) { entered(); await new Promise(resolve => { release = resolve; }); } + return { ok: true, status: 200 }; + }; + const pending = notify(owner.id, "token_expired", resource); + try { + await gate; await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); await reset(owner.id, resource); + } finally { release(); await pending; } + expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + await recover(owner.id, resource, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("invalidates deferred recovery if its source binding changes before delivery completes", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { source: { repositoryName: "owner/repo" } } }); + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + global.fetch = async (_url, options) => { + sent.push(options); if (sent.length === 1) { entered(); await new Promise(resolve => { release = resolve; }); } + return { ok: true, status: 200 }; + }; + const pending = notify(owner.id, "token_expired", resource); + try { + await gate; await reset(owner.id, resource); + await recover(owner.id, resource, async () => {}, { "source.repositoryName": "owner/repo" }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { "source.repositoryName": "new/repo" } }); + } finally { release(); await pending; } + expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); + await recover(owner.id, resource, async () => {}, { "source.repositoryName": "new/repo" }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("rejects dispatch when reset starts after the guard reservation but before the owner commitment", async () => { + const collection = guards(), update = collection.updateOne, updateMany = collection.updateMany; + let entered, release, resetting, releaseReset; + const gate = new Promise(resolve => { entered = resolve; }), resetGate = new Promise(resolve => { resetting = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + const result = await update.call(this, filter, operation, ...args); + if (operation.$set?.state === "sending") { entered(); await new Promise(resolve => { release = resolve; }); } + return result; + }; + collection.updateMany = async function (...args) { resetting(); await new Promise(resolve => { releaseReset = resolve; }); return updateMany.apply(this, args); }; + const pending = notify(owner.id, "token_expired", resource); let action; + try { + await gate; action = reset(owner.id, resource); await resetGate; + expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending"); + release(); await pending; expect(sent).to.have.length(0); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + } finally { + collection.updateOne = update; collection.updateMany = updateMany; + if (release) release(); if (releaseReset) releaseReset(); await pending; if (action) await action; + } + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + }); + it("applies fresh recovery recorded before the reset lease is cleared without another upstream read", async () => { + await notify(owner.id, "token_expired", resource); + const users = mongoose.connection.collection("users"), update = users.updateOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + users.updateOne = async function (filter, operation, ...args) { + if (operation.$unset?.accessAlertResetUntil !== undefined) { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const action = reset(owner.id, resource); + try { + await gate; await recover(owner.id, resource, async () => {}); + expect((await guards().findOne({ _id: recipient() })).deferredRecoveries).to.have.length(1); + } finally { users.updateOne = update; release(); await action; } + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("retries saved recovery after a crashed reset lease expires", async () => { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: { accessAlertResetUntil: new Date(Date.now() + 150) } }); + await recover(owner.id, resource, async () => {}); + expect(await guards().findOne({ _id: recipient() })).not.to.equal(null); + for (let i = 0; i < 100 && (await guards().findOne({ _id: recipient() }))?.state !== "recovered"; i++) await new Promise(resolve => global.setTimeout(resolve, 20)); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + for (const state of ["prepared", "cancelled", "committed"]) it(`reconciles interrupted guard reservation with a ${state} owner dispatch record`, async () => { + const attemptId = `interrupted-${state}`; + await guards().insertMany([{ _id: "recipient-alerts-v2", state: "ready" }, { + _id: recipient(), owner: owner._id, resourceKind: resource.kind, resourceId: new mongoose.Types.ObjectId(resource.id), + attemptId, state: "sending", dispatchPrepared: true, resourceRevision: 0, claimedAt: new Date(0), + }]); + await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: { + [`accessAlertDispatches.${attemptId}`]: { state, claimedAt: new Date(0) }, + } }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { + accessAlertClaimedAt: new Date(0), accessAlertAttemptId: attemptId, accessAlertClaimRevision: 1, + } }); + await alerts.initializeOwnerAccessAlerts(); + const record = await guards().findOne({ _id: recipient() }); + const account = await mongoose.connection.collection("users").findOne({ _id: owner._id }); + if (state === "committed") { + expect(record.state).to.equal("sending"); expect(account.accessAlertDispatches[attemptId].state).to.equal(state); + } else { + expect(record).to.equal(null); expect(account.accessAlertDispatches[attemptId]).to.equal(undefined); + expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined); + } + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(state === "committed" ? 0 : 1); + }); + it("prunes expired uncommitted owner dispatch records and cleans completed attempts", async () => { + await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: { accessAlertDispatches: { + abandoned: { state: "prepared", claimedAt: new Date(0) }, cancelled: { state: "cancelled", claimedAt: new Date(0) }, + unknown: { state: "committed", claimedAt: new Date(0) }, + } } }); + await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1); + const account = await mongoose.connection.collection("users").findOne({ _id: owner._id }); + expect(Object.keys(account.accessAlertDispatches)).to.deep.equal(["unknown"]); + }); + it("keeps shared-address suppression when the original owner is banned", async () => { + await notify(owner.id, "token_expired", resource); + await Users.updateOne({ _id: owner._id }, { $set: { status: "banned" } }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("resumes an indexed migration from its last completed checkpoint", async () => { + await Users.updateOne({ _id: otherOwner._id }, { $set: { notificationEmail: "second@example.com" } }); + const second = await create(otherOwner._id); + await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(second.id) }, { $set: { accessAlertClaimedAt: new Date(2) } }); + await guards().insertOne({ _id: "recipient-alerts-v2", state: "new", leaseUntil: new Date(0), + progress: { repository: { claimedAt: new Date(1), id: new mongoose.Types.ObjectId(resource.id) } } }); + await alerts.initializeOwnerAccessAlerts(); + expect(await guards().findOne({ _id: recipient() })).to.equal(null); + expect(await guards().findOne({ _id: alerts.recipientAlertKey("second@example.com") })).not.to.equal(null); + expect((await guards().findOne({ _id: "recipient-alerts-v2" })).progress.repository.claimedAt.getTime()).to.equal(2); + expect((await coll("repository").indexes()).some(i => i.name === "owner_access_alert_backfill" && i.partialFilterExpression)).to.equal(true); + }); + it("confirms recovery on a fresh uncached file read but never on a cache hit", async () => { + const database = require("../src/server/database"); + const GitHubStream = require("../src/core/source/GitHubStream").default; + const storage = require("../src/core/storage").default; + const { Readable } = require("stream"); + const originals = { isConnected: database.isConnected, fileInfo: storage.fileInfo, write: storage.write, read: storage.read }; + await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { repoId: resource.id, source: { repositoryName: "owner/repo", commit: "pinned" } } }); + let cached = false; + database.isConnected = true; + storage.fileInfo = async () => { if (!cached) throw Object.assign(new Error("miss"), { code: "ENOENT" }); return { size: 11, contentType: "text/plain" }; }; + storage.write = async (_repo, _path, input) => { for await (const _chunk of input) { /* Consume the upstream stream. */ } cached = true; }; + storage.read = async () => Readable.from(["fresh-bytes"]); + try { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + const source = new GitHubStream({ repoId: resource.id, organization: "owner", repoName: "repo", commit: "pinned", getToken: async () => "mock-token" }); + source.downloadWithFallback = async () => Readable.from(["fresh-bytes"]); + const read = () => source.getFileContentCache("a.txt", resource.id, () => ({ sha: "a".repeat(40), size: 11 })); + (await read()).destroy(); expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + const secondKey = alerts.recipientAlertKey("second@example.com"); + await Users.updateOne({ _id: owner._id }, { $set: { notificationEmail: "second@example.com" } }); + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + (await read()).destroy(); expect((await guards().findOne({ _id: secondKey })).state).to.equal("accepted"); + } finally { database.isConnected = originals.isConnected; storage.fileInfo = originals.fileInfo; storage.write = originals.write; storage.read = originals.read; } + }); + it("confirms recovery when a successful scheduled refresh finds the same commit", async () => { + const Repository = require("../src/core/Repository").default; + const Model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default; + const github = require("../src/core/source/GitHubRepository"); + const original = github.getRepositoryFromGitHub; + const source = { repositoryName: "owner/repo", commit: "pinned", branch: "main" }; + await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { source } }); + const repo = new Repository(new Model({ _id: resource.id, owner: owner._id, repoId: resource.id, status: "ready", + source, lastView: new Date(0), options: { update: true, expirationMode: "never" } })); + repo.getToken = async () => "mock-token"; + github.getRepositoryFromGitHub = async () => ({ fullName: "owner/repo", model: { defaultBranch: "main" }, + branches: async () => [{ name: "main", commit: "pinned" }], getCommitInfo: async () => { throw new Error("Unexpected commit fetch"); } }); + try { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + await repo.updateIfNeeded({ force: false }); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + } finally { github.getRepositoryFromGitHub = original; } + }); + it("seeds old claims once while five instances start concurrently", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date() } }); + await Promise.all(Array.from({ length: 5 }, () => alerts.initializeOwnerAccessAlerts())); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("legacy"); + await notify(otherOwner.id, "repo_not_found", other); expect(sent).to.have.length(0); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await alerts.initializeOwnerAccessAlerts(); expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + }); + it("preserves every legacy claim for a shared address until each triggering resource recovers", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date(2) } }); + await alerts.initializeOwnerAccessAlerts(); + expect((await guards().findOne({ _id: recipient() })).legacyClaims).to.have.length(2); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + expect((await guards().findOne({ _id: recipient() })).legacyClaims.filter(claim => !claim.recovered)).to.have.length(1); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {}); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + }); + it("does not retire a legacy group when only its first triggering resource is removed", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date(2) } }); + await alerts.initializeOwnerAccessAlerts(); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } }); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + }); + it("atomically marks concurrent legacy recoveries without releasing suppression", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date(2) } }); + await alerts.initializeOwnerAccessAlerts(); await reset(owner.id, resource); await reset(otherOwner.id, other); + await Promise.all([recover(owner.id, resource, async () => {}), recover(otherOwner.id, other, async () => {})]); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + }); + it("requires a fresh read after a newer reset of a partially recovered legacy group", async () => { + for (const item of [resource, other]) await coll(item.kind).updateOne({ _id: new mongoose.Types.ObjectId(item.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await alerts.initializeOwnerAccessAlerts(); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await reset(owner.id, resource); + await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("legacy"); + await recover(owner.id, resource, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + }); + it("fences legacy claim mutation when reset races after the final resource check", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await alerts.initializeOwnerAccessAlerts(); await reset(owner.id, resource); + const collection = guards(), update = collection.updateOne; + let entered, release; const gate = new Promise(resolve => { entered = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + if (filter.attemptId && Array.isArray(operation)) { entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const pending = recover(owner.id, resource, async () => {}); + try { await gate; await reset(owner.id, resource); } + finally { collection.updateOne = update; release(); await pending; } + const record = await guards().findOne({ _id: recipient() }); + expect(record.state).to.equal("legacy"); expect(record.legacyClaims[0].recovered).to.equal(false); + }); + it("reconciles claims written by legacy instances after rollback without reseeding ordinary restarts", async () => { + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } }); + await alerts.initializeOwnerAccessAlerts(); await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await alerts.initializeOwnerAccessAlerts(); expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date() } }); + await alerts.prepareLegacyOwnerAlertMigration(); + const marker = await guards().findOne({ _id: "recipient-alerts-v2" }); expect(marker.state).to.equal("new"); expect(marker.progress).to.equal(undefined); + await alerts.initializeOwnerAccessAlerts(); await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + expect((await guards().findOne({ _id: recipient() })).legacyClaims.filter(claim => !claim.recovered)).to.have.length(1); + await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0); + }); + it("preserves a native guard's origin when rollback reconciliation adds another legacy claim", async () => { + await alerts.initializeOwnerAccessAlerts(); await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date() } }); + await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts(); + expect((await guards().findOne({ _id: recipient() })).legacyClaims.filter(claim => !claim.recovered)).to.have.length(2); + await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + await recover(owner.id, resource, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("keeps suppression through removal, reactivation, reset and recovery", async () => { + await notify(owner.id, "token_expired", resource); + const original = await guards().findOne({ _id: recipient() }); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } }); + await notify(otherOwner.id, "token_expired", other); + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "ready" } }); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + expect((await guards().findOne({ _id: recipient() })).attemptId).to.equal(original.attemptId); + }); + for (const accepted of [true, false]) it(`retries the guard update after a definite ${accepted ? "accepted" : "rejected"} provider response`, async () => { + const collection = guards(), update = collection.updateOne; let writes = 0; + global.fetch = async (_url, options) => { sent.push(options); return { ok: accepted, status: accepted ? 200 : 403 }; }; + collection.updateOne = async function (filter, operation, ...args) { + if (["accepted", "failed"].includes(operation.$set?.state)) { + expect(await mongoose.connection.collection("owner_access_alert_events").findOne({ _id: `${filter.attemptId}/completion` })).not.to.equal(null); + if (++writes === 1) throw new Error("transient result write failure"); + } + return update.call(this, filter, operation, ...args); + }; + try { await notify(owner.id, "token_expired", resource); } + finally { collection.updateOne = update; } + expect(writes).to.equal(2); expect(sent).to.have.length(1); + expect((await guards().findOne({ _id: recipient() })).state).to.equal(accepted ? "accepted" : "failed"); + }); + for (const startup of [true, false]) it(`reconciles a durable provider result during ${startup ? "startup" : "the next notification"}`, async () => { + await alerts.initializeOwnerAccessAlerts(); + const collection = guards(), update = collection.updateOne; let writes = 0; + collection.updateOne = async function (filter, operation, ...args) { + if (["accepted", "failed"].includes(operation.$set?.state)) { writes++; throw new Error("database unavailable"); } + return update.call(this, filter, operation, ...args); + }; + try { await notify(owner.id, "token_expired", resource); } + finally { collection.updateOne = update; } + expect(writes).to.equal(3); expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending"); + if (startup) await alerts.initializeOwnerAccessAlerts(); else await notify(otherOwner.id, "token_expired", other); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); expect(sent).to.have.length(1); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1); + }); + it("records a provider completion once after its insertion acknowledgement is lost", async () => { + const collection = mongoose.connection.collection("owner_access_alert_events"), insert = collection.insertOne; + let lost = false; + collection.insertOne = async function (record, ...args) { + const result = await insert.call(this, record, ...args); + if (record.state === "accepted" && !lost) { lost = true; throw new Error("lost acknowledgement"); } + return result; + }; + try { await notify(owner.id, "token_expired", resource); } + finally { collection.insertOne = insert; } + expect(lost).to.equal(true); expect(sent).to.have.length(1); + expect(await collection.countDocuments({ state: "accepted" })).to.equal(1); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); + }); + it("does not let a late provider-result write resurrect a recovered guard", async () => { + const collection = guards(), update = collection.updateOne; + let entered, release, paused = false; const gate = new Promise(resolve => { entered = resolve; }); + collection.updateOne = async function (filter, operation, ...args) { + if (operation.$set?.state === "accepted" && !paused) { paused = true; entered(); await new Promise(resolve => { release = resolve; }); } + return update.call(this, filter, operation, ...args); + }; + const pending = notify(owner.id, "token_expired", resource); + try { + await gate; await notify(otherOwner.id, "token_expired", other); + await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); + } finally { collection.updateOne = update; release(); await pending; } + expect(sent).to.have.length(1); expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + }); + it("confirms truncated-file metadata recovery only on a successful fresh matching-source probe", async () => { + const database = require("../src/server/database"); + const GitHubStream = require("../src/core/source/GitHubStream").default; + const utils = require("../src/core/GitHubUtils"); + const originals = { isConnected: database.isConnected, octokit: utils.octokit }; + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { repoId: resource.id, source: { repositoryName: "owner/repo", commit: "pinned" } } }); + let response = { type: "file", sha: "a".repeat(40), size: 11 }; + database.isConnected = true; + utils.octokit = () => ({ repos: { getContent: async () => { if (response instanceof Error) throw response; return { data: response }; } } }); + const source = new GitHubStream({ repoId: resource.id, organization: "owner", repoName: "repo", commit: "pinned", getToken: async () => "mock-token" }); + try { + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + expect((await source.fetchFileInfoFromPath("a.txt")).sha).to.equal("a".repeat(40)); + expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered"); + const secondKey = alerts.recipientAlertKey("second@example.com"); + await Users.updateOne({ _id: owner._id }, { $set: { notificationEmail: "second@example.com" } }); + await notify(owner.id, "token_expired", resource); await reset(owner.id, resource); + response = { type: "dir" }; expect(await source.fetchFileInfoFromPath("a.txt")).to.equal(null); + expect((await guards().findOne({ _id: secondKey })).state).to.equal("accepted"); + response = Object.assign(new Error("Forbidden"), { status: 403 }); + try { await source.fetchFileInfoFromPath("a.txt"); throw new Error("Expected a failed probe"); } + catch (error) { expect(error.message).to.equal("Forbidden"); } + expect((await guards().findOne({ _id: secondKey })).state).to.equal("accepted"); + response = { type: "file", sha: "a".repeat(40), size: 11 }; + await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { "source.repositoryName": "different/source" } }); + await source.fetchFileInfoFromPath("a.txt"); + expect((await guards().findOne({ _id: secondKey })).state).to.equal("accepted"); + } finally { database.isConnected = originals.isConnected; utils.octokit = originals.octokit; } + }); + it("resumes a migration after a startup crashed and its lease expired", async () => { + await guards().insertOne({ _id: "recipient-alerts-v2", state: "new", lease: "crashed", leaseUntil: new Date(0) }); + await alerts.initializeOwnerAccessAlerts(); expect((await guards().findOne({ _id: "recipient-alerts-v2" })).state).to.equal("ready"); + }); + it("continues waiting beyond 300 polls while another migration holds a healthy lease", async () => { + await guards().insertOne({ _id: "recipient-alerts-v2", state: "new", lease: "active-migration", leaseUntil: new Date(Date.now() + 600000) }); + const collection = guards(), update = collection.updateOne, timeout = global.setTimeout; + let polls = 0; + collection.updateOne = async function (filter, ...args) { + if (filter._id === "recipient-alerts-v2" && filter.$or && ++polls === 305) { + await update.call(this, { _id: "recipient-alerts-v2" }, { $set: { state: "ready" } }); + } + return update.call(this, filter, ...args); + }; + global.setTimeout = (callback, delay, ...args) => delay === 250 ? timeout(callback, 0, ...args) : timeout(callback, delay, ...args); + try { await alerts.initializeOwnerAccessAlerts(); } + finally { collection.updateOne = update; global.setTimeout = timeout; } + expect(polls).to.equal(305); + expect((await guards().findOne({ _id: "recipient-alerts-v2" })).state).to.equal("ready"); + }); +}); diff --git a/test/registration-email.test.js b/test/registration-email.test.js new file mode 100644 index 0000000..88b5176 --- /dev/null +++ b/test/registration-email.test.js @@ -0,0 +1,144 @@ +/* global fetch, AbortSignal, URL */ +const { expect } = require("chai"); +const express = require("express"); +const session = require("express-session"); +const passport = require("passport"); +require("ts-node/register/transpile-only"); +const Users = require("../src/core/model/users/users.model").default; +const credentials = require("../src/core/credentials"); +const notifications = require("../src/core/owner-notifications"); +const github = require("../src/core/github-app"); +const { saveRegistrationEmail } = require("../src/core/registration-email"); +const { verify, router } = require("../src/server/routes/connection"); +const { githubAppRouter } = require("../src/server/routes/github-app"); + +const verified = (email, primary = true) => ({ email, primary, verified: true }); + +describe("registration email collection", function () { + const restores = []; + const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; }; + let response, calls, writes, user, saved, login, resetCalls; + beforeEach(() => { + response = [verified("primary@example.com")]; calls = []; writes = []; saved = []; resetCalls = []; + user = new Users({ username: "owner", externalIDs: { github: "10" }, emails: [] }); + stub(global, "fetch", async (url, options) => { + calls.push({ url, options }); + return { ok: true, json: async () => response }; + }); + stub(Users, "updateOne", async (filter, update) => { writes.push({ filter, update }); return { modifiedCount: 1 }; }); + stub(Users, "findOne", async () => user); + stub(Users.prototype, "save", async function () { saved.push(this); return this; }); + stub(credentials, "setCredential", async () => {}); + stub(notifications, "resetOwnerAccessAlerts", async id => { resetCalls.push(id); }); + stub(github, "saveAppGrant", async () => {}); + stub(github, "exchangeAppToken", async () => ({ access_token: "fresh-app-token" })); + stub(github, "githubRequest", async path => { + expect(path).to.equal("/user"); return { id: 10, login: "owner" }; + }); + login = () => new Promise((resolve, reject) => verify({ githubOAuthContext: { expires: Date.now() + 60000 } }, + "fresh-oauth-token", "", { id: "10", username: "owner", emails: [] }, (error, identity) => error ? reject(error) : resolve(identity))); + }); + afterEach(() => { while (restores.length) restores.pop()(); }); + + it("saves the verified primary private address instead of an unverified or GitHub placeholder address", async () => { + response = [verified("secondary@example.com", false), { email: "unverified@example.com", primary: true, verified: false }, + verified("123+owner@users.noreply.github.com"), verified(" primary@example.com "), null]; + await saveRegistrationEmail(user, "fresh-token"); + expect(user.emails.map(e => ({ email: e.email, default: e.default }))).to.deep.equal([{ email: "primary@example.com", default: true }]); + expect(calls[0].url).to.equal("https://api.github.com/user/emails?per_page=100"); + expect(calls[0].options.headers.Authorization).to.equal("Bearer fresh-token"); + expect(calls[0].options.signal).to.be.instanceOf(AbortSignal); + expect(writes[0].update.$set.emails).to.deep.equal([{ email: "primary@example.com", default: true }]); + }); + + it("uses a verified secondary address when the primary is a privacy placeholder", async () => { + response = [verified("owner@users.noreply.github.com"), verified("secondary@example.com", false)]; + await saveRegistrationEmail(user, "token"); + expect(user.emails[0].email).to.equal("secondary@example.com"); + }); + + for (const preference of ["stored-email", "custom-alert"]) it(`preserves ${preference} without requesting private email data`, async () => { + if (preference === "stored-email") user.emails = [{ email: "saved@example.com", default: true }]; + else user.notificationEmail = "custom@example.com"; + await saveRegistrationEmail(user, "token"); + expect(calls).to.have.length(0); expect(writes).to.have.length(0); + }); + + it("does not overwrite a concurrent manual email save", async () => { + stub(Users, "updateOne", async (filter) => { + expect(filter.notificationEmail).to.equal(null); + expect(filter.$or).to.deep.equal([{ emails: [] }, { emails: { $exists: false } }]); + return { modifiedCount: 0 }; + }); + await saveRegistrationEmail(user, "token"); + expect(user.emails).to.have.length(0); + }); + + for (const body of [[], {}, [verified("owner@users.noreply.github.com")], [{ email: "unverified@example.com", verified: false }], [verified("bad\r\nBcc:other@example.com")]]) { + it(`leaves the manual prompt available when no usable verified address is returned: ${JSON.stringify(body)}`, async () => { + response = body; await saveRegistrationEmail(user, "token"); + expect(user.emails).to.have.length(0); expect(writes).to.have.length(0); + }); + } + + for (const provider of ["OAuth", "App"]) { + async function callback() { + if (provider === "OAuth") return (await login()).user; + const handler = githubAppRouter.stack.find(l => l.route?.path === "/app/callback").route.stack.at(-1).handle; + let identity, location; + const req = { query: { code: "code", state: "state" }, session: { githubAppFlow: { state: "state", expires: Date.now() + 60000, returnTo: "/dashboard" }, save: cb => cb() }, + isAuthenticated: () => false, login: (value, cb) => { identity = value; cb(); } }; + await handler(req, { redirect: value => { location = value; } }); + expect(location).to.equal("/dashboard"); return identity.user; + } + it(`saves an email during new ${provider} registration`, async () => { + stub(Users, "findOne", async () => null); + const identity = await callback(); + expect(saved).to.have.length(1); expect(writes).to.have.length(1); + expect(identity.emails[0].email).to.equal("primary@example.com"); + }); + it(`does not reset access alerts during ordinary ${provider} sign-in`, async () => { + await callback(); expect(resetCalls).to.have.length(0); + }); + it(`fills missing email on existing ${provider} sign-in`, async () => { + expect((await callback()).emails[0].email).to.equal("primary@example.com"); + expect(saved).to.have.length(0); + }); + for (const failure of ["permission", "timeout", "database"]) it(`allows ${provider} sign-in when email collection fails due to ${failure}`, async () => { + if (failure === "permission") stub(global, "fetch", async () => ({ ok: false, status: 403 })); + if (failure === "timeout") stub(global, "fetch", async () => { throw new Error("timeout"); }); + if (failure === "database") stub(Users, "updateOne", async () => { throw new Error("database unavailable"); }); + expect((await callback()).id).to.equal(user.id); + expect(user.emails).to.have.length(0); + }); + } + + it("prepares recovery for an explicit OAuth reconnect", async () => { + await new Promise((resolve, reject) => verify({ githubOAuthContext: { ownerId: user.id, githubId: "10", expires: Date.now() + 60000 } }, + "token", "", { id: "10", username: "owner" }, error => error ? reject(error) : resolve())); + expect(resetCalls).to.deep.equal([user.id]); + }); + + it("rejects an identity mismatch before requesting or saving email", async () => { + const error = await new Promise(resolve => verify({ githubOAuthContext: { githubId: "different", expires: Date.now() + 60000 } }, + "token", "", { id: "10", username: "owner" }, resolve)); + expect(error.message).to.equal("github_identity_mismatch"); expect(calls).to.have.length(0); expect(writes).to.have.length(0); + }); +}); + +describe("OAuth registration permissions", function () { + it("requests private email access on the actual GitHub authorization redirect", async () => { + const app = express(); + app.use(session({ secret: "registration-test", saveUninitialized: false, resave: false })); + app.use(passport.initialize()); app.use("/github", router); + let server; + await new Promise(resolve => { server = app.listen(0, "127.0.0.1", resolve); }); + try { + const response = await fetch(`http://127.0.0.1:${server.address().port}/github/login`, { redirect: "manual" }); + expect(response.status).to.equal(302); + const url = new URL(response.headers.get("location")); + expect(url.hostname).to.equal("github.com"); + expect(url.searchParams.get("scope").split(",")).to.include.members(["repo", "user:email"]); + } finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } + }); +}); diff --git a/test/repository-activity.test.js b/test/repository-activity.test.js index 78bd5ee..0555cac 100644 --- a/test/repository-activity.test.js +++ b/test/repository-activity.test.js @@ -66,6 +66,94 @@ describe("repository activity dates", () => { expect(saved.source.commit).to.equal("abc123"); expect(saved).not.to.have.property("publishedAt"); }); + it("allows privacy-only saves after hosting capacity or conference admission ends", async () => { + const repo = repository(); auth(repo); + stub(require("../src/config").default, "BILLING_ENABLED", true); + const quotas = require("../src/core/billing/quotas"); + repo.model.billingManaged = true; + repo.model.conference = "ended-conf"; + repo.model.options.expirationMode = "remove"; + stub(quotas, "assertConferenceAdmission", async () => { throw Error("must not check conference"); }); + stub(quotas, "reserveRepository", async () => { throw Error("must not reserve"); }); + let saved; + stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; return { matchedCount: 1 }; } })); + await handler("repository-private", "/:repoId/", "post")({ body: { + repoId: repo.repoId, fullName: "owner/repo", conference: "ended-conf", source: { commit: "abc123", branch: "main" }, + terms: ["private"], options: { expirationMode: "remove", expirationDate: "2027-01-01", image: false, pdf: false, update: false }, + } }, { json: () => {} }); + expect(saved.options).to.include({ image: false, pdf: false, update: false }); + }); + it("rejects an oversized edited source before removing the published files", async () => { + const repo = repository(); auth(repo); repo.model.billingManaged = true; + stub(require("../src/config").default, "BILLING_ENABLED", true); + const quotas = require("../src/core/billing/quotas"); + const access = require("../src/core/github-app"); + const github = require("../src/core/source/GitHubRepository"); + const Stream = require("../src/core/source/GitHubStream").default; + stub(quotas, "assertConferenceAdmission", async () => {}); + stub(access, "selectRepositoryAccess", async () => ({ token: "test", binding: { kind: "oauth" } })); + stub(github, "getRepositoryFromGitHub", async () => ({ model: { id: "gh_1" }, fullName: "owner/repo", getCommitInfo: async () => ({ commit: {} }) })); + stub(Stream.prototype, "iterateFiles", async function* () { expect(this.data.commit).to.equal("abcdef456"); yield { size: 500 }; }); + stub(quotas, "reserveRepository", async (_owner, _repo, bytes) => { expect(bytes).to.equal(500); throw Error("quota_exceeded"); }); + repo.remove = async () => { throw Error("must preserve snapshot"); }; + let error; + try { await handler("repository-private", "/:repoId/", "post")({ body: { + repoId: repo.repoId, fullName: "owner/repo", source: { commit: "abcdef456", branch: "main" }, + terms: [], options: { expirationMode: "never" }, + } }, { json: () => {} }); } catch (caught) { error = caught; } + expect(error?.message).to.equal("quota_exceeded"); + }); + for (const mode of ["never", "remove"]) { + it(`persists the billing deadline when settings change to ${mode}`, async () => { + const config = require("../src/config").default; + const expiration = require("../src/core/billing/expiration"); + const quotas = require("../src/core/billing/quotas"); + const repo = repository(); auth(repo); let saved; + const deadline = new Date("2027-06-01"); + repo.model.options.expirationMode = "remove"; + repo.model.billingNeverExpiresUntil = deadline; + stub(config, "BILLING_ENABLED", true); + stub(expiration, "neverExpirationDeadline", async () => deadline); + stub(quotas, "assertConferenceAdmission", async () => {}); + stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update; return { matchedCount: 1 }; } })); + await handler("repository-private", "/:repoId/", "post")({ body: { + repoId: repo.repoId, fullName: "owner/repo", source: { commit: "abc123", branch: "main" }, + terms: [], options: { expirationMode: mode, expirationDate: "2027-01-01" }, + } }, { json: () => {} }); + if (mode === "never") expect(saved.$set.billingNeverExpiresUntil).to.deep.equal(deadline); + else expect(saved.$unset).to.have.property("billingNeverExpiresUntil", 1); + }); + } + it("rejects repeated extensions beyond one year before changing stored settings", async () => { + const repo = repository(); auth(repo); + stub(require("../src/config").default, "BILLING_ENABLED", true); + repo.model.options.expirationMode = "remove"; + repo.model.options.expirationDate = new Date(); + let writes = 0; + stub(Model, "updateOne", () => ({ exec: async () => { writes++; return { matchedCount: 1 }; } })); + const extend = handler("repository-private", "/:repoId/extend", "post"); + await extend({}, { json: () => {} }); + await extend({}, { json: () => {} }); + const savedDate = repo.model.options.expirationDate; + let error; + try { await extend({}, { json: () => {} }); } catch (caught) { error = caught; } + expect(error?.message).to.equal("invalid_billing_expiration"); + expect(writes).to.equal(2); + expect(repo.model.options.expirationDate).to.deep.equal(savedDate); + }); + it("does not extend an expired submission in a conference that no longer admits it", async () => { + const repo = repository("expired"); auth(repo); + repo.model.conference = "ended"; + const quotas = require("../src/core/billing/quotas"); + stub(quotas, "assertConferenceAdmission", async (_owner, _id, conference) => { + expect(conference).to.equal("ended"); throw Error("conf_not_activated"); + }); + stub(Model, "updateOne", () => { throw Error("must not persist extension"); }); + let error; + try { await handler("repository-private", "/:repoId/extend", "post")({}, {}); } catch (caught) { error = caught; } + expect(error?.message).to.equal("conf_not_activated"); + expect(repo.status).to.equal("expired"); + }); it("dates a dashboard expiration extension", async () => { const repo = repository(); auth(repo); let saved; stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; } })); @@ -131,5 +219,15 @@ describe("repository activity dates", () => { expect(body.publishedAt).to.equal(undefined); expect(body.settingsSavedAt).to.equal(undefined); expect(body.lastUpdateDate).to.deep.equal(repo.model.anonymizeDate); + expect(body.expirationDate).to.equal(null); + }); + it("tells reviewers when an expiring repository stops being available", async () => { + const repo = repository(); auth(repo); + repo.model.options.expirationMode = "remove"; + repo.model.options.expirationDate = new Date("2099-01-01"); + stub(Files, "exists", async () => null); + let body; + await handler("repository-public", "/:repoId/options", "get")({}, { header: () => {}, json: value => { body = value; } }); + expect(body.expirationDate).to.deep.equal(new Date("2099-01-01")); }); }); diff --git a/test/repository-reconnect.test.js b/test/repository-reconnect.test.js index dcac1aa..794306a 100644 --- a/test/repository-reconnect.test.js +++ b/test/repository-reconnect.test.js @@ -106,7 +106,7 @@ describe("reconnecting a recreated repository", () => { expect(saved.update.$set.status).to.equal("preparing"); expect(saved.update.$set.statusDate).to.be.instanceOf(Date); expect(saved.update.$set.source.commitDate).to.deep.equal(new Date("2026-09-29T00:00:00Z")); - expect(removals).to.deep.equal([{ accessRevision: "old" }]); + expect(removals).to.deep.equal([{ accessRevision: "old", replacement: true }]); expect(jobs[0][1]).to.deep.equal({ repoId: "recreated" }); }); it("rebuilds when only the repository identity changed", async () => { diff --git a/test/repository-refresh.test.js b/test/repository-refresh.test.js index 7014877..3e63db9 100644 --- a/test/repository-refresh.test.js +++ b/test/repository-refresh.test.js @@ -50,6 +50,22 @@ describe("repository refresh and restoration", () => { expect(failure.httpStatus).to.equal(403); }); + for (const actingOwner of [true, false]) { + it(`${actingOwner ? "rearms" : "does not rearm"} alerts when ${actingOwner ? "the owner" : "an admin"} refreshes`, async () => { + const repo = repository("ready"); + const notifications = require("../src/core/owner-notifications"); + const resets = []; + stub(notifications, "resetOwnerAccessAlerts", async (...args) => { resets.push(args); }); + const id = actingOwner ? repo.owner.id : "admin"; + stub(utils, "getUser", async () => ({ id, model: { id }, isAdmin: !actingOwner })); + repo.refresh = async () => { + expect(resets).to.have.length(actingOwner ? 1 : 0); + }; + await refresh({}, { json: () => {} }); + expect(resets).to.deep.equal(actingOwner ? [[repo.owner.id, { kind: "repository", id: String(repo.model._id) }]] : []); + }); + } + for (const commit of ["saved-sha", "new-sha"]) { it(`rebuilds a removed repository at ${commit} while preserving its ID`, async () => { const repo = repository("removed"); @@ -72,6 +88,37 @@ describe("repository refresh and restoration", () => { }); } + for (const force of [true, false]) { + it(`preserves the published snapshot when a ${force ? "manual" : "automatic"} refresh exceeds capacity`, async () => { + const config = require("../src/config").default; + const quotas = require("../src/core/billing/quotas"); + const Stream = require("../src/core/source/GitHubStream").default; + const repo = repository("ready"); + repo.model.billingManaged = true; + repo.model.options.update = true; + repo.model.lastView = new Date("2020-01-01"); + stub(config, "BILLING_ENABLED", true); + stub(db, "isConnected", false); + stub(quotas, "allowance", async () => ({ plan: "author_monthly" })); + stub(quotas, "reserveRepository", async (_owner, _id, bytes) => { + expect(bytes).to.equal(999); + throw new Error("quota_exceeded"); + }); + stub(Stream.prototype, "iterateFiles", async function* () { + expect(this.data.commit).to.equal("new-sha"); yield { size: 999 }; + }); + repo.getToken = async () => "test-token"; + stub(github, "getRepositoryFromGitHub", async () => ({ fullName: "owner/repo", model: {}, + branches: async () => [{ name: "main", commit: "new-sha" }], + })); + repo.resetSate = async () => { throw new Error("must preserve files"); }; + let error; + try { await repo.updateIfNeeded({ force }); } catch (caught) { error = caught; } + expect(error?.message).to.equal(force ? "quota_exceeded" : undefined); + expect(repo.model.source.commit).to.equal("saved-sha"); + expect(repo.status).to.equal("ready"); + }); + } it("preserves a removed repository and reports a GitHub access failure", async () => { const repo = repository("removed"); const failure = new Error("token_expired"); @@ -113,6 +160,46 @@ describe("repository refresh and restoration", () => { return stored; } + for (const force of [false, true]) { + for (const commit of ["saved-sha", "new-sha"]) { + it(`refreshes a ready snapshot with force=${force}, commit=${commit}`, async () => { + const repo = repository("ready"); + repo.model.options.update = true; + repo.model.lastView = new Date(0); + const stored = database(repo); + repo.getToken = async () => "token"; + stub(github, "getRepositoryFromGitHub", async options => { + expect(options.force).to.equal(true); + return { fullName: "owner/repo", model: { defaultBranch: "main" }, + branches: async options => { + expect(options.force).to.equal(true); + return [{ name: "main", commit }]; + }, getCommitInfo: async () => ({ commit: {} }) }; + }); + let cleared = false, queued = false; + repo.resetSate = async () => { + expect(stored.source.commit).to.equal(commit); + expect(stored.status).to.equal("preparing"); + cleared = true; + }; + stub(queue, "downloadQueue", { getJob: async () => undefined, add: async (_id, data) => { + expect(cleared).to.equal(true); + expect(data.repoId).to.equal("restore-me"); + queued = true; + } }); + if (force) await repo.refresh(); + else await repo.updateIfNeeded(); + const rebuilt = force || commit !== "saved-sha"; + expect(cleared).to.equal(rebuilt); + expect(queued).to.equal(rebuilt); + expect(stored.status).to.equal(rebuilt ? "preparing" : "ready"); + expect(stored.source.commit).to.equal(commit); + expect(stored.repoId).to.equal("restore-me"); + expect(stored).not.to.have.property("refreshToken"); + }); + } + } + it("claims a lease before GitHub work and rejects a concurrent refresh", async () => { const first = repository("ready"); const second = new Repository(new Model(first.model.toObject())); @@ -135,6 +222,35 @@ describe("repository refresh and restoration", () => { expect(stored).not.to.have.property("refreshToken"); }); + for (const recoveryFirst of [false, true]) { + it(`serializes missing-tree recovery and dashboard refresh (recovery first: ${recoveryFirst})`, async () => { + const first = repository("ready"); + const second = new Repository(new Model(first.model.toObject())); + const stored = database(first); + const Files = require("../src/core/model/files/files.model").default; + stub(Files, "exists", () => ({ exec: async () => null })); + let release, started; + const entered = new Promise(resolve => { started = resolve; }); + let updates = 0; + first.updateIfNeeded = async () => { + updates++; + started(); + await new Promise(resolve => { release = resolve; }); + }; + second.updateIfNeeded = async () => { updates++; }; + const running = recoveryFirst ? first.isReady() : first.refresh(); + await entered; + try { + let failure; + try { await (recoveryFirst ? second.refresh() : second.isReady()); } catch (error) { failure = error; } + expect(failure?.message).to.equal("invalid_status"); + expect(updates).to.equal(1); + expect(stored.status).to.equal("ready"); + } finally { release(); await running; } + expect(stored).not.to.have.property("refreshToken"); + }); + } + it("releases a failed lease so reconnecting can be retried", async () => { const repo = repository("removed"); const stored = database(repo); diff --git a/test/request-monitoring.test.js b/test/request-monitoring.test.js index 4b29ecc..ccebfd8 100644 --- a/test/request-monitoring.test.js +++ b/test/request-monitoring.test.js @@ -95,6 +95,19 @@ describe("bounded request monitoring", function () { expect(metrics.drain().find(row => row.data.metric === "request").data.bytes).to.equal(0); }); + it("reports time after headers and delivery rate without conflating them with startup time", async function () { + app.get("/api/repo/:repoId/file/*path", async (_req, res) => { + await delay(30); res.flushHeaders(); await delay(40); res.end(Buffer.alloc(4096)); + }); + await listen(); + await fetch(server, "/api/repo/test/file/asset.mp4"); + const detail = logs[0][1]; + expect(detail.firstByteMs).to.be.at.least(20); + expect(detail.responseMs).to.be.at.least(30); + expect(detail.ms).to.be.closeTo(detail.firstByteMs + detail.responseMs, 1); + expect(detail.bytesPerSecond).to.be.closeTo(detail.bytes * 1000 / detail.responseMs, detail.bytes * 1000 / detail.responseMs * .05); + }); + for (const headersSent of [false, true]) { it(`records a disconnect ${headersSent ? "after" : "before"} headers exactly once`, async function () { let arrived; const ready = new Promise(resolve => { arrived = resolve; }); @@ -107,7 +120,11 @@ describe("bounded request monitoring", function () { expect(logs[0][1].outcome).to.equal("interrupted"); const row = metrics.drain().find(row => row.data.metric === "request").data; expect(row.count).to.equal(1); expect(row.aborted).to.equal(1); expect(activeRequestCount()).to.equal(0); - if (!headersSent) expect(logs[0][1].firstByteMs).to.equal(null); + if (!headersSent) { + expect(logs[0][1].firstByteMs).to.equal(null); + expect(logs[0][1].responseMs).to.equal(null); + expect(logs[0][1].bytesPerSecond).to.equal(null); + } }); } @@ -149,7 +166,7 @@ describe("bounded request monitoring", function () { try { const input = await source.getFileContentCache("file.txt", source.data.repoId, () => ({ sha: "blob", size: 7 })); input.destroy(); res.send("ok"); - } catch (error) { res.status(502).send(error === upstreamError ? "original error" : "unexpected error"); } + } catch (error) { res.status(502).send(error.cause === upstreamError ? "original error" : "unexpected error"); } }); try { await listen(); const response = await fetch(server, "/"); diff --git a/test/review-callbacks.test.js b/test/review-callbacks.test.js new file mode 100644 index 0000000..2c4e205 --- /dev/null +++ b/test/review-callbacks.test.js @@ -0,0 +1,22 @@ +require('ts-node/register/transpile-only'); +const { expect } = require('chai'); +const { createReviewCallbacks } = require('../src/server/service/review-callbacks'); +const entry = () => ({ clientId:'1'.repeat(32),callbackId:'2'.repeat(32),url:'https://review.example.test/api/v1/artifacts/callback' }); +const raw = callbacks => JSON.stringify({version:1,callbacks}); +describe('registered review callback destinations', () => { + it('binds exact HTTPS destinations to both client and callback IDs', () => { + const registry=createReviewCallbacks(raw([entry()])); + expect(registry.resolve(entry().clientId,entry().callbackId)).equal(entry().url); + expect(registry.resolve('3'.repeat(32),entry().callbackId)).equal(undefined); + expect(registry.resolve(entry().clientId,'4'.repeat(32))).equal(undefined); + expect(registry.formAction).equal("'self' "+entry().url); + }); + it('rejects redirect parameters, URL credentials, aliases and unsafe CSP characters', () => { + for(const url of ['http://review.example.test/callback','https://owner:secret@review.example.test/callback','https://review.example.test/callback?','https://review.example.test/callback#','https://review.example.test/callback?redirect=https://other.test','https://review.example.test/a/../callback','https://review.example.test/%63allback','https://review.example.test/callback/','https://review.example.test/callback\n','https://review.example.test/','https://review.example.test/callback;script-src']) expect(()=>createReviewCallbacks(raw([{...entry(),url}]))).to.throw('Invalid review callback registry'); + }); + it('rejects duplicate decoded configuration fields and bounded registry overflows', () => { + for(const value of [raw([]),raw([entry(),entry()]),raw([{...entry(),extra:'x'}]),raw(Array.from({length:33},(_,i)=>({...entry(),callbackId:i.toString(16).padStart(32,'0')}))),raw([entry()]).replace('"version":1','"version":1,"ver\\u0073ion":1'),'{']) expect(()=>createReviewCallbacks(value)).to.throw('Invalid review callback registry'); + const entries=Array.from({length:4},(_,i)=>({...entry(),callbackId:String(i).repeat(32),url:'https://review.example.test/'+('x'.repeat(1100))+i})); + expect(()=>createReviewCallbacks(raw(entries))).to.throw('Invalid review callback registry'); + }); +}); diff --git a/test/review-capabilities.test.js b/test/review-capabilities.test.js new file mode 100644 index 0000000..a854907 --- /dev/null +++ b/test/review-capabilities.test.js @@ -0,0 +1,309 @@ +require("ts-node/register/transpile-only"); +const { expect } = require("chai"); +const { createHash } = require("crypto"); +const http = require("http"); +const process = require("process"); +const express = require("express"); +const { + createReviewCapabilities, +} = require("../src/server/service/review-capabilities"); +const client = "a".repeat(32), + token = "b".repeat(64); +const digest = (value) => + createHash("sha256").update(value, "ascii").digest("hex"); +const key = (overrides = {}) => ({ + clientId: client, + keyId: "rotation-1", + tokenSHA256: digest(token), + notBefore: "2026-01-01T00:00:00Z", + notAfter: "2027-01-01T00:00:00Z", + ...overrides, +}); +const config = (keys) => JSON.stringify({ version: 1, keys }); +const headers = { + "X-4open-Artifact-Client-Id": client, + "X-4open-Artifact-Service-Key-Id": "rotation-1", + Authorization: "Bearer " + token, +}; +describe("review service capabilities", function () { + let server, port, time, downstream; + async function start(raw = config([key()])) { + time = Date.parse("2026-06-01T00:00:00Z"); + downstream = 0; + const app = express(); + app.use( + "/service", + createReviewCapabilities(raw, () => time), + ); + app.use((_req, res) => { + downstream++; + res.setHeader("Set-Cookie", "browser=1"); + res.send("browser"); + }); + server = http.createServer(app); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + port = server.address().port; + } + function request(options = {}) { + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: "127.0.0.1", + port, + path: "/service/v1/capabilities", + method: "GET", + headers, + ...options, + }, + (res) => { + const parts = []; + res.on("data", (data) => parts.push(data)); + res.on("end", () => + resolve({ + status: res.statusCode, + headers: res.headers, + text: Buffer.concat(parts).toString(), + }), + ); + }, + ); + req.on("error", reject); + req.end(options.body); + }); + } + afterEach(async () => { + if (server) { + await new Promise((resolve) => server.close(resolve)); + server = undefined; + } + }); + it("is disabled without a registry and bypasses browser middleware", async () => { + // Explicitly omit configuration, rather than start()'s test default. + const app = express(); + app.use("/service", createReviewCapabilities()); + app.use((_q, r) => r.send("browser")); + server = app.listen(0, "127.0.0.1"); + await new Promise((resolve) => server.once("listening", resolve)); + port = server.address().port; + const result = await request(); + expect(result.status).to.equal(404); + expect(result.headers["set-cookie"]).to.equal(undefined); + }); + it("advertises only the supported contract, bound to the authenticated client", async () => { + await start(); + const result = await request(); + expect(result.status).to.equal(200); + expect(JSON.parse(result.text)).to.deep.equal({ + contract: "4open.artifacts/1", + clientId: client, + contracts: ["4open.artifacts/1"], + available: [], + }); + expect(result.headers["cache-control"]).to.equal("no-store"); + expect(result.headers["referrer-policy"]).to.equal("no-referrer"); + expect(result.headers["set-cookie"]).to.equal(undefined); + expect(result.headers["access-control-allow-origin"]).to.equal(undefined); + expect(downstream).to.equal(0); + }); + it("rejects missing, wrong, duplicated and cross-client credentials without reflecting secrets", async () => { + await start(); + for (const input of [ + {}, + { ...headers, Authorization: "Bearer " + "c".repeat(64) }, + { ...headers, "X-4open-Artifact-Client-Id": "d".repeat(32) }, + { ...headers, "X-4open-Artifact-Service-Key-Id": "unknown" }, + { + ...headers, + Authorization: [headers.Authorization, headers.Authorization], + }, + { ...headers, "X-4open-Artifact-Client-Id": [client, client] }, + { + ...headers, + "X-4open-Artifact-Service-Key-Id": ["rotation-1", "rotation-1"], + }, + ]) { + const r = await request({ headers: input }); + expect(r.status).to.equal(401); + const failure = JSON.parse(r.text); + expect(failure.code).to.equal("unauthorized"); + expect(failure.requestId).to.match(/^[a-f0-9]{32}$/); + expect(r.text).not.to.contain(token); + expect(r.text).not.to.contain(client); + } + expect(downstream).to.equal(0); + }); + it("has exact paths and rejects browser context, methods, queries and bodies", async () => { + await start(); + for (const path of [ + "/service/v1/capabilities/", + "/service/v1/capabilities?code=secret", + "/service/v1/%63apabilities", + "/service/v1/bindings", + "/service/V1/capabilities", + ]) + expect((await request({ path })).status, path).to.equal(404); + for (const method of ["POST", "PUT", "OPTIONS", "HEAD"]) + expect((await request({ method })).status).to.equal(405); + for (const name of [ + "Cookie", + "Origin", + "Referer", + "Sec-Fetch-Site", + "Sec-Fetch-Mode", + "Sec-Fetch-Dest", + "Sec-Fetch-User", + ]) + expect( + (await request({ headers: { ...headers, [name]: "browser" } })).status, + ).to.equal(403); + expect( + ( + await request({ + headers: { ...headers, "Content-Type": "application/json" }, + }) + ).status, + ).to.equal(400); + expect( + ( + await request({ + headers: { ...headers, "Content-Length": "2" }, + body: "{}", + }) + ).status, + ).to.equal(400); + expect(downstream).to.equal(0); + }); + it("supports overlapping keys with inclusive start, exclusive expiry and restart revocation", async () => { + const other = "c".repeat(64); + await start( + config([key(), key({ keyId: "rotation-2", tokenSHA256: digest(other) })]), + ); + time = Date.parse(key().notBefore) - 1; + expect((await request()).status).to.equal(401); + time++; + expect((await request()).status).to.equal(200); + expect( + ( + await request({ + headers: { + ...headers, + "X-4open-Artifact-Service-Key-Id": "rotation-2", + Authorization: "Bearer " + other, + }, + }) + ).status, + ).to.equal(200); + time = Date.parse(key().notAfter); + expect((await request()).status).to.equal(401); + await new Promise((resolve) => server.close(resolve)); + server = undefined; + await start( + config([key({ keyId: "rotation-2", tokenSHA256: digest(other) })]), + ); + expect((await request()).status).to.equal(401); + }); + it("limits reads per client across keys and resets without allocating anonymous counters", async () => { + const other = "c".repeat(64); + await start( + config([key(), key({ keyId: "rotation-2", tokenSHA256: digest(other) })]), + ); + const results = await Promise.all( + Array.from({ length: 60 }, () => request()), + ); + expect(results.every((r) => r.status === 200)).to.equal(true); + const r = await request({ + headers: { + ...headers, + "X-4open-Artifact-Service-Key-Id": "rotation-2", + Authorization: "Bearer " + other, + }, + }); + expect(r.status).to.equal(429); + expect(r.headers["retry-after"]).to.equal("60"); + expect(JSON.parse(r.text).retryAfterSeconds).to.equal(60); + time += 60000; + expect((await request()).status).to.equal(200); + }); + it("validates bounded digest-only closed configuration without leaking it", () => { + const bad = [ + "", + "{", + "x".repeat(65537), + "null", + "[]", + config([]), + JSON.stringify({ version: 2, keys: [key()] }), + config([key({ secret: token })]), + config([key({ tokenSHA256: token.toUpperCase() })]), + config([key(), key()]), + config([key(), key({ keyId: "other" })]), + config([key({ notAfter: key().notBefore })]), + config([key({ notBefore: "2026-02-30T00:00:00Z" })]), + config([key({ notBefore: "2026-01-01T01:00:00+01:00" })]), + config([key({ clientId: "../client" })]), + config([key({ keyId: "a:b" })]), + config( + Array.from({ length: 17 }, (_, i) => + key({ keyId: "k" + i, tokenSHA256: digest(String(i)) }), + ), + ), + ]; + for (const raw of bad) + expect(() => createReviewCapabilities(raw)).to.throw( + "Invalid REVIEW_SERVICE_KEYS configuration", + ); + }); + it("closes rejected incomplete bodies without waiting for upload completion", async function () { + this.timeout(5000); + await start(); + const result = await new Promise((resolve, reject) => { + const socket = require("net").createConnection({ + host: "127.0.0.1", + port, + }); + let received = ""; + socket.setTimeout(1500, () => { + socket.destroy(); + reject(new Error("rejected request still waiting for its unread body")); + }); + socket.on("error", reject); + socket.on("data", (data) => { + received += data.toString(); + }); + socket.on("close", () => resolve(received)); + socket.on("connect", () => + socket.write( + "GET /service/v1/capabilities HTTP/1.1\r\nHost: localhost\r\nContent-Length: 10000000\r\nConnection: keep-alive\r\n\r\n", + ), + ); + }); + expect(result).to.contain("HTTP/1.1 400"); + expect(result.toLowerCase()).to.contain("connection: close"); + expect(downstream).to.equal(0); + }); + if (process.env.RUN_REVIEW_SERVICE_BENCHMARK === "1") + it("records bounded loopback read performance", async () => { + await start(); + const samples = []; + for (let i = 0; i < 150; i++) { + if (i % 50 === 0) time += 60000; + const begin = process.hrtime.bigint(); + const r = await request(); + expect(r.status).to.equal(200); + samples.push(Number(process.hrtime.bigint() - begin) / 1e6); + } + samples.sort((a, b) => a - b); + const report = { + requests: samples.length, + transport: "loopback HTTP, fresh connections, synthetic credentials", + meanMs: samples.reduce((a, b) => a + b, 0) / samples.length, + medianMs: samples[75], + p95Ms: samples[142], + providerCalls: false, + }; + require("fs").writeFileSync( + "/tmp/upstream-capabilities-perf.json", + JSON.stringify(report, null, 2) + "\n", + ); + }); +}); diff --git a/test/review-completion-http.test.js b/test/review-completion-http.test.js new file mode 100644 index 0000000..6e1d02f --- /dev/null +++ b/test/review-completion-http.test.js @@ -0,0 +1,348 @@ +require("ts-node/register/transpile-only"); +const { expect } = require("chai"); +const express = require("express"); +const http = require("http"); +const { createHash } = require("crypto"); +const { setTimeout: delay } = require("timers/promises"); +const { + createReviewCompletionService, +} = require("../src/server/service/review-completion-http"); +const { + ReviewConsentError, +} = require("../src/server/service/review-owner-consent"); +const client = "1".repeat(32), + intent = "2".repeat(32), + token = "3".repeat(64); +const exchangePath = "/service/v1/completions/exchange"; +const readPath = `/service/v1/intents/${intent}/receipt`; +const command = () => ({ + contract: "4open.artifacts/1", + clientId: client, + intentId: intent, + code: "4".repeat(64), + requestId: "5".repeat(32), +}); +const receipt = () => ({ + contract: "4open.artifacts/1", + clientId: client, + intentId: intent, + bindingId: "6".repeat(32), + submissionRef: "7".repeat(32), + entitlementId: "8".repeat(32), + policy: { + version: 1, + access: "restricted-review", + retainUntil: "2199-01-01T00:00:00Z", + }, +}); +const config = (scopes = ["completion.exchange", "receipt.read"]) => + JSON.stringify({ + version: 1, + keys: [ + { + clientId: client, + keyId: "current", + tokenSHA256: createHash("sha256").update(token).digest("hex"), + notBefore: "2026-01-01T00:00:00Z", + notAfter: "2027-01-01T00:00:00Z", + scopes, + }, + ], + }); +describe("review completion service HTTP transport", function () { + this.timeout(20000); + let server, backend, calls, clock; + async function start(raw = config(), beforeParser = true) { + const app = express(); + app.set("trust proxy", "loopback"); + if (!beforeParser) app.use(express.json()); + app.use( + createReviewCompletionService( + raw === null ? undefined : raw, + backend, + () => clock, + ), + ); + server = await new Promise((resolve) => { + const s = app.listen(0, "127.0.0.1", () => resolve(s)); + }); + } + beforeEach(async () => { + clock = Date.parse("2026-09-27T12:00:00Z"); + calls = []; + backend = { + exchange: async (id, body) => { + calls.push({ id, body }); + return receipt(); + }, + receipt: async (id) => { + calls.push({ id }); + return receipt(); + }, + }; + }); + afterEach(async () => { + if (server) { + server.closeAllConnections(); + await new Promise((resolve) => server.close(resolve)); + server = undefined; + } + }); + function request(path = exchangePath, options = {}) { + const body = + options.raw === undefined + ? path === exchangePath && options.body !== false + ? JSON.stringify(options.body || command()) + : undefined + : options.raw; + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: "127.0.0.1", + port: server.address().port, + path, + method: options.method || (body === undefined ? "GET" : "POST"), + headers: { + "X-Forwarded-Proto": "https", + "X-4open-Artifact-Client-ID": client, + "X-4open-Artifact-Service-Key-ID": "current", + Authorization: "Bearer " + token, + ...(body === undefined + ? {} + : { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + }), + ...options.headers, + }, + }, + (res) => { + let text = ""; + res.on("data", (chunk) => { + text += chunk; + }); + res.on("end", () => + resolve({ + status: res.statusCode, + headers: res.headers, + body: JSON.parse(text), + }), + ); + }, + ); + req.on("error", reject); + if (body !== undefined) req.write(body); + req.end(); + }); + } + it("requires scoped service credentials and returns the exact client receipt", async () => { + await start(); + const result = await request(); + expect(result.status).equal(200); + expect(result.body).deep.equal(receipt()); + expect(result.headers["cache-control"]).equal("no-store"); + expect(result.headers["referrer-policy"]).equal("no-referrer"); + expect(result.headers["set-cookie"]).equal(undefined); + expect(result.headers["access-control-allow-origin"]).equal(undefined); + expect((await request(readPath)).status).equal(200); + expect(calls).length(2); + }); + it("stays disabled without a dedicated registry", async () => { + await start(null); + expect((await request()).status).equal(404); + expect(calls).length(0); + }); + it("does not permit a read credential to exchange a completion", async () => { + await start(config(["receipt.read"])); + expect((await request()).status).equal(401); + expect((await request(readPath)).status).equal(200); + expect(calls).length(1); + }); + it("rejects capabilities-only configuration and malformed or duplicate registry fields", () => { + const old = JSON.parse(config()); + delete old.keys[0].scopes; + for (const raw of [ + JSON.stringify(old), + config().replace('"version":1', '"version":1,"version":1'), + config().replace('"receipt.read"', '"unknown"'), + "{", + ]) + expect(() => createReviewCompletionService(raw, backend)).to.throw( + "Invalid review exchange credentials", + ); + }); + it("rejects browser context, cleartext, encodings and duplicate authentication headers", async () => { + await start(); + for (const headers of [ + { Cookie: "session=untrusted" }, + { Origin: "https://example.test" }, + { Referer: "https://example.test" }, + { "Sec-Fetch-Site": "none" }, + { "X-Forwarded-Proto": "http" }, + { "Content-Encoding": "gzip" }, + { Authorization: ["Bearer " + token, "Bearer " + token] }, + ]) { + const result = await request(exchangePath, { headers }); + expect(result.status).oneOf([400, 401, 403]); + expect(result.headers.connection).equal("close"); + } + expect(calls).length(0); + }); + it("rejects noncanonical routes, queries, read bodies and wrong methods", async () => { + await start(); + for (const path of [ + exchangePath + "/", + exchangePath + "?x=1", + readPath + "?x=1", + "/service/v1/intents/" + intent.toUpperCase() + "F/receipt", + ]) + expect((await request(path, { method: "POST", raw: "{}" })).status).equal( + 404, + ); + expect( + (await request(exchangePath, { method: "GET", body: false })).status, + ).equal(405); + expect( + (await request(readPath, { method: "GET", raw: "{}" })).status, + ).equal(400); + expect(calls).length(0); + }); + it("rejects duplicate decoded JSON keys, unknown fields, malformed UTF-8 and excessive bodies", async () => { + await start(); + const valid = JSON.stringify(command()); + for (const raw of [ + valid.replace('"code":', '"code":"' + "0".repeat(64) + '","co\\u0064e":'), + JSON.stringify({ ...command(), extra: true }), + valid + "{}", + "[]", + Buffer.from([123, 34, 120, 34, 58, 34, 255, 34, 125]), + " ".repeat(65537), + ]) + expect((await request(exchangePath, { raw })).status).equal(400); + expect(calls).length(0); + }); + it("rejects wrong client scope and unsupported wire versions", async () => { + await start(); + expect( + ( + await request(exchangePath, { + body: { ...command(), clientId: "a".repeat(32) }, + }) + ).status, + ).equal(403); + expect( + ( + await request(exchangePath, { + body: { ...command(), contract: "4open.artifacts/2" }, + }) + ).status, + ).equal(422); + expect(calls).length(0); + }); + it("fails closed if mounted after a JSON parser", async () => { + await start(config(), false); + expect((await request()).status).equal(503); + expect(calls).length(0); + }); + it("rechecks credential expiry after backend work and filters invalid receipts", async () => { + backend.exchange = async () => { + clock = Date.parse("2027-01-01T00:00:00Z"); + return receipt(); + }; + await start(); + expect((await request()).status).equal(401); + clock = Date.parse("2026-09-27T12:00:00Z"); + backend.exchange = async () => ({ ...receipt(), owner: "private-owner" }); + const result = await request(); + expect(result.status).equal(503); + expect(JSON.stringify(result.body)).not.include("private-owner"); + }); + it("uses fixed errors without exposing backend details", async () => { + await start(); + for (const [kind, status] of [ + ["invalid", 400], + ["forbidden", 403], + ["expired", 410], + ["conflict", 409], + ["unavailable", 503], + ]) { + backend.exchange = async () => { + throw new ReviewConsentError(kind); + }; + const result = await request(); + expect(result.status).equal(status); + expect(result.body.requestId).match(/^[a-f0-9]{32}$/); + } + backend.exchange = async () => { + throw new Error("secret database diagnostics"); + }; + const result = await request(); + expect(result.status).equal(503); + expect(JSON.stringify(result.body)).not.include("diagnostics"); + }); + it("limits concurrent operations until the backend settles", async () => { + const pending = []; + backend.exchange = () => new Promise((resolve) => pending.push(resolve)); + await start(); + const requests = Array.from({ length: 4 }, () => request()); + for (let i = 0; i < 100 && pending.length < 4; i++) await delay(5); + expect(pending).length(4); + expect((await request()).status).equal(503); + pending.forEach((resolve) => resolve(receipt())); + expect((await Promise.all(requests)).every((r) => r.status === 200)).equal( + true, + ); + backend.exchange = async () => receipt(); + expect((await request()).status).equal(200); + }); + it("limits client requests and restores capacity in the next minute", async () => { + await start(); + for (let i = 0; i < 60; i++) + expect((await request(readPath)).status).equal(200); + expect((await request(readPath)).status).equal(429); + clock += 60000; + expect((await request(readPath)).status).equal(200); + }); + it("closes unauthorized incomplete uploads before reading their bodies", async () => { + await start(); + const startTime = Date.now(); + const result = await request(exchangePath, { + raw: "{", + headers: { + Authorization: "Bearer " + "0".repeat(64), + "Content-Length": "60000", + }, + }); + expect(result.status).equal(401); + expect(Date.now() - startTime).lessThan(2000); + expect(calls).length(0); + }); + it("bounds authenticated incomplete uploads without invoking the backend", async () => { + await start(); + const result = await request(exchangePath, { + raw: "{", + headers: { "Content-Length": "60000" }, + }); + expect(result.status).equal(503); + expect(calls).length(0); + }); + it('supports overlapping rotation keys with independent validity windows', async () => { + const settings=JSON.parse(config()), nextToken='b'.repeat(64); + settings.keys[0].notAfter='2026-09-27T12:00:01Z'; + settings.keys.push({...settings.keys[0],keyId:'next',tokenSHA256:createHash('sha256').update(nextToken).digest('hex'),notBefore:'2026-09-27T12:00:00Z',notAfter:'2027-01-01T00:00:00Z'}); + await start(JSON.stringify(settings)); + const headers={'X-4open-Artifact-Service-Key-ID':'next',Authorization:'Bearer '+nextToken}; + expect((await request(readPath)).status).equal(200); + expect((await request(readPath,{headers})).status).equal(200); + clock+=1000; expect((await request(readPath)).status).equal(401); + expect((await request(readPath,{headers})).status).equal(200); + clock-=2000; expect((await request(readPath,{headers})).status).equal(401); + }); + it('measures 50 authenticated receipt reads with a synthetic backend', async () => { + await start(); const times=[]; const process=require('process'); + for(let i=0;i<50;i++) { const startTime=process.hrtime.bigint(); expect((await request(readPath)).status).equal(200); times.push(Number(process.hrtime.bigint()-startTime)/1e6); } + times.sort((a,b)=>a-b); + if(process.env.TEST_REVIEW_COMPLETION_HTTP_PERF) require('fs').writeFileSync(process.env.TEST_REVIEW_COMPLETION_HTTP_PERF,JSON.stringify({calls:50,workload:'loopback HTTP with trusted-proxy TLS metadata and synthetic backend, no provider or database',medianMs:times[24],p95Ms:times[47]},null,2)+'\n'); + }); + +}); diff --git a/test/review-consent-http.test.js b/test/review-consent-http.test.js new file mode 100644 index 0000000..f38b040 --- /dev/null +++ b/test/review-consent-http.test.js @@ -0,0 +1,584 @@ +require("ts-node/register/transpile-only"); +const { expect } = require("chai"); +const express = require("express"); +const session = require("express-session"); +const http = require("http"); +const { setImmediate } = require("timers"); +const { randomBytes } = require("crypto"); +const { + createReviewConsentRouter, +} = require("../src/server/service/review-consent-http"); +const { createReviewCallbacks } = require("../src/server/service/review-callbacks"); +const callbackURL = "https://review.example.test/api/v1/artifacts/callback"; +const callbacks = createReviewCallbacks(JSON.stringify({version:1,callbacks:[{clientId:"1".repeat(32),callbackId:"6".repeat(32),url:callbackURL}]})); +const origin = "https://anonymous.example.test"; +const owner = "a".repeat(24); +const intent = { + contract: "4open.artifacts/1", + clientId: "1".repeat(32), + intentId: "2".repeat(32), + token: "3".repeat(64), + requestId: "4".repeat(32), +}; +const previewInput = () => ({ repositoryId: "synthetic-repository", intent }); +const confirmInput = () => ({ + ticket: "synthetic-signed-ticket", + requestId: "5".repeat(32), + acceptAccess: true, + acceptRetention: true, +}); +const policy = { + version: 1, + access: "restricted-review", + retainUntil: "2099-01-01T00:00:00Z", +}; + +describe("review consent browser HTTP transport", function () { + this.timeout(20000); + let server, store, cookie, sid, csrf, backend, calls; + function request(path, options = {}) { + const body = + options.raw === undefined + ? options.body === undefined + ? undefined + : JSON.stringify(options.body) + : options.raw; + return new Promise((resolve, reject) => { + const req = http.request( + { + host: "127.0.0.1", + port: server.address().port, + path, + method: options.method || (body === undefined ? "GET" : "POST"), + headers: { + "X-Forwarded-Proto": "https", + ...(cookie ? { Cookie: cookie } : {}), + ...(body === undefined + ? {} + : { + Origin: origin, + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + "X-Review-CSRF": csrf || "", + }), + ...options.headers, + }, + }, + (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => { + const raw = Buffer.concat(chunks).toString(); + resolve({ + status: res.statusCode, + headers: res.headers, + body: raw ? JSON.parse(raw) : null, + }); + }); + }, + ); + req.on("error", reject); + if (body !== undefined) req.write(body); + req.end(); + }); + } + beforeEach(async () => { + cookie = ""; + sid = ""; + csrf = ""; + calls = []; + store = new session.MemoryStore(); + backend = { + preview: async (actor, repository, input) => { + calls.push({ actor, repository, input }); + return { + ticket: "synthetic-signed-ticket", + repositoryName: repository, + policy, + expiresAt: "2098-01-01T00:00:00Z", + }; + }, + confirm: async (actor, ticket, input) => { + calls.push({ actor, ticket, input }); + return { + clientId: intent.clientId, + intentId: intent.intentId, + accountId: actor.accountId, + repositoryId: "b".repeat(24), + role: "owner", + requestId: input.requestId, + policy, + confirmedAt: "2026-01-01T00:00:00Z", + }; + }, + }; + backend.handoff = async (actor, ticket, resolveCallback) => { + calls.push({actor,ticket,handoff:true}); + return { callbackUrl: resolveCallback(intent.clientId,"6".repeat(32)), completion: { contract: intent.contract, clientId: intent.clientId, intentId: intent.intentId, code: "9".repeat(64), expiresAt: new Date(Date.now()+60000).toISOString().replace(/\.\d{3}Z$/, "Z"), privateField: "private-field" }, privateField: "private-field" }; + }; + const app = express(); + // Model a TLS-terminating trusted loopback proxy, never trust arbitrary peers. + app.set("trust proxy", "loopback"); + app.use( + session({ + secret: "synthetic-local-session-secret", + resave: false, + saveUninitialized: false, + store, + }), + ); + // Test-only login stands in for Passport's existing authenticated session. + app.get("/test-login", (req, res) => { + req.session.passport = { user: owner }; + req.session.save((error) => + error ? res.sendStatus(500) : res.json({ sid: req.sessionID }), + ); + }); + app.use((req, _res, next) => { + const id = req.session.passport?.user; + if (id) req.user = { user: { _id: id } }; + req.isAuthenticated = () => !!id; + next(); + }); + app.use( + "/api/review-consent", + createReviewConsentRouter(origin, backend, randomBytes(32), callbacks), + ); + app.use("/api/review-consent-disabled", createReviewConsentRouter(origin, backend, randomBytes(32))); + server = http.createServer(app); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const login = await request("/test-login"); + cookie = login.headers["set-cookie"][0].split(";")[0]; + sid = login.body.sid; + const issued = await request("/api/review-consent/csrf"); + expect(issued.status).to.equal(200); + csrf = issued.body.csrf; + }); + afterEach(async () => { + server.closeAllConnections(); + await new Promise((resolve) => server.close(resolve)); + store.clear(); + }); + it("binds CSRF to the current Passport session and omits owner identifiers from confirmation responses", async () => { + const preview = await request("/api/review-consent/preview", { + body: previewInput(), + }); + expect(preview.status).to.equal(200); + expect(calls[0].actor).to.deep.equal({ accountId: owner, sessionId: sid }); + expect(calls[0].input).to.deep.equal(intent); + expect(preview.headers["cache-control"]).to.equal("no-store"); + expect(preview.headers["referrer-policy"]).to.equal("no-referrer"); + expect(preview.headers["access-control-allow-origin"]).to.equal(undefined); + const result = await request("/api/review-consent/confirm", { + body: confirmInput(), + }); + expect(result.status).to.equal(200); + expect(result.body).to.deep.equal({ + requestId: "5".repeat(32), + policy, + confirmedAt: "2026-01-01T00:00:00Z", + }); + expect(JSON.stringify(result.body)).not.to.include(owner); + }); + it("rejects insecure, cross-origin, bearer and missing-CSRF requests before backend work", async () => { + for (const headers of [ + { "X-Forwarded-Proto": "http" }, + { Origin: "https://attacker.example" }, + { Origin: "null" }, + { Authorization: "Bearer synthetic" }, + { "X-Review-CSRF": "0".repeat(64) }, + { "X-Review-CSRF": "" }, + { "Sec-Fetch-Site": "same-site" }, + { "Content-Encoding": "gzip" }, + ]) + expect( + ( + await request("/api/review-consent/preview", { + body: previewInput(), + headers, + }) + ).status, + ).to.equal(403); + expect( + ( + await request("/api/review-consent/preview", { + body: previewInput(), + headers: { Cookie: "" }, + }) + ).status, + ).to.equal(401); + expect(calls).to.have.length(0); + }); + it("accepts only bounded JSON with the exact operation fields and explicit acceptance", async () => { + for (const input of [ + { ...previewInput(), accountId: owner }, + { ...previewInput(), policy }, + { ...previewInput(), repositoryId: "../other" }, + { ...previewInput(), intent: { ...intent, requestId: "bad" } }, + ]) + expect( + (await request("/api/review-consent/preview", { body: input })).status, + ).to.equal(422); + for (const input of [ + { ...confirmInput(), acceptAccess: false }, + { ...confirmInput(), acceptRetention: "true" }, + { ...confirmInput(), policy }, + ]) + expect( + (await request("/api/review-consent/confirm", { body: input })).status, + ).to.equal(422); + expect( + (await request("/api/review-consent/preview", { raw: "{" })).status, + ).to.equal(400); + expect( + (await request("/api/review-consent/preview", { raw: " ".repeat(13000) })) + .status, + ).to.equal(413); + expect( + ( + await request("/api/review-consent/preview", { + body: previewInput(), + headers: { "Content-Type": "text/plain" }, + }) + ).status, + ).to.equal(415); + for (const path of ["/csrf?x=1", "/csrf/", "/CSRF", "/%63srf", "/other"]) + expect((await request("/api/review-consent" + path)).status).to.equal( + 404, + ); + expect((await request("/api/review-consent/preview")).status).to.equal(405); + expect( + (await request("/api/review-consent/csrf", { method: "GET", body: {} })) + .status, + ).to.equal(400); + expect(calls).to.have.length(0); + }); + it("rejects a token copied from another session of the same account", async () => { + const old = csrf; + cookie = ""; + const login = await request("/test-login"); + cookie = login.headers["set-cookie"][0].split(";")[0]; + expect( + ( + await request("/api/review-consent/preview", { + body: previewInput(), + headers: { "X-Review-CSRF": old }, + }) + ).status, + ).to.equal(403); + expect(calls).to.have.length(0); + }); + for (const operation of ["preview", "confirm", "handoff"]) { + for (const change of ["logout", "account replacement"]) { + it(`rejects ${operation} before backend work after ${change} during upload`, async () => { + const original = store.get.bind(store); + let reads = 0, ready; + const authenticated = new Promise((resolve) => { ready = resolve; }); + store.get = (id, callback) => original(id, (error, value) => { + callback(error, value); + // The first read restores the session; the second authenticates + // the request before the JSON parser waits for the remaining body. + if (++reads === 2) setImmediate(ready); + }); + const body = JSON.stringify(operation === "preview" ? previewInput() : operation === "handoff" ? { ticket: "synthetic-signed-ticket" } : confirmInput()); + let upload; + const response = new Promise((resolve, reject) => { + upload = http.request({ + host: "127.0.0.1", port: server.address().port, + path: "/api/review-consent/" + operation, method: "POST", + headers: { + "X-Forwarded-Proto": "https", Cookie: cookie, Origin: origin, + "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), + "X-Review-CSRF": csrf, + }, + }, (res) => { + res.resume(); + res.on("end", () => resolve(res.statusCode)); + }); + upload.on("error", reject); + upload.write(body.slice(0, 1)); + }); + try { + await authenticated; + if (change === "logout") { + await new Promise((resolve) => store.destroy(sid, resolve)); + } else { + const saved = await new Promise((resolve) => original(sid, (_error, value) => resolve(value))); + saved.passport.user = "b".repeat(24); + await new Promise((resolve) => store.set(sid, saved, resolve)); + } + upload.end(body.slice(1)); + expect(await response).to.equal(401); + expect(calls).to.have.length(0); + } finally { + upload.destroy(); + store.get = original; + } + }); + } + } + it("reloads session authority before returning a pending policy preview", async () => { + let arrived, release; + const ready = new Promise((resolve) => { + arrived = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + backend.preview = async () => { + arrived(); + await gate; + return { + ticket: "private-policy-preview", + policy, + repositoryName: "synthetic-repository", + expiresAt: "2098-01-01T00:00:00Z", + }; + }; + const pending = request("/api/review-consent/preview", { + body: previewInput(), + }); + await ready; + await new Promise((resolve) => store.destroy(sid, resolve)); + release(); + const result = await pending; + expect(result.status).to.equal(401); + expect(JSON.stringify(result.body)).not.to.include( + "private-policy-preview", + ); + }); + it("never rewrites or resurrects a session when a CSRF read races with logout", async () => { + const original = store.get.bind(store); + let reads = 0, + arrived, + release; + const ready = new Promise((resolve) => { + arrived = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + store.get = (id, callback) => + original(id, (error, value) => { + if (++reads === 3) { + arrived(); + gate.then(() => callback(error, value)); + } else callback(error, value); + }); + let writes = 0; + const set = store.set.bind(store); + store.set = (...args) => { + writes++; + return set(...args); + }; + const pending = request("/api/review-consent/csrf"); + await ready; + await new Promise((resolve) => store.destroy(sid, resolve)); + release(); + await pending; + expect(writes).to.equal(0); + expect( + await new Promise((resolve) => + original(sid, (_error, value) => resolve(value)), + ), + ).to.equal(undefined); + expect( + (await request("/api/review-consent/preview", { body: previewInput() })) + .status, + ).to.equal(401); + expect(calls).to.have.length(0); + }); + it("returns fixed failure codes without exposing provider or database messages", async () => { + for (const [kind, status] of [ + ["forbidden", 403], + ["conflict", 409], + ["expired", 410], + ["invalid", 422], + ["unavailable", 503], + [undefined, 503], + ]) { + backend.preview = async () => { + const error = new Error("private credential and database details"); + error.kind = kind; + throw error; + }; + const result = await request("/api/review-consent/preview", { + body: previewInput(), + }); + expect(result.status).to.equal(status); + expect(JSON.stringify(result.body)).not.to.include("private"); + } + }); + it("bounds authenticated request rates without accepting a different client identity", async () => { + for (let n = 0; n < 59; n++) + expect((await request("/api/review-consent/csrf")).status).to.equal(200); + const response = await request("/api/review-consent/csrf"); + expect(response.status).to.equal(429); + expect(response.headers["retry-after"]).to.equal("60"); + expect(calls).to.have.length(0); + }); + it("closes an unauthorized incomplete upload without waiting for its declared body", async () => { + const started = Date.now(); + let pending; + try { + const result = await new Promise((resolve, reject) => { + pending = http.request( + { + host: "127.0.0.1", + port: server.address().port, + path: "/api/review-consent/preview", + method: "POST", + headers: { + "X-Forwarded-Proto": "https", + Origin: origin, + "Content-Type": "application/json", + "Content-Length": 10000000, + }, + }, + (response) => { + response.resume(); + response.on("end", () => + resolve({ + status: response.statusCode, + connection: response.headers.connection, + }), + ); + }, + ); + pending.on("error", reject); + pending.write("{"); + }); + expect(result).to.deep.equal({ status: 401, connection: "close" }); + expect(Date.now() - started).to.be.lessThan(2000); + expect(calls).to.have.length(0); + } finally { + pending?.destroy(); + } + }); + it("cancels a pending preview when its browser connection closes", async () => { + let entered, aborted; + const ready = new Promise((resolve) => { + entered = resolve; + }); + const cancelled = new Promise((resolve) => { + aborted = resolve; + }); + backend.preview = async (_actor, _repo, _input, signal) => { + entered(); + await new Promise((resolve) => + signal.addEventListener("abort", resolve, { once: true }), + ); + aborted(); + throw new Error("private pending request cancelled"); + }; + const body = JSON.stringify(previewInput()); + const pending = http.request({ + host: "127.0.0.1", + port: server.address().port, + path: "/api/review-consent/preview", + method: "POST", + headers: { + Cookie: cookie, + "X-Forwarded-Proto": "https", + Origin: origin, + "Content-Type": "application/json", + "X-Review-CSRF": csrf, + "Content-Length": Buffer.byteLength(body), + }, + }); + pending.on("error", () => undefined); + pending.end(body); + await ready; + pending.destroy(); + await cancelled; + }); + it("expires an authenticated incomplete upload at the total request deadline", async () => { + const started = Date.now(); + let pending; + try { + const status = await new Promise((resolve) => { + pending = http.request( + { + host: "127.0.0.1", + port: server.address().port, + path: "/api/review-consent/preview", + method: "POST", + headers: { + Cookie: cookie, + "X-Forwarded-Proto": "https", + Origin: origin, + "Content-Type": "application/json", + "Content-Length": 1000, + "X-Review-CSRF": csrf, + }, + }, + (response) => { + response.resume(); + response.on("end", () => resolve(response.statusCode)); + }, + ); + pending.on("error", () => resolve("closed")); + pending.write("{"); + }); + expect([408, "closed"]).to.include(status); + expect(Date.now() - started).to.be.within(14000, 19000); + expect(calls).to.have.length(0); + } finally { + pending?.destroy(); + } + }); + it("measures authenticated HTTP preview overhead when explicitly requested", async function () { + const process = require("process"); + if (!process.env.TEST_REVIEW_CONSENT_HTTP_PERF) this.skip(); + const times = []; + for (let n = 0; n < 50; n++) { + const start = process.hrtime.bigint(); + expect( + (await request("/api/review-consent/preview", { body: previewInput() })) + .status, + ).to.equal(200); + times.push(Number(process.hrtime.bigint() - start) / 1e6); + } + times.sort((a, b) => a - b); + expect(calls).to.have.length(50); + require("fs").writeFileSync( + process.env.TEST_REVIEW_CONSENT_HTTP_PERF, + JSON.stringify( + { + calls: times.length, + transport: "loopback HTTP with synthetic trusted-proxy TLS metadata", + sessionStore: "in-memory test store", + backend: "synthetic response, no MongoDB or provider timing", + meanMs: times.reduce((a, b) => a + b, 0) / times.length, + medianMs: times[25], + p95Ms: times[47], + }, + null, + 2, + ) + "\n", + ); + }); + it('offers only a configured, current-session completion POST handoff', async function () { + expect((await request('/api/review-consent/csrf')).body.handoffEnabled).equal(true); + const response=await request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket'}}); + expect(response.status).equal(200);expect(response.body.callbackUrl).equal(callbackURL); + expect(response.body.completion.code).equal('9'.repeat(64)); + expect(Object.keys(response.body.completion).sort()).deep.equal(['clientId','code','contract','expiresAt','intentId']); + expect(JSON.stringify(response.body)).not.include('private-field'); + expect(response.headers['cache-control']).equal('no-store'); + expect((await request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket',callbackUrl:'https://other.example.test/callback'}})).status).equal(400); + expect((await request('/api/review-consent-disabled/csrf')).body.handoffEnabled).equal(false); + expect((await request('/api/review-consent-disabled/handoff',{body:{ticket:'synthetic-signed-ticket'}})).status).equal(404); + }); + it('does not disclose a completion code after concurrent logout', async function () { + let entered, release; + const pending=new Promise(resolve=>{entered=resolve;}); + const wait=new Promise(resolve=>{release=resolve;}); + const original=backend.handoff; + backend.handoff=async(...args)=>{entered();await wait;return original(...args);}; + const response=request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket'}}); + await pending;await new Promise(resolve=>store.destroy(sid,resolve));release(); + const result=await response;expect(result.status).equal(401);expect(JSON.stringify(result.body)).not.include('9'.repeat(64)); + }); + +}); diff --git a/test/review-consent-page.test.js b/test/review-consent-page.test.js new file mode 100644 index 0000000..2dffd37 --- /dev/null +++ b/test/review-consent-page.test.js @@ -0,0 +1,127 @@ +require("ts-node/register/transpile-only"); +const { expect } = require("chai"); +const express = require("express"); +const http = require("http"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { + createReviewConsentPage, + isReviewConsentPagePath, +} = require("../src/server/review-consent-page"); + +describe("isolated review consent page", function () { + let server, directory, manifest; + beforeEach(async function () { + directory = fs.mkdtempSync(path.join(os.tmpdir(), "review-page-")); + manifest = path.join(directory, "manifest.json"); + const app = express(); + const page = createReviewConsentPage(manifest); + app.use((req, res) => + isReviewConsentPagePath(req.path) + ? page(req, res) + : res.status(418).end(), + ); + server = await new Promise((resolve) => { + const s = app.listen(0, "127.0.0.1", () => resolve(s)); + }); + }); + afterEach(async function () { + await new Promise((resolve) => server.close(resolve)); + fs.rmSync(directory, { recursive: true, force: true }); + }); + function get(url, method = "GET") { + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: "127.0.0.1", + port: server.address().port, + path: url, + method, + }, + (res) => { + let body = ""; + res.on("data", (chunk) => { + body += chunk; + }); + res.on("end", () => + resolve({ status: res.statusCode, headers: res.headers, body }), + ); + }, + ); + req.on("error", reject); + req.end(); + }); + } + it("serves only first-party assets with no inline scripts or external frames", async function () { + const response = await get("/review-link"); + expect(response.status).equal(200); + expect(response.headers["cache-control"]).equal("no-store"); + expect(response.headers["referrer-policy"]).equal("no-referrer"); + expect(response.headers["x-frame-options"]).equal("DENY"); + expect(response.headers["content-security-policy"]) + .include("script-src 'self'") + .include("frame-ancestors 'none'"); + expect(response.body).include('name="review-consent-page"'); + expect(response.body).not.match(/https?:|kofi|ko-fi|/i); + expect(response.body.match(/