diff --git a/alembic/versions/o6i7j8k9l012_add_series_rescan_job_type.py b/alembic/versions/o6i7j8k9l012_add_series_rescan_job_type.py new file mode 100644 index 00000000..bdc26054 --- /dev/null +++ b/alembic/versions/o6i7j8k9l012_add_series_rescan_job_type.py @@ -0,0 +1,40 @@ +"""Allow durable per-series rescan jobs in the existing utility queue.""" + +from alembic import op +from sqlalchemy import text + +revision = "o6i7j8k9l012" +down_revision = "n5h6i7j8k901" +branch_labels = None +depends_on = None + +_OLD_TYPES = ( + "file_convert", + "mass_convert_pipeline", + "mass_rename", + "db_check_cleanup", + "export_library", + "integrity_check", + "library_permissions", + "rollback", +) + + +def _replace(values: tuple[str, ...]) -> None: + with op.batch_alter_table("utility_jobs") as batch: + batch.drop_constraint("ck_utility_jobs_job_type", type_="check") + batch.create_check_constraint( + "ck_utility_jobs_job_type", "job_type IN (" + ", ".join(repr(v) for v in values) + ")" + ) + + +def upgrade() -> None: + _replace((*_OLD_TYPES, "series_rescan")) + + +def downgrade() -> None: + if op.get_bind().scalar( + text("SELECT count(*) FROM utility_jobs WHERE job_type = 'series_rescan'") + ): + raise RuntimeError("Remove saved series rescan jobs before downgrading this migration.") + _replace(_OLD_TYPES) diff --git a/docs/development/IMPORT_REVIEW_RECOVERY.md b/docs/development/IMPORT_REVIEW_RECOVERY.md index 77943e6d..a011dc08 100644 --- a/docs/development/IMPORT_REVIEW_RECOVERY.md +++ b/docs/development/IMPORT_REVIEW_RECOVERY.md @@ -34,6 +34,34 @@ recovery is available in Import Follow-up and does not require an offline command. It works for Mylar and folder imports. This is not a full rescan, a database restore, or an import. +## Mylar Inventory And Missing References + +Copy and keep-in-place scans retain the same Mylar file inventory, including +recorded filenames that no longer exist. Missing-only annual groups retain their +own series identity; a missing annual must not disappear in copy mode or become +an ordinary numbered issue. Keep-in-place reference-root checks still apply +independently of discovery. + +Steps 2 and 3 report available file records separately from missing references. +Missing references remain visible in review (in Info when no other issue needs +attention), but are not counted as available comics, archive safety decisions, +or unsettled file-review work. Durable +`scan_total_files` and `files_total` remain record totals for compatibility. +`files_present` and `files_missing_references` expose the split. Mylar batch +diagnostics also record the handling mode and reconciled-reference count. + +A renamed source can replace a stale Mylar path only when one same-folder file +has an independent, agreeing ComicInfo issue ID. Reconciliation checks the +issue publication year, not ComicInfo.Volume's series start year. A Mylar +ordinary-issue row may represent a collected volume only when its saved +filename explicitly identifies a volume. Conflicting IDs, numbers, publication +years, annual types, and competing copies remain unresolved. Source signature, +root boundary, archive safety, and manual-review protections still apply. + +The shared checks cover fresh Mylar discovery and saved-review reconciliation. +They do not modify Mylar, rename source files, or change completed-import +recovery and ownership rules. No metadata-provider requests are introduced. + ## Import Follow-up The Follow-up tab groups actionable work by import job rather than rendering diff --git a/docs/development/IMPORT_REVIEW_REDESIGN.md b/docs/development/IMPORT_REVIEW_REDESIGN.md new file mode 100644 index 00000000..75d7a325 --- /dev/null +++ b/docs/development/IMPORT_REVIEW_REDESIGN.md @@ -0,0 +1,322 @@ +# Import Review Redesign + +Status: prototype v2 presentation implemented; release validation remains required. + +## Approved V2 Contract + +The revised clickable prototype is the Step 3 presentation and interaction target. +Its simulated counts, provider results and mutations are not implementation guidance. + +- Restore the compact status header, attention cards, lane descriptions, Why links + and contextual action menus. Do not restore the old generic More-details dump. +- Match Step 3 and its dialogs while keeping the app frame, theme tokens, + accessibility, and pagination in the fixed footer. Steps 4/5 and Follow-up retain + their existing behavior. +- Keep resolved children visible with their saved Allowed, Matched or Skipped + outcome while their group still has work. Preserve expansion, focus and scroll. +- Label scope explicitly: Skip series excludes a series; Skip file excludes a file. + Neither action modifies the source library. +- Fit root, resource-limit, mixed-folder and legacy cases into the same compact + lanes/details; do not invent additional top-level dashboard cards. +- Derive counts and progress from persisted outcomes, including partial-series + readiness. Never turn a series choice into an assumed match for all its files. + +### Screen And Action Checklist + +- Status header: readiness, saved review progress, open/blocked counts, Back, + Cancel and import gate; compact attention cards navigate to the corresponding lane. +- One table surface: keyboard-accessible lane rail, descriptions and underlined + reason filters; Ready-only selection controls; fixed footer pagination. +- Problem rows: series/folder identity, matched/total files, concise reason, primary + action, contextual menu and stable disclosure. Ready rows use selection, actual + ComicVine identity and confidence, with individual file selection still available. +- Expanded decisions: series candidates, issue assignment, same-comic choices, + duplicate keepers, one-page/large-file decisions, source fixes and stale references. +- Dialogs: import-now/stays-behind summary, source-preserving cancel/skip, + validated ComicVine search and signed source verification. +- Regression evidence: action scopes, source preservation, actual counts, stable + DOM/focus/scroll, both browsers, themes, mobile and paginated large jobs. + +Completion and verification of this checklist must be recorded separately from +the historical evidence below. + +### V2 Implementation Notes + +- The compact header uses persisted settled/open file counts, not simulated + progress. Safety summaries count affected series as well as files, including + failures recorded through source revalidation. +- Ready and problem rows have different, purpose-specific columns. The fixed + footer retains collection pagination. Explicit series skips are reversible in + Info, preserve all file evidence, and never restore selection automatically. +- Issue choices load inline only when requested. They are searchable and paged + in groups of 25 through the existing verified assignment service. No confidence + percentage is invented when the provider has not supplied a scored candidate. +- Duplicate choices use radio controls and an explicit keeper action. Only a + unique saved recommendation can power a suggested-keeper shortcut. The bulk + shortcut explicitly says "on this page" and deduplicates shared conflict groups. + Detected archive formats, page counts, and saved preference reasons explain + the choices without exposing a generic diagnostic dump. +- One-page archive groups retain completed child outcomes while other files in + that group remain unresolved. File decisions and series decisions remain + separate scopes. Reassigning to another series still uses the existing target + ownership and collision checks rather than merely marking a row resolved. +- One-page series rows have Allow All / Skip All, scoped to their pending one-page + files, and no general series menu. Individual files have View File / Allow / Skip. + Allow retains its import-scoped safety exception; it never disables global checks. + View File reuses the comic reader in read-only preview mode. It checks saved + source boundaries and scan identity, retains reader resource limits, and displays + unreadable-file errors without approving, importing, or marking a comic read. + Closing the reader preserves the expanded review row and restores button focus. +- Source repair and stale-reference pairing keep their signed previews and + durable background verification. The prototype's unrestricted bulk recheck, + mock replacement candidates, and simulated completion are not copied. +- Legacy root and Story Arc requirements remain available. Steps 4/5, + completed-job Follow-up, and recovery execution are not redesigned here. + +### V2 Verification + +September 15 local evidence (these suites overlap; do not add their counts): + +- Broader import review, safety, matching and recovery regression run: 407 passed. +- Story Arc review and import-confirmation policy checks: 34 passed. +- Final affected decision, file-detail, projection and summary checks: 83 passed. +- Chromium and Firefox workspace checks: 8 passed. They cover stable DOM and + expansion, scoped inline assignment, modal focus, contextual menus, both + explicit themes, mobile overflow, and accessibility checks on review controls. +- The read-only synthetic projection loaded 20,000 series / 120,000 file records + in 0.516 seconds, with a 25-series page and 150 file details. This measures saved + review-state loading, not scan or import throughput. +- Focused lint, formatting, strict types for the seven affected source modules, + JavaScript syntax, stylesheet generation and whitespace checks passed. +- The isolated port-8785 lab was updated and inspected without applying decisions + to its current review job or resetting source fixtures. The primary development + instance was not changed. + +Full CI, production-style recovery replay and user acceptance remain release gates. + +One-page preview follow-up: 171 focused import/reader route and regression checks +passed, plus 10 Chromium/Firefox workspace checks. Real-file preview was verified +on the isolated 8785 lab without applying an import decision. Corrupt archive/image, +source-change, unsafe-path, authentication and read-only Mylar-root cases are covered. + +### Post-Release Identity And Reporting Follow-Up + +September 16 changes preserve the existing import execution and completed-job +recovery paths: + +- Dated filenames now inform copy-group identity instead of relying only on + inherited Mylar series titles and start years. Different titles or publication + years require a decision, not a suggested keeper. Annual copies with the same + publication year, identical content hashes, and trailing release-site labels + retain the existing duplicate behavior. Undated ambiguous filenames retain + their saved identity rather than treating copy counters as series names. +- The shared file matcher protects a narrower case: an extended file title such + as `New Avengers Finale` cannot silently match the shorter parent title when + ComicInfo or strong archive-page-name evidence corroborates the extension. + The parent series stays identified. This is not a replacement for alternate + title, annual/special, or collected-volume matching, and adds no provider calls. +- Every competing copy offers individual reassignment, even when Pullbox suggests + a keeper. Existing issue ownership checks and source-preservation rules remain + authoritative. Existing jobs are interpreted without rewriting them on display. +- Reason filters explicitly count series. Conflict descriptions distinguish + issue groups, files, and affected series. Files marked unmatched while scan + rows are created now emit the same detail event as files rejected later during + matching, for both Mylar and folder-layout decisions. +- The folder label uses the standard secondary text token so it remains readable + on a hovered light-theme row. + +Local verification (overlapping suites are not additive): + +- Import unit and UI regression suite: 2,147 passed. +- Chromium/Firefox workspace suite: 14 passed, including stable review DOM, + file-scoped reassignment, and light/dark accessibility checks. Copy-choice + screenshots were also inspected. +- Read-only replay of all 7,671 matched/conflicting records in the supplied first + scan flagged only the two corroborated New Avengers mismatches with the new + title guard. Its nine saved conflict groups classify as five copy choices, + two title disagreements, and two year disagreements. No database writes, + archive reads, provider requests, or recovery actions were performed. +- Synthetic review projection: 20,000 series / 120,000 saved files in 0.551 seconds. + This measures review loading, not scan/import throughput. +- Focused lint, formatting, strict source types, stylesheet generation and + whitespace checks passed. Full CI has not been rerun for this follow-up. + +The supplied diagnostic contains only the first scan (630 series / 7,721 files). +The reported second scan (632 / 7,952) requires its own diagnostic before a +cause or scan-mode parity fix can be confirmed. No count reconciliation or source +mode changes were inferred from the first package alone. + +## Boundary + +Step 3 becomes a decision-oriented review without replacing the import engine. +The established matching, source ownership, safety, and recovery rules remain +authoritative. The design prototype is a presentation reference, not permission +to replace those rules with weaker heuristics. + +This redesign does not migrate the database or rewrite saved jobs on page load. +It does not change Step 4 execution, Step 5 receipts, job-specific Follow-up, +completed-import recovery, library organization, or Story Arc handling. + +## Implemented + +- Six mutually exclusive series lanes: Needs a decision, Review suggestions, + Fix source, Cannot import, Ready, and Info. Secondary reasons remain attached + to the same row; readiness is counted separately from unresolved files. +- Compact review table with labeled actions, inline candidate acceptance, + immediate duplicate-copy decisions, issue reconciliation, signed safety + previews, and existing individual duplicate-series file selection. +- The import gate reports selected files and remaining follow-up using canonical + saved selection state rather than browser storage. +- Matched files in a series can be selected even when other files in that series + remain in conflict. Existing confirmation still imports only eligible files. +- Conflict suggestions distinguish differing parsed titles or years from copies + of the same comic. Differing identities have no recommended keeper. Older + saved groups are interpreted without rewriting their diagnostics. +- Missing-source references are presented as information when the saved reason + proves absence, while unreadable roots and unsafe content remain separate. +- Review updates retain the root controller, header controls, focus, expansion, + and scroll position. Candidate rematching uses the existing completion poll. +- Source details, matched issue IDs, Mylar diagnostics, and safety codes remain + available in expandable details. + +## Completed Decision Paths + +1. Ready files have individual persisted checkboxes for both new and duplicate + series. An explicit diagnostics marker preserves legacy matched-file defaults. + Confirmation imports only selected files. Excluded files retain target evidence + in Follow-up, where a subsequent explicit assignment re-enables their import. +2. Mixed-comic conflicts offer Find correct issue for each file. Series search + leads to a shared issue dropdown. The provider's issue list validates ownership; + exact embedded identity cannot be contradicted. Reassignment changes only the + staged file target, not source files or sibling choices. A collision creates a + copy decision instead of silently selecting a keeper or overwriting ownership. +3. Recheck source queues one durable background inspection. It keeps the existing + approved-root and safety rules, revalidates the saved review, and resumes across + a restart between inspection and matching. Errors terminate progress and leave + evidence for another decision. Unknown/dangerous content cannot be overridden. +4. A stale Mylar reference can be paired with one independently identified actual + file in the same folder. Signed previews bind actor, file and candidate scope. + Verification requires exact IDs, absence of the old path, an unchanged actual + file and fresh safety checks. Resolving the redundant reference is recorded as + verified pairing, not a guessed match or an unlabeled Skip. Sources stay intact. +5. Archive reading, conversion and safety use shared bounded header detection. + ZIP content under a CBR filename is supported without bypassing member/path, + payload, link, resource-limit or minimum-page checks. Detected mismatches are + shown unobtrusively beside the file. Unknown formats retain normal parser errors. +6. Existing Follow-up actions use Find series, Match issues and Review suggestions + labels. Completed-import retries, mixed-folder recovery, library organization, + ownership receipts and Story Arc recovery remain the established workflows. + +The prototype guides hierarchy and interaction, not weaker metadata or safety +policy. Repair actions are deliberately scoped, not an indiscriminate Fix all. +Preserved Step 5 and completed-recovery surfaces remain part of acceptance testing. + +## Regression Gates + +- Mylar and folder imports retain the same applicable identity and safety rules. +- Partial series import only matched files; conflicting, missing, unsafe, and + unresolved files retain their evidence and remain recoverable. +- No source writes, moves, deletions, scans, or provider calls on review page loads. + Explicit series/issue search previews can fetch metadata but never inspect files. +- No downgrade of exact ComicVine identity or target-summary requirements. +- Existing completed jobs, retries, mixed-folder recovery, duplicate handling, + in-place references, and Story Arc recovery remain usable. +- Page loads use compact job-level aggregates and page-scoped file details, not + full archive or provider inspection. +- Browser tests cover stable controls, expansion, modal focus, selection, + pagination, and both explicit themes in Chromium and Firefox. +- Full CI and copied diagnostic-database comparison are required before release. + +## Repeatable Scale Check + +`tests/ui/test_import_review_projection_scale.py` creates saved review rows with +nonexistent source paths to verify that context loading is read-only and does +not inspect archives. It checks a 25-series page with 150 file details. + +```sh +PULLBOX_REVIEW_BENCHMARK_SERIES=20000 .venv/bin/pytest \ + tests/ui/test_import_review_projection_scale.py -q -s --no-cov +``` + +Initial local measurement: 20,000 series / 120,000 files, 0.499 seconds for review +context loading. This is a synthetic saved-state benchmark, not a claim about +scan/import throughput or full browser rendering on a production database. + +## Lettered Issue Identity Follow-Up + +Issue-number suffixes are identities, not disposable formatting. `13`, `13A`, +`13B`, and `13C` remain distinct; case and zero padding normalize (`013a` becomes +`13A`). The existing numeric issue column remains the compatibility/sort value. + +- Folder and Mylar discovery preserve the selected exact issue designation. +- New-series issue indexes, provisional targets, targeted metadata requests, + per-job caches, and persistent caches retain suffixes without adding per-file + metadata requests. Plain numeric cache keys remain compatible. +- Manual and automatic search use the same exact-number validation for Usenet, + torrents, Direct providers, and AirDC++. Artifact coverage cannot substitute + another suffix or the unsuffixed issue. Trusted issue-ID precedence and existing + collection/title matching remain unchanged. +- Library, issue detail, search, download, intervention, and import-choice labels + show the exact issue number where catalog identity is available. + +Regression coverage includes Gen13 `13A`/`13B`/`13C`, `50X`/`50O`, `0.5`, and +`-1`, including exact download handoff and source-preserving Mylar discovery. +These changes do not migrate the schema, rewrite existing library files, or +silently reassign previously imported issues. A reporter's original failed +download still needs verification with its actual release title and logs. + +## Checkpoint Evidence + +The initial checkpoint is preserved in commit `cfe8ee7`. The evidence in this +section predates the remaining decision paths and is not their validation record. + +- Import-focused unit suite: 1,932 passed. +- Import UI, API, and integration suites: 627 passed. These include the review + projection, signed safety approvals, Story Arc review, and completed-import + recovery. Existing safety-path redaction is also covered in the new lanes. +- Existing review browser workflows: 34 passed across Chromium and Firefox. +- New stable-review browser scenario: passed in Chromium and Firefox, including + scoped accessibility checks, modal focus, persisted controls, selection state, + both explicit themes, and a 390-pixel-wide import gate. This is not an all-pages + accessibility audit. +- Strict type checking, focused lint, JavaScript syntax, generated stylesheet + build, and whitespace checks passed. Full CI has not been run. + +A private, temporary copy of a supplied September 14 diagnostic database was +opened with SQLite `mode=ro`. The new projection classified 21,569 series and +184,190 saved file records in 1.129 seconds. Saved series/file status counts were +identical before and after, and the session had no pending changes. The original +diagnostic files were not opened for writes. + +That completed/failed historical job grouped as 20,691 Info, 770 Needs a decision, +73 Ready, and 35 Fix source rows. These are display classifications of saved +records, not newly imported files or a claim that those rows can be retried +without the existing recovery checks. No archives, provider requests, recovery +actions, or import execution were exercised by this measurement. + +## Completion Evidence + +The remaining decision paths were developed after the no-validation checkpoint. +Regression runs and live acceptance drills include: + +- Import, archive, safety and converter unit/regression suite: 1,810 passed. +- Import API/UI/integration: 633 passed. +- Chromium and Firefox import/Follow-up browser suite: 122 passed, 2 skipped. +- Strict type checking: all 691 application source files passed. Focused lint, + formatting, JavaScript syntax and whitespace checks passed. +- An isolated real-file lab exercises persisted selection, individual reassignment, + exact stale-reference pairing and ZIP content under a CBR name. Source verification + must finish both its file decision and its global activity record. Normal + rematching retains the durable work marker and the user's explicit selection. +- Eleven live previews cover all ten existing completed-recovery action types. + Five representative recovery actions were applied successfully. Physical cleanup + previews and moves/Trash were rerun against clones with content hashes preserved + and stale approvals rejected. +- Browser-discovered regressions have targeted coverage: modal submission must not + trigger boosted page navigation; ready files must not carry duplicate-exclusion + text; rechecked archives display format evidence from their source diagnostics. +- Filesystem root resolution as well as archive inspection runs off the event loop. + +The private lab, originals and primary development instance remain separate. +Its manual checklist has 214 checks, not 214 claimed passes. Full CI, full-library +acceptance runs and production-style recovery replay remain release gates. diff --git a/docs/development/INFRASTRUCTURE.md b/docs/development/INFRASTRUCTURE.md index d4acc11b..279a0fb1 100644 --- a/docs/development/INFRASTRUCTURE.md +++ b/docs/development/INFRASTRUCTURE.md @@ -266,6 +266,10 @@ tracked in the active CI/CD path. AMD64/ARM64 platform builds can run concurrently. Production releases use architecture-specific GitHub Actions cache scopes and merge the platform digests only after validation succeeds. +- The Docker runners are organization-level runners in `pullbox-docker-builders`, + not repository-level registrations. Check the organization's runner inventory + and the group's selected-repository access before attempting restoration; an + empty repository-only listing does not establish that these runners are missing. - Untrusted Docker PRs run a reduced public sanity check (`Dockerfile.dev` build) instead of the full DHI-backed production build. - `Docker Validate Required` is the stable aggregate check for Docker diff --git a/docs/development/SERIES_RESCAN.md b/docs/development/SERIES_RESCAN.md new file mode 100644 index 00000000..10484b66 --- /dev/null +++ b/docs/development/SERIES_RESCAN.md @@ -0,0 +1,49 @@ +# Series Folder Rescan + +The series detail **Rescan folder** action reconciles local files with the existing +issue catalog. It is separate from metadata refresh and library organization. + +- Scans only the configured series folder and its subfolders, plus individually + registered paths for that series. It never scans those other paths' parent + folders or an entire library root. +- Inspects archives off the event loop using the shared safety, content, local + metadata, conflict, and semantic matching helpers. No metadata-provider calls + are needed. Exact issue-number text preserves lettered and fractional issues. +- Plans the complete candidate set before registration. Ambiguous copies, foreign + series, weak matches, unsafe archives, one-page archives, and recently modified + files require review instead of being silently chosen. +- Rechecks source signatures, root policy, catalog identity, current ownership, + active imports, and downloads before writing. New files are referenced, not + managed artifacts. Read-only mounts are supported. +- Existing valid copies win. Proven replacements for missing links preserve the + LibraryFile ID and become references. Missing/unreadable files never downgrade + ownership or trigger downloads. Rescans never move, rename, convert, overwrite, + delete, or write ComicInfo into source files. + +## Progress and Review + +`series_rescan` uses the durable utility queue and shared background activity +projection. Initial inspection is indeterminate until the complete candidate set +is known; registration has measured progress. Each result is saved as a job item. +The series dialog shows added, repaired, unchanged, and review counts, with paged +exceptions. **Review in Import** opens an explicit single-file, keep-in-place +import for the existing matching and safety-review controls. This action does not +approve or bypass safety findings. Files still being copied or on an unavailable +mount should instead be rescanned after the source is stable. + +The issue panel updates in place on completion, without replacing the page. Jobs +continue when the dialog is closed or the user navigates elsewhere. Saved reports +are also retained in utility history. A repeat click reuses the active series job. + +The additive migration `o6i7j8k9l012` extends the utility job-type constraint. No +catalog or ownership rows are backfilled. Downgrade requires removal of rescan job +history first rather than silently reclassifying or deleting its records. + +## Regression Coverage + +`test_series_rescan.py` covers source preservation, referenced registration, +idempotence, stale ownership/link repair, duplicates, exact letter suffixes, +one-page/unsafe archives, unavailable roots, linked files in mixed folders, active +downloads, durable queue execution, report counts, and duplicate start requests. +Migration coverage preserves dependent history. Browser coverage exercises +progress, completion, saved results, keyboard focus, and stable page identity. diff --git a/src/pullbox/api/v1/reader.py b/src/pullbox/api/v1/reader.py index 9fe92f3e..8d294286 100644 --- a/src/pullbox/api/v1/reader.py +++ b/src/pullbox/api/v1/reader.py @@ -8,7 +8,11 @@ from fastapi import APIRouter, HTTPException, Query, Request, Response from fastapi.responses import FileResponse, JSONResponse -from pullbox.api.deps import AuthenticatedStreamUser, get_request_session_factory +from pullbox.api.deps import ( + AuthenticatedStreamUser, + InteractiveOperatorUser, + get_request_session_factory, +) from pullbox.config import get_settings from pullbox.core.events import ( ReaderCompletionChanged, @@ -125,6 +129,108 @@ def _raise_http_error(exc: PageSourceError) -> Never: ) from exc +async def _import_source(request: Request, job_id: int, file_id: int) -> ResolvedReaderSource: + from pullbox.core.exceptions import ConfigurationError, ValidationError + from pullbox.services.import_reader_service import ( + load_import_reader_record, + resolve_import_reader_source, + ) + + _require_reader_enabled() + try: + async with get_request_session_factory(request)() as session: + record = await load_import_reader_record(session, job_id, file_id) + return await anyio.to_thread.run_sync(resolve_import_reader_source, record) + except (ValidationError, ConfigurationError, ValueError, OSError, RuntimeError) as exc: + raise HTTPException( + status_code=409, + detail="This file changed, is unavailable, or cannot be previewed safely. " + "Close the reader and recheck it, or choose Skip.", + ) from exc + except PageSourceError as exc: + _raise_import_reader_error(exc) + + +def _raise_import_reader_error(exc: PageSourceError) -> Never: + message = str(exc) + if exc.code in {PageSourceErrorCode.CORRUPT_SOURCE, PageSourceErrorCode.EMPTY_SOURCE}: + message = "This archive or its image is damaged or contains no readable pages." + raise HTTPException( + status_code=_ERROR_STATUS[exc.code], + detail={ + "code": exc.code.value, + "message": message + + " Close the reader and choose Skip if you do not want to import it.", + }, + ) from exc + + +@router.get("/imports/{job_id}/files/{file_id}/manifest") +async def import_reader_manifest( + request: Request, job_id: int, file_id: int, _user: InteractiveOperatorUser +) -> JSONResponse: + """Preview a staged file without approval, registration, or reading-state writes.""" + source = await _import_source(request, job_id, file_id) + try: + service = _content_service(request) + manifest = await service.get_manifest(source) + if manifest.page_count != 1: + raise HTTPException( + status_code=409, + detail="This file is no longer a one-page archive. " + "Close the reader and recheck it, or choose Skip.", + ) + # Validate the image too, so a broken page gets an actionable reader error. + await service.get_page(source, page_index=0, revision=manifest.revision) + except ReaderWorkerBusyError as exc: + _raise_reader_busy(exc) + except PageSourceError as exc: + _raise_import_reader_error(exc) + return JSONResponse( + content={ + "title": manifest.title, + "issue_label": "Import preview", + "page_count": manifest.page_count, + "revision": manifest.revision, + "initial_page_index": 0, + "page_url_template": ( + f"/api/v1/reader/imports/{job_id}/files/{file_id}/pages/{{page_index}}" + f"?revision={manifest.revision}" + ), + }, + headers={"Cache-Control": "private, no-store"}, + ) + + +@router.get("/imports/{job_id}/files/{file_id}/pages/{page_index}") +async def import_reader_page( + request: Request, + job_id: int, + file_id: int, + page_index: int, + _user: InteractiveOperatorUser, + revision: Annotated[str, Query(min_length=1, max_length=64)], +) -> Response: + source = await _import_source(request, job_id, file_id) + if page_index != 0: + raise HTTPException(status_code=404, detail="Preview page not found.") + try: + page = await _content_service(request).get_page(source, page_index=0, revision=revision) + except ReaderWorkerBusyError as exc: + _raise_reader_busy(exc) + except StaleReaderRevisionError as exc: + raise HTTPException( + status_code=409, detail="The file changed. Open View File again." + ) from exc + except PageSourceError as exc: + _raise_import_reader_error(exc) + return FileResponse( + page.path, + media_type=page.media_type, + headers={"Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff"}, + ) + + @router.get("/issues/{issue_id}/manifest", response_model=ReaderManifestResponse) async def reader_manifest( request: Request, diff --git a/src/pullbox/api/v1/router.py b/src/pullbox/api/v1/router.py index 4aae8f42..4ec73d50 100644 --- a/src/pullbox/api/v1/router.py +++ b/src/pullbox/api/v1/router.py @@ -27,6 +27,7 @@ from pullbox.api.v1.reader import router as reader_router from pullbox.api.v1.search import router as search_router from pullbox.api.v1.series import router as series_router +from pullbox.api.v1.series_rescan import router as series_rescan_router from pullbox.api.v1.story_arc_placements import router as story_arc_placements_router from pullbox.api.v1.story_arcs import router as story_arcs_router from pullbox.api.v1.suggestions import router as suggestions_router @@ -42,6 +43,7 @@ v1_router.include_router(blocklist_router) v1_router.include_router(auth_router) v1_router.include_router(series_router) +v1_router.include_router(series_rescan_router) v1_router.include_router(story_arcs_router) v1_router.include_router(story_arc_placements_router) v1_router.include_router(issues_router) diff --git a/src/pullbox/api/v1/series_rescan.py b/src/pullbox/api/v1/series_rescan.py new file mode 100644 index 00000000..3f716ffe --- /dev/null +++ b/src/pullbox/api/v1/series_rescan.py @@ -0,0 +1,147 @@ +"""Start and inspect a saved per-series filesystem reconciliation.""" + +from __future__ import annotations + +import json +from typing import Any + +from fastapi import APIRouter, Query +from sqlalchemy import JSON, Text, cast, func, or_, select, type_coerce, update + +from pullbox.api.deps import AuthenticatedUser, DbSession, InteractiveOperatorUser # noqa: TC001 +from pullbox.core.exceptions import NotFoundError, ValidationError +from pullbox.models.series import Series +from pullbox.utilities.import_guards import ensure_no_active_import_file_mutation +from pullbox.utilities.models import JobState, JobType, UtilityJob, UtilityJobItem +from pullbox.utilities.router import _get_manager, _schedule_dispatch + +router = APIRouter(prefix="/series", tags=["series"]) +_ACTIVE = [ + JobState.QUEUED, + JobState.RUNNING, + JobState.PAUSING, + JobState.PAUSED, + JobState.CANCELLING, +] +# These columns store JSON in Text. PostgreSQL needs a JSON cast, while SQLite's +# CAST AS JSON would turn the document into a number rather than preserve it. +_JSON_DOCUMENT = JSON().with_variant(Text(), "sqlite") + + +def _jobs(series_id: int) -> Any: + return select(UtilityJob).where( + UtilityJob.job_type == JobType.SERIES_RESCAN, + type_coerce(cast(UtilityJob.config, _JSON_DOCUMENT), JSON)["series_id"].as_integer() + == series_id, + ) + + +@router.post("/{series_id}/rescan", status_code=202) +async def start_series_rescan( + series_id: int, session: DbSession, user: InteractiveOperatorUser +) -> dict[str, str]: + """Queue one read-only scan, reusing an already-active rescan if present.""" + series = await session.get(Series, series_id) + if series is None: + raise NotFoundError("Series", series_id) + if not series.path: + raise ValidationError( + "This series has no configured folder. Set its folder before rescanning." + ) + await ensure_no_active_import_file_mutation(session) + await session.execute(update(Series).where(Series.id == series_id).values(path=Series.path)) + active = await session.scalar(_jobs(series_id).where(UtilityJob.state.in_(_ACTIVE)).limit(1)) + if active is not None: + return {"job_id": active.id, "state": active.state} + manager = _get_manager() + job = await manager.create_job( + session, + JobType.SERIES_RESCAN, + f"Rescan: {series.title}", + {"series_id": series_id}, + created_by=user.username, + ) + await session.commit() + _schedule_dispatch(manager) + return {"job_id": job.id, "state": job.state} + + +@router.get("/{series_id}/rescan") +async def series_rescan_report( + series_id: int, + session: DbSession, + user: AuthenticatedUser, + job_id: str | None = None, + page: int = Query(1, ge=1), +) -> dict[str, Any]: + """Return the latest (or requested) report, with bounded exception pages.""" + query = _jobs(series_id) + if job_id: + query = query.where(UtilityJob.id == job_id) + job = await session.scalar( + query.order_by(UtilityJob.created_at.desc(), UtilityJob.id.desc()).limit(1) + ) + if job is None: + return {"job": None, "items": [], "counts": {}, "page": page, "pages": 0} + outcome = type_coerce(cast(UtilityJobItem.after_state, _JSON_DOCUMENT), JSON)[ + "outcome" + ].as_string() + counts: dict[str, int] = { + str(label): count + for label, count in ( + await session.execute( + select(outcome, func.count()) + .where( + UtilityJobItem.job_id == job.id, + ) + .group_by(outcome) + ) + ).all() + if label is not None + } + review = or_(outcome == "review", UtilityJobItem.state == "FAILED") + total = int( + await session.scalar( + select(func.count()) + .select_from(UtilityJobItem) + .where(UtilityJobItem.job_id == job.id, review) + ) + or 0 + ) + rows = ( + await session.scalars( + select(UtilityJobItem) + .where(UtilityJobItem.job_id == job.id, review) + .order_by(UtilityJobItem.item_index) + .offset((page - 1) * 25) + .limit(25) + ) + ).all() + if total: + counts["review"] = total + return { + "job": { + "id": job.id, + "state": job.state, + "active": job.state in _ACTIVE, + "percent": 100 + if job.state == JobState.COMPLETED + else job.progress_pct + if job.total_items is not None + else None, + "error": job.error_message, + "total": job.total_items, + }, + "counts": counts, + "page": page, + "pages": (total + 24) // 25, + "items": [ + { + "id": row.id, + "path": row.file_path, + "reason": row.error_message + or json.loads(row.after_state or "{}").get("reason", "Review required."), + } + for row in rows + ], + } diff --git a/src/pullbox/app.py b/src/pullbox/app.py index 877f58d5..460628a9 100644 --- a/src/pullbox/app.py +++ b/src/pullbox/app.py @@ -748,6 +748,7 @@ def _cleanup_whats_new_startup_task(task: asyncio.Task[object]) -> None: from pullbox.utilities.executors.mass_convert_pipeline import MassConvertPipelineExecutor from pullbox.utilities.executors.mass_rename import MassRenameExecutor from pullbox.utilities.executors.rollback_executor import RollbackExecutor + from pullbox.utilities.executors.series_rescan import SeriesRescanExecutor from pullbox.utilities.job_queue import JobQueueManager from pullbox.utilities.router import set_queue_manager @@ -758,6 +759,7 @@ def _cleanup_whats_new_startup_task(task: asyncio.Task[object]) -> None: queue_mgr.register_executor("integrity_check", IntegrityCheckerExecutor) queue_mgr.register_executor("library_permissions", LibraryPermissionsExecutor) queue_mgr.register_executor("db_check_cleanup", DBCheckCleanupExecutor) + queue_mgr.register_executor("series_rescan", SeriesRescanExecutor) queue_mgr.register_executor("export_library", ExportLibraryExecutor) queue_mgr.register_executor("rollback", RollbackExecutor) set_queue_manager(queue_mgr) diff --git a/src/pullbox/core/archive.py b/src/pullbox/core/archive.py index d0de1e79..0ebc3e58 100644 --- a/src/pullbox/core/archive.py +++ b/src/pullbox/core/archive.py @@ -1,7 +1,7 @@ """Archive reader for comic book files (CBZ, CBR, CB7, CBT). Provides a unified interface for reading comic book archives regardless -of underlying format. Auto-detects the format from the file extension. +of underlying format. Detects recognized container headers before the suffix. All archive operations run synchronously — callers should use a thread pool executor for CPU-bound work. """ @@ -17,6 +17,7 @@ import structlog +from pullbox.core.archive_format import archive_format from pullbox.core.comicinfo import ComicInfoData, parse_comicinfo from pullbox.core.rar_backend import RarBackendUnavailableError, configure_rarfile_backend @@ -64,7 +65,7 @@ class ArchiveReader: def __init__(self, path: Path) -> None: self._path = path - self._extension = path.suffix.lower() + self._extension = "." + archive_format(path) @property def path(self) -> Path: @@ -72,7 +73,7 @@ def path(self) -> Path: @property def format(self) -> str: - """Return the archive format based on file extension.""" + """Return the detected comic container format.""" return self._extension.lstrip(".") def list_files(self) -> list[str]: diff --git a/src/pullbox/core/archive_format.py b/src/pullbox/core/archive_format.py new file mode 100644 index 00000000..2c585b5e --- /dev/null +++ b/src/pullbox/core/archive_format.py @@ -0,0 +1,33 @@ +"""Bounded container identification shared by comic readers and safety checks.""" + +from pathlib import Path + +ARCHIVE_SUFFIXES = frozenset({".cbz", ".zip", ".cbr", ".rar", ".cb7", ".7z", ".cbt", ".tar"}) +_ALIASES = {"zip": "cbz", "rar": "cbr", "7z": "cb7", "tar": "cbt"} + + +def archive_format(path: Path) -> str: + """Prefer a recognized header, retaining normal parser errors for corrupt files. + + Do not identify arbitrary executable or document suffixes as comic archives. + Call from an I/O worker. Reading 512 bytes is independent of archive size. + """ + declared = path.suffix.lower().lstrip(".") + if path.suffix.lower() not in ARCHIVE_SUFFIXES: + return declared + try: + with path.open("rb") as source: + header = source.read(512) + except OSError: + return _ALIASES.get(declared, declared) + if header.startswith((b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08")): + return "cbz" + if header.startswith((b"Rar!\x1a\x07\x00", b"Rar!\x1a\x07\x01\x00")): + return "cbr" + if header.startswith(b"7z\xbc\xaf\x27\x1c"): + return "cb7" + if header.startswith(b"%PDF"): + return "pdf" + if header[257:262] == b"ustar": + return "cbt" + return _ALIASES.get(declared, declared) diff --git a/src/pullbox/core/collection_scanner.py b/src/pullbox/core/collection_scanner.py index 78414f2a..7b780e7a 100644 --- a/src/pullbox/core/collection_scanner.py +++ b/src/pullbox/core/collection_scanner.py @@ -1308,12 +1308,11 @@ def apply_layout_value( if conflicts: metadata_diagnostics["source_layout_conflicts"] = conflicts - parsed = metadata.parsed_release issue_number_raw: str | None = None if layout_issue_number_raw is not None: issue_number_raw = layout_issue_number_raw - elif parsed is not None and parsed.issue_number is not None: - issue_number_raw = format_issue_number(parsed.issue_number) + elif metadata.issue_number_text is not None: + issue_number_raw = metadata.issue_number_text elif parsed_issue_number is not None: issue_number_raw = format_issue_number(parsed_issue_number) diff --git a/src/pullbox/core/file_ops.py b/src/pullbox/core/file_ops.py index ba0fc1d3..8cfc568d 100644 --- a/src/pullbox/core/file_ops.py +++ b/src/pullbox/core/file_ops.py @@ -20,6 +20,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload +from pullbox.core.archive_format import archive_format from pullbox.core.exceptions import ConfigurationError from pullbox.core.library_comicinfo import ( apply_comicinfo_to_imported_artifact as _apply_comicinfo_to_imported_artifact, @@ -892,8 +893,8 @@ async def notify_placement_completed(target_path: Path) -> None: stat = await asyncio.to_thread(final_path.stat) # 9. Create LibraryFile record - extension = final_path.suffix.lstrip(".").lower() - file_format = _FORMAT_MAP.get(extension, FileFormat.CBZ) + detected_format = await asyncio.to_thread(archive_format, final_path) + file_format = _FORMAT_MAP.get(detected_format, FileFormat.CBZ) registered_signature = ( referenced_signature @@ -1147,12 +1148,12 @@ async def _update_existing_library_file_from_path( ) -> None: """Refresh an existing LibraryFile row from the current artifact on disk.""" stat = await asyncio.to_thread(final_path.stat) - extension = final_path.suffix.lstrip(".").lower() + detected_format = await asyncio.to_thread(archive_format, final_path) library_file.file_path = str(final_path) library_file.file_name = final_path.name library_file.file_size = stat.st_size - library_file.file_format = _FORMAT_MAP.get(extension, FileFormat.CBZ) + library_file.file_format = _FORMAT_MAP.get(detected_format, FileFormat.CBZ) library_file.file_modified_at = datetime.fromtimestamp(stat.st_mtime, tz=UTC) library_file.match_confidence = confidence library_file.parsed_series = series.title if isinstance(series, Series) else None @@ -1246,8 +1247,11 @@ async def _recover_materialized_target_without_source( select(LibraryFile).where(LibraryFile.file_path == str(target_path)) ) existing = existing_result.scalars().first() + detected_format = await asyncio.to_thread(archive_format, target_path) + file_format = _FORMAT_MAP.get(detected_format, FileFormat.CBZ) if existing is not None: existing.issue_id = issue.id + existing.file_format = file_format existing.match_confidence = confidence existing.naming_snapshot = naming_snapshot issue.status = IssueStatus.OWNED @@ -1266,8 +1270,6 @@ async def _recover_materialized_target_without_source( ) stat = await asyncio.to_thread(target_path.stat) - extension = target_path.suffix.lstrip(".").lower() - file_format = _FORMAT_MAP.get(extension, FileFormat.CBZ) lf = LibraryFile( file_path=str(target_path), diff --git a/src/pullbox/core/file_safety.py b/src/pullbox/core/file_safety.py index e0e75fe3..1c79b013 100644 --- a/src/pullbox/core/file_safety.py +++ b/src/pullbox/core/file_safety.py @@ -20,6 +20,7 @@ import structlog from pullbox.core.archive import comicinfo_member_sort_key +from pullbox.core.archive_format import ARCHIVE_SUFFIXES, archive_format from pullbox.core.comicinfo import ComicInfoData, parse_comicinfo from pullbox.core.filesystem_scan import iter_supported_files_with_handler from pullbox.core.page_sources.base import canonical_page_names @@ -321,7 +322,7 @@ def _has_path_traversal(entry_name: str) -> bool: def ensure_zip_archive_inspectable(archive_path: Path) -> None: """Fail closed when a ZIP-based archive cannot be inspected.""" - if archive_path.suffix.lower() not in (".cbz", ".zip"): + if archive_format(archive_path) != "cbz": return try: @@ -347,7 +348,7 @@ def check_archive_path_traversal(archive_path: Path) -> list[str]: and we have stdlib support. RAR/7z support can be added later. """ offending: list[str] = [] - ext = archive_path.suffix.lower() + ext = "." + archive_format(archive_path) if ext in (".cbz", ".zip"): try: @@ -371,7 +372,7 @@ def check_archive_size(archive_path: Path, max_bytes: int) -> int | None: Returns the total uncompressed size in bytes, or None if the archive cannot be read. Raises FileSafetyError if the size exceeds the limit. """ - ext = archive_path.suffix.lower() + ext = "." + archive_format(archive_path) if ext not in (".cbz", ".zip"): return None @@ -403,7 +404,7 @@ def check_archive_contents_for_dangerous_files(archive_path: Path) -> list[str]: Returns a list of dangerous entry names (empty = safe). """ dangerous: list[str] = [] - ext = archive_path.suffix.lower() + ext = "." + archive_format(archive_path) if ext in (".cbz", ".zip"): try: @@ -428,7 +429,7 @@ def inspect_zip_archive_safety( block_dangerous: bool, ) -> ZipArchiveSafetyReport | None: """Inspect a ZIP-based archive once and return all safety facts needed.""" - if archive_path.suffix.lower() not in (".cbz", ".zip"): + if archive_format(archive_path) != "cbz": return None try: @@ -558,7 +559,7 @@ def run_safety_checks( # Find archive files to inspect archive_files: list[Path] = [] if download_path.is_file(): - if download_path.suffix.lower() in (".cbz", ".zip"): + if download_path.suffix.lower() in ARCHIVE_SUFFIXES: archive_files.append(download_path) elif download_path.is_dir(): @@ -572,7 +573,7 @@ def _on_archive_scan_error(root: Path, exc: OSError) -> None: archive_files.extend( iter_supported_files_with_handler( download_path, - frozenset({".cbz", ".zip"}), + ARCHIVE_SUFFIXES, _on_archive_scan_error, ) ) @@ -585,6 +586,27 @@ def _on_archive_scan_error(root: Path, exc: OSError) -> None: block_dangerous=block_dangerous, ) if safety_report is None: + from pullbox.core.archive import ArchiveError, ArchiveReader + + try: + members = ArchiveReader(archive).list_members() + except ArchiveError as exc: + raise FileSafetyError("Archive inspection failed", details=[str(archive)]) from exc + if any( + member.is_link or has_archive_member_path_traversal(member.name) + for member in members + ): + raise FileSafetyError( + "Archive contains dangerous path or link", details=[str(archive)] + ) + if sum(member.size for member in members) > max_archive_size: + raise FileSafetyError( + "Archive decompressed size exceeds limit", details=[str(archive)] + ) + if block_dangerous and any( + Path(member.name).suffix.lower() in DANGEROUS_EXTENSIONS for member in members + ): + raise FileSafetyError("Archive contains dangerous payload", details=[str(archive)]) continue # 2. Path traversal diff --git a/src/pullbox/core/issue_numbers.py b/src/pullbox/core/issue_numbers.py index 826404f6..c954b0f5 100644 --- a/src/pullbox/core/issue_numbers.py +++ b/src/pullbox/core/issue_numbers.py @@ -5,6 +5,10 @@ import math import re from decimal import Decimal, InvalidOperation +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable _MAX_ISSUE_NUMBER_TEXT_LENGTH = 320 _NUMERIC_SUFFIX_PATTERN = re.compile(r"^([+-]?(?:\d+(?:\.\d*)?|\.\d+))([A-Za-z]+)$") @@ -93,3 +97,16 @@ def parse_issue_number_text(value: str | float | int) -> tuple[float, str]: if not math.isfinite(float_value): raise ValueError("issue number exceeds the numeric compatibility range") return float_value, exact_text + + +def issue_number_lookup_value(value: str | float | int) -> float | str: + """Keep legacy numeric cache keys, but never discard an identity suffix.""" + numeric, exact = parse_issue_number_text(value) + return numeric if exact == format_issue_number(numeric) else exact + + +def normalize_issue_number_queries(values: Iterable[str | float | int]) -> list[float | str]: + """Deduplicate targeted lookups by exact identity in stable numeric order.""" + return sorted( + {issue_number_lookup_value(value) for value in values}, key=parse_issue_number_text + ) diff --git a/src/pullbox/core/mylar3_reader.py b/src/pullbox/core/mylar3_reader.py index a411e6b4..bf090d7b 100644 --- a/src/pullbox/core/mylar3_reader.py +++ b/src/pullbox/core/mylar3_reader.py @@ -1397,7 +1397,9 @@ def _build_files( issue_type = IssueType(parsed.issue_type) except ValueError: issue_type = IssueType.ISSUE - issue_number_raw = format_issue_number(parsed.issue_number) + issue_number_raw = parsed.issue_number_text or format_issue_number( + parsed.issue_number + ) if parsed_issue_number is not None: metadata_signals["issue_number"] = MetadataSignal.RELEASE_TITLE.value if parsed.issue_type != IssueType.ISSUE.value: @@ -1646,7 +1648,7 @@ def _include_recorded_issue_paths( series_resolution: _ResolvedMylarPath, issue_records: list[_MylarIssueRecord], ) -> list[Path]: - """Merge safely resolved records, retaining missing paths only in-place.""" + """Merge safely resolved records, retaining missing paths when requested.""" paths = set(comic_paths) for record in issue_records: if not record.location: diff --git a/src/pullbox/core/naming.py b/src/pullbox/core/naming.py index 4336a84d..c293238e 100644 --- a/src/pullbox/core/naming.py +++ b/src/pullbox/core/naming.py @@ -163,6 +163,7 @@ class ParsedFilename: issue_type: str = "issue" # IssueType.value string tags: list[str] = field(default_factory=list) extension: str | None = None + issue_number_text: str | None = None @dataclass(frozen=True) @@ -864,6 +865,7 @@ def parse_filename(filename: str) -> ParsedFilename | None: return ParsedFilename( series=parsed.series_name, issue_number=parsed.issue_number, + issue_number_text=parsed.issue_number_text, year=parsed.year, volume=parsed.volume, issue_type=parsed.issue_type.value, diff --git a/src/pullbox/core/release_parser.py b/src/pullbox/core/release_parser.py index ec2c737b..07fed693 100644 --- a/src/pullbox/core/release_parser.py +++ b/src/pullbox/core/release_parser.py @@ -137,7 +137,7 @@ _YEAR_PAREN_RE = re.compile(r"\((\d{4})\)") # Issue number with hash prefix: #045, #5, #5.1 -_ISSUE_HASH_RE = re.compile(r"#(\d+(?:\.\d+)?[A-Za-z]*)") +_ISSUE_HASH_RE = re.compile(r"#([+-]?\d+(?:\.\d+)?[A-Za-z]*)") # DC's One Million event used the literal issue number 1,000,000 across # multiple ongoing titles. Keep this exact exception narrow so arbitrary long diff --git a/src/pullbox/providers/metadata/comicvine.py b/src/pullbox/providers/metadata/comicvine.py index f6be830c..75596c40 100644 --- a/src/pullbox/providers/metadata/comicvine.py +++ b/src/pullbox/providers/metadata/comicvine.py @@ -21,7 +21,11 @@ import httpx import structlog -from pullbox.core.issue_numbers import format_issue_number, parse_issue_number_text +from pullbox.core.issue_numbers import ( + normalize_issue_number_queries, + normalize_issue_number_text, + parse_issue_number_text, +) from pullbox.core.naming import detect_issue_type from pullbox.providers.base import ( IssueMetadata, @@ -203,9 +207,9 @@ def _parse_issue_number_fields(value: str | None) -> tuple[float, str | None]: return 0.0, None -def _format_issue_number_filter(value: float) -> str: +def _format_issue_number_filter(value: float | str) -> str: """Format an issue number for ComicVine's exact issue_number filter.""" - return format_issue_number(value) + return normalize_issue_number_text(value) def _safe_int(value: Any) -> int | None: @@ -761,6 +765,9 @@ async def _fetch_global_series_search( async def get_series(self, provider_id: str) -> SeriesMetadata: """Get full series (volume) metadata by ComicVine volume ID.""" + if re.fullmatch(r"[1-9][0-9]{0,18}", provider_id) is None: + raise ValueError("ComicVine provider IDs must be positive integers") + resource_id = int(provider_id) log = logger.bind(provider_id=provider_id) log.debug("comicvine_get_series") @@ -771,7 +778,7 @@ async def get_series(self, provider_id: str) -> SeriesMetadata: ), } - data = await self._request(f"/volume/{_VOLUME_PREFIX}-{provider_id}/", params) + data = await self._request(f"/volume/{_VOLUME_PREFIX}-{resource_id}/", params) item: dict[str, Any] = data.get("results", {}) return _series_metadata_from_item(item, fallback_provider_id=provider_id) @@ -805,6 +812,9 @@ async def get_series_batch(self, provider_ids: Sequence[str]) -> dict[str, Serie async def get_issue(self, provider_id: str) -> IssueMetadata: """Get full issue metadata by ComicVine issue ID.""" + if re.fullmatch(r"[1-9][0-9]{0,18}", provider_id) is None: + raise ValueError("ComicVine provider IDs must be positive integers") + resource_id = int(provider_id) log = logger.bind(provider_id=provider_id) log.debug("comicvine_get_issue") @@ -816,7 +826,7 @@ async def get_issue(self, provider_id: str) -> IssueMetadata: ), } - data = await self._request(f"/issue/{_ISSUE_PREFIX}-{provider_id}/", params) + data = await self._request(f"/issue/{_ISSUE_PREFIX}-{resource_id}/", params) item: dict[str, Any] = data.get("results", {}) return _issue_metadata_from_item(item, fallback_provider_id=provider_id) @@ -950,7 +960,7 @@ async def get_recent_issues_for_series( async def get_issues_for_series_by_numbers( self, series_provider_id: str, - issue_numbers: list[float], + issue_numbers: Sequence[float | str], ) -> list[IssueSummary]: """Get selected issues for a series by issue number.""" log = logger.bind( @@ -960,7 +970,7 @@ async def get_issues_for_series_by_numbers( log.debug("comicvine_get_issues_for_series_by_numbers") summaries: list[IssueSummary] = [] - seen_numbers = sorted({float(number) for number in issue_numbers}) + seen_numbers = normalize_issue_number_queries(issue_numbers) for issue_number in seen_numbers: params: dict[str, Any] = { "filter": ( diff --git a/src/pullbox/services/airdcpp_search_coordinator.py b/src/pullbox/services/airdcpp_search_coordinator.py index fbcba21c..0b214804 100644 --- a/src/pullbox/services/airdcpp_search_coordinator.py +++ b/src/pullbox/services/airdcpp_search_coordinator.py @@ -193,6 +193,7 @@ async def run(client: AirDcppSearchClient) -> _ClientResult: releases, wanted_series=target.series_title, wanted_issue=target.issue_number, + wanted_issue_number_text=target.effective_issue_number_text, wanted_year=target.search_year, year_context=target.year_context, wanted_issue_type=target.issue_type, diff --git a/src/pullbox/services/catalog/lookup.py b/src/pullbox/services/catalog/lookup.py index 0607ac59..3c5bd30b 100644 --- a/src/pullbox/services/catalog/lookup.py +++ b/src/pullbox/services/catalog/lookup.py @@ -4,9 +4,12 @@ from typing import TYPE_CHECKING, Any +from pullbox.core.issue_numbers import normalize_issue_number_text from pullbox.services.catalog.contract import CatalogError if TYPE_CHECKING: + from collections.abc import Sequence + from pullbox.providers.base import ( IssueMetadata, IssueSummary, @@ -74,13 +77,13 @@ async def get_issues_for_series(self, series_provider_id: str) -> list[IssueSumm return await self.reader.issues(int(series_provider_id)) async def get_issues_for_series_by_numbers( - self, series_provider_id: str, issue_numbers: list[float] + self, series_provider_id: str, issue_numbers: Sequence[float | str] ) -> list[IssueSummary]: - numbers = set(issue_numbers) + numbers = {normalize_issue_number_text(number) for number in issue_numbers} return [ issue for issue in await self.get_issues_for_series(series_provider_id) - if issue.issue_number in numbers + if normalize_issue_number_text(issue.issue_number_text or issue.issue_number) in numbers ] async def get_issue(self, issue_provider_id: str) -> IssueMetadata: diff --git a/src/pullbox/services/comicvine_persistent_cache.py b/src/pullbox/services/comicvine_persistent_cache.py index 6c31fd2a..4c534655 100644 --- a/src/pullbox/services/comicvine_persistent_cache.py +++ b/src/pullbox/services/comicvine_persistent_cache.py @@ -15,13 +15,14 @@ from sqlalchemy import select from sqlalchemy.exc import IntegrityError, SQLAlchemyError +from pullbox.core.issue_numbers import issue_number_lookup_value, normalize_issue_number_queries from pullbox.core.name_matcher import NameMatcher from pullbox.models.provider_cache import MetadataProviderCacheEntry from pullbox.providers.base import IssueMetadata, IssueSummary, SeriesMetadata, SeriesSearchResult from pullbox.providers.story_arcs import StoryArcSearchResult if TYPE_CHECKING: - from collections.abc import Awaitable, Callable + from collections.abc import Awaitable, Callable, Sequence from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker @@ -410,12 +411,12 @@ async def refresh_issue_catalog(self, series_provider_id: str) -> list[IssueSumm async def get_issues_for_series_by_numbers( self, series_provider_id: str, - issue_numbers: list[float], + issue_numbers: Sequence[float | str], ) -> list[IssueSummary]: kind = "get_issues_for_series_by_number" - normalized_numbers = sorted({float(number) for number in issue_numbers}) + normalized_numbers = normalize_issue_number_queries(issue_numbers) summaries: list[IssueSummary] = [] - missing_numbers: list[float] = [] + missing_numbers: list[float | str] = [] for issue_number in normalized_numbers: request = _issue_number_request(series_provider_id, issue_number) @@ -425,7 +426,12 @@ async def get_issues_for_series_by_numbers( missing_numbers.append(issue_number) continue self._stats.hits[kind] += 1 - summaries.extend(_issue_summaries_from_payload(payload)) + summaries.extend( + summary + for summary in _issue_summaries_from_payload(payload) + if issue_number_lookup_value(summary.issue_number_text or summary.issue_number) + == issue_number + ) if not missing_numbers: return summaries @@ -441,17 +447,25 @@ async def get_issues_for_series_by_numbers( missing_numbers, ) else: - missing_set = {float(number) for number in missing_numbers} + missing_set = set(missing_numbers) all_summaries = await self.get_issues_for_series(series_provider_id) fetched = [ - summary for summary in all_summaries if float(summary.issue_number) in missing_set + summary + for summary in all_summaries + if issue_number_lookup_value(summary.issue_number_text or summary.issue_number) + in missing_set ] self._stats.external_calls[kind] += len(missing_numbers) self._stats.external_duration_ms[kind] += (time.monotonic() - started_at) * 1000 - by_number: dict[float, list[IssueSummary]] = {number: [] for number in missing_numbers} + by_number: dict[float | str, list[IssueSummary]] = { + number: [] for number in missing_numbers + } for summary in fetched: - by_number.setdefault(float(summary.issue_number), []).append(summary) + number_key = issue_number_lookup_value( + summary.issue_number_text or summary.issue_number + ) + by_number.setdefault(number_key, []).append(summary) for issue_number in missing_numbers: number_summaries = by_number.get(issue_number, []) @@ -733,10 +747,10 @@ def _clear_inflight( _CACHE_MISS_INFLIGHT.pop(inflight_key, None) -def _issue_number_request(series_provider_id: str, issue_number: float) -> dict[str, Any]: +def _issue_number_request(series_provider_id: str, issue_number: float | str) -> dict[str, Any]: return { "series_provider_id": str(series_provider_id), - "issue_number": float(issue_number), + "issue_number": issue_number_lookup_value(issue_number), } diff --git a/src/pullbox/services/direct_acquisition_planner_service.py b/src/pullbox/services/direct_acquisition_planner_service.py index 44e07ac2..6e27e473 100644 --- a/src/pullbox/services/direct_acquisition_planner_service.py +++ b/src/pullbox/services/direct_acquisition_planner_service.py @@ -13,9 +13,9 @@ from sqlalchemy.orm import selectinload from pullbox.core.exceptions import ValidationError +from pullbox.core.issue_numbers import normalize_issue_number_text from pullbox.core.log_sanitizer import sanitize_log_string from pullbox.core.name_matcher import NameMatcher -from pullbox.core.release_parser import issues_match, normalize_issue_number from pullbox.core.type_semantics import TypeFamily, issue_type_family from pullbox.models.direct_acquisition import ( DirectAcquisitionAttempt, @@ -1039,9 +1039,10 @@ def _requested_volume( def _coverage_numbers_match(requested: str, offered: str) -> bool: if requested.casefold() == offered.casefold(): return True - requested_number = normalize_issue_number(requested) - offered_number = normalize_issue_number(offered) - return requested_number is not None and issues_match(requested_number, offered_number) + try: + return normalize_issue_number_text(requested) == normalize_issue_number_text(offered) + except ValueError: + return False def _candidate_confidence(snapshot: Mapping[str, object]) -> float: diff --git a/src/pullbox/services/direct_search_coordinator.py b/src/pullbox/services/direct_search_coordinator.py index a5bd31fa..6bb26b46 100644 --- a/src/pullbox/services/direct_search_coordinator.py +++ b/src/pullbox/services/direct_search_coordinator.py @@ -486,6 +486,7 @@ def validate( [candidate_release], wanted_series=target.series_title, wanted_issue=target.issue_number, + wanted_issue_number_text=target.effective_issue_number_text, wanted_year=target.search_year, year_context=target.year_context, wanted_issue_type=target.issue_type, diff --git a/src/pullbox/services/import_confirmation.py b/src/pullbox/services/import_confirmation.py index a30ed809..e7aaabdc 100644 --- a/src/pullbox/services/import_confirmation.py +++ b/src/pullbox/services/import_confirmation.py @@ -18,6 +18,10 @@ ImportSeriesStatus, ) from pullbox.models.issue import Issue +from pullbox.services.import_file_selection import ( + defer_excluded_review_files, + not_excluded_from_review, +) from pullbox.services.import_job_execution_items import ( ensure_target_issue_summary_for_import_file, ) @@ -75,6 +79,17 @@ async def confirm_import_job( if job.status != ImportJobStatus.REVIEW: raise ValidationError(f"Job must be in REVIEW state to confirm (current: {job.status})") + if await session.scalar( + sa_select(ImportedFile.id) + .where( + ImportedFile.import_job_id == job_id, + ImportedFile.diagnostics["review_source_action"]["state"] + .as_string() + .in_(["pending", "matching"]), + ) + .limit(1) + ): + raise ValidationError("Wait for source verification to finish before starting the import.") affected_series_ids: set[int] = set() @@ -152,6 +167,9 @@ async def confirm_import_job( await reopen_review_after_managed_copy_preflight_failure(session, job, exc) raise + deferred_series_ids = await defer_excluded_review_files(session, job_id) + if deferred_series_ids: + await recompute_file_counters(session, job, series_ids=sorted(deferred_series_ids)) job.status = ImportJobStatus.IMPORTING job.control_request = ImportControlRequest.NONE progress_snapshot = initialize_progress_snapshot( @@ -235,6 +253,7 @@ async def _load_confirmed_series( ImportedFile.status.in_( [ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED] ), + not_excluded_from_review(), ) .distinct() ) @@ -268,7 +287,7 @@ async def _load_confirmed_series( if item.status == ImportSeriesStatus.MATCHED and ( item.id in importable_series_ids or no_persisted_files - or item.id not in conflict_series_ids + or (item.id not in conflict_series_ids and not item.files_matched) ): valid_items.append(item) continue @@ -363,6 +382,7 @@ async def _confirm_matched_files( sa_select(ImportedFile).where( ImportedFile.import_series_id.in_(confirmed_series_ids), ImportedFile.status == ImportedFileStatus.MATCHED, + not_excluded_from_review(), ) ) for matched_file in matched_result.scalars().all(): diff --git a/src/pullbox/services/import_content_inspection.py b/src/pullbox/services/import_content_inspection.py index 96ed5543..9b615132 100644 --- a/src/pullbox/services/import_content_inspection.py +++ b/src/pullbox/services/import_content_inspection.py @@ -7,6 +7,7 @@ import structlog from pullbox.core.archive import ArchiveError, ArchiveReader +from pullbox.core.archive_format import archive_format from pullbox.core.file_safety import FileSafetyError from pullbox.core.page_sources.base import canonical_page_names from pullbox.services.import_safety_diagnostics import build_import_safety_diagnostics @@ -53,6 +54,11 @@ def inspect_import_content(path: Path, inspection: FileSafetyInspection) -> dict ) diagnostics: dict[str, object] = { "content_inspection": {"version": 1, "page_count": page_count}, + "archive_format": { + "declared": path.suffix.lower().lstrip("."), + "detected": archive_format(path), + "mismatch": path.suffix.lower().lstrip(".") != archive_format(path), + }, } if page_count < 2: code = "archive_no_pages" if page_count == 0 else "single_page_comic" diff --git a/src/pullbox/services/import_file_conflicts.py b/src/pullbox/services/import_file_conflicts.py index d4aa8a95..5d1a2077 100644 --- a/src/pullbox/services/import_file_conflicts.py +++ b/src/pullbox/services/import_file_conflicts.py @@ -2,11 +2,59 @@ from __future__ import annotations +import re from typing import Any +from pullbox.core.name_matcher import NameMatcher +from pullbox.core.release_parser import parse_release_title from pullbox.models.import_job import ImportedFile, ImportedFileStatus from pullbox.services.import_duplicates import confidence_rank +_RELEASE_SITE_SUFFIX = re.compile( + r"\s+(?:www\.)?[\w-]+\.(?:com|org|net|info)(?=\.(?:cbz|cbr|cb7|pdf|epub)$)", + re.IGNORECASE, +) +_SCAN_LABEL_SUFFIX = re.compile(r"\s+\(scan\)(?=\.(?:cbz|cbr|cb7|cbt|pdf|epub)$)", re.IGNORECASE) + + +def review_filename_identity( + file_name: str, parsed_series: str | None, parsed_year: int | None +) -> tuple[str, int | None]: + """Use dated filename evidence, not inherited Mylar series-start metadata. + + Undated filenames can contain copy counters or issue ordinals that the release + parser folds into the title. Retain their saved identity rather than guessing. + """ + name = _SCAN_LABEL_SUFFIX.sub("", _RELEASE_SITE_SUFFIX.sub("", file_name or "")) + parsed = parse_release_title(name) + if parsed is None or parsed.year is None: + return NameMatcher.normalize(parsed_series or ""), parsed_year + return ( + NameMatcher.normalize(parsed.series_name or parsed_series or ""), + parsed.year if parsed.year is not None else parsed_year, + ) + + +def classify_conflict_group(files: list[ImportedFile]) -> str: + """Describe whether competing copies agree about their comic identity.""" + hashes = {file.content_hash for file in files} + if len(files) > 1 and len(hashes) == 1 and None not in hashes and "" not in hashes: + return "identical_copy" + identities = [ + review_filename_identity(file.file_name, file.parsed_series, file.parsed_year) + for file in files + ] + names = {title for title, _ in identities if title} + saved_names = { + NameMatcher.normalize(file.parsed_series) for file in files if file.parsed_series + } + if len(names) > 1 or len(saved_names) > 1: + return "series_mismatch" + years = {year for _, year in identities if year} + if len(years) > 1: + return "year_disagreement" + return "duplicate_copy" + def preferred_conflict_reasons( preferred: ImportedFile, @@ -70,20 +118,23 @@ def sort_key(imp_file: ImportedFile) -> tuple[int, int, int]: group.sort(key=sort_key, reverse=True) preferred = group[0] - preferred_reasons = preferred_conflict_reasons(preferred, group[1:]) + conflict_class = classify_conflict_group(group) + suggest_keeper = conflict_class in {"duplicate_copy", "identical_copy"} + preferred_reasons = preferred_conflict_reasons(preferred, group[1:]) if suggest_keeper else [] for idx, imp_file in enumerate(group): imp_file.status = ImportedFileStatus.CONFLICT imp_file.conflict_group_id = group_counter - imp_file.is_preferred = idx == 0 + imp_file.is_preferred = idx == 0 and suggest_keeper imp_file.include_in_import = False imp_file.diagnostics = { "kind": "file_conflict", + "conflict_class": conflict_class, "scope": scope, "conflict_group_id": group_counter, "group_size": len(group), - "preferred_file_id": preferred.id, - "preferred_file_name": preferred.file_name, + "preferred_file_id": preferred.id if suggest_keeper else None, + "preferred_file_name": preferred.file_name if suggest_keeper else None, "preferred_reasons": preferred_reasons, "selection_basis": { "has_comicinfo": imp_file.has_comicinfo, @@ -91,18 +142,21 @@ def sort_key(imp_file: ImportedFile) -> tuple[int, int, int]: "file_size": imp_file.file_size, }, "why_not_selected": ( - [] if idx == 0 else rejected_conflict_reasons(imp_file, preferred) + [] + if idx == 0 or not suggest_keeper + else rejected_conflict_reasons(imp_file, preferred) ), "previous_diagnostics": dict(imp_file.diagnostics or {}), } return { "kind": "file_conflict", + "conflict_class": conflict_class, "scope": scope, "conflict_group_id": group_counter, "group_size": len(group), - "preferred_file_id": preferred.id, - "preferred_file_name": preferred.file_name, + "preferred_file_id": preferred.id if suggest_keeper else None, + "preferred_file_name": preferred.file_name if suggest_keeper else None, "preferred_reasons": preferred_reasons, "files": [ { diff --git a/src/pullbox/services/import_file_match_candidates.py b/src/pullbox/services/import_file_match_candidates.py index 0162558b..82632d2e 100644 --- a/src/pullbox/services/import_file_match_candidates.py +++ b/src/pullbox/services/import_file_match_candidates.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +from pullbox.core.issue_numbers import format_issue_number from pullbox.core.release_parser import parse_release_title from pullbox.core.source_metadata import volume_subtitle_hint_from_filename from pullbox.core.type_semantics import ( @@ -181,8 +182,14 @@ def select_file_match_candidate( exact_issue_number = candidate_issue_number_text(imp_file) if exact_issue_number is not None and target_index.exact_number_map: exact_target = target_index.exact_number_map.get(exact_issue_number) - if exact_target is None: + if exact_target is None and ( + target_index.existing_series is not None + or exact_issue_number != format_issue_number(candidate_issue_number(imp_file) or 0.0) + ): return None + else: + exact_target = None + if exact_target is not None: matched_issue_id, matched_issue_cv_id, has_library_file, matched_issue, issue_title = ( exact_target ) @@ -191,6 +198,9 @@ def select_file_match_candidate( if matched_issue is not None else candidate_issue_number(imp_file) ) + if target_issue_number is None: + return None + provisional_type = target_index.provisional_exact_types.get(exact_issue_number or "") return FileMatchCandidate( matched_issue_id=matched_issue_id, matched_issue_cv_id=matched_issue_cv_id, @@ -199,7 +209,10 @@ def select_file_match_candidate( matched_issue=matched_issue, target_issue_title=issue_title, confidence="high" if series_high_confidence else "medium", - method="issue_number", + method=PROVIDER_MISSING_ISSUE_PLACEHOLDER_METHOD + if provisional_type + else "issue_number", + synthetic_issue_type=provisional_type, ) issue_number = candidate_issue_number(imp_file) diff --git a/src/pullbox/services/import_file_match_outcomes.py b/src/pullbox/services/import_file_match_outcomes.py index 079743fd..01290136 100644 --- a/src/pullbox/services/import_file_match_outcomes.py +++ b/src/pullbox/services/import_file_match_outcomes.py @@ -53,6 +53,11 @@ class FileMatchLogEvent: "metadata_signals", "source_metadata", "mylar3_cross_folder_reconciliation", + "mylar3_path_reconciliation", + "review_selection", + "review_source_action", + "review_source_previous", + "review_deferred", ) @@ -101,7 +106,7 @@ def apply_matched_file_outcome( target_state = duplicate_target_state(matched_issue) imp_file.status = ImportedFileStatus.MATCHED - imp_file.include_in_import = False + imp_file.include_in_import = source_evidence.get("review_selection") is True imp_file.diagnostics = { **source_evidence, **_duplicate_file_diagnostics( @@ -122,7 +127,7 @@ def apply_matched_file_outcome( ) imp_file.status = ImportedFileStatus.MATCHED - imp_file.include_in_import = False + imp_file.include_in_import = source_evidence.get("review_selection") is True if ( match_candidate.method in { diff --git a/src/pullbox/services/import_file_match_targets.py b/src/pullbox/services/import_file_match_targets.py index fe09beb1..0de723ee 100644 --- a/src/pullbox/services/import_file_match_targets.py +++ b/src/pullbox/services/import_file_match_targets.py @@ -9,6 +9,11 @@ from sqlalchemy import select as sa_select from pullbox.core.exceptions import ImportProviderDegradedError +from pullbox.core.issue_numbers import ( + format_issue_number, + normalize_issue_number_queries, + normalize_issue_number_text, +) from pullbox.core.naming import detect_issue_type from pullbox.core.release_parser import parse_release_title from pullbox.core.source_metadata import volume_subtitle_hint_from_filename @@ -17,7 +22,10 @@ from pullbox.models.series import Series from pullbox.providers.base import IssueSummary from pullbox.services.import_embedded_title_match import embedded_issue_number_title_match -from pullbox.services.import_file_issue_signals import candidate_issue_number +from pullbox.services.import_file_issue_signals import ( + candidate_issue_number, + candidate_issue_number_text, +) if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession @@ -47,6 +55,7 @@ class FileMatchTargetIndex: synthetic_issue_types: dict[float, IssueType] = field(default_factory=dict) synthetic_issue_titles: dict[float, str | None] = field(default_factory=dict) provisional_issue_numbers: set[float] = field(default_factory=set) + provisional_exact_types: dict[str, IssueType] = field(default_factory=dict) existing_series: Series | None = None issue_entries: list[tuple[Issue, bool]] = field(default_factory=list) @@ -145,7 +154,12 @@ async def load_file_match_target_index( entry = (None, provider_id, False, None, summary.title) if provider_id is not None: target_index.cv_id_map[provider_id] = entry - target_index.number_map[summary.issue_number] = entry + exact_text = normalize_issue_number_text( + summary.issue_number_text or summary.issue_number + ) + target_index.exact_number_map[exact_text] = entry + if exact_text == format_issue_number(summary.issue_number): + target_index.number_map[summary.issue_number] = entry return target_index @@ -197,10 +211,14 @@ def _trusted_source_issue_target_index( entry = (None, issue_cv_id, False, None, issue_title) target_index.cv_id_map[issue_cv_id] = entry if issue_number is not None: - target_index.number_map[issue_number] = entry - target_index.synthetic_issue_types.pop(issue_number, None) - target_index.synthetic_issue_titles.pop(issue_number, None) - target_index.provisional_issue_numbers.discard(issue_number) + exact_text = candidate_issue_number_text(imp_file) or format_issue_number(issue_number) + target_index.exact_number_map[exact_text] = entry + if exact_text == format_issue_number(issue_number): + target_index.number_map[issue_number] = entry + target_index.synthetic_issue_types.pop(issue_number, None) + target_index.synthetic_issue_titles.pop(issue_number, None) + target_index.provisional_issue_numbers.discard(issue_number) + target_index.provisional_exact_types.pop(exact_text, None) return target_index @@ -213,13 +231,20 @@ def _add_trusted_provisional_target( if imp_file.comicvine_issue_id is not None: return issue_number = candidate_issue_number(imp_file) - if issue_number is None or issue_number in target_index.number_map: + if issue_number is None: + return + exact_text = candidate_issue_number_text(imp_file) or format_issue_number(issue_number) + if exact_text in target_index.exact_number_map: return issue_type = _file_placeholder_issue_type(imp_file) or IssueType.ISSUE - target_index.number_map.setdefault(issue_number, (None, None, False, None, None)) - target_index.synthetic_issue_types[issue_number] = issue_type - target_index.synthetic_issue_titles[issue_number] = None - target_index.provisional_issue_numbers.add(issue_number) + entry: FileMatchTargetEntry = (None, None, False, None, None) + target_index.exact_number_map[exact_text] = entry + target_index.provisional_exact_types[exact_text] = issue_type + if exact_text == format_issue_number(issue_number): + target_index.number_map.setdefault(issue_number, entry) + target_index.synthetic_issue_types[issue_number] = issue_type + target_index.synthetic_issue_titles[issue_number] = None + target_index.provisional_issue_numbers.add(issue_number) def _has_trusted_issue_identity(imp_file: ImportedFile) -> bool: @@ -464,13 +489,12 @@ def _requested_issue_ids(files: list[ImportedFile]) -> list[int]: return sorted(issue_ids) -def _requested_issue_numbers(files: list[ImportedFile]) -> list[float]: - issue_numbers = { - float(issue_number) +def _requested_issue_numbers(files: list[ImportedFile]) -> list[float | str]: + return normalize_issue_number_queries( + candidate_issue_number_text(imp_file) or issue_number for imp_file in files if (issue_number := candidate_issue_number(imp_file)) is not None - } - return sorted(issue_numbers) + ) async def _load_issue_summaries_by_issue_ids( @@ -487,6 +511,7 @@ async def _load_issue_summaries_by_issue_ids( IssueSummary( provider_id=str(issue.provider_id), issue_number=issue.issue_number, + issue_number_text=issue.issue_number_text, title=issue.title, release_date=issue.release_date, cover_url=issue.cover_url, diff --git a/src/pullbox/services/import_file_matching.py b/src/pullbox/services/import_file_matching.py index b5283bcd..a363a482 100644 --- a/src/pullbox/services/import_file_matching.py +++ b/src/pullbox/services/import_file_matching.py @@ -76,6 +76,7 @@ scan_review_progress_pct, ) from pullbox.services.import_source_metadata import ( + corroborated_import_title_conflict, import_file_has_deferred_archive_metadata, load_archive_entry_issue_hint_for_import_file, ) @@ -2027,6 +2028,11 @@ def _evaluate_file_match_candidate( metadata_conflict: dict[str, Any] | None = None if match_candidate is not None: + metadata_conflict = corroborated_import_title_conflict( + file_metadata, imp_series.cv_title or imp_series.raw_series_name + ) + if metadata_conflict is not None: + return None, metadata_conflict if trusted_source_issue_identity_matches_target( imp_series, imp_file, @@ -2044,6 +2050,11 @@ def _evaluate_file_match_candidate( metadata=file_metadata, wanted_series=target_context.series_title, wanted_issue=float(target_context.issue_number or 0.0), + wanted_issue_number_text=( + match_candidate.matched_issue.effective_issue_number_text + if match_candidate.matched_issue is not None + else file_metadata.issue_number_text + ), wanted_year=target_context.series_year, wanted_issue_type=target_context.issue_type, wanted_issue_cv_id=target_context.issue_cv_id, diff --git a/src/pullbox/services/import_file_preparation.py b/src/pullbox/services/import_file_preparation.py index 1ff58c8e..4f73a695 100644 --- a/src/pullbox/services/import_file_preparation.py +++ b/src/pullbox/services/import_file_preparation.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any from pullbox.core.archive import inspect_archive_page_count as inspect_archive_page_count +from pullbox.core.archive_format import archive_format from pullbox.core.exceptions import NotFoundError, ValidationError from pullbox.core.file_safety import is_resource_safety_exception_allowed from pullbox.core.issue_numbers import format_issue_number @@ -65,7 +66,10 @@ async def prepare_import_file( converted=False, ) - if source_path.suffix.lower().lstrip(".") == "cbz": + if ( + source_path.suffix.lower() == ".cbz" + and await asyncio.to_thread(archive_format, source_path) == "cbz" + ): return PreparedImportFile( registration_source=source_path, original_source=source_path, @@ -211,7 +215,10 @@ async def rewrite_import_file_comicinfo( ) -> tuple[Path, str, str]: """Repair an imported archive's ComicInfo.xml, normalizing to CBZ when needed.""" source_path = Path(imp_file.file_path) - if source_path.suffix.lower() == ".cbz": + if ( + source_path.suffix.lower() == ".cbz" + and await asyncio.to_thread(archive_format, source_path) == "cbz" + ): await asyncio.to_thread( embedder, source_path, diff --git a/src/pullbox/services/import_file_resolution.py b/src/pullbox/services/import_file_resolution.py index 498f982e..1f7d6d14 100644 --- a/src/pullbox/services/import_file_resolution.py +++ b/src/pullbox/services/import_file_resolution.py @@ -11,6 +11,7 @@ from pullbox.models.issue import Issue from pullbox.models.series import Series from pullbox.services.import_file_issue_signals import candidate_issue_number_text +from pullbox.services.import_file_selection import not_excluded_from_review if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession @@ -26,6 +27,7 @@ async def load_importable_files( file_filters = [ ImportedFile.import_series_id == item.id, ImportedFile.status.in_([ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED]), + not_excluded_from_review(), ] if duplicate_mode: file_filters.append(ImportedFile.include_in_import.is_(True)) diff --git a/src/pullbox/services/import_file_selection.py b/src/pullbox/services/import_file_selection.py new file mode 100644 index 00000000..90f32ded --- /dev/null +++ b/src/pullbox/services/import_file_selection.py @@ -0,0 +1,71 @@ +"""Explicit review choices without changing legacy matched-file defaults.""" + +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql.elements import ColumnElement + +from pullbox.models.import_job import ( + ImportedFile, + ImportedFileStatus, + ImportedSeries, + ImportSeriesStatus, +) + + +def not_excluded_from_review() -> ColumnElement[bool]: + """Old rows with no explicit choice retain their existing eligibility.""" + return func.coalesce(ImportedFile.diagnostics["review_selection"].as_boolean(), True).is_(True) + + +def set_review_file_selection(file: ImportedFile, selected: bool) -> None: + file.include_in_import = selected + file.diagnostics = {**dict(file.diagnostics or {}), "review_selection": selected} + + +async def defer_excluded_review_files(session: AsyncSession, job_id: int) -> set[int]: + """At confirmation, retain opted-out files as actionable Follow-up decisions.""" + files = list( + ( + await session.scalars( + select(ImportedFile).where( + ImportedFile.import_job_id == job_id, + ImportedFile.status.in_( + [ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED] + ), + ImportedFile.diagnostics["review_selection"].as_boolean().is_(False), + ) + ) + ).all() + ) + affected = {file.import_series_id for file in files} + for file in files: + file.status = ImportedFileStatus.NO_MATCH + file.diagnostics = {**file.diagnostics, "review_deferred": True} + if affected: + selected_parents = set( + ( + await session.scalars( + select(ImportedFile.import_series_id) + .where( + ImportedFile.import_series_id.in_(affected), + ImportedFile.status.in_( + [ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED] + ), + not_excluded_from_review(), + ) + .distinct() + ) + ).all() + ) + parents = ( + await session.scalars(select(ImportedSeries).where(ImportedSeries.id.in_(affected))) + ).all() + for parent in parents: + if ( + parent.status in {ImportSeriesStatus.MATCHED, ImportSeriesStatus.DUPLICATE} + and parent.id not in selected_parents + ): + parent.status = ImportSeriesStatus.RECOVERY_PENDING + parent.selected_for_import = False + await session.flush() + return affected diff --git a/src/pullbox/services/import_orphans.py b/src/pullbox/services/import_orphans.py index 9dce5308..3098c9f9 100644 --- a/src/pullbox/services/import_orphans.py +++ b/src/pullbox/services/import_orphans.py @@ -30,6 +30,7 @@ volume_issue_number, ) from pullbox.services.import_file_resolution import load_issue_lookup_for_series +from pullbox.services.import_file_selection import set_review_file_selection from pullbox.services.import_job_actions import build_series_created_action_payload from pullbox.services.import_job_execution_items import ensure_target_issue_summary_for_import_file from pullbox.services.import_retry_helpers import require_retained_import_destination @@ -399,6 +400,7 @@ def apply_orphan_recovery_decisions( "kind": "orphan_recovery", "resolution": "assigned", } + set_review_file_selection(imp_file, True) def summarize_orphan_recovery_result( diff --git a/src/pullbox/services/import_path_identity.py b/src/pullbox/services/import_path_identity.py index 8c575934..bebd9047 100644 --- a/src/pullbox/services/import_path_identity.py +++ b/src/pullbox/services/import_path_identity.py @@ -11,12 +11,37 @@ validate_file_identity_signature, ) from pullbox.core.name_matcher import NameMatcher -from pullbox.core.source_metadata import MetadataSignal, SourceMetadata +from pullbox.core.release_parser import parse_release_title +from pullbox.core.source_metadata import MetadataSignal, SourceMetadata, SourceMetadataExtractor +from pullbox.models.issue import IssueType if TYPE_CHECKING: from pathlib import Path +def _same_issue_type(recorded: SourceMetadata, actual: SourceMetadata) -> bool: + if recorded.issue_type == actual.issue_type: + return True + # Mylar's ordinary issue table also holds numbered collected volumes. + parsed = SourceMetadataExtractor().from_release_title(recorded.original_title) + return bool( + recorded.issue_type == IssueType.ISSUE + and actual.issue_type == IssueType.VOLUME + and parsed.issue_type == IssueType.VOLUME + and parsed.volume is not None + and parsed.issue_number == recorded.issue_number + ) + + +def _publication_year(metadata: SourceMetadata) -> int | None: + comicinfo = metadata.diagnostics.get("comicinfo") + year = comicinfo.get("year") if isinstance(comicinfo, dict) else None + if isinstance(year, int) and not isinstance(year, bool) and year > 0: + return year + parsed = parse_release_title(metadata.original_title) + return parsed.year if parsed is not None else None + + def same_trusted_issue(recorded: SourceMetadata, actual: SourceMetadata) -> bool: """Require independent exact IDs; folder names and issue numbers are not proof.""" if ( @@ -26,7 +51,8 @@ def same_trusted_issue(recorded: SourceMetadata, actual: SourceMetadata) -> bool or actual.signals.get("comicvine_issue_id") != MetadataSignal.COMICINFO or recorded.diagnostics.get("identity_conflicts") or actual.diagnostics.get("identity_conflicts") - or recorded.issue_type != actual.issue_type + or actual.diagnostics.get("comicinfo_issue_number_ignored") + or not _same_issue_type(recorded, actual) ): return False if ( @@ -51,11 +77,13 @@ def same_trusted_issue(recorded: SourceMetadata, actual: SourceMetadata) -> bool return False recorded_issue = recorded.diagnostics.get("mylar3_issue") date = recorded_issue.get("release_date") if isinstance(recorded_issue, dict) else None + # SourceMetadata.year may be ComicInfo.Volume (the series start year). + publication_year = _publication_year(actual) return not ( isinstance(date, str) and date[:4].isdigit() - and actual.year is not None - and int(date[:4]) != actual.year + and publication_year is not None + and int(date[:4]) != publication_year ) diff --git a/src/pullbox/services/import_provider_cache.py b/src/pullbox/services/import_provider_cache.py index 5655e768..683384d3 100644 --- a/src/pullbox/services/import_provider_cache.py +++ b/src/pullbox/services/import_provider_cache.py @@ -17,11 +17,12 @@ from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker +from pullbox.core.issue_numbers import issue_number_lookup_value, normalize_issue_number_queries from pullbox.core.name_matcher import NameMatcher from pullbox.services.comicvine_persistent_cache import PersistentComicVineCacheProvider if TYPE_CHECKING: - from collections.abc import Callable, Coroutine + from collections.abc import Callable, Coroutine, Sequence from sqlalchemy.ext.asyncio import AsyncSession @@ -78,7 +79,9 @@ def __init__(self, provider: Any) -> None: self._series_cache: dict[str, asyncio.Task[Any]] = {} self._issue_cache: dict[str, asyncio.Task[Any]] = {} self._issues_cache: dict[str, asyncio.Task[Any]] = {} - self._issues_by_number_cache: dict[tuple[str, tuple[float, ...]], asyncio.Task[Any]] = {} + self._issues_by_number_cache: dict[ + tuple[str, tuple[float | str, ...]], asyncio.Task[Any] + ] = {} self._stats = _MemoryCacheStats() def __getattr__(self, name: str) -> Any: @@ -245,9 +248,9 @@ async def get_issues_for_series(self, series_provider_id: str) -> Any: async def get_issues_for_series_by_numbers( self, series_provider_id: str, - issue_numbers: list[float], + issue_numbers: Sequence[float | str], ) -> Any: - normalized_numbers = tuple(sorted({float(number) for number in issue_numbers})) + normalized_numbers = tuple(normalize_issue_number_queries(issue_numbers)) key = (str(series_provider_id), normalized_numbers) async def fetch() -> Any: @@ -265,7 +268,10 @@ async def fetch() -> Any: return [ summary for summary in summaries - if float(getattr(summary, "issue_number", 0.0)) in number_set + if issue_number_lookup_value( + getattr(summary, "issue_number_text", None) or summary.issue_number + ) + in number_set ] return await self._memoize( diff --git a/src/pullbox/services/import_reader_service.py b/src/pullbox/services/import_reader_service.py new file mode 100644 index 00000000..5b2b1409 --- /dev/null +++ b/src/pullbox/services/import_reader_service.py @@ -0,0 +1,82 @@ +"""Resolve staged one-page files for a read-only reader preview.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from pullbox.core.exceptions import ValidationError +from pullbox.core.filesystem_policy import resolve_preview_source +from pullbox.core.library_file_ownership import ( + build_file_identity_signature, + validate_file_identity_signature, +) +from pullbox.models.library import FileFormat +from pullbox.services.import_review_file_assignment import load_review_file +from pullbox.services.import_review_recheck import _retry_source_roots +from pullbox.services.reader_content_service import ( + ReaderSourceRecord, + ResolvedReaderSource, + resolve_reader_source, +) + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncSession + + +@dataclass(frozen=True) +class ImportReaderRecord: + source: ReaderSourceRecord + roots: tuple[Path, ...] + signature: dict[str, Any] + + +async def load_import_reader_record( + session: AsyncSession, job_id: int, file_id: int +) -> ImportReaderRecord: + job, series, file = await load_review_file(session, job_id, file_id) + block = file.diagnostics.get("safety_block", {}) + if block.get("category") != "single_page_comic": + raise ValidationError("This preview is only available for one-page archive review.") + roots = await _retry_source_roots(session, job, file_ids=[file_id]) + detected = file.diagnostics.get("archive_format", {}).get("detected") + return ImportReaderRecord( + source=ReaderSourceRecord( + issue_id=-file.id, + issue_title=Path(file.file_name).name, + issue_number="", + issue_number_value=0, + series_id=-series.id, + series_title=series.cv_title or series.raw_series_name, + # Separate staged previews from registered library cache identities. + library_file_id=-file.id, + file_path=file.file_path, + root_path="", + file_format=FileFormat(detected or file.file_format), + stored_file_hash=None, + ), + roots=tuple(roots), + signature=dict(file.source_signature or {}), + ) + + +def resolve_import_reader_source(record: ImportReaderRecord) -> ResolvedReaderSource: + """Recheck containment and scan identity outside the database session.""" + from dataclasses import replace + + raw = Path(record.source.file_path) + path = resolve_preview_source(raw) + lexical = raw.expanduser().absolute() + root = next( + ( + root + for root in record.roots + if lexical.is_relative_to(root) and path.is_relative_to(root.resolve(strict=True)) + ), + None, + ) + if root is None: + raise ValidationError("The file is outside this import's approved source folders.") + validate_file_identity_signature(record.signature, build_file_identity_signature(path)) + return resolve_reader_source(replace(record.source, root_path=str(root))) diff --git a/src/pullbox/services/import_review_actions.py b/src/pullbox/services/import_review_actions.py index d956584d..2f87d0e3 100644 --- a/src/pullbox/services/import_review_actions.py +++ b/src/pullbox/services/import_review_actions.py @@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable, Mapping from datetime import UTC, datetime -from sqlalchemy import or_ from sqlalchemy import select as sa_select from sqlalchemy.ext.asyncio import AsyncSession @@ -20,6 +19,7 @@ ImportSeriesStatus, ) from pullbox.services.import_duplicates import duplicate_merge_is_actionable, is_duplicate_series +from pullbox.services.import_file_selection import set_review_file_selection from pullbox.services.import_safety_diagnostics import normalize_import_safety_diagnostics from pullbox.services.import_story_arc_resolution import ( refresh_story_arc_entries_for_import_files, @@ -161,7 +161,7 @@ async def resolve_conflicts( if imp_file.id == normalized_chosen_file_id: imp_file.status = ImportedFileStatus.CONFIRMED imp_file.is_preferred = True - imp_file.include_in_import = True + set_review_file_selection(imp_file, True) else: imp_file.status = ImportedFileStatus.SKIPPED imp_file.is_preferred = False @@ -301,8 +301,6 @@ async def update_series_selection( raise NotFoundError("ImportedSeries", imported_series_id) if imported_series.status != ImportSeriesStatus.MATCHED: raise ValidationError("Only matched series can be selected for import") - if (imported_series.files_conflict or 0) > 0: - raise ValidationError("Resolve file conflicts before selecting this series") if include_in_import and (imported_series.files_matched or 0) <= 0: raise ValidationError("This series has no importable files to select") @@ -644,10 +642,6 @@ async def bulk_update_series_selection( filters.extend( [ ImportedSeries.status == ImportSeriesStatus.MATCHED, - or_( - ImportedSeries.files_conflict.is_(None), - ImportedSeries.files_conflict == 0, - ), ImportedSeries.files_matched > 0, ] ) @@ -682,15 +676,6 @@ async def bulk_update_series_selection( f"resolve or exclude these items first: {invalid}" ) - unresolved_conflicts = { - item.id: item.files_conflict for item in items if (item.files_conflict or 0) > 0 - } - if unresolved_conflicts: - raise ValidationError( - "Resolve file conflicts before selecting import series; " - f"conflicted series: {unresolved_conflicts}" - ) - empty_matches = { item.id for item in items if include_in_import and (item.files_matched or 0) <= 0 } @@ -714,7 +699,7 @@ async def update_file_selection( *, include_in_import: bool, ) -> ImportedFile: - """Persist include/exclude state for an importable duplicate-series file.""" + """Persist an explicit choice for a ready file without changing its match.""" job = await session.get(ImportJob, job_id) if job is None: raise NotFoundError("ImportJob", job_id) @@ -726,14 +711,17 @@ async def update_file_selection( raise NotFoundError("ImportedFile", file_id) parent_series = await session.get(ImportedSeries, imp_file.import_series_id) - if not is_duplicate_series(parent_series): - raise ValidationError("Only duplicate-series files can be selected individually") - if not duplicate_merge_is_actionable(parent_series): + if parent_series is None or parent_series.status not in { + ImportSeriesStatus.MATCHED, + ImportSeriesStatus.DUPLICATE, + }: + raise ValidationError("Only files in an identified series can be selected individually") + if is_duplicate_series(parent_series) and not duplicate_merge_is_actionable(parent_series): raise ValidationError("This duplicate series has no wanted or missing issues to import.") - if imp_file.status != ImportedFileStatus.MATCHED: + if imp_file.status not in {ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED}: raise ValidationError("Only importable matched files can be selected") - imp_file.include_in_import = include_in_import + set_review_file_selection(imp_file, include_in_import) await session.flush() return imp_file diff --git a/src/pullbox/services/import_review_file_assignment.py b/src/pullbox/services/import_review_file_assignment.py new file mode 100644 index 00000000..36c261cb --- /dev/null +++ b/src/pullbox/services/import_review_file_assignment.py @@ -0,0 +1,217 @@ +"""Explicit per-file reassignment without modifying source files.""" + +from __future__ import annotations + +from dataclasses import asdict +from typing import TYPE_CHECKING + +from sqlalchemy import func, select + +from pullbox.core.exceptions import NotFoundError, ValidationError +from pullbox.models.import_job import ( + ImportControlRequest, + ImportedFile, + ImportedFileStatus, + ImportedSeries, + ImportJob, + ImportJobStatus, + ImportSeriesStatus, +) +from pullbox.models.issue import Issue +from pullbox.models.library import LibraryFile +from pullbox.models.series import Series +from pullbox.services.import_counters import recompute_file_counters, recompute_series_counters +from pullbox.services.import_review_scope import review_scope +from pullbox.services.import_story_arc_resolution import refresh_story_arc_entries_for_import_files + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncSession + + from pullbox.services.metadata_service import MetadataService + + +async def load_review_file( + session: AsyncSession, job_id: int, file_id: int +) -> tuple[ImportJob, ImportedSeries, ImportedFile]: + job = await session.get(ImportJob, job_id) + file = await session.get(ImportedFile, file_id) + if job is None or file is None or file.import_job_id != job_id: + raise NotFoundError("ImportedFile", file_id) + if ( + job.status is not ImportJobStatus.REVIEW + or job.control_request is not ImportControlRequest.NONE + ): + raise ValidationError("This import is no longer available for review.") + parent = await session.get(ImportedSeries, file.import_series_id) + if parent is None or parent.import_job_id != job_id: + raise NotFoundError("ImportedSeries", file.import_series_id) + if parent.diagnostics.get("rematch_pending"): + raise ValidationError("Wait for this series to finish preparing its match.") + return job, parent, file + + +async def assign_review_file( + session: AsyncSession, + job_id: int, + file_id: int, + *, + cv_id: int, + issue_cv_id: int, + metadata_service: MetadataService, +) -> ImportedFile: + """Change one staged identity; never move a file or rewrite its source metadata.""" + job, parent, file = await load_review_file(session, job_id, file_id) + original_scope = review_scope(job, parent, file) + if file.status not in { + ImportedFileStatus.CONFLICT, + ImportedFileStatus.NO_MATCH, + ImportedFileStatus.MATCHED, + ImportedFileStatus.CONFIRMED, + }: + raise ValidationError("Resolve source inspection before assigning this file.") + signals = file.diagnostics.get("metadata_signals", {}) + if ( + signals.get("comicvine_issue_id") in {"comicinfo", "sidecar"} + and file.comicvine_issue_id + and file.comicvine_issue_id != issue_cv_id + ): + raise ValidationError( + "The embedded ComicVine identity identifies a different issue. " + "Correct that source evidence and recheck it first." + ) + series_metadata = await metadata_service.get_series_metadata(cv_id) + summaries = await metadata_service.get_issue_summaries_for_series(cv_id) + await session.refresh(job) + await session.refresh(parent) + await session.refresh(file) + if review_scope(job, parent, file) != original_scope or parent.diagnostics.get( + "rematch_pending" + ): + raise ValidationError( + "This review changed while the issue choices were loading. Try again." + ) + summary = next((value for value in summaries if value.provider_id == str(issue_cv_id)), None) + if str(series_metadata.provider_id) != str(cv_id) or summary is None: + raise ValidationError("The chosen issue does not belong to the selected series.") + library_series = await session.scalar(select(Series).where(Series.comicvine_id == cv_id)) + issue = await session.scalar(select(Issue).where(Issue.comicvine_id == issue_cv_id)) + if issue is not None and (library_series is None or issue.series_id != library_series.id): + raise ValidationError("The saved issue belongs to a different library series.") + owned = ( + issue is not None + and await session.scalar(select(LibraryFile.id).where(LibraryFile.issue_id == issue.id)) + is not None + ) + target = await session.scalar( + select(ImportedSeries) + .where( + ImportedSeries.import_job_id == job_id, + ImportedSeries.cv_id == cv_id, + ImportedSeries.status.in_([ImportSeriesStatus.MATCHED, ImportSeriesStatus.DUPLICATE]), + ) + .order_by(ImportedSeries.id) + .limit(1) + ) + if target is not None and target.diagnostics.get("rematch_pending"): + raise ValidationError("Wait for the destination series to finish preparing its match.") + if target is None: + target = ImportedSeries( + import_job_id=job_id, + raw_series_name=series_metadata.title, + raw_year=series_metadata.year_start, + cv_id=cv_id, + user_selected_cv_id=cv_id, + cv_title=series_metadata.title, + cv_year=series_metadata.year_start, + cv_publisher=series_metadata.publisher, + cv_issue_count=series_metadata.issue_count, + cv_url=series_metadata.comicvine_url, + cv_match_method="user_override", + cv_match_score=1.0, + source_folder=parent.source_folder, + sample_paths=[file.file_path], + has_files=True, + status=ImportSeriesStatus.DUPLICATE if library_series else ImportSeriesStatus.MATCHED, + series_id=library_series.id if library_series else None, + selected_for_import=not owned, + diagnostics={ + "kind": "duplicate_series" if library_series else "manual_file_assignment" + }, + ) + session.add(target) + await session.flush() + other_targets = list( + ( + await session.scalars( + select(ImportedFile).where( + ImportedFile.import_job_id == job_id, + ImportedFile.import_series_id == target.id, + ImportedFile.id != file.id, + ImportedFile.matched_issue_cv_id == issue_cv_id, + ImportedFile.status.in_( + [ + ImportedFileStatus.MATCHED, + ImportedFileStatus.CONFIRMED, + ImportedFileStatus.CONFLICT, + ] + ), + ) + ) + ).all() + ) + old_series_id = parent.id + file.import_series_id = target.id + file.matched_issue_cv_id = issue_cv_id + file.matched_issue_id = issue.id if issue is not None else None + file.status = ImportedFileStatus.ALREADY_OWNED if owned else ImportedFileStatus.MATCHED + file.include_in_import = not owned + file.match_method = "import_reconcile" + file.match_confidence = "manual" + file.conflict_group_id = None + file.duplicate_group_id = None + file.duplicate_of_file_id = None + file.is_preferred = False + file.error_message = None + file.diagnostics = { + **file.diagnostics, + "review_selection": not owned, + "target_issue_summary": asdict(summary), + "target_state": "already_owned" if owned else "missing", + "manual_file_assignment": { + "source_series_id": old_series_id, + "target_series_id": target.id, + "target_series_cv_id": cv_id, + "target_issue_cv_id": issue_cv_id, + }, + } + # An explicit reassignment cannot silently choose between two files for one issue. + if other_targets and not owned: + group_id = ( + int( + await session.scalar( + select(func.max(ImportedFile.conflict_group_id)).where( + ImportedFile.import_job_id == job_id + ) + ) + or 0 + ) + + 1 + ) + for value in [file, *other_targets]: + value.status = ImportedFileStatus.CONFLICT + value.conflict_group_id = group_id + value.include_in_import = False + value.is_preferred = False + await session.flush() + await recompute_file_counters(session, job, series_ids=list({old_series_id, target.id})) + for row in (parent, target): + row.file_count = row.files_total + if not row.files_total: + row.status = ImportSeriesStatus.SKIPPED + row.selected_for_import = False + await recompute_series_counters(session, job) + await refresh_story_arc_entries_for_import_files( + session, import_job_id=job_id, import_file_ids=[file.id] + ) + await session.flush() + return file diff --git a/src/pullbox/services/import_review_one_page.py b/src/pullbox/services/import_review_one_page.py new file mode 100644 index 00000000..4d962cd3 --- /dev/null +++ b/src/pullbox/services/import_review_one_page.py @@ -0,0 +1,125 @@ +"""Exact, source-preserving decisions for one series' one-page archives.""" + +from collections.abc import Mapping, Sequence +from typing import Any, Literal + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from pullbox.core.exceptions import NotFoundError, ValidationError +from pullbox.models.import_job import ( + ImportControlRequest, + ImportedFile, + ImportedFileStatus, + ImportedSeries, + ImportJob, + ImportJobStatus, + ImportSeriesStatus, +) +from pullbox.services.import_counters import recompute_file_counters, recompute_series_counters +from pullbox.services.import_review_actions import ( + apply_safety_allow_once_to_file, + apply_safety_skip_to_file, + prepare_series_for_safety_rematch, +) +from pullbox.services.import_review_scope import review_scope, verify_review_scope +from pullbox.services.import_safety_diagnostics import normalize_import_safety_diagnostics +from pullbox.services.import_story_arc_resolution import refresh_story_arc_entries_for_import_files + +OnePageAction = Literal["allow", "skip"] + + +def one_page_files(files: Sequence[ImportedFile]) -> list[ImportedFile]: + """Exclude every other safety category, even in the same series folder.""" + result = [] + for file in files: + block = (file.diagnostics or {}).get("safety_block") + if ( + file.status == ImportedFileStatus.SAFETY_BLOCKED + and isinstance(block, Mapping) + and normalize_import_safety_diagnostics(block)["category"] == "single_page_comic" + ): + result.append(file) + return sorted(result, key=lambda file: file.id) + + +def one_page_scope( + job: ImportJob, + series: ImportedSeries, + files: Sequence[ImportedFile], + *, + actor_id: int, + action: OnePageAction, +) -> dict[str, Any]: + return { + "actor": actor_id, + "action": f"one-page-{action}", + "job": job.id, + "series": series.id, + "files": [review_scope(job, series, file) for file in files], + } + + +async def decide_one_page_series( + session: AsyncSession, + job_id: int, + series_id: int, + *, + actor_id: int, + action: OnePageAction, + token: str, +) -> ImportedSeries: + job = await session.get(ImportJob, job_id) + series = await session.get(ImportedSeries, series_id) + if job is None: + raise NotFoundError("ImportJob", job_id) + if series is None or series.import_job_id != job_id: + raise NotFoundError("ImportedSeries", series_id) + if job.status != ImportJobStatus.REVIEW or job.control_request != ImportControlRequest.NONE: + raise ValidationError("This import is no longer ready for review.") + files = one_page_files( + ( + await session.scalars( + select(ImportedFile).where( + ImportedFile.import_job_id == job_id, + ImportedFile.import_series_id == series_id, + ImportedFile.status == ImportedFileStatus.SAFETY_BLOCKED, + ) + ) + ).all() + ) + if not files: + raise ValidationError("These one-page files have already changed. Refresh the review.") + await verify_review_scope( + session, token, one_page_scope(job, series, files, actor_id=actor_id, action=action) + ) + if action == "allow" and any( + normalize_import_safety_diagnostics(file.diagnostics["safety_block"])["overrideable"] + is not True + for file in files + ): + raise ValidationError("One or more of these files cannot be allowed once.") + + # Verify the complete visible scope before making any changes, and rematch only once. + for file in files: + if action == "allow": + apply_safety_allow_once_to_file(file) + else: + apply_safety_skip_to_file(file) + if action == "allow": + prepare_series_for_safety_rematch(series) + series.selected_for_import = False + await refresh_story_arc_entries_for_import_files( + session, import_job_id=job_id, import_file_ids=[file.id for file in files] + ) + await recompute_file_counters(session, job, series_ids=[series.id]) + if action == "skip" and not ( + series.files_matched + or series.files_conflict + or series.files_no_match + or (series.diagnostics or {}).get("safety_blocked_files") + ): + series.status = ImportSeriesStatus.SKIPPED + await recompute_series_counters(session, job) + await session.flush() + return series diff --git a/src/pullbox/services/import_review_recheck.py b/src/pullbox/services/import_review_recheck.py index 29f2d86e..0fa09a57 100644 --- a/src/pullbox/services/import_review_recheck.py +++ b/src/pullbox/services/import_review_recheck.py @@ -417,10 +417,12 @@ async def _retry_source_roots( else: candidates.extend(Path(value) for value in dict(job.mylar3_path_map or {}).values()) signature_query = select(ImportedFile.source_signature).where( - *retryable_failed_source_filters(job.id) + ImportedFile.import_job_id == job.id ) if file_ids is not None: signature_query = signature_query.where(ImportedFile.id.in_(file_ids)) + else: + signature_query = signature_query.where(*retryable_failed_source_filters(job.id)) signature_rows = await session.scalars(signature_query) root_ids = { root_id @@ -442,6 +444,11 @@ async def _retry_source_roots( root_query = root_query.where(LibraryRoot.allow_referenced_registrations.is_(True)) candidates.extend(Path(root.path) for root in (await session.scalars(root_query)).all()) + return await asyncio.to_thread(_validated_source_roots, candidates) + + +def _validated_source_roots(candidates: list[Path]) -> list[Path]: + """Resolve possibly remote filesystem roots without blocking the event loop.""" roots: list[Path] = [] seen: set[tuple[str, str]] = set() for candidate in candidates: diff --git a/src/pullbox/services/import_review_scope.py b/src/pullbox/services/import_review_scope.py new file mode 100644 index 00000000..fb7053a7 --- /dev/null +++ b/src/pullbox/services/import_review_scope.py @@ -0,0 +1,60 @@ +"""Short-lived actor-bound previews for individual review mutations.""" + +from __future__ import annotations + +import hashlib +import json +from typing import TYPE_CHECKING, Any + +from itsdangerous import BadSignature, URLSafeTimedSerializer + +from pullbox.core.config_resolver import get_application_secret +from pullbox.core.exceptions import ValidationError + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncSession + + from pullbox.models.import_job import ImportedFile, ImportedSeries, ImportJob + + +def review_scope(job: ImportJob, parent: ImportedSeries, file: ImportedFile) -> str: + diagnostics = { + key: value for key, value in file.diagnostics.items() if key != "review_source_action" + } + payload = [ + job.id, + job.status.value, + job.control_request.value, + job.source_path, + job.mylar3_path_map, + job.file_handling_mode.value, + parent.id, + parent.cv_id, + parent.user_selected_cv_id, + parent.series_id, + file.id, + file.file_path, + file.status.value, + file.matched_issue_id, + file.matched_issue_cv_id, + file.include_in_import, + file.conflict_group_id, + file.source_signature, + diagnostics, + ] + return hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest() + + +async def sign_review_scope(session: AsyncSession, payload: dict[str, Any]) -> str: + signer = URLSafeTimedSerializer(get_application_secret(), salt="import-review-file-v1") + return signer.dumps(payload) + + +async def verify_review_scope(session: AsyncSession, token: str, expected: dict[str, Any]) -> None: + signer = URLSafeTimedSerializer(get_application_secret(), salt="import-review-file-v1") + try: + actual = signer.loads(token, max_age=900) + except BadSignature as exc: + raise ValidationError("This preview expired. Open the action again.") from exc + if actual != expected: + raise ValidationError("This file changed after the preview. Open the action again.") diff --git a/src/pullbox/services/import_review_selection.py b/src/pullbox/services/import_review_selection.py index 18b18172..b4f17e9a 100644 --- a/src/pullbox/services/import_review_selection.py +++ b/src/pullbox/services/import_review_selection.py @@ -13,6 +13,7 @@ ImportSeriesStatus, ) from pullbox.services.import_duplicates import duplicate_merge_is_actionable +from pullbox.services.import_file_selection import not_excluded_from_review if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession @@ -30,7 +31,6 @@ async def load_import_review_selection_state( .where( ImportedSeries.import_job_id == job_id, ImportedSeries.status == ImportSeriesStatus.MATCHED, - or_(ImportedSeries.files_conflict.is_(None), ImportedSeries.files_conflict == 0), ImportedSeries.files_matched > 0, ) .order_by(ImportedSeries.id.asc()) @@ -42,9 +42,22 @@ async def load_import_review_selection_state( .where( ImportedSeries.import_job_id == job_id, ImportedSeries.status == ImportSeriesStatus.MATCHED, - or_(ImportedSeries.files_conflict.is_(None), ImportedSeries.files_conflict == 0), ImportedSeries.files_matched > 0, ImportedSeries.selected_for_import.is_(True), + or_( + select(ImportedFile.id) + .where( + ImportedFile.import_series_id == ImportedSeries.id, + ImportedFile.status.in_(_IMPORTABLE_FILE_STATUSES), + not_excluded_from_review(), + ) + .exists(), + ~select(ImportedFile.id) + .where( + ImportedFile.import_series_id == ImportedSeries.id, + ) + .exists(), + ), ) .order_by(ImportedSeries.id.asc()) ) diff --git a/src/pullbox/services/import_review_series_choice.py b/src/pullbox/services/import_review_series_choice.py new file mode 100644 index 00000000..c6616e71 --- /dev/null +++ b/src/pullbox/services/import_review_series_choice.py @@ -0,0 +1,109 @@ +"""Source-preserving series exclusion with actor-bound, revalidated previews.""" + +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from pullbox.core.exceptions import NotFoundError, ValidationError +from pullbox.models.import_job import ( + ImportControlRequest, + ImportedFile, + ImportedSeries, + ImportJob, + ImportJobStatus, + ImportSeriesStatus, +) +from pullbox.services.import_counters import recompute_series_counters +from pullbox.services.import_review_scope import review_scope, verify_review_scope + + +async def series_choice_scope( + session: AsyncSession, + job_id: int, + series_id: int, + action: str, + actor_id: int, +) -> tuple[ImportJob, ImportedSeries, dict[str, object]]: + job = await session.get(ImportJob, job_id) + series = await session.get(ImportedSeries, series_id) + if job is None or series is None or series.import_job_id != job_id: + raise NotFoundError("ImportedSeries", series_id) + if ( + job.status != ImportJobStatus.REVIEW + or job.control_request != ImportControlRequest.NONE + or (series.diagnostics or {}).get("rematch_pending") + ): + raise ValidationError("This import is busy or no longer ready for review.") + prior = (series.diagnostics or {}).get("review_series_skip") + if action == "restore": + if series.status != ImportSeriesStatus.SKIPPED or not isinstance(prior, dict): + raise ValidationError("This series has no review skip to undo.") + elif action != "skip" or series.status in { + ImportSeriesStatus.SKIPPED, + ImportSeriesStatus.IMPORTED, + ImportSeriesStatus.CONFIRMED, + }: + raise ValidationError("This series cannot be skipped from review.") + files = ( + await session.scalars( + select(ImportedFile) + .where( + ImportedFile.import_job_id == job_id, + ImportedFile.import_series_id == series_id, + ) + .order_by(ImportedFile.id) + ) + ).all() + if any( + (f.diagnostics or {}).get("review_source_action", {}).get("state") + in {"pending", "matching"} + for f in files + ): + raise ValidationError("Wait for this series' source verification before changing it.") + return ( + job, + series, + { + "action": "review-series-" + action, + "actor": actor_id, + "job": job_id, + "series": series_id, + "status": series.status.value, + "selected": series.selected_for_import, + "diagnostics": series.diagnostics, + "files": [review_scope(job, series, file) for file in files], + }, + ) + + +async def apply_series_choice( + session: AsyncSession, + job_id: int, + series_id: int, + action: str, + actor_id: int, + token: str, +) -> None: + job, series, scope = await series_choice_scope(session, job_id, series_id, action, actor_id) + await verify_review_scope(session, token, scope) + diagnostics = dict(series.diagnostics or {}) + if action == "skip": + diagnostics["review_series_skip"] = {"previous_status": series.status.value} + series.status = ImportSeriesStatus.SKIPPED + else: + previous = diagnostics.pop("review_series_skip") + series.status = ImportSeriesStatus(previous["previous_status"]) + if series.status == ImportSeriesStatus.DUPLICATE: + # Duplicate-series eligibility is stored per file, not on the parent checkbox. + await session.execute( + update(ImportedFile) + .where( + ImportedFile.import_job_id == job_id, + ImportedFile.import_series_id == series_id, + ) + .values(include_in_import=False) + ) + # Keep file matches and safety evidence intact. Restoring never auto-selects a series. + series.diagnostics = diagnostics + series.selected_for_import = False + await recompute_series_counters(session, job) + await session.flush() diff --git a/src/pullbox/services/import_review_source_actions.py b/src/pullbox/services/import_review_source_actions.py new file mode 100644 index 00000000..27f95295 --- /dev/null +++ b/src/pullbox/services/import_review_source_actions.py @@ -0,0 +1,403 @@ +"""Durable, bounded reinspection and exact stale-reference pairing in review.""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from sqlalchemy import select + +from pullbox.core.exceptions import ValidationError +from pullbox.core.file_safety import ( + get_archive_size_limit_bytes, + is_dangerous_file_blocking_enabled, +) +from pullbox.models.import_job import ( + ImportControlRequest, + ImportedFile, + ImportedFileStatus, + ImportedSeries, + ImportJob, + ImportJobStatus, + ImportSeriesStatus, +) +from pullbox.models.library import LibraryFile +from pullbox.models.operation_progress import OperationProgressState, OperationProgressType +from pullbox.services.import_counters import recompute_file_counters, recompute_series_counters +from pullbox.services.import_path_identity import ( + reconciliation_evidence, + same_trusted_issue, + unchanged_same_folder_pair, +) +from pullbox.services.import_review_file_assignment import load_review_file +from pullbox.services.import_review_recheck import ( + _apply_file, + _retry_source_roots, + inspect_review_source, +) +from pullbox.services.import_review_scope import review_scope +from pullbox.services.import_source_metadata import source_metadata_for_import_file +from pullbox.services.import_story_arc_resolution import refresh_story_arc_entries_for_import_files +from pullbox.services.operation_progress import ( + OperationProgressMeasure, + OperationProgressUpdate, + publish_operation_progress, +) + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +RECHECK_CATEGORIES = frozenset( + { + "permission_unreadable", + "archive_inspection_failed", + "zero_byte", + "archive_no_pages", + "source_changed", + "source_missing", + } +) + + +async def source_action_progress( + session: AsyncSession, job_id: int, file_id: int, state: OperationProgressState +) -> None: + await publish_operation_progress( + session, + OperationProgressUpdate( + operation_type=OperationProgressType.UTILITY, + operation_key=f"import-review-source:{job_id}:{file_id}", + group_key=f"import:{job_id}", + revision=None, + state=state, + phase="source_verification", + title="Import source verification", + message="Return to review for the result." + if state.is_terminal + else "Checking the source and its identity in the background.", + detail_url=f"/import?resume_job_id={job_id}&resume_step=3", + overall=OperationProgressMeasure(current=1, total=1, unit="files") + if state is OperationProgressState.COMPLETED + else OperationProgressMeasure(), + ), + ) + + +async def replacement_candidate( + session: AsyncSession, file: ImportedFile, parent: ImportedSeries +) -> ImportedFile | None: + """Use bounded saved evidence only. The worker must still verify both paths.""" + if ( + not file.comicvine_issue_id + or file.status is not ImportedFileStatus.SAFETY_BLOCKED + or file.diagnostics.get("safety_block", {}).get("category") != "source_missing" + ): + return None + candidates = list( + ( + await session.scalars( + select(ImportedFile) + .where( + ImportedFile.import_series_id == parent.id, + ImportedFile.id != file.id, + ImportedFile.comicvine_issue_id == file.comicvine_issue_id, + ImportedFile.file_size > 0, + ImportedFile.status.in_( + [ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED] + ), + ) + .limit(2) + ) + ).all() + ) + if len(candidates) != 1: + return None + actual = candidates[0] + if Path(actual.file_path).parent != Path(file.file_path).parent or not actual.source_signature: + return None + if not same_trusted_issue( + source_metadata_for_import_file(parent, file), + source_metadata_for_import_file(parent, actual), + ): + return None + if await session.scalar( + select(LibraryFile.id).where(LibraryFile.file_path == actual.file_path).limit(1) + ): + return None + return actual + + +async def queue_source_action( + session: AsyncSession, job_id: int, file_id: int, *, action: str = "recheck", actor_id: int = 1 +) -> None: + job, parent, file = await load_review_file(session, job_id, file_id) + category = file.diagnostics.get("safety_block", {}).get("category") + if file.status is not ImportedFileStatus.SAFETY_BLOCKED or category not in RECHECK_CATEGORIES: + raise ValidationError( + "This file cannot be rechecked here. Resolve its safety or root setup first." + ) + if action not in {"recheck", "pair"}: + raise ValidationError("Unknown source review action.") + candidate = await replacement_candidate(session, file, parent) if action == "pair" else None + if action == "pair" and candidate is None: + raise ValidationError("There is no unique, proven replacement for this reference.") + work = { + "state": "pending", + "action": action, + "actor_id": actor_id, + "scope": review_scope(job, parent, file), + "queued_at": datetime.now(UTC).isoformat(), + "candidate_id": candidate.id if candidate else None, + "candidate_scope": review_scope(job, parent, candidate) if candidate else None, + } + file.diagnostics = {**file.diagnostics, "review_source_action": work} + parent.diagnostics = { + **parent.diagnostics, + "rematch_pending": True, + "review_source_file_id": file.id, + } + await source_action_progress(session, job_id, file_id, OperationProgressState.QUEUED) + await session.flush() + + +def _pair_is_unchanged(recorded: Path, actual: Path, signature: dict[str, Any]) -> None: + if not unchanged_same_folder_pair(recorded, actual, signature): + raise ValidationError("The recorded path or replacement changed. Recheck the source first.") + + +async def process_source_action( + factory: async_sessionmaker[AsyncSession], job_id: int, file_id: int +) -> int | None: + """Inspect one archive off-thread with no open database transaction or writer lock.""" + async with factory() as session: + file = await session.get(ImportedFile, file_id) + job = await session.get(ImportJob, job_id) + if file is None or job is None or file.import_job_id != job_id: + return None + parent = await session.get(ImportedSeries, file.import_series_id) + if parent is None: + return None + work = dict(file.diagnostics.get("review_source_action") or {}) + if work.get("state") == "matching": + if ( + job.status is ImportJobStatus.REVIEW + and job.control_request is ImportControlRequest.NONE + ): + return parent.id + raise ValidationError("This import is no longer accepting review actions.") + if work.get("state") != "pending": + return None + original_scope = review_scope(job, parent, file) + candidate = ( + await session.get(ImportedFile, work["candidate_id"]) + if work.get("candidate_id") + else None + ) + inspected = candidate or file + base = source_metadata_for_import_file(parent, inspected) + recorded_base = source_metadata_for_import_file(parent, file) + source_path = Path(inspected.file_path) + recorded_path = Path(file.file_path) + signature = dict(inspected.source_signature) + roots = await _retry_source_roots(session, job, file_ids=[inspected.id]) + block_dangerous = await is_dangerous_file_blocking_enabled(session) + limit = await get_archive_size_limit_bytes(session) + valid = ( + original_scope == work.get("scope") + and job.status is ImportJobStatus.REVIEW + and job.control_request is ImportControlRequest.NONE + ) + if candidate: + valid = valid and review_scope(job, parent, candidate) == work.get("candidate_scope") + await source_action_progress(session, job_id, file_id, OperationProgressState.RUNNING) + await session.commit() + error = "The review changed while this action was queued. Open it again." + result = None + if valid: + try: + if candidate: + await asyncio.to_thread(_pair_is_unchanged, recorded_path, source_path, signature) + resolved_roots = await asyncio.to_thread( + lambda: [(root, root.resolve()) for root in roots] + ) + result = await asyncio.to_thread( + inspect_review_source, + source_path, + base, + signature, + roots=resolved_roots, + block_dangerous=block_dangerous, + max_archive_size=limit, + accept_replaced_files=candidate is None, + sidecars={}, + ) + if candidate: + metadata, content, _signature = result + if "file_safety" in content or not same_trusted_issue(recorded_base, metadata): + raise ValidationError( + "The replacement did not pass source and exact-identity verification." + ) + await asyncio.to_thread(_pair_is_unchanged, recorded_path, source_path, signature) + except Exception: + result = None + error = ( + "Source verification could not complete. " + "Check access and the replacement, then recheck." + ) + async with factory() as session: + file = await session.get(ImportedFile, file_id) + job = await session.get(ImportJob, job_id) + if file is None or job is None: + return None + parent = await session.get(ImportedSeries, file.import_series_id) + if parent is None: + return None + current_work = file.diagnostics.get("review_source_action") or {} + if current_work != work: + return None + candidate = ( + await session.get(ImportedFile, work["candidate_id"]) + if work.get("candidate_id") + else None + ) + if ( + review_scope(job, parent, file) != original_scope + or job.status is not ImportJobStatus.REVIEW + or job.control_request is not ImportControlRequest.NONE + ): + result = None + if work.get("candidate_id") and ( + candidate is None or review_scope(job, parent, candidate) != work.get("candidate_scope") + ): + result = None + current_roots = await _retry_source_roots( + session, job, file_ids=[candidate.id if candidate else file.id] + ) + if set(current_roots) != set(roots): + result = None + error = "The allowed source locations changed. Recheck the import setup first." + needs_match = False + if result is not None: + metadata, content, fresh_signature = result + if candidate: + evidence = reconciliation_evidence( + file.file_path, candidate.file_path, int(candidate.comicvine_issue_id or 0) + ) + evidence["recorded_file_id"] = file.id + file.status = ImportedFileStatus.SKIPPED + file.include_in_import = False + file.match_method = "verified_path_reconciliation" + file.diagnostics = {**file.diagnostics, "mylar3_path_reconciliation": evidence} + candidate.diagnostics = { + **candidate.diagnostics, + "mylar3_path_reconciliation": evidence, + } + else: + previous = dict(file.diagnostics) + previous_signature = dict(file.source_signature) + _apply_file(file, metadata, content, fresh_signature) + file.source_signature = {**previous_signature, **file.source_signature} + file.diagnostics = { + **file.diagnostics, + "review_source_previous": previous.get("safety_block"), + **( + {"review_selection": previous["review_selection"]} + if "review_selection" in previous + else {} + ), + } + if file.status is ImportedFileStatus.PENDING: + needs_match = parent.status in { + ImportSeriesStatus.MATCHED, + ImportSeriesStatus.DUPLICATE, + } + file.status = ( + ImportedFileStatus.SAFETY_APPROVED + if needs_match + else ImportedFileStatus.NO_MATCH + ) + file.diagnostics = { + **file.diagnostics, + "review_source_action": { + **work, + "state": "matching" + if needs_match + else "completed" + if result is not None + else "failed", + "error": None if result is not None else error, + "completed_at": datetime.now(UTC).isoformat(), + }, + } + diagnostics = dict(parent.diagnostics) + if not needs_match and diagnostics.get("review_source_file_id") == file.id: + diagnostics.pop("rematch_pending", None) + diagnostics.pop("review_source_file_id", None) + parent.diagnostics = diagnostics + await session.flush() + await recompute_file_counters(session, job, series_ids=[parent.id]) + await recompute_series_counters(session, job) + await refresh_story_arc_entries_for_import_files( + session, import_job_id=job_id, import_file_ids=[file.id] + ) + await source_action_progress( + session, + job_id, + file_id, + OperationProgressState.RUNNING + if needs_match + else OperationProgressState.COMPLETED + if result is not None + else OperationProgressState.FAILED, + ) + await session.commit() + return parent.id if needs_match else None + + +async def finish_source_action( + factory: async_sessionmaker[AsyncSession], job_id: int, file_id: int, *, failed: bool = False +) -> None: + """Close durable work after matching, including unexpected worker failures.""" + async with factory() as session: + file = await session.get(ImportedFile, file_id) + if file is None or file.import_job_id != job_id: + return + work = dict(file.diagnostics.get("review_source_action") or {}) + if work.get("state") not in {"pending", "matching"}: + return + parent = await session.get(ImportedSeries, file.import_series_id) + job = await session.get(ImportJob, job_id) + failed = failed or parent is None or job is None + if job: + failed = ( + failed + or job.status is not ImportJobStatus.REVIEW + or job.control_request is not ImportControlRequest.NONE + ) + if parent: + failed = failed or bool(parent.diagnostics.get("rematch_error")) + diagnostics = dict(parent.diagnostics) + if diagnostics.get("review_source_file_id") == file.id: + diagnostics.pop("rematch_pending", None) + diagnostics.pop("review_source_file_id", None) + parent.diagnostics = diagnostics + file.diagnostics = { + **file.diagnostics, + "review_source_action": { + **work, + "state": "failed" if failed else "completed", + "error": "Source matching could not complete. Recheck the source or find its issue." + if failed + else None, + "completed_at": datetime.now(UTC).isoformat(), + }, + } + await source_action_progress( + session, + job_id, + file_id, + OperationProgressState.FAILED if failed else OperationProgressState.COMPLETED, + ) + await session.commit() diff --git a/src/pullbox/services/import_scan_helpers.py b/src/pullbox/services/import_scan_helpers.py index 69c08a4c..8d4e96a2 100644 --- a/src/pullbox/services/import_scan_helpers.py +++ b/src/pullbox/services/import_scan_helpers.py @@ -138,6 +138,31 @@ def inspect(path: str) -> InspectionResult: yield results +def _mark_missing_mylar_references(discovered: list[DiscoveredSeries]) -> None: + for series in discovered: + for file in series.files: + diagnostics = file.metadata_diagnostics + if ( + file.source_signature + or "mylar3_issue" not in diagnostics + or "file_safety" in diagnostics + ): + continue + try: + Path(file.file_path).lstat() + except FileNotFoundError: + diagnostics["file_safety"] = build_import_safety_diagnostics( + "The Mylar comic file is missing or unavailable.", + kind="source_revalidation", + code="source_missing", + source="source_revalidation", + overrideable_hint=False, + ) + except OSError: + # An unreadable path is not proof that the file is absent. + continue + + async def validate_discovered_files_safety( session: AsyncSession, discovered_list: list[DiscoveredSeries], @@ -147,6 +172,7 @@ async def validate_discovered_files_safety( worker_count: int = 0, ) -> None: """Run safety checks once per unique discovered source file.""" + await asyncio.to_thread(_mark_missing_mylar_references, discovered_list) files_by_path: dict[str, list[DiscoveredFile]] = {} for discovered in discovered_list: for discovered_file in discovered.files: diff --git a/src/pullbox/services/import_scan_materialization.py b/src/pullbox/services/import_scan_materialization.py index 1af45367..3e8c9013 100644 --- a/src/pullbox/services/import_scan_materialization.py +++ b/src/pullbox/services/import_scan_materialization.py @@ -15,6 +15,8 @@ ) if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from sqlalchemy.ext.asyncio import AsyncSession from pullbox.core.collection_scanner import DiscoveredSeries @@ -49,6 +51,8 @@ async def materialize_discovered_scan_results( session: AsyncSession, job: ImportJob, discovered_list: list[DiscoveredSeries], + *, + log_event: Callable[..., Awaitable[None]] | None = None, ) -> list[tuple[DiscoveredSeries, ImportedSeries]]: """Persist discovered scanner output as import review series/file rows.""" series_pairs: list[tuple[DiscoveredSeries, ImportedSeries]] = [] @@ -178,6 +182,19 @@ async def materialize_discovered_scan_results( diagnostics=diagnostics, ) file_rows.append(file_item) + if file_status == ImportedFileStatus.NO_MATCH and log_event is not None: + await log_event( + session, + job.id, + "DEBUG", + "import_file_no_match_detail", + message=f"File needs review: {df.file_name}", + file_name=df.file_name, + parsed_issue_number=df.parsed_issue_number, + series=series_item.raw_series_name, + reason=diagnostics.get("reason"), + diagnostics=diagnostics, + ) if len(file_rows) >= 500: await session.execute(insert(ImportedFile), file_rows) file_rows.clear() diff --git a/src/pullbox/services/import_scan_pipeline.py b/src/pullbox/services/import_scan_pipeline.py index 9a7291ef..7bc189d5 100644 --- a/src/pullbox/services/import_scan_pipeline.py +++ b/src/pullbox/services/import_scan_pipeline.py @@ -640,9 +640,8 @@ async def _load_mylar3_discovered_series( in_place_root_boundaries = ( await load_mylar_reference_root_boundaries(session) if in_place else () ) - reader_options: dict[str, object] = {} + reader_options: dict[str, object] = {"include_missing_files": True} if in_place: - reader_options["include_missing_files"] = True reader_options["reference_root_boundaries"] = tuple( (boundary.lexical, boundary.resolved) for boundary in in_place_root_boundaries ) @@ -763,10 +762,13 @@ def combine_staging( incompatible_series = 0 series_count = 0 file_count = 0 + missing_reference_count = 0 + reconciled_reference_count = 0 fallback_source_folders: set[str] = set() async def persist_series_page(raw_page: tuple[Any, ...]) -> None: nonlocal incompatible_series, mapping_applied_series, series_count, file_count + nonlocal missing_reference_count, reconciled_reference_count await check_mylar_staging_cancellation() page = cast("list[DiscoveredSeries]", list(raw_page)) if not page: @@ -802,6 +804,14 @@ async def persist_series_page(raw_page: tuple[Any, ...]) -> None: for series in page: series_count += 1 file_count += series.file_count + for file in series.files: + block = file.metadata_diagnostics.get("file_safety") + missing_reference_count += int( + isinstance(block, dict) and block.get("code") == "source_missing" + ) + reconciled_reference_count += int( + "mylar3_path_reconciliation" in file.metadata_diagnostics + ) if materialize_discovered_scan_results is None and series.source_folder: fallback_source_folders.add(series.source_folder) path_details = series.diagnostics.get("mylar3_path") @@ -825,6 +835,10 @@ async def persist_series_page(raw_page: tuple[Any, ...]) -> None: message=f"Prepared {series_count} series and {file_count} file records from Mylar.", series_found=series_count, files_found=file_count, + file_handling_mode=job.file_handling_mode.value, + files_present=file_count - missing_reference_count, + missing_references=missing_reference_count, + reconciled_references=reconciled_reference_count, inspection_duration_ms=inspection_duration_ms, persistence_duration_ms=persistence_duration_ms, duration_ms=round((time.monotonic() - page_started_at) * 1000), diff --git a/src/pullbox/services/import_service.py b/src/pullbox/services/import_service.py index 769b327c..3e44c2b0 100644 --- a/src/pullbox/services/import_service.py +++ b/src/pullbox/services/import_service.py @@ -8,6 +8,7 @@ import asyncio from dataclasses import dataclass +from functools import partial from pathlib import Path from typing import TYPE_CHECKING, Any from uuid import uuid4 @@ -380,7 +381,9 @@ async def start_scan( reset_scan_artifacts=self._reset_scan_artifacts, resolve_import_file_extensions=self._resolve_import_file_extensions, validate_discovered_files_safety=self._validate_discovered_files_safety, - materialize_discovered_scan_results=materialize_discovered_scan_results, + materialize_discovered_scan_results=partial( + materialize_discovered_scan_results, log_event=self._log_event + ), deduplicate_series=self._deduplicate_series, run_matching=self._run_matching, consolidate_logical_series_groups=self._consolidate_logical_series_groups, diff --git a/src/pullbox/services/import_source_metadata.py b/src/pullbox/services/import_source_metadata.py index 9806bf4d..942de31e 100644 --- a/src/pullbox/services/import_source_metadata.py +++ b/src/pullbox/services/import_source_metadata.py @@ -22,6 +22,7 @@ from pullbox.core.type_semantics import issue_type_family from pullbox.models.import_job import ImportedFile, ImportedFileStatus, ImportedSeries from pullbox.models.issue import IssueType +from pullbox.services.import_file_issue_signals import candidate_issue_number_text if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession @@ -303,6 +304,7 @@ def source_metadata_for_import_file( series_name=series_name, issue_number=issue_number, year=imp_file.parsed_year or imp_series.raw_year, + issue_number_text=candidate_issue_number_text(imp_file), volume=_filename_parse_volume(diagnostics, imp_file.file_name), issue_type=_source_issue_type(raw_issue_type), comicvine_series_id=_optional_int(diagnostics.get("comicvine_series_id")), @@ -1006,6 +1008,54 @@ async def source_metadata_for_matching_series( return metadata +def corroborated_import_title_conflict( + metadata: SourceMetadata, target_series_title: str +) -> dict[str, Any] | None: + """Protect extended comic titles from a shorter parent-ID or number match.""" + if metadata.issue_type in _TYPE_QUALIFIED_SERIES_HINT_TYPES: + return None + filename = metadata.diagnostics.get("filename_parse") + if not isinstance(filename, dict): + return None + source_title = str(filename.get("series_name") or "").strip() + exact_types = {"exact", "alternate", "token_set"} + target_name = NameMatcher.normalize(target_series_title) + # Limit this guard to corroborated title extensions. Alternate catalog names + # and type-qualified annual/special buckets still use the established matcher. + if ( + not target_name + or not NameMatcher.normalize(source_title).startswith(f"{target_name} ") + or _matcher.match(source_title, target_series_title).match_type in exact_types + ): + return None + corroboration: dict[str, str] = {} + comicinfo = metadata.diagnostics.get("comicinfo") + if isinstance(comicinfo, dict): + corroboration["comicinfo"] = str(comicinfo.get("series") or "") + hint = _archive_entry_issue_hint(metadata.diagnostics) + if hint is not None: + corroboration["archive_page_names"] = str(hint.get("series_name") or "") + agreeing = [ + signal + for signal, title in corroboration.items() + if title and _matcher.match(source_title, title).match_type in exact_types + ] + if not agreeing: + return None + return { + "kind": "metadata_conflict", + "conflict_type": "corroborated_file_series_mismatch", + "preserve_series_match": True, + "source_series": source_title, + "target_series": target_series_title, + "corroborating_signals": agreeing, + "rejection_reason": ( + f"The filename and local file metadata identify {source_title}, " + f"not {target_series_title}. Choose the correct series and issue for this file." + ), + } + + def build_import_metadata_conflict( *, metadata: SourceMetadata, diff --git a/src/pullbox/services/matching_service.py b/src/pullbox/services/matching_service.py index aeb74a84..d4b05e47 100644 --- a/src/pullbox/services/matching_service.py +++ b/src/pullbox/services/matching_service.py @@ -393,6 +393,7 @@ async def _match_metadata_to_candidates( metadata=metadata, wanted_series=str(series.title), wanted_issue=issue.issue_number, + wanted_issue_number_text=issue.effective_issue_number_text, wanted_year=series.year_start, wanted_issue_type=issue.issue_type, alternate_names=alternates, diff --git a/src/pullbox/services/release_validator.py b/src/pullbox/services/release_validator.py index 1567d789..672194c8 100644 --- a/src/pullbox/services/release_validator.py +++ b/src/pullbox/services/release_validator.py @@ -241,6 +241,7 @@ def validate_results( wanted_issue_title: str | None = None, wanted_series_issue_count: int | None = None, year_context: ReleaseYearContext | None = None, + wanted_issue_number_text: str | None = None, ) -> list[ValidationResult]: """Validate a list of search results against a wanted issue. @@ -263,6 +264,7 @@ def validate_results( result, wanted_series=wanted_series, wanted_issue=wanted_issue, + wanted_issue_number_text=wanted_issue_number_text, wanted_year=wanted_year, wanted_issue_type=wanted_issue_type, alternate_names=alternate_names, @@ -324,6 +326,7 @@ def validate_all_results( wanted_issue_title: str | None = None, wanted_series_issue_count: int | None = None, year_context: ReleaseYearContext | None = None, + wanted_issue_number_text: str | None = None, ) -> tuple[list[ValidationResult], list[ValidationResult]]: """Validate results, returning (matched, rejected) tuples. @@ -351,6 +354,7 @@ def validate_all_results( result, wanted_series=wanted_series, wanted_issue=wanted_issue, + wanted_issue_number_text=wanted_issue_number_text, wanted_year=wanted_year, wanted_issue_type=wanted_issue_type, alternate_names=alternate_names, @@ -386,6 +390,7 @@ def _validate_one( wanted_issue_title: str | None = None, wanted_series_issue_count: int | None = None, year_context: ReleaseYearContext | None = None, + wanted_issue_number_text: str | None = None, ) -> ValidationResult: """Run the validation pipeline on a single result.""" metadata = self._extractor.from_release_title( @@ -475,6 +480,7 @@ def _validate_one( metadata=metadata, wanted_series=wanted_series, wanted_issue=wanted_issue, + wanted_issue_number_text=wanted_issue_number_text, wanted_year=wanted_year, wanted_issue_type=wanted_issue_type, alternate_names=alternate_names, diff --git a/src/pullbox/services/search_evaluation.py b/src/pullbox/services/search_evaluation.py index a6adbb00..89606738 100644 --- a/src/pullbox/services/search_evaluation.py +++ b/src/pullbox/services/search_evaluation.py @@ -341,6 +341,7 @@ def evaluate_results( *, wanted_series: str | None = None, wanted_issue: float | None = None, + wanted_issue_number_text: str | None = None, wanted_year: int | None = None, wanted_issue_type: IssueType = IssueType.ISSUE, wanted_issue_title: str | None = None, @@ -382,6 +383,7 @@ def evaluate_results( "search_evaluate_start", wanted_series=wanted_series, wanted_issue=wanted_issue, + wanted_issue_number_text=wanted_issue_number_text, wanted_year=wanted_year, wanted_type=str(wanted_issue_type), total_results=len(results), @@ -402,6 +404,7 @@ def evaluate_results( results, wanted_series=wanted_series, wanted_issue=wanted_issue, + wanted_issue_number_text=wanted_issue_number_text, wanted_year=wanted_year, wanted_issue_type=wanted_issue_type, alternate_names=alternate_names, diff --git a/src/pullbox/services/search_issue_runner.py b/src/pullbox/services/search_issue_runner.py index 00163b2a..1239f28c 100644 --- a/src/pullbox/services/search_issue_runner.py +++ b/src/pullbox/services/search_issue_runner.py @@ -149,6 +149,7 @@ async def search_issue_target( filtered_results, wanted_series=target.series_title, wanted_issue=target.issue_number, + wanted_issue_number_text=target.effective_issue_number_text, wanted_year=target.search_year, year_context=target.year_context, wanted_issue_type=target.issue_type, diff --git a/src/pullbox/services/search_service.py b/src/pullbox/services/search_service.py index 30f66a05..dc837227 100644 --- a/src/pullbox/services/search_service.py +++ b/src/pullbox/services/search_service.py @@ -775,6 +775,7 @@ def evaluate_results( *, wanted_series: str | None = None, wanted_issue: float | None = None, + wanted_issue_number_text: str | None = None, wanted_year: int | None = None, wanted_issue_type: IssueType = IssueType.ISSUE, wanted_issue_title: str | None = None, @@ -805,6 +806,7 @@ def evaluate_results( min_score, wanted_series=wanted_series, wanted_issue=wanted_issue, + wanted_issue_number_text=wanted_issue_number_text, wanted_year=wanted_year, wanted_issue_type=wanted_issue_type, wanted_issue_title=wanted_issue_title, diff --git a/src/pullbox/services/semantic_matching.py b/src/pullbox/services/semantic_matching.py index 1b994476..8d1a82da 100644 --- a/src/pullbox/services/semantic_matching.py +++ b/src/pullbox/services/semantic_matching.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any +from pullbox.core.issue_numbers import format_issue_number, normalize_issue_number_text from pullbox.core.issue_title import collection_title_number, collection_title_subtitle from pullbox.core.name_matcher import NameMatcher, NameMatchResult from pullbox.core.naming import extract_base_series_title @@ -151,6 +152,7 @@ def match_against_issue( metadata: SourceMetadata, wanted_series: str, wanted_issue: float, + wanted_issue_number_text: str | None = None, wanted_year: int | None = None, wanted_issue_type: IssueType = IssueType.ISSUE, alternate_names: list[str] | None = None, @@ -291,6 +293,25 @@ def match_against_issue( if effective_issue is None and metadata.volume and compatibility.prefer_volume_number: effective_issue = _normalize_volume_number(metadata.volume) + wanted_text = normalize_issue_number_text(wanted_issue_number_text or wanted_issue) + source_value = metadata.issue_number_text or effective_issue + source_text = ( + normalize_issue_number_text(source_value) if source_value is not None else None + ) + qualified_identity = wanted_text != format_issue_number(wanted_issue) or ( + source_text is not None + and effective_issue is not None + and source_text != format_issue_number(effective_issue) + ) + if qualified_identity and source_text != wanted_text: + return IssueMatchDecision( + is_match=False, + confidence=MatchConfidence.LOW, + match_method="issue_mismatch", + rejection_reason=f"Issue mismatch: got {source_text} wanted {wanted_text}", + match_diagnostics={"type_mode": compatibility.mode}, + ) + wanted_is_collection = issue_type_family(wanted_issue_type) == TypeFamily.COLLECTION issue_omitted_ok = effective_issue is None and wanted_issue == 1.0 subtitle_corroboration_required = ( diff --git a/src/pullbox/services/series_rescan.py b/src/pullbox/services/series_rescan.py new file mode 100644 index 00000000..212af07d --- /dev/null +++ b/src/pullbox/services/series_rescan.py @@ -0,0 +1,167 @@ +"""Read-only filesystem reconciliation for one existing catalog series.""" + +import os +import time +from collections import Counter +from pathlib import Path +from typing import Any + +from pullbox.core.exceptions import ConfigurationError +from pullbox.core.file_safety import FileSafetyError, run_safety_checks +from pullbox.core.issue_numbers import normalize_issue_number_text +from pullbox.core.library_file_ownership import build_file_identity_signature +from pullbox.core.source_metadata import SourceMetadataExtractor +from pullbox.models.issue import IssueType +from pullbox.models.library import MatchConfidence +from pullbox.services.import_content_inspection import inspect_import_content +from pullbox.services.import_source_metadata import ( + build_import_metadata_conflict, + corroborated_import_title_conflict, +) +from pullbox.services.semantic_matching import ImportPolicy, SemanticMatchEngine + +COMIC_EXTENSIONS = frozenset({".cbz", ".cbr", ".cb7", ".cbt", ".pdf", ".epub"}) + + +def _root_for(path: Path, context: dict[str, Any]) -> int: + for root in sorted(context["roots"], key=lambda root: len(root["path"]), reverse=True): + if path.is_relative_to(Path(root["path"]).resolve()): + return int(root["id"]) + raise ValueError("This path is outside enabled roots that allow referencing existing files.") + + +def _inspect(path: Path, context: dict[str, Any]) -> dict[str, Any]: + item: dict[str, Any] = {"file_path": str(path), "operation": "rescan", "outcome": "review"} + try: + if path.is_symlink(): + raise ValueError("Symbolic links need manual review; the rescan does not follow them.") + path = path.resolve(strict=True) + item["file_path"] = str(path) + item["root_id"] = _root_for(path, context) + if path.suffix.lower() not in context.get("extensions", COMIC_EXTENSIONS): + raise ValueError("This file type is not enabled in the import settings.") + signature = build_file_identity_signature(path) + if time.time() - path.stat().st_mtime < 30: + raise ValueError("File was modified recently. Wait for copying to finish, then rescan.") + inspection = run_safety_checks( + path, + block_dangerous=context["block_dangerous"], + max_archive_size=context["max_archive_size"], + ) + content = inspect_import_content(path, inspection) + if content.get("file_safety"): + raise ValueError( + "Archive has fewer than two pages. Review it through Import before adding it." + ) + metadata = SourceMetadataExtractor().from_archive_path(path) + series = context["series"] + if metadata.comicvine_series_id not in {None, series["comicvine_id"]}: + raise ValueError("Embedded or sidecar metadata identifies a different series.") + candidates = [ + issue + for issue in context["issues"] + if ( + issue["cv_id"] == metadata.comicvine_issue_id + if metadata.comicvine_issue_id + else normalize_issue_number_text(issue["text"]) + == normalize_issue_number_text(metadata.issue_number_text or "") + ) + ] + if len(candidates) != 1: + raise ValueError("No unique issue in this series matches the file's local identity.") + issue = candidates[0] + item["issue_id"] = issue["id"] + item["target_number"] = issue["text"] + item["target_cv_id"] = issue["cv_id"] + conflict = build_import_metadata_conflict( + metadata=metadata, + target_series_title=series["title"], + target_series_year=series["year_start"], + target_issue_number=issue["number"], + target_issue_cv_id=issue["cv_id"], + target_issue_title=issue.get("title"), + ) or corroborated_import_title_conflict(metadata, series["title"]) + if conflict: + raise ValueError(str(conflict["rejection_reason"])) + decision = SemanticMatchEngine(policy=ImportPolicy()).match_against_issue( + metadata=metadata, + wanted_series=series["title"], + wanted_issue=issue["number"], + wanted_issue_number_text=issue["text"], + wanted_year=issue.get("year") or series["year_start"], + wanted_issue_type=IssueType(issue["type"]), + wanted_issue_cv_id=issue["cv_id"], + wanted_issue_title=issue.get("title"), + ) + if float(decision.match_diagnostics.get("series_similarity", 0)) >= 0.7: + item["candidate_issue_id"] = issue["id"] + if not decision.is_match or decision.confidence != MatchConfidence.HIGH: + raise ValueError( + decision.rejection_reason or "The local match needs review before registration." + ) + if signature != build_file_identity_signature(path): + raise ValueError( + "File changed during inspection. Wait for copying to finish, then rescan." + ) + item.update( + outcome="register", + issue_id=issue["id"], + signature=signature, + reason="Proven local match.", + ) + except FileNotFoundError: + item.update( + missing=True, + reason="File is missing. Check its mount or location; ownership was not changed.", + ) + except (OSError, ValueError, ConfigurationError, FileSafetyError) as exc: + item["reason"] = str(exc) + return item + + +def plan_series_rescan(context: dict[str, Any]) -> list[dict[str, Any]]: + """Inspect local candidates before deciding which registrations are unique.""" + folder = Path(context["folder"]) + try: + folder = folder.resolve(strict=True) + if not folder.is_dir(): + raise ValueError("Series folder is unavailable. Check the mount before rescanning.") + _root_for(folder, context) + if any(folder == Path(root["path"]).resolve() for root in context["roots"]): + raise ValueError("Choose a series folder, not an entire library root.") + paths: set[Path] = set() + + def fail_scan(error: OSError) -> None: + raise error + + for parent, directories, names in os.walk(folder, followlinks=False, onerror=fail_scan): + directories[:] = [name for name in directories if not name.startswith(".")] + for name in names: + path = Path(parent) / name + if not name.startswith(".") and path.suffix.lower() in COMIC_EXTENSIONS: + paths.add(path) + except OSError as exc: + raise ValueError( + "Series folder is unavailable or unreadable. Check the mount before rescanning." + ) from exc + # Check already-linked files outside this folder, but never enumerate their parents. + paths.update(Path(record["path"]) for record in context["files"]) + items = [_inspect(path, context) for path in sorted(paths)] + counts = Counter(item["candidate_issue_id"] for item in items if "candidate_issue_id" in item) + registered_paths = {record["path"]: record["issue_id"] for record in context["files"]} + for item in items: + if "issue_id" in item and counts[item["issue_id"]] > 1: + if registered_paths.get(item["file_path"]) == item["issue_id"]: + continue + item.update( + outcome="review", + reason="Multiple files match this issue. Choose a copy through Import review.", + ) + replacements = {item["issue_id"] for item in items if item["outcome"] == "register"} + # A proven replacement and its obsolete missing path are one repair, not an + # additional warning that the user must resolve after that repair succeeds. + return [ + item + for item in items + if not (item.get("missing") and registered_paths.get(item["file_path"]) in replacements) + ] diff --git a/src/pullbox/services/series_rescan_registration.py b/src/pullbox/services/series_rescan_registration.py new file mode 100644 index 00000000..f0298a54 --- /dev/null +++ b/src/pullbox/services/series_rescan_registration.py @@ -0,0 +1,171 @@ +"""Database application of proven, read-only series rescan matches.""" + +import asyncio +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from sqlalchemy import select, update + +from pullbox.core.archive_format import archive_format +from pullbox.core.exceptions import ConfigurationError +from pullbox.core.file_ops import register_library_file +from pullbox.core.library_file_ownership import build_file_identity_signature +from pullbox.models.download import DownloadHistory, DownloadState +from pullbox.models.issue import Issue, IssueStatus +from pullbox.models.library import ( + FileFormat, + LibraryFile, + LibraryFileStorageMode, + LibraryRoot, + MatchConfidence, +) +from pullbox.utilities.import_guards import active_import_file_mutation_job_id + + +async def apply_rescan_match(session: Any, series_id: int, item: dict[str, Any]) -> tuple[str, str]: + """Recheck ownership immediately before registering a proven local file.""" + if item["outcome"] != "register": + return "review", item["reason"] + path = Path(item["file_path"]) + try: + file_format = FileFormat(await asyncio.to_thread(archive_format, path)) + signature = await asyncio.to_thread(build_file_identity_signature, path) + except (OSError, ConfigurationError): + return ( + "review", + "File is unavailable. Check the mount and rescan; ownership was not changed.", + ) + if signature != item["signature"]: + return "review", "File changed after inspection. Wait for copying to finish, then rescan." + with session.no_autoflush: + existing = list( + ( + await session.scalars( + select(LibraryFile).where(LibraryFile.issue_id == item["issue_id"]) + ) + ).all() + ) + existing_snapshot = [(record.id, record.file_path) for record in existing] + for record in existing: + if record.file_path == str(path): + continue + try: + await asyncio.to_thread(Path(record.file_path).stat) + except FileNotFoundError: + continue + except OSError: + return ( + "review", + "Existing copy is unreadable. Check the mount; no replacement was made.", + ) + return "review", "An existing copy is already registered. No file was replaced." + if len(existing) > 1: + return ( + "review", + "Multiple registrations already exist for this issue. Review them before retrying.", + ) + # Serialize the final ownership checks with other database writers, without + # holding that write lock during archive inspection or hashing. + await session.execute( + update(Issue).where(Issue.id == item["issue_id"]).values(status=Issue.status) + ) + issue = await session.get(Issue, item["issue_id"], populate_existing=True) + if issue is None or issue.series_id != series_id: + return "review", "The catalog issue changed or was removed. Rescan again." + if ( + issue.issue_number_text != item["target_number"] + or issue.comicvine_id != item["target_cv_id"] + ): + return "review", "The catalog identity changed during inspection. Rescan again." + if await active_import_file_mutation_job_id(session) is not None: + return "review", "An import is active. Wait for it to finish, then rescan." + busy = await session.scalar( + select(DownloadHistory.id) + .where( + DownloadHistory.issue_id == issue.id, + DownloadHistory.state.not_in([DownloadState.FAILED, DownloadState.IMPORTED]), + DownloadHistory.imported_at.is_(None), + ) + .limit(1) + ) + if busy is not None or issue.status == IssueStatus.DOWNLOADING: + return ( + "review", + "This issue has an active download or post-processing task. " + "Rescan after it finishes.", + ) + root = await session.get(LibraryRoot, item["root_id"], populate_existing=True) + if root is None or not root.enabled or not root.allow_referenced_registrations: + return "review", "The library root no longer allows referencing existing files." + resolved_root = await asyncio.to_thread(Path(root.path).resolve) + if not path.is_relative_to(resolved_root): + return "review", "The library root moved during inspection. Rescan again." + path_record = await session.scalar( + select(LibraryFile).where(LibraryFile.file_path == str(path)) + ) + if path_record is not None and path_record.issue_id not in {None, issue.id}: + return ( + "review", + "This path is registered to another issue. " + "Resolve the existing match through Import.", + ) + # Refresh after taking the writer lock: another registration may have + # committed while the filesystem checks ran. + current = list( + ( + await session.scalars( + select(LibraryFile) + .where(LibraryFile.issue_id == issue.id) + .execution_options(populate_existing=True) + ) + ).all() + ) + if [(f.id, f.file_path) for f in current] != existing_snapshot: + return "review", "Ownership changed during the rescan. Rescan again." + if path_record is not None: + if path_record.issue_id is None and current: + return "review", "This issue already has a registration. Review both file records." + outcome = ( + "unchanged" + if issue.status == IssueStatus.OWNED + and path_record.issue_id == issue.id + and path_record.file_format == file_format + else "repaired" + ) + path_record.issue_id = issue.id + path_record.file_format = file_format + path_record.match_confidence = MatchConfidence.HIGH + issue.status = IssueStatus.OWNED + return outcome, "Existing file verified." + if current: + record = current[0] + record.file_path = str(path) + record.file_name = path.name + record.file_size = signature["size"] + record.file_modified_at = datetime.fromtimestamp(int(signature["mtime_ns"]) / 1e9, UTC) + record.file_format = file_format + record.file_hash = None + record.source_signature = signature + record.storage_mode = LibraryFileStorageMode.REFERENCED + record.library_root_id = root.id + record.match_confidence = MatchConfidence.HIGH + issue.status = IssueStatus.OWNED + return ( + "repaired", + "Missing file link repaired using a proven local match; source left unchanged.", + ) + await register_library_file( + session, + path, + issue, + MatchConfidence.HIGH, + move_to_library=False, + rename=False, + normalize_to_cbz=False, + update_embedded_comicinfo_from_match=False, + storage_mode=LibraryFileStorageMode.REFERENCED, + library_root_id=root.id, + expected_source_signature=item["signature"], + ) + return "added", "Registered in place. Source file left unchanged." diff --git a/src/pullbox/services/utility_operation_progress.py b/src/pullbox/services/utility_operation_progress.py index 0ba7b717..337036cc 100644 --- a/src/pullbox/services/utility_operation_progress.py +++ b/src/pullbox/services/utility_operation_progress.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING from pullbox.models.operation_progress import ( @@ -79,6 +80,14 @@ def build_utility_operation_update( visibility = OperationProgressVisibility.QUIET message = job.error_message or job_state.value.replace("_", " ").title() + detail_url = "/utilities?tab=queue" + if job.job_type == JobType.SERIES_RESCAN: + config = json.loads(job.config) + detail_url = f"/series/{int(config['series_id'])}?rescan={job.id}" + if job_state == JobState.RUNNING and total is None: + message = "Inspecting local files and checking for duplicates" + elif job_state == JobState.COMPLETED: + message = f"Rescan complete. {job.warning_count or 0} files need review." return OperationProgressUpdate( operation_type=OperationProgressType.UTILITY, operation_key=job.id, @@ -89,7 +98,7 @@ def build_utility_operation_update( title=job.display_name, message=message, source_label="Utilities", - detail_url="/utilities?tab=queue", + detail_url=detail_url, visibility=visibility, tone=tone, attention_required=attention, diff --git a/src/pullbox/tasks/import_task.py b/src/pullbox/tasks/import_task.py index cbdcade6..780bed07 100644 --- a/src/pullbox/tasks/import_task.py +++ b/src/pullbox/tasks/import_task.py @@ -432,6 +432,7 @@ def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None: async def recover_and_dispatch(self) -> int: """Recover interrupted imports and resume any runnable job.""" recovered = await recover_stuck_import_jobs(self._session_factory) + await recover_pending_review_source_actions(self._session_factory) await self._dispatch_recovered_job() return recovered @@ -1056,6 +1057,55 @@ async def _run_import_series_rematch_task(job_id: int, imported_series_id: int) await session.rollback() +async def run_import_review_source_action(job_id: int, file_id: int) -> None: + from pullbox.services.import_review_source_actions import ( + finish_source_action, + process_source_action, + ) + + lock = _review_rematch_locks.setdefault(job_id, asyncio.Lock()) + async with lock: + try: + series_id = await process_source_action(get_session_factory(), job_id, file_id) + if series_id is not None: + await _run_import_series_rematch_task(job_id, series_id) + await finish_source_action(get_session_factory(), job_id, file_id) + except Exception: + logger.exception("import_review_source_action_failed", job_id=job_id, file_id=file_id) + await finish_source_action(get_session_factory(), job_id, file_id, failed=True) + + +def trigger_import_review_source_action(job_id: int, file_id: int) -> None: + _fire_and_forget(run_import_review_source_action(job_id, file_id)) + + +async def recover_pending_review_source_actions(factory: async_sessionmaker[AsyncSession]) -> None: + async def recover() -> None: + cursor = 0 + while True: + async with factory() as session: + rows = ( + await session.execute( + sa_select(ImportedFile.import_job_id, ImportedFile.id) + .where( + ImportedFile.id > cursor, + ImportedFile.diagnostics["review_source_action"]["state"] + .as_string() + .in_(["pending", "matching"]), + ) + .order_by(ImportedFile.id) + .limit(50) + ) + ).all() + if not rows: + return + for job_id, file_id in rows: + await run_import_review_source_action(job_id, file_id) + cursor = file_id + + _fire_and_forget(recover()) + + def trigger_import_scan(job_id: int) -> None: """Schedule an import scan on the durable runner.""" _fire_and_forget(get_import_runner().request_scan(job_id)) diff --git a/src/pullbox/tasks/search_task.py b/src/pullbox/tasks/search_task.py index 80f5f23c..987b8e41 100644 --- a/src/pullbox/tasks/search_task.py +++ b/src/pullbox/tasks/search_task.py @@ -823,6 +823,7 @@ async def _build_mocked_issue_outcome( wanted_series=target.series_title, wanted_issue=target.issue_number, wanted_year=target.search_year, + wanted_issue_number_text=target.effective_issue_number_text, wanted_issue_type=target.issue_type, wanted_issue_title=target.issue_title, alternate_names=target.alternate_names, @@ -839,6 +840,7 @@ async def _build_mocked_issue_outcome( [best], wanted_series=target.series_title, wanted_issue=target.issue_number, + wanted_issue_number_text=target.effective_issue_number_text, wanted_year=target.search_year, wanted_issue_type=target.issue_type, alternate_names=target.alternate_names, @@ -886,6 +888,7 @@ async def _build_mocked_wanted_outcome( filtered_results, wanted_series=target.series_title, wanted_issue=target.issue_number, + wanted_issue_number_text=target.effective_issue_number_text, wanted_year=target.search_year, wanted_issue_type=target.issue_type, wanted_issue_title=target.issue_title, @@ -912,6 +915,7 @@ async def _build_mocked_wanted_outcome( [best], wanted_series=target.series_title, wanted_issue=target.issue_number, + wanted_issue_number_text=target.effective_issue_number_text, wanted_year=target.search_year, wanted_issue_type=target.issue_type, alternate_names=target.alternate_names, diff --git a/src/pullbox/ui/formatters.py b/src/pullbox/ui/formatters.py index c859091b..44f023b6 100644 --- a/src/pullbox/ui/formatters.py +++ b/src/pullbox/ui/formatters.py @@ -10,6 +10,7 @@ from pullbox.core.duration_format import format_duration_ms_label from pullbox.core.html_sanitizer import sanitize_rich_html from pullbox.core.issue_numbers import format_issue_number as _format_issue_number +from pullbox.core.issue_numbers import normalize_issue_number_text from pullbox.models.series import SeriesStatus @@ -18,8 +19,10 @@ def sanitize_rich_html_filter(value: str | None) -> Markup: return Markup(sanitize_rich_html(value)) # nosec -def format_issue_number(value: float) -> str: - """Format issue number, stripping unnecessary trailing zeros.""" +def format_issue_number(value: float, exact_text: str | None = None) -> str: + """Prefer the exact catalog designation over its numeric compatibility value.""" + if exact_text: + return normalize_issue_number_text(exact_text) return _format_issue_number(value) diff --git a/src/pullbox/ui/import_review_context.py b/src/pullbox/ui/import_review_context.py index 366fb742..0acea889 100644 --- a/src/pullbox/ui/import_review_context.py +++ b/src/pullbox/ui/import_review_context.py @@ -6,7 +6,7 @@ from collections.abc import Mapping from typing import TYPE_CHECKING, Any, cast -from sqlalchemy import func, select +from sqlalchemy import func, or_, select from pullbox.models.import_job import ( ImportedFile, @@ -15,6 +15,8 @@ ImportSeriesStatus, ) from pullbox.models.library import LibraryRoot +from pullbox.services.import_duplicates import duplicate_merge_is_actionable +from pullbox.services.import_file_conflicts import classify_conflict_group from pullbox.services.import_review_selection import load_import_review_selection_state from pullbox.services.import_safety_diagnostics import normalize_import_safety_diagnostics from pullbox.services.import_split_series import load_selected_split_series_review @@ -24,6 +26,7 @@ ) from pullbox.services.library_root_management import list_library_roots from pullbox.ui.import_conflict_review import _load_import_conflict_review_context +from pullbox.ui.import_review_lanes import LANES, REASONS, load_review_rows from pullbox.ui.import_review_summary import ( load_import_review_summary, load_import_safety_failure_summary, @@ -61,7 +64,7 @@ def _resolve_review_view(status: str | None) -> tuple[str, ImportSeriesStatus | requested_series_status: ImportSeriesStatus | None = None current_view = "series" if status == "conflicts": - current_view = "conflicts" + current_view = "conflict_rows" elif status in {"needs_series", "needs_issue", "safety_blocked", "story_arcs"}: current_view = status elif status: @@ -78,7 +81,14 @@ def _review_filters( requested_series_status: ImportSeriesStatus | None, ) -> list[Any]: filters: list[Any] = [ImportedSeries.import_job_id == job_id] - if current_view == "needs_issue": + if current_view == "conflict_rows": + filters.append( + or_( + ImportedSeries.files_conflict > 0, + ImportedSeries.diagnostics["kind"].as_string() == "series_conflict", + ) + ) + elif current_view == "needs_issue": filters.append(_needs_issue_match_filter()) elif current_view == "needs_series": filters.append(_needs_series_match_filter()) @@ -103,7 +113,7 @@ async def _load_selected_review_series_ids( .where( ImportedSeries.import_job_id == job_id, ImportedSeries.status == ImportSeriesStatus.MATCHED, - ImportedSeries.files_conflict == 0, + ImportedSeries.files_matched > 0, ImportedSeries.selected_for_import.is_(True), ) .order_by(ImportedSeries.id.asc()) @@ -215,6 +225,21 @@ async def has_pending_import_safety_rematch(session: AsyncSession, job_id: int) return pending_file_id is not None +async def has_pending_import_review_rematch(session: AsyncSession, job_id: int) -> bool: + """Series choices and safety approvals use the same completion poll.""" + return ( + await session.scalar( + select(ImportedSeries.id) + .where( + ImportedSeries.import_job_id == job_id, + ImportedSeries.diagnostics["rematch_pending"].as_boolean().is_(True), + ) + .limit(1) + ) + is not None + ) + + async def _load_safety_blocked_files_by_series_id( session: AsyncSession, job_id: int, @@ -231,8 +256,12 @@ async def _load_safety_blocked_files_by_series_id( .where( ImportedFile.import_job_id == job_id, ImportedFile.import_series_id.in_(visible_series_ids), - ImportedFile.status.in_( - [ImportedFileStatus.SAFETY_BLOCKED, ImportedFileStatus.SAFETY_APPROVED] + or_( + ImportedFile.status.in_( + [ImportedFileStatus.SAFETY_BLOCKED, ImportedFileStatus.SAFETY_APPROVED] + ), + ImportedFile.diagnostics["safety_block"]["category"].as_string().is_not(None), + ImportedFile.diagnostics["safety_exception"]["allowed_once"].as_boolean().is_(True), ), ) .order_by(ImportedFile.import_series_id.asc(), ImportedFile.id.asc()) @@ -256,6 +285,8 @@ def _build_safety_block_context_by_file_id( if not isinstance(diagnostics, Mapping): continue safety_block = diagnostics.get("safety_block") + if not isinstance(safety_block, Mapping): + safety_block = dict(diagnostics.get("safety_exception") or {}).get("previous_block") if isinstance(safety_block, Mapping): result[imp_file.id] = normalize_import_safety_diagnostics(safety_block) return result @@ -277,6 +308,63 @@ def _build_safety_file_display_name_by_file_id( return result +async def _load_inline_conflicts( + session: AsyncSession, job_id: int, series_ids: list[int] +) -> dict[int, list[dict[str, object]]]: + """Use saved metadata to review whole conflict groups, including shared folders.""" + if not series_ids: + return {} + group_ids = ( + select(ImportedFile.conflict_group_id) + .where( + ImportedFile.import_job_id == job_id, + ImportedFile.import_series_id.in_(series_ids), + ImportedFile.status == ImportedFileStatus.CONFLICT, + ImportedFile.conflict_group_id.is_not(None), + ) + .distinct() + ) + files = ( + await session.scalars( + select(ImportedFile) + .where( + ImportedFile.import_job_id == job_id, + ImportedFile.status == ImportedFileStatus.CONFLICT, + ImportedFile.conflict_group_id.in_(group_ids), + ) + .order_by(ImportedFile.conflict_group_id, ImportedFile.id) + ) + ).all() + groups: dict[int, list[ImportedFile]] = {} + for file in files: + if file.conflict_group_id is not None: + groups.setdefault(file.conflict_group_id, []).append(file) + result: dict[int, list[dict[str, object]]] = {} + for group_id, members in groups.items(): + classification = classify_conflict_group(members) + disagreement = classification in {"series_mismatch", "year_disagreement"} + group: dict[str, object] = { + "id": group_id, + "files": members, + "recommended": not disagreement, + "preferred_file_id": next((file.id for file in members if file.is_preferred), None) + if not disagreement and sum(file.is_preferred for file in members) == 1 + else None, + "label": "Do these files belong to the same comic?" + if disagreement + else "Choose a copy", + "description": ( + "The titles or years disagree. Check the issue matches before keeping one copy." + if disagreement + else "Keep one copy for this issue. Other copies are excluded from this import; " + "source files stay untouched." + ), + } + for series_id in {file.import_series_id for file in members}.intersection(series_ids): + result.setdefault(series_id, []).append(group) + return result + + async def load_import_review_context( session: AsyncSession, job: ImportJob, @@ -287,10 +375,25 @@ async def load_import_review_context( story_arc_id: int | None = None, arc_entry_state: StoryArcEntryResolutionFilter = StoryArcEntryResolutionFilter.ALL, arc_entry_page: int = 1, + reason: str | None = None, + actor_id: int | None = None, ) -> dict[str, object]: """Load the template context for the Step 3 review table.""" job_id = int(job.id) current_view, requested_series_status = _resolve_review_view(status) + review_rows = await load_review_rows(session, job_id) + lane_counts = {lane: sum(row.lane == lane for row in review_rows.values()) for lane in LANES} + if status in LANES or not status: + current_view = status or next( + (lane for lane, count in lane_counts.items() if count), "ready" + ) + requested_series_status = None + active_lane = current_view if current_view in LANES else "" + active_reason = reason if reason in REASONS else None + reason_counts = { + key: sum(row.lane == active_lane and key in row.reasons for row in review_rows.values()) + for key in REASONS + } page_size = 25 normalized_sort = _normalize_import_review_series_sort(sort) total = 0 @@ -373,14 +476,37 @@ async def load_import_review_context( current_view=current_view, requested_series_status=requested_series_status, ) - count_result = await session.execute(select(func.count(ImportedSeries.id)).where(*filters)) - total = count_result.scalar_one() + if active_lane: + ordered_ids = ( + await session.scalars( + select(ImportedSeries.id) + .where(ImportedSeries.import_job_id == job_id) + .order_by(*_get_import_review_series_order_by(normalized_sort)) + ) + ).all() + matching_ids = [ + series_id + for series_id in ordered_ids + if review_rows[series_id].lane == active_lane + and (not active_reason or active_reason in review_rows[series_id].reasons) + ] + total = len(matching_ids) + page = min(page, max(1, (total + page_size - 1) // page_size)) + # Only a page of IDs enters SQL, even for a 50,000-series library. + filters.append( + ImportedSeries.id.in_(matching_ids[(page - 1) * page_size : page * page_size]) + ) + else: + count_result = await session.execute( + select(func.count(ImportedSeries.id)).where(*filters) + ) + total = count_result.scalar_one() series_result = await session.execute( select(ImportedSeries) .where(*filters) .order_by(*_get_import_review_series_order_by(normalized_sort)) - .offset((page - 1) * page_size) + .offset(0 if active_lane else (page - 1) * page_size) .limit(page_size) ) series_items = list(series_result.scalars().all()) @@ -410,7 +536,40 @@ async def load_import_review_context( safety_review_series_count, safety_review_file_count = await _load_safety_review_counts( session, job_id ) - safety_rematch_pending = await has_pending_import_safety_rematch(session, job_id) + safety_rematch_pending = any(row.updating for row in review_rows.values()) + safety_failure_summary = await load_import_safety_failure_summary(session, job) + from pullbox.services.import_review_one_page import one_page_files, one_page_scope + from pullbox.services.import_review_scope import sign_review_scope + + safety_context = _build_safety_block_context_by_file_id(safety_blocked_files_by_series_id) + one_page_review: dict[int, dict[str, object]] = {} + for item in series_items: + files = one_page_files(safety_blocked_files_by_series_id.get(item.id, [])) + if not files: + continue + entry: dict[str, object] = { + "files": [ + file + for file in safety_blocked_files_by_series_id.get(item.id, []) + if safety_context.get(file.id, {}).get("category") == "single_page_comic" + ], + "count": len(files), + } + if actor_id is not None: + entry["skip_token"] = await sign_review_scope( + session, one_page_scope(job, item, files, actor_id=actor_id, action="skip") + ) + if all( + normalize_import_safety_diagnostics(file.diagnostics["safety_block"])[ + "overrideable" + ] + is True + for file in files + ): + entry["allow_token"] = await sign_review_scope( + session, one_page_scope(job, item, files, actor_id=actor_id, action="allow") + ) + one_page_review[item.id] = entry managed_library_root_options: list[dict[str, Any]] = [] if split_series_review.requires_preferred_destination: managed_library_root_options = [ @@ -423,9 +582,40 @@ async def load_import_review_context( and bool(root["writable"]) ] + from pullbox.ui.import_review_presentation import ( + FILTER_LABELS, + LANE_DESCRIPTIONS, + REASON_DESCRIPTIONS, + ) + template_ctx: dict[str, object] = { + "lane_descriptions": LANE_DESCRIPTIONS, + "filter_labels": FILTER_LABELS, + "reason_descriptions": REASON_DESCRIPTIONS, "job": job, + "one_page_review": one_page_review, + "review_rows": review_rows, + "review_open_series": sum( + row.decision_files > 0 or row.updating for row in review_rows.values() + ), + "inline_conflicts": await _load_inline_conflicts( + session, job_id, [item.id for item in series_items] + ), + "safety_reason_keys": {str(item["category"]) for item in safety_failure_summary}, + "review_lanes": LANES, + "review_reasons": REASONS, + "lane_counts": lane_counts, + "lane_file_counts": { + lane: sum(row.decision_files for row in review_rows.values() if row.lane == lane) + for lane in LANES + }, + "reason_counts": reason_counts, + "active_reason": active_reason, + "active_lane": active_lane, "series_items": series_items, + "actionable_duplicate_ids": { + item.id for item in series_items if duplicate_merge_is_actionable(item) + }, "story_arc_items": story_arc_items, "story_arc_total": story_arc_total, "story_arc_selected_item": story_arc_selected_item, @@ -451,16 +641,14 @@ async def load_import_review_context( "review_summary": await load_import_review_summary(session, job), "split_series_review": split_series_review, "managed_library_root_options": managed_library_root_options, - "safety_failure_summary": await load_import_safety_failure_summary(session, job), + "safety_failure_summary": safety_failure_summary, "selected_series_ids": await _load_selected_review_series_ids(session, job_id), "duplicate_selected_file_counts": await _load_duplicate_selected_file_counts( session, job_id, ), "safety_blocked_files_by_series_id": safety_blocked_files_by_series_id, - "safety_block_context_by_file_id": _build_safety_block_context_by_file_id( - safety_blocked_files_by_series_id - ), + "safety_block_context_by_file_id": safety_context, "safety_file_display_name_by_file_id": _build_safety_file_display_name_by_file_id( safety_blocked_files_by_series_id ), diff --git a/src/pullbox/ui/import_review_file_routes.py b/src/pullbox/ui/import_review_file_routes.py new file mode 100644 index 00000000..a27448dd --- /dev/null +++ b/src/pullbox/ui/import_review_file_routes.py @@ -0,0 +1,307 @@ +"""Bounded individual file decisions for the guided import review.""" + +from typing import Annotated + +from fastapi import APIRouter, Form, Query, Request +from fastapi.responses import JSONResponse +from starlette.responses import Response + +from pullbox.api.deps import DbSession, InteractiveOperatorUser +from pullbox.composition.services import build_metadata_service +from pullbox.services.import_review_file_assignment import assign_review_file, load_review_file +from pullbox.services.import_review_scope import ( + review_scope, + sign_review_scope, + verify_review_scope, +) + +router = APIRouter() + + +@router.get("/import/{job_id}/series/{series_id}/files", include_in_schema=False) +async def view_series_files( + job_id: int, + series_id: int, + request: Request, + user: InteractiveOperatorUser, + session: DbSession, + files_page: int = Query(1, ge=1), +) -> Response: + from pullbox.ui.import_review_files import load_series_file_inventory + from pullbox.ui.import_routes import _ctx, _templates + + inventory = await load_series_file_inventory(session, job_id, series_id, files_page) + return _templates().TemplateResponse( + request, + "partials/import_review_files_inventory.html", + _ctx(request, user, inventory=inventory, job_id=job_id), + headers={"Cache-Control": "no-store"}, + ) + + +@router.get("/import/{job_id}/series/{series_id}/review-{action}", include_in_schema=False) +async def preview_series_choice( + job_id: int, + series_id: int, + action: str, + request: Request, + user: InteractiveOperatorUser, + session: DbSession, +) -> Response: + from fastapi import HTTPException + + from pullbox.core.exceptions import ValidationError + from pullbox.services.import_review_series_choice import series_choice_scope + from pullbox.ui.import_routes import _ctx, _templates + + try: + job, series, scope = await series_choice_scope(session, job_id, series_id, action, user.id) + except ValidationError as exc: + raise HTTPException(status_code=409, detail=exc.message) from exc + token = await sign_review_scope(session, scope) + return _templates().TemplateResponse( + request, + "partials/import_review_series_choice.html", + _ctx( + request, + user, + job=job, + series=series, + action=action, + token=token, + ), + ) + + +@router.post("/import/{job_id}/series/{series_id}/review-{action}", include_in_schema=False) +async def decide_series_choice( + job_id: int, + series_id: int, + action: str, + user: InteractiveOperatorUser, + session: DbSession, + token: Annotated[str, Form()], +) -> Response: + from fastapi import HTTPException + + from pullbox.core.exceptions import ValidationError + from pullbox.services.import_review_series_choice import apply_series_choice + + try: + await apply_series_choice(session, job_id, series_id, action, user.id, token) + except ValidationError as exc: + await session.rollback() + raise HTTPException(status_code=409, detail=exc.message) from exc + await session.commit() + return JSONResponse( + { + "message": "Series skipped. Source files are unchanged." + if action == "skip" + else "Series returned to review. Check its selection before importing." + } + ) + + +@router.post("/import/{job_id}/series/{series_id}/one-page/{action}", include_in_schema=False) +async def decide_one_page_archives( + job_id: int, + series_id: int, + action: str, + request: Request, + user: InteractiveOperatorUser, + session: DbSession, + token: Annotated[str, Form()], + status: str = Query("decide"), + page: int = Query(1, ge=1), + sort: str | None = Query(None), +) -> Response: + from fastapi import HTTPException + + from pullbox.core.exceptions import ValidationError + from pullbox.services.import_review_one_page import decide_one_page_series + from pullbox.tasks.import_task import trigger_import_series_rematch + from pullbox.ui.import_routes import _render_import_review_partial + + if action not in ("allow", "skip"): + raise HTTPException(status_code=404) + try: + series = await decide_one_page_series( + session, + job_id, + series_id, + actor_id=user.id, + action="allow" if action == "allow" else "skip", + token=token, + ) + except ValidationError as exc: + await session.rollback() + raise HTTPException(status_code=409, detail=exc.message) from exc + await session.commit() + if action == "allow" and (series.diagnostics or {}).get("rematch_pending"): + trigger_import_series_rematch(job_id, series_id) + return await _render_import_review_partial( + job_id, request, user, session, status=status, page=page, sort=sort + ) + + +@router.get("/import/{job_id}/files/{file_id}/source", include_in_schema=False) +async def preview_source_action( + job_id: int, file_id: int, request: Request, user: InteractiveOperatorUser, session: DbSession +) -> Response: + from pullbox.services.import_review_source_actions import replacement_candidate + from pullbox.ui.import_routes import _ctx, _templates + + job, parent, file = await load_review_file(session, job_id, file_id) + candidate = await replacement_candidate(session, file, parent) + action = "pair" if candidate is not None else "recheck" + token = await sign_review_scope( + session, + { + "actor": user.id, + "action": action, + "scope": review_scope(job, parent, file), + "candidate_scope": review_scope(job, parent, candidate) if candidate else None, + }, + ) + return _templates().TemplateResponse( + request, + "partials/import_review_file_action.html", + _ctx( + request, + user, + job=job, + file=file, + candidate=candidate, + source_action=action, + token=token, + action="source", + ), + ) + + +@router.post("/import/{job_id}/files/{file_id}/source", include_in_schema=False) +async def apply_source_action( + job_id: int, + file_id: int, + user: InteractiveOperatorUser, + session: DbSession, + source_action: Annotated[str, Form()], + token: Annotated[str, Form()], +) -> Response: + from pullbox.services.import_review_source_actions import ( + queue_source_action, + replacement_candidate, + ) + from pullbox.tasks.import_task import trigger_import_review_source_action + + job, parent, file = await load_review_file(session, job_id, file_id) + candidate = ( + await replacement_candidate(session, file, parent) if source_action == "pair" else None + ) + await verify_review_scope( + session, + token, + { + "actor": user.id, + "action": source_action, + "scope": review_scope(job, parent, file), + "candidate_scope": review_scope(job, parent, candidate) if candidate else None, + }, + ) + await queue_source_action(session, job_id, file_id, action=source_action, actor_id=user.id) + await session.commit() + trigger_import_review_source_action(job_id, file_id) + return JSONResponse( + {"message": "Source verification queued. You can keep reviewing other series."} + ) + + +@router.get("/import/{job_id}/files/{file_id}/assign", include_in_schema=False) +async def preview_file_assignment( + job_id: int, + file_id: int, + request: Request, + user: InteractiveOperatorUser, + session: DbSession, + cv_id: int = Query(gt=0), + inline: bool = Query(False), + issue_page: int = Query(1, ge=1), + q: str = Query("", max_length=200), +) -> Response: + from pullbox.ui.import_routes import _ctx, _templates + + job, parent, file = await load_review_file(session, job_id, file_id) + service = await build_metadata_service(session) + series = await service.get_series_metadata(cv_id) + issues = await service.get_issue_summaries_for_series(cv_id) + query = q.strip().casefold() + filtered = [ + issue + for issue in issues + if not query or query in f"{issue.issue_number} {issue.title or ''}".casefold() + ] + page_count = max(1, (len(filtered) + 24) // 25) + issue_page = min(issue_page, page_count) + token = await sign_review_scope( + session, + { + "actor": user.id, + "action": "assign", + "cv_id": cv_id, + "scope": review_scope(job, parent, file), + }, + ) + return _templates().TemplateResponse( + request, + "partials/import_review_issue_choices.html" + if inline + else "partials/import_review_file_action.html", + _ctx( + request, + user, + job=job, + file=file, + series=series, + issues=filtered[(issue_page - 1) * 25 : issue_page * 25] if inline else issues, + issue_page=issue_page, + issue_page_count=page_count, + query=q, + token=token, + action="assign", + ), + ) + + +@router.post("/import/{job_id}/files/{file_id}/assign", include_in_schema=False) +async def apply_file_assignment( + job_id: int, + file_id: int, + user: InteractiveOperatorUser, + session: DbSession, + cv_id: Annotated[int, Form(gt=0)], + issue_cv_id: Annotated[int, Form(gt=0)], + token: Annotated[str, Form()], +) -> Response: + job, parent, file = await load_review_file(session, job_id, file_id) + await verify_review_scope( + session, + token, + { + "actor": user.id, + "action": "assign", + "cv_id": cv_id, + "scope": review_scope(job, parent, file), + }, + ) + await assign_review_file( + session, + job_id, + file_id, + cv_id=cv_id, + issue_cv_id=issue_cv_id, + metadata_service=await build_metadata_service(session), + ) + await session.commit() + return JSONResponse( + {"message": "File assigned. Source files and other issue decisions are unchanged."} + ) diff --git a/src/pullbox/ui/import_review_files.py b/src/pullbox/ui/import_review_files.py new file mode 100644 index 00000000..ed5e618c --- /dev/null +++ b/src/pullbox/ui/import_review_files.py @@ -0,0 +1,77 @@ +"""Bounded, read-only inventory of files recorded for an import series.""" + +from dataclasses import dataclass + +from fastapi import HTTPException +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from pullbox.models.import_job import ImportedFile, ImportedSeries +from pullbox.services.import_safety_diagnostics import normalize_import_safety_diagnostics + +FILES_PER_PAGE = 25 + + +@dataclass(frozen=True, slots=True) +class InventoryFile: + id: int + name: str + size: int + folder: str + missing_reference: bool + + +@dataclass(frozen=True, slots=True) +class SeriesFileInventory: + series: ImportedSeries + files: list[InventoryFile] + page: int + total_pages: int + total: int + + +async def load_series_file_inventory( + session: AsyncSession, job_id: int, series_id: int, page: int +) -> SeriesFileInventory: + series = await session.scalar( + select(ImportedSeries).where( + ImportedSeries.id == series_id, + ImportedSeries.import_job_id == job_id, + ) + ) + if series is None: + raise HTTPException(status_code=404, detail="Import series not found.") + scope = (ImportedFile.import_job_id == job_id, ImportedFile.import_series_id == series_id) + total = int(await session.scalar(select(func.count(ImportedFile.id)).where(*scope)) or 0) + total_pages = max(1, (total + FILES_PER_PAGE - 1) // FILES_PER_PAGE) + page = max(1, min(page, total_pages)) + rows = await session.execute( + select( + ImportedFile.id, + ImportedFile.file_name, + ImportedFile.file_path, + ImportedFile.file_size, + ImportedFile.diagnostics, + ) + .where(*scope) + .order_by(ImportedFile.file_name, ImportedFile.id) + .offset((page - 1) * FILES_PER_PAGE) + .limit(FILES_PER_PAGE) + ) + files = [] + for row in rows: + block = (row.diagnostics or {}).get("safety_block") + missing = ( + isinstance(block, dict) + and normalize_import_safety_diagnostics(block)["category"] == "source_missing" + ) + files.append( + InventoryFile( + id=row.id, + name=row.file_name, + size=row.file_size, + folder=row.file_path.replace("\\", "/").rsplit("/", 1)[0], + missing_reference=missing, + ) + ) + return SeriesFileInventory(series, files, page, total_pages, total) diff --git a/src/pullbox/ui/import_review_lanes.py b/src/pullbox/ui/import_review_lanes.py new file mode 100644 index 00000000..06c48c80 --- /dev/null +++ b/src/pullbox/ui/import_review_lanes.py @@ -0,0 +1,257 @@ +"""Read-only presentation of saved import outcomes.""" + +from __future__ import annotations + +from collections import Counter, defaultdict +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from sqlalchemy import func, select + +from pullbox.core.name_matcher import NameMatcher +from pullbox.models.import_job import ImportedFile, ImportedFileStatus, ImportedSeries +from pullbox.services.import_file_conflicts import review_filename_identity +from pullbox.services.import_safety_diagnostics import normalize_import_safety_diagnostics + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncSession + + +LANES = { + "decide": "Needs a decision", + "confirm": "Review suggestions", + "fix_source": "Fix source", + "blocked": "Cannot import", + "ready": "Ready", + "info": "Info", +} + +# The same labels can describe a saved outcome in review and in Follow-up. +REASONS = { + "needs_series": ("No series matched", "Find series", "decide"), + "series_conflict": ("Choose between series matches", "Choose series", "decide"), + "source_layout_review": ("Files need a layout decision", "Review layout", "decide"), + "needs_issue": ("Some files need an issue match", "Match issues", "decide"), + "same_comic_review": ("Files disagree about the comic", "Review files", "decide"), + "single_page_comic": ("One-page archive", "Review file", "decide"), + "decompression_size_limit": ("Large file needs approval", "Review file", "decide"), + "preparing_match": ("Updating the match", "Updating...", "decide"), + "duplicate_copy_confirm": ("Choose which copy to import", "Review copies", "confirm"), + "permission_unreadable": ("File cannot be read", "View details", "fix_source"), + "archive_inspection_failed": ("Archive could not be inspected", "View details", "fix_source"), + "source_changed": ("Source changed after scanning", "View details", "fix_source"), + "outside_approved_root": ("Library access needs attention", "View details", "fix_source"), + "zero_byte": ("File is empty", "View details", "fix_source"), + "archive_no_pages": ("Archive has no comic pages", "View details", "fix_source"), + "failed": ("File needs another inspection", "View details", "fix_source"), + "dangerous_path_or_payload": ("Unsafe archive content", "View details", "blocked"), + "unsupported_file_type": ("Unsupported file type", "View details", "blocked"), + "unknown": ("Safety could not be established", "View details", "blocked"), + "source_missing": ("Recorded file not found", "View details", "info"), + "already_handled": ("Already handled", "View details", "info"), + "ready": ("Ready to import", "View files", "ready"), +} + + +@dataclass(frozen=True, slots=True) +class ReviewFacts: + status: str + known_target: bool = False + kind: str = "" + pending: bool = False + ready_files: int = 0 + unmatched_files: int = 0 + conflict_files: int = 0 + approved_files: int = 0 + failed_files: int = 0 + identity_conflict: bool = False + safety_counts: dict[str, int] = field(default_factory=dict) + missing_references: int = 0 + + +@dataclass(frozen=True, slots=True) +class ReviewRow: + lane: str + reasons: tuple[str, ...] + ready_files: int + attention_files: int + updating: bool = False + missing_references: int = 0 + open_missing_references: int = 0 + + @property + def decision_files(self) -> int: + return max(0, self.attention_files - self.open_missing_references) + + @property + def label(self) -> str: + return REASONS[self.reasons[0]][0] + + @property + def action(self) -> str: + return "Updating..." if self.updating else REASONS[self.reasons[0]][1] + + +def classify_review_row(facts: ReviewFacts) -> ReviewRow: + """Derive a display lane without changing persisted outcomes.""" + reasons: list[str] = [] + if facts.status in {"skipped", "imported"}: + return ReviewRow( + "info", ("already_handled",), 0, 0, missing_references=facts.missing_references + ) + if facts.kind in {"series_conflict", "source_layout_review"}: + reasons.append(facts.kind) + elif ( + (facts.status == "no_match" and not facts.safety_counts and not facts.approved_files) + or facts.unmatched_files + or (facts.approved_files and not facts.pending) + ): + reasons.append("needs_issue" if facts.known_target else "needs_series") + if facts.conflict_files: + reasons.append("same_comic_review" if facts.identity_conflict else "duplicate_copy_confirm") + reasons.extend( + category if category in REASONS else "unknown" for category in facts.safety_counts + ) + if facts.kind == "mylar3_path_incompatible" and not facts.safety_counts: + reasons.append("outside_approved_root") + if facts.failed_files or facts.status == "failed": + reasons.append("failed") + if facts.pending: + reasons.append("preparing_match") + if not reasons: + reasons.append("ready" if facts.ready_files else "already_handled") + order = list(LANES) + reasons = sorted(dict.fromkeys(reasons), key=lambda reason: order.index(REASONS[reason][2])) + # Missing references can accompany useful files without making the files wait. + lane = REASONS[reasons[0]][2] + if lane == "info" and facts.ready_files: + lane = "ready" + reasons.insert(0, "ready") + return ReviewRow( + lane, + tuple(reasons), + facts.ready_files, + facts.unmatched_files + + facts.approved_files + + facts.conflict_files + + facts.failed_files + + sum(facts.safety_counts.values()), + facts.pending, + max(facts.missing_references, facts.safety_counts.get("source_missing", 0)), + facts.safety_counts.get("source_missing", 0), + ) + + +async def load_review_rows(session: AsyncSession, job_id: int) -> dict[int, ReviewRow]: + """Project compact saved facts; never load archives or change matching state.""" + status_counts: dict[int, Counter[str]] = defaultdict(Counter) + safety_counts: dict[int, Counter[str]] = defaultdict(Counter) + missing_references: Counter[int] = Counter() + identity_conflicts = await _identity_conflict_series_ids(session, job_id) + block = ImportedFile.diagnostics["safety_block"] + category = block["category"].as_string() + code = block["code"].as_string() + reason = block["reason"].as_string() + conflict_class = ImportedFile.diagnostics["conflict_class"].as_string() + groups = await session.execute( + select( + ImportedFile.import_series_id, + ImportedFile.status, + category, + code, + reason, + conflict_class, + func.count(), + ) + .where(ImportedFile.import_job_id == job_id) + .group_by( + ImportedFile.import_series_id, + ImportedFile.status, + category, + code, + reason, + conflict_class, + ) + ) + for series_id, status, raw_category, raw_code, raw_reason, group_class, count in groups: + status_counts[series_id][status.value] += count + if raw_code == "source_missing": + missing_references[series_id] += count + if status.value == "safety_blocked": + normalized = normalize_import_safety_diagnostics( + {"category": raw_category, "code": raw_code, "reason": raw_reason or raw_code or ""} + ) + safety_counts[series_id][str(normalized["category"])] += count + if status.value == "conflict" and group_class in {"series_mismatch", "year_disagreement"}: + identity_conflicts.add(series_id) + series_rows = await session.execute( + select( + ImportedSeries.id, + ImportedSeries.status, + ImportedSeries.cv_id, + ImportedSeries.user_selected_cv_id, + ImportedSeries.series_id, + ImportedSeries.diagnostics["kind"].as_string(), + ImportedSeries.diagnostics["rematch_pending"].as_boolean(), + ).where(ImportedSeries.import_job_id == job_id) + ) + rows: dict[int, ReviewRow] = {} + for series_id, status, cv_id, chosen_id, library_id, kind, pending in series_rows: + counts = status_counts[series_id] + rows[series_id] = classify_review_row( + ReviewFacts( + status=status.value, + known_target=bool(cv_id or chosen_id or library_id), + kind=kind or "", + pending=bool(pending), + ready_files=counts["matched"] + counts["confirmed"], + unmatched_files=counts["no_match"], + conflict_files=counts["conflict"], + approved_files=counts["safety_approved"], + failed_files=counts["failed"], + identity_conflict=series_id in identity_conflicts, + safety_counts=dict(safety_counts[series_id]), + missing_references=missing_references[series_id], + ) + ) + return rows + + +async def _identity_conflict_series_ids(session: AsyncSession, job_id: int) -> set[int]: + """Older jobs have no conflict class; interpret saved fields without rewriting them.""" + groups: dict[int, tuple[set[int], set[str], set[str], set[int], set[str | None]]] = {} + records = await session.execute( + select( + ImportedFile.conflict_group_id, + ImportedFile.import_series_id, + ImportedFile.parsed_series, + ImportedFile.parsed_year, + ImportedFile.content_hash, + ImportedFile.file_name, + ) + .where( + ImportedFile.import_job_id == job_id, + ImportedFile.status == ImportedFileStatus.CONFLICT, + ImportedFile.conflict_group_id.is_not(None), + ) + .distinct() + ) + for group_id, series_id, title, year, content_hash, file_name in records: + series, titles, saved_titles, years, hashes = groups.setdefault( + group_id, (set(), set(), set(), set(), set()) + ) + series.add(series_id) + if title: + saved_titles.add(NameMatcher.normalize(title)) + file_title, file_year = review_filename_identity(file_name, title, year) + if file_title: + titles.add(file_title) + if file_year: + years.add(file_year) + hashes.add(content_hash) + result: set[int] = set() + for series, titles, saved_titles, years, hashes in groups.values(): + identical = len(hashes) == 1 and None not in hashes and "" not in hashes + if not identical and (len(titles) > 1 or len(saved_titles) > 1 or len(years) > 1): + result.update(series) + return result diff --git a/src/pullbox/ui/import_review_presentation.py b/src/pullbox/ui/import_review_presentation.py new file mode 100644 index 00000000..ee493e3f --- /dev/null +++ b/src/pullbox/ui/import_review_presentation.py @@ -0,0 +1,49 @@ +"""Presentation vocabulary for review; never changes matching or selection.""" + +LANE_DESCRIPTIONS = { + "decide": "Choose a match, a copy, or what to do with an unusual file.", + "confirm": "Pullbox suggested a keeper. Accept it or choose another copy.", + "fix_source": "Fix the source, then recheck. Ready files do not have to wait.", + "blocked": "These files cannot import safely. Replace them or leave them out.", + "ready": "Matched files ready for your library. Choose what to import.", + "info": "Missing references and files already handled. Nothing here blocks ready files.", + "story_arcs": "Optional reading lists. Canonical comics import independently.", +} + +FILTER_LABELS = { + "needs_series": "Series match", + "series_conflict": "Series choice", + "needs_issue": "Issue match", + "same_comic_review": "Same comic?", + "single_page_comic": "One-page archives", + "decompression_size_limit": "Large files", + "duplicate_copy_confirm": "Duplicate copies", + "source_missing": "Stale Mylar references", + "already_handled": "Already handled", +} + +REASON_DESCRIPTIONS = { + "needs_series": "No confident series match. Search ComicVine or choose a candidate.", + "series_conflict": "More than one series fits. Choose the right one.", + "needs_issue": "The series is known, but some files still need an issue.", + "same_comic_review": "Titles or years disagree. Check before choosing a keeper.", + "single_page_comic": "Cover art, a damaged download, or an intentional one-pager.", + "decompression_size_limit": "The archive exceeds your configured safety limit.", + "duplicate_copy_confirm": "Multiple copies of one issue. Keep one for this import.", + "permission_unreadable": "Pullbox could not read this source. Check access, then recheck.", + "archive_inspection_failed": ( + "The source could not be inspected. Repair or replace it, then recheck." + ), + "source_changed": "The source no longer matches the scan. Verify it again before importing.", + "outside_approved_root": "Register or map the source in import setup before continuing.", + "zero_byte": "An empty file cannot contain a comic. Replace it or skip it.", + "archive_no_pages": "No readable comic pages were found in this archive.", + "dangerous_path_or_payload": "Unsafe archive content cannot be allowed once.", + "unsupported_file_type": "This file is not a supported comic archive.", + "source_missing": ( + "The saved filename is not on disk. Pair a proven replacement or skip the reference." + ), + "already_handled": "These files were skipped, already owned, or handled by the import.", + "ready": "The saved issue matches are ready. Original files remain protected.", + "preparing_match": "Your decision was saved. Matching continues in the background.", +} diff --git a/src/pullbox/ui/import_review_summary.py b/src/pullbox/ui/import_review_summary.py index 858f119e..ece37bfa 100644 --- a/src/pullbox/ui/import_review_summary.py +++ b/src/pullbox/ui/import_review_summary.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections import Counter from collections.abc import Mapping from typing import TYPE_CHECKING, Any, cast @@ -16,6 +17,7 @@ ) from pullbox.models.story_arc import ImportedStoryArcStatus, StoryArcResolutionState from pullbox.models.story_arc_import import ImportedStoryArc, ImportedStoryArcEntry +from pullbox.services.import_file_selection import not_excluded_from_review from pullbox.services.import_review_selection import load_import_review_selection_state from pullbox.services.import_safety_diagnostics import ( ImportSafetyCategory, @@ -58,14 +60,41 @@ async def load_import_review_summary( } file_counts_result = await session.execute( - select(ImportedFile.status, func.count(ImportedFile.id)) + select( + ImportedFile.status, + ImportedSeries.status, + ImportedFile.diagnostics["safety_block"]["code"].as_string(), + func.count(ImportedFile.id), + ) + .join(ImportedSeries, ImportedSeries.id == ImportedFile.import_series_id) .where(ImportedFile.import_job_id == job.id) - .group_by(ImportedFile.status) + .group_by( + ImportedFile.status, + ImportedSeries.status, + ImportedFile.diagnostics["safety_block"]["code"].as_string(), + ) ) - file_counts = { - status.value if hasattr(status, "value") else str(status): count - for status, count in file_counts_result.all() + file_counts: Counter[str] = Counter() + settled_count = 0 + missing_references = 0 + missing_blocked = 0 + settled_file_statuses = { + ImportedFileStatus.MATCHED, + ImportedFileStatus.CONFIRMED, + ImportedFileStatus.SKIPPED, + ImportedFileStatus.DUPLICATE_FILE, + ImportedFileStatus.ALREADY_OWNED, + ImportedFileStatus.IMPORTED, } + for status, parent_status, safety_code, count in file_counts_result.all(): + file_counts[status.value] += count + if safety_code == "source_missing": + missing_references += count + if status == ImportedFileStatus.SAFETY_BLOCKED: + missing_blocked += count + continue + if status in settled_file_statuses or parent_status == ImportSeriesStatus.SKIPPED: + settled_count += count duplicate_file_counts_result = await session.execute( select(ImportedFile.status, func.count(ImportedFile.id)) @@ -93,6 +122,21 @@ async def load_import_review_summary( ) selection_state = await load_import_review_selection_state(session, job.id) duplicate_selected_count = _object_to_int(selection_state["duplicate_files_selected"]) + matched_selected_file_count = int( + await session.scalar( + select(func.count(ImportedFile.id)) + .join(ImportedSeries, ImportedSeries.id == ImportedFile.import_series_id) + .where( + ImportedFile.import_job_id == job.id, + ImportedSeries.status == ImportSeriesStatus.MATCHED, + ImportedSeries.selected_for_import.is_(True), + ImportedSeries.files_matched > 0, + ImportedFile.status.in_([ImportedFileStatus.MATCHED, ImportedFileStatus.CONFIRMED]), + not_excluded_from_review(), + ) + ) + or 0 + ) duplicate_selected_series_count = _object_to_int(selection_state["duplicate_series_selected"]) matched_selected_series_count = _object_to_int(selection_state["matched_series_selected"]) duplicate_importable_series_count = _object_to_int( @@ -180,6 +224,8 @@ async def load_import_review_summary( ) row_summary = { + "review_files_settled": settled_count, + "review_files_open": sum(file_counts.values()) - missing_references - settled_count, "series_total": sum(series_counts.values()), "series_in_library": series_counts.get(ImportSeriesStatus.DUPLICATE.value, 0), "series_matched": series_counts.get(ImportSeriesStatus.MATCHED.value, 0), @@ -192,13 +238,16 @@ async def load_import_review_summary( "series_imported": series_counts.get(ImportSeriesStatus.IMPORTED.value, 0), "series_failed": series_counts.get(ImportSeriesStatus.FAILED.value, 0), "files_total": sum(file_counts.values()), + "files_present": sum(file_counts.values()) - missing_references, + "files_missing_references": missing_references, "files_matched": file_counts.get(ImportedFileStatus.MATCHED.value, 0), "files_duplicate": file_counts.get(ImportedFileStatus.DUPLICATE_FILE.value, 0), "files_already_owned": file_counts.get(ImportedFileStatus.ALREADY_OWNED.value, 0), "files_confirmed": file_counts.get(ImportedFileStatus.CONFIRMED.value, 0), "files_conflict": file_counts.get(ImportedFileStatus.CONFLICT.value, 0), "files_no_match": file_counts.get(ImportedFileStatus.NO_MATCH.value, 0), - "files_safety_blocked": file_counts.get(ImportedFileStatus.SAFETY_BLOCKED.value, 0), + "files_safety_blocked": file_counts.get(ImportedFileStatus.SAFETY_BLOCKED.value, 0) + - missing_blocked, "files_imported": file_counts.get(ImportedFileStatus.IMPORTED.value, 0), "files_failed": file_counts.get(ImportedFileStatus.FAILED.value, 0), "duplicate_files_importable": duplicate_file_counts.get(ImportedFileStatus.MATCHED.value, 0) @@ -211,10 +260,11 @@ async def load_import_review_summary( "selected_series_total": matched_selected_series_count + duplicate_selected_series_count, # Story Arcs are optional follow-up work, not canonical import items. "selected_items_total": _object_to_int(selection_state["selected_item_count"]), + "selected_files_total": matched_selected_file_count + duplicate_selected_count, "importable_items_total": _object_to_int(selection_state["importable_item_count"]), "ready_to_import_total": _object_to_int(selection_state["importable_item_count"]), "needs_attention_total": needs_attention_series_total, - "needs_attention_files_total": needs_attention_files_total, + "needs_attention_files_total": needs_attention_files_total - missing_blocked, "resolved_file_conflict_groups": resolved_file_conflict_groups, "story_arcs_total": sum(story_arc_counts.values()), "story_arcs_detected": story_arc_counts.get(ImportedStoryArcStatus.DETECTED.value, 0), @@ -277,6 +327,15 @@ async def load_import_review_summary( row_summary["series_conflicts_total"] = ( row_summary["series_file_conflicts"] + series_candidate_conflicts ) + row_summary["file_conflict_groups"] = int( + await session.scalar( + select(func.count(func.distinct(ImportedFile.conflict_group_id))).where( + ImportedFile.import_job_id == job.id, + ImportedFile.status == ImportedFileStatus.CONFLICT, + ) + ) + or 0 + ) active_scan_statuses = { ImportJobStatus.PENDING, @@ -325,6 +384,7 @@ async def load_import_safety_failure_summary( accumulator = ImportSafetyFailureSummaryAccumulator() bulk_overrideable_counts: dict[str, int] = {} + series_by_category: dict[str, set[int]] = {} # Avoid rescanning/sorting failures for every page or loading their source metadata. result = await session.stream( select( @@ -332,6 +392,7 @@ async def load_import_safety_failure_summary( ImportedFile.diagnostics["safety_block"].label("safety_block"), ImportedFile.diagnostics["source_revalidation"].label("source_revalidation"), ImportedFile.status, + ImportedFile.import_series_id, ) .where( ImportedFile.import_job_id == job.id, @@ -342,11 +403,12 @@ async def load_import_safety_failure_summary( ) try: async for rows in result.partitions(page_size): - for file_name, safety_block, source_revalidation, status in rows: + for file_name, safety_block, source_revalidation, status, series_id in rows: if isinstance(safety_block, Mapping): accumulator.add(str(file_name), safety_block) normalized = normalize_import_safety_diagnostics(safety_block) category = str(normalized["category"]) + series_by_category.setdefault(category, set()).add(series_id) if ( status == ImportedFileStatus.SAFETY_BLOCKED and category == ImportSafetyCategory.DECOMPRESSION_SIZE_LIMIT.value @@ -358,12 +420,15 @@ async def load_import_safety_failure_summary( continue if isinstance(source_revalidation, Mapping): accumulator.add(str(file_name), source_revalidation) + normalized = normalize_import_safety_diagnostics(source_revalidation) + series_by_category.setdefault(str(normalized["category"]), set()).add(series_id) finally: await result.close() summaries = accumulator.summaries() for summary in summaries: category = str(summary["category"]) + summary["series_count"] = len(series_by_category.get(category, set())) bulk_overrideable_count = bulk_overrideable_counts.get(category, 0) summary["bulk_overrideable_count"] = bulk_overrideable_count summary["bulk_overrideable"] = bulk_overrideable_count > 0 diff --git a/src/pullbox/ui/import_review_tables.py b/src/pullbox/ui/import_review_tables.py index 3441d796..60381db4 100644 --- a/src/pullbox/ui/import_review_tables.py +++ b/src/pullbox/ui/import_review_tables.py @@ -355,19 +355,36 @@ def _build_import_review_file_detail_row( ) -> dict[str, object]: """Build the inline Step 3 file-detail payload used by review dropdowns.""" diagnostics = dict(imp_file.diagnostics or {}) + source = diagnostics.get("source_metadata") or {} + issue = issue_map.get(imp_file.matched_issue_id) if imp_file.matched_issue_id else None + issue_cv_id = imp_file.matched_issue_cv_id or (issue.comicvine_id if issue else None) + issue_number = issue.issue_number if issue else diagnostics.get("target_issue_number") + if issue_number is None: + issue_number = imp_file.parsed_issue_number return { "id": imp_file.id, "file_name": imp_file.file_name, "file_size": imp_file.file_size, "has_comicinfo": imp_file.has_comicinfo, "status": imp_file.status.value, + "include_in_import": imp_file.include_in_import, + "review_selected": dict(imp_file.diagnostics or {}).get("review_selection", True), + "archive_format": diagnostics.get("archive_format", source.get("archive_format", {})), + "page_count": diagnostics.get( + "content_inspection", source.get("content_inspection", {}) + ).get("page_count"), "match_confidence": imp_file.match_confidence, "matched_issue_label": _import_review_matched_issue_label(imp_file, issue_map), + "matched_issue_cv_id": issue_cv_id, + "matched_issue_url": _comicvine_issue_url(issue_cv_id) if issue_cv_id else None, + "matched_issue_number": _format_import_review_issue_number(issue_number), "duplicate_reason_label": _import_review_duplicate_reason_label( diagnostics.get("duplicate_reason") if isinstance(diagnostics.get("duplicate_reason"), str) else None - ), + ) + if imp_file.status is ImportedFileStatus.DUPLICATE_FILE + else None, "duplicate_keep_label": diagnostics.get("representative_file_name"), } @@ -380,6 +397,10 @@ def _import_review_file_group_key( return "matched" if imp_file.status == ImportedFileStatus.CONFLICT: return "conflict" + if imp_file.status == ImportedFileStatus.SKIPPED and ( + imp_file.match_method == "import_reconcile_skip" or imp_file.conflict_group_id is not None + ): + return "skipped" if imp_file.status == ImportedFileStatus.ALREADY_OWNED: return "already_owned" if imp_file.status == ImportedFileStatus.DUPLICATE_FILE: @@ -443,6 +464,7 @@ async def _load_import_review_file_detail_groups( "already_owned": [], "duplicate_file": [], "no_match": [], + "skipped": [], } for series_id in series_ids } @@ -458,6 +480,7 @@ async def _load_import_review_file_detail_groups( ) group_definitions = [ + ("skipped", "Skipped", "Your saved file decisions. Source files are unchanged."), ( "matched", "Importable files", diff --git a/src/pullbox/ui/import_routes.py b/src/pullbox/ui/import_routes.py index ed5a313d..104a1e14 100644 --- a/src/pullbox/ui/import_routes.py +++ b/src/pullbox/ui/import_routes.py @@ -51,7 +51,7 @@ snapshot_mode_for_job, ) from pullbox.tasks.import_task import trigger_import_safety_bulk_rematch -from pullbox.ui import import_orphaned_routes +from pullbox.ui import import_orphaned_routes, import_review_file_routes from pullbox.ui.comicvine_series_search import ( COMICVINE_SERIES_SEARCH_LIMIT, IMPORT_CV_MATCH_DISPLAY_LIMIT, @@ -73,7 +73,7 @@ from pullbox.ui.import_progress_snapshot import build_import_progress_snapshot from pullbox.ui.import_results_context import load_import_results_context from pullbox.ui.import_review_context import ( - has_pending_import_safety_rematch, + has_pending_import_review_rematch, load_import_review_context, ) from pullbox.ui.import_review_summary import load_import_review_summary @@ -81,6 +81,7 @@ from pullbox.ui.import_story_arc_entry_review import StoryArcEntryResolutionFilter router = APIRouter() +router.include_router(import_review_file_routes.router) _GetTemplates = Callable[[], Jinja2Templates] _BuildContext = Callable[..., dict[str, object]] @@ -563,7 +564,7 @@ async def import_review_rematch_status( if job is None: raise NotFoundError("ImportJob", job_id) - pending = await has_pending_import_safety_rematch(session, job_id) + pending = await has_pending_import_review_rematch(session, job_id) if pending: escaped_job_id = escape(str(job_id), quote=True) return HTMLResponse( @@ -594,6 +595,13 @@ async def _render_import_review_partial( ) -> Response: """Render the canonical review partial with optional route-local state.""" + from sqlalchemy import inspect + + from pullbox.models.user import User + + # Rollback expires ORM attributes, but the authenticated identity remains available. + identity = inspect(user).identity if isinstance(user, User) else None + job = await session.get(ImportJob, job_id) if job is None: raise NotFoundError("ImportJob", job_id) @@ -607,6 +615,8 @@ async def _render_import_review_partial( story_arc_id=story_arc_id, arc_entry_state=arc_entry_state, arc_entry_page=arc_entry_page, + reason=request.query_params.get("reason"), + actor_id=int(identity[0]) if identity else None, ) if extra_context: template_ctx.update(extra_context) @@ -1464,6 +1474,7 @@ async def import_cv_search( user: AuthenticatedUser, session: DbSession, q: str = Query(""), + file_id: Annotated[int | None, Query(gt=0)] = None, ) -> Response: """Search ComicVine for matching series and return inline result partial.""" from pullbox.core.comicvine_key import get_comicvine_api_key @@ -1475,6 +1486,12 @@ async def import_cv_search( item = await session.get(ImportedSeries, series_id) if item is None or item.import_job_id != job_id: raise NotFoundError("ImportedSeries", series_id) + if file_id is not None: + from pullbox.services.import_review_file_assignment import load_review_file + + _job, file_parent, _file = await load_review_file(session, job_id, file_id) + if file_parent.id != series_id: + raise NotFoundError("ImportedFile", file_id) results: list[dict[str, object]] = [] search_error = "" @@ -1530,6 +1547,7 @@ async def import_cv_search( job_id=job_id, series_id=series_id, query=q, + file_id=file_id, results=results, results_limit=IMPORT_CV_MATCH_DISPLAY_LIMIT, search_error=search_error, diff --git a/src/pullbox/ui/standalone_shell.py b/src/pullbox/ui/standalone_shell.py index d0d8b02b..411e0d81 100644 --- a/src/pullbox/ui/standalone_shell.py +++ b/src/pullbox/ui/standalone_shell.py @@ -21,6 +21,7 @@ _STATIC_DIR / "js" / "pullbox.js", _STATIC_DIR / "js" / "story-arc-preview.js", _STATIC_DIR / "js" / "story-arc-detail.js", + _STATIC_DIR / "js" / "series-rescan.js", ) diff --git a/src/pullbox/ui/static/css/tailwind.css b/src/pullbox/ui/static/css/tailwind.css index 14c8ea04..2b9d7229 100644 --- a/src/pullbox/ui/static/css/tailwind.css +++ b/src/pullbox/ui/static/css/tailwind.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--color-red-300:oklch(80.8% .114 19.571);--color-slate-950:oklch(12.9% .042 264.695);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-xs:4px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:DM Sans,sans-serif;line-height:1.5}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:JetBrains Mono,Fira Code,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:root,[data-theme=dark]{--pb-surface-app:#101824;--pb-surface-shell:#141e2c;--pb-surface-card:#172231;--pb-surface-raised:#1d2a3d;--pb-surface-input:#132030;--pb-surface-overlay:#070b12b8;--pb-surface-selected:#8fb9ee29;--pb-text-primary:#f7f1e8;--pb-text-secondary:#cbd5e1;--pb-text-tertiary:#9aaaba;--pb-text-inverse:#101824;--pb-border-subtle:#cbd5e11f;--pb-border-default:#cbd5e133;--pb-border-strong:#cbd5e152;--pb-interactive:#8fb9ee;--pb-interactive-hover:#7da9df;--pb-interactive-active:#6a96c8;--pb-interactive-selected:#8fb9ee2e;--pb-brand:#c6a17b;--pb-brand-muted:#c6a17b29;--pb-status-success:#68b88b;--pb-status-warning:#d7a15b;--pb-status-danger:#e38473;--pb-status-info:#8fb9ee;--pb-focus-ring:#8fb9ee47;--pb-focus-outline:#8fb9ee85;--pb-selection:#8fb9ee38;--pb-shadow-0:none;--pb-shadow-1:0 1px 2px #070b1238, 0 10px 24px #070b1229;--pb-shadow-2:0 2px 6px #070b1247, 0 18px 40px #070b1238;--pb-shadow-overlay:0 20px 56px #00000075;--pb-brand-hover:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-hover:color-mix(in srgb, var(--pb-brand) 84%, black)}}:root,[data-theme=dark]{--pb-brand-dim:var(--pb-brand-muted);--pb-brand-border:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-border:color-mix(in srgb, var(--pb-brand) 28%, transparent)}}:root,[data-theme=dark]{--pb-brand-signal:var(--pb-status-warning);--pb-brand-signal-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-signal-dim:color-mix(in srgb, var(--pb-status-warning) 16%, transparent)}}:root,[data-theme=dark]{--pb-brand-signal-hover:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-signal-hover:color-mix(in srgb, var(--pb-status-warning) 22%, transparent)}}:root,[data-theme=dark]{--pb-brand-signal-border:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-signal-border:color-mix(in srgb, var(--pb-status-warning) 28%, transparent)}}:root,[data-theme=dark]{--pb-interactive-dim:var(--pb-interactive-selected);--pb-interactive-border:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-interactive-border:color-mix(in srgb, var(--pb-interactive) 30%, transparent)}}:root,[data-theme=dark]{--pb-success:var(--pb-status-success);--pb-success-dim:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-success-dim:color-mix(in srgb, var(--pb-status-success) 18%, transparent)}}:root,[data-theme=dark]{--pb-warning:var(--pb-status-warning);--pb-warning-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-warning-dim:color-mix(in srgb, var(--pb-status-warning) 20%, transparent)}}:root,[data-theme=dark]{--pb-error:var(--pb-status-danger);--pb-error-dim:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-error-dim:color-mix(in srgb, var(--pb-status-danger) 18%, transparent)}}:root,[data-theme=dark]{--pb-info:var(--pb-status-info);--pb-info-dim:var(--pb-status-info)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-info-dim:color-mix(in srgb, var(--pb-status-info) 18%, transparent)}}:root,[data-theme=dark]{--pb-purple:#a88bda;--pb-purple-dim:#a88bda2e;--pb-bg-base:var(--pb-surface-app);--pb-bg-surface:var(--pb-surface-shell);--pb-bg-card:var(--pb-surface-card);--pb-bg-card-hover:var(--pb-surface-card)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-bg-card-hover:color-mix(in srgb, var(--pb-surface-card) 72%, var(--pb-surface-raised))}}:root,[data-theme=dark]{--pb-bg-input:var(--pb-surface-input);--pb-bg-overlay:var(--pb-surface-overlay);--pb-table-row-stripe:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-table-row-stripe:color-mix(in srgb, var(--pb-bg-card-hover) 68%, transparent)}}:root,[data-theme=dark]{--pb-table-row-hover:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-table-row-hover:color-mix(in srgb, var(--pb-bg-card-hover) 88%, transparent)}}:root,[data-theme=dark]{--pb-table-cell-x:1.25rem;--pb-table-cell-x-tight:1rem;--pb-page-footer-clearance:1.5rem;--pb-text-dim:var(--pb-text-tertiary);--pb-border:var(--pb-border-default);--pb-border-hover:var(--pb-border-strong)}[data-theme=light]{--pb-surface-app:#f6f1e8;--pb-surface-shell:#fbf6ee;--pb-surface-card:#fffcf7;--pb-surface-raised:#fff;--pb-surface-input:#fffdfa;--pb-surface-overlay:#1a16136b;--pb-surface-selected:#2f5e8c1f;--pb-text-primary:#1e1a17;--pb-text-secondary:#51473d;--pb-text-tertiary:#74675b;--pb-text-inverse:#f7f1e8;--pb-border-subtle:#51473d24;--pb-border-default:#51473d33;--pb-border-strong:#51473d57;--pb-interactive:#2f5e8c;--pb-interactive-hover:#274f76;--pb-interactive-active:#21435f;--pb-interactive-selected:#2f5e8c24;--pb-brand:#855e3d;--pb-brand-muted:#8c68471f;--pb-status-success:#2e6a4f;--pb-status-warning:#8a5a1f;--pb-status-danger:#b24432;--pb-status-info:#2f5e8c;--pb-focus-ring:#2f5e8c42;--pb-focus-outline:#2f5e8cb8;--pb-selection:#2f5e8c33;--pb-shadow-0:none;--pb-shadow-1:0 1px 2px #1a161314, 0 10px 24px #1a16130f;--pb-shadow-2:0 2px 6px #1a16131a, 0 18px 40px #1a161314;--pb-shadow-overlay:0 20px 56px #1a161333;--pb-brand-hover:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-hover:color-mix(in srgb, var(--pb-brand) 84%, black)}}[data-theme=light]{--pb-brand-dim:var(--pb-brand-muted);--pb-brand-border:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-border:color-mix(in srgb, var(--pb-brand) 28%, transparent)}}[data-theme=light]{--pb-brand-signal:var(--pb-status-warning);--pb-brand-signal-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-signal-dim:color-mix(in srgb, var(--pb-status-warning) 12%, transparent)}}[data-theme=light]{--pb-brand-signal-hover:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-signal-hover:color-mix(in srgb, var(--pb-status-warning) 18%, transparent)}}[data-theme=light]{--pb-brand-signal-border:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-signal-border:color-mix(in srgb, var(--pb-status-warning) 24%, transparent)}}[data-theme=light]{--pb-interactive-dim:var(--pb-interactive-selected);--pb-interactive-border:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-interactive-border:color-mix(in srgb, var(--pb-interactive) 28%, transparent)}}[data-theme=light]{--pb-success:var(--pb-status-success);--pb-success-dim:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-success-dim:color-mix(in srgb, var(--pb-status-success) 12%, transparent)}}[data-theme=light]{--pb-warning:var(--pb-status-warning);--pb-warning-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-warning-dim:color-mix(in srgb, var(--pb-status-warning) 14%, transparent)}}[data-theme=light]{--pb-error:var(--pb-status-danger);--pb-error-dim:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-error-dim:color-mix(in srgb, var(--pb-status-danger) 12%, transparent)}}[data-theme=light]{--pb-info:var(--pb-status-info);--pb-info-dim:var(--pb-status-info)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-info-dim:color-mix(in srgb, var(--pb-status-info) 12%, transparent)}}[data-theme=light]{--pb-purple:#7b5c91;--pb-purple-dim:#7b5c9124;--pb-bg-base:var(--pb-surface-app);--pb-bg-surface:var(--pb-surface-shell);--pb-bg-card:var(--pb-surface-card);--pb-bg-card-hover:var(--pb-surface-card)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-bg-card-hover:color-mix(in srgb, var(--pb-surface-card) 72%, var(--pb-surface-raised))}}[data-theme=light]{--pb-bg-input:var(--pb-surface-input);--pb-bg-overlay:var(--pb-surface-overlay);--pb-table-row-stripe:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-table-row-stripe:color-mix(in srgb, var(--pb-surface-shell) 40%, transparent)}}[data-theme=light]{--pb-table-row-hover:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-table-row-hover:color-mix(in srgb, var(--pb-surface-shell) 58%, transparent)}}[data-theme=light]{--pb-table-cell-x:1.25rem;--pb-table-cell-x-tight:1rem;--pb-text-dim:var(--pb-text-tertiary);--pb-border:var(--pb-border-default);--pb-border-hover:var(--pb-border-strong)}@media (prefers-color-scheme:light){:root:not([data-theme]){--pb-surface-app:#f6f1e8;--pb-surface-shell:#fbf6ee;--pb-surface-card:#fffcf7;--pb-surface-raised:#fff;--pb-surface-input:#fffdfa;--pb-surface-overlay:#1a16136b;--pb-surface-selected:#2f5e8c1f;--pb-text-primary:#1e1a17;--pb-text-secondary:#51473d;--pb-text-tertiary:#74675b;--pb-text-inverse:#f7f1e8;--pb-border-subtle:#51473d24;--pb-border-default:#51473d33;--pb-border-strong:#51473d57;--pb-interactive:#2f5e8c;--pb-interactive-hover:#274f76;--pb-interactive-active:#21435f;--pb-interactive-selected:#2f5e8c24;--pb-brand:#855e3d;--pb-brand-muted:#8c68471f;--pb-status-success:#2e6a4f;--pb-status-warning:#8a5a1f;--pb-status-danger:#b24432;--pb-status-info:#2f5e8c;--pb-focus-ring:#2f5e8c42;--pb-focus-outline:#2f5e8cb8;--pb-selection:#2f5e8c33;--pb-shadow-0:none;--pb-shadow-1:0 1px 2px #1a161314, 0 10px 24px #1a16130f;--pb-shadow-2:0 2px 6px #1a16131a, 0 18px 40px #1a161314;--pb-shadow-overlay:0 20px 56px #1a161333;--pb-brand-hover:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-hover:color-mix(in srgb, var(--pb-brand) 84%, black)}}:root:not([data-theme]){--pb-brand-dim:var(--pb-brand-muted);--pb-brand-border:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-border:color-mix(in srgb, var(--pb-brand) 28%, transparent)}}:root:not([data-theme]){--pb-brand-signal:var(--pb-status-warning);--pb-brand-signal-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-signal-dim:color-mix(in srgb, var(--pb-status-warning) 12%, transparent)}}:root:not([data-theme]){--pb-brand-signal-hover:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-signal-hover:color-mix(in srgb, var(--pb-status-warning) 18%, transparent)}}:root:not([data-theme]){--pb-brand-signal-border:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-signal-border:color-mix(in srgb, var(--pb-status-warning) 24%, transparent)}}:root:not([data-theme]){--pb-interactive-dim:var(--pb-interactive-selected);--pb-interactive-border:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-interactive-border:color-mix(in srgb, var(--pb-interactive) 28%, transparent)}}:root:not([data-theme]){--pb-success:var(--pb-status-success);--pb-success-dim:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-success-dim:color-mix(in srgb, var(--pb-status-success) 12%, transparent)}}:root:not([data-theme]){--pb-warning:var(--pb-status-warning);--pb-warning-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-warning-dim:color-mix(in srgb, var(--pb-status-warning) 14%, transparent)}}:root:not([data-theme]){--pb-error:var(--pb-status-danger);--pb-error-dim:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-error-dim:color-mix(in srgb, var(--pb-status-danger) 12%, transparent)}}:root:not([data-theme]){--pb-info:var(--pb-status-info);--pb-info-dim:var(--pb-status-info)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-info-dim:color-mix(in srgb, var(--pb-status-info) 12%, transparent)}}:root:not([data-theme]){--pb-purple:#7b5c91;--pb-purple-dim:#7b5c9124;--pb-bg-base:var(--pb-surface-app);--pb-bg-surface:var(--pb-surface-shell);--pb-bg-card:var(--pb-surface-card);--pb-bg-card-hover:var(--pb-surface-card)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-bg-card-hover:color-mix(in srgb, var(--pb-surface-card) 72%, var(--pb-surface-raised))}}:root:not([data-theme]){--pb-bg-input:var(--pb-surface-input);--pb-bg-overlay:var(--pb-surface-overlay);--pb-table-row-stripe:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-table-row-stripe:color-mix(in srgb, var(--pb-surface-shell) 40%, transparent)}}:root:not([data-theme]){--pb-table-row-hover:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-table-row-hover:color-mix(in srgb, var(--pb-surface-shell) 58%, transparent)}}:root:not([data-theme]){--pb-table-cell-x:1.25rem;--pb-table-cell-x-tight:1rem;--pb-text-dim:var(--pb-text-tertiary);--pb-border:var(--pb-border-default);--pb-border-hover:var(--pb-border-strong)}}html,body{overscroll-behavior:none;overflow:hidden}:focus-visible{outline:2px solid var(--pb-focus-outline);outline-offset:3px;box-shadow:0 0 0 4px var(--pb-focus-ring);border-radius:4px}::selection{background-color:var(--pb-selection)}:where(a[href]:not([aria-disabled=true]),button:not(:disabled):not([aria-disabled=true]),input[type=button]:not(:disabled),input[type=submit]:not(:disabled),input[type=reset]:not(:disabled),input[type=checkbox]:not(:disabled),input[type=radio]:not(:disabled),input[type=file]:not(:disabled),input[type=image]:not(:disabled),select:not(:disabled),summary,[role=button]:not([aria-disabled=true]),[role=link]:not([aria-disabled=true]),label:has(:is(input[type=checkbox],input[type=radio],input[type=file]):not(:disabled))){cursor:pointer}:where(button:disabled,input:disabled,select:disabled,[aria-disabled=true]){cursor:not-allowed}}@layer components{.control-size-sm,.btn-sm,.chip-btn-sm,.icon-btn-sm{--pb-control-min-height:2rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.4375rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:.875rem;--pb-control-panel-item-px:.75rem;--pb-control-panel-item-py:.5rem;--pb-control-search-icon-left:.625rem;--pb-control-search-padding-left:2rem;--pb-control-search-padding-right:1.875rem;--pb-control-clear-right:.5rem}.control-size-md,.btn-md,.chip-btn,.icon-btn,.dropdown-select,.search-field,.btn-primary,.btn-warning,.btn-ghost,.btn-danger{--pb-control-min-height:2.5rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.5rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:1rem;--pb-control-panel-item-px:.75rem;--pb-control-panel-item-py:.5rem;--pb-control-search-icon-left:.75rem;--pb-control-search-padding-left:2.5rem;--pb-control-search-padding-right:2.25rem;--pb-control-clear-right:.625rem}.control-size-lg,.btn-lg,.chip-btn-lg,.icon-btn-lg{--pb-control-min-height:2.875rem;--pb-control-radius:.625rem;--pb-control-gap:.625rem;--pb-control-px:.875rem;--pb-control-py:.625rem;--pb-control-font-size:.9375rem;--pb-control-line-height:1.375rem;--pb-control-icon-size:1rem;--pb-control-panel-item-px:.875rem;--pb-control-panel-item-py:.625rem;--pb-control-search-icon-left:.875rem;--pb-control-search-padding-left:2.75rem;--pb-control-search-padding-right:2.5rem;--pb-control-clear-right:.75rem}.btn-primary.btn-sm,.btn-warning.btn-sm,.btn-ghost.btn-sm,.btn-danger.btn-sm,.btn-primary.control-size-sm,.btn-warning.control-size-sm,.btn-ghost.control-size-sm,.btn-danger.control-size-sm{--pb-control-min-height:2rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.4375rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:.875rem}.btn-primary.btn-md,.btn-warning.btn-md,.btn-ghost.btn-md,.btn-danger.btn-md,.btn-primary.control-size-md,.btn-warning.control-size-md,.btn-ghost.control-size-md,.btn-danger.control-size-md{--pb-control-min-height:2.5rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.5rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:1rem}.btn-primary.btn-lg,.btn-warning.btn-lg,.btn-ghost.btn-lg,.btn-danger.btn-lg,.btn-primary.control-size-lg,.btn-warning.control-size-lg,.btn-ghost.control-size-lg,.btn-danger.control-size-lg{--pb-control-min-height:2.875rem;--pb-control-radius:.625rem;--pb-control-gap:.625rem;--pb-control-px:.875rem;--pb-control-py:.625rem;--pb-control-font-size:.9375rem;--pb-control-line-height:1.375rem;--pb-control-icon-size:1rem}.btn-primary{background-color:var(--pb-interactive);--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:10px;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.btn-primary:hover{background-color:var(--pb-interactive-hover)}}.btn-primary:disabled{cursor:not-allowed;opacity:.5}.btn-primary{color:var(--pb-text-inverse);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.header-add-menu{flex-shrink:0;display:inline-flex;position:relative}.header-add-menu__primary{gap:calc(var(--spacing) * 2);white-space:nowrap;border-top-right-radius:0;border-bottom-right-radius:0;padding-right:.625rem}.header-add-menu__trigger{gap:calc(var(--spacing) * 2);white-space:nowrap;border-left:1px solid var(--pb-text-inverse);border-top-left-radius:0;border-bottom-left-radius:0;justify-content:space-between;min-width:7rem;padding-inline:.5rem}@supports (color:color-mix(in lab, red, red)){.header-add-menu__trigger{border-left:1px solid color-mix(in srgb, var(--pb-text-inverse) 26%, transparent)}}.header-add-menu__chevron{width:.875rem;height:.875rem;transition:transform .15s}.header-add-menu__panel{z-index:50;padding:calc(var(--spacing) * 1.5);border:1px solid var(--pb-border);background:var(--pb-bg-card);min-width:17rem;box-shadow:var(--pb-shadow-2);border-radius:.75rem;position:absolute;top:calc(100% + .5rem);left:0}.header-add-menu__item{align-items:center;gap:calc(var(--spacing) * 2.5);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);min-height:2.5rem;color:var(--pb-text);border-radius:.5rem;transition:color .15s,background-color .15s;display:flex}.header-add-menu__item:hover,.header-add-menu__item:focus-visible,.header-add-menu__item--selected{color:var(--pb-interactive);background:var(--pb-interactive-dim)}.header-add-menu__item--disabled{color:var(--pb-text-ter);cursor:not-allowed;opacity:.72}.header-add-menu__item--disabled:hover{color:var(--pb-text-ter);background:0 0}.header-add-menu__item-label{flex:1;min-width:0}.header-add-menu__check{color:var(--pb-success)}.header-add-menu__soon{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);white-space:nowrap;text-transform:uppercase;color:var(--pb-text-ter);flex-shrink:0}.header-add-menu__item svg{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);flex-shrink:0}@media (prefers-reduced-motion:reduce){.header-add-menu__chevron,.header-add-menu__item{transition:none}}.btn-ghost{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;border-radius:10px;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.btn-ghost:hover{border-color:var(--pb-border-strong);color:var(--pb-text-primary)}}.btn-ghost:disabled{cursor:not-allowed;opacity:.5}.btn-ghost{min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.btn-warning{--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:10px;justify-content:center;align-items:center;display:inline-flex}.btn-warning:disabled{cursor:not-allowed;opacity:.5}.btn-warning{background-color:var(--pb-warning);color:var(--pb-text-inverse);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.btn-warning:hover{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.btn-warning:hover{background-color:color-mix(in srgb, var(--pb-warning) 86%, var(--pb-text-primary))}}.btn-danger{justify-content:center;align-items:center;gap:var(--pb-control-gap);background-color:var(--pb-error);--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:10px;display:inline-flex}@media (hover:hover){.btn-danger:hover{--tw-brightness:brightness(90%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.btn-danger:disabled{cursor:not-allowed;opacity:.5}.btn-danger{color:var(--pb-text-inverse);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.pill,.badge-success,.badge-warning,.badge-error,.badge-info,.badge-neutral,.badge-muted,.badge-purple{justify-content:center;align-items:center;gap:var(--spacing);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);text-align:center;--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-size:11px;font-weight:var(--font-weight-medium);border-radius:3.40282e38px;display:inline-flex}.pill-success,.badge-success{background-color:var(--pb-success-dim);color:var(--pb-success)}.pill-warning,.badge-warning{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.pill-error,.badge-error{background-color:var(--pb-error-dim);color:var(--pb-error)}.pill-info,.badge-info{background-color:var(--pb-info-dim);color:var(--pb-info)}.pill-neutral,.badge-neutral{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary)}.pill-muted,.badge-muted{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card-hover);color:var(--pb-text-dim)}.pill-purple,.badge-purple{background-color:var(--pb-purple-dim);color:var(--pb-purple)}.count-badge{min-width:1.25rem;min-height:1.25rem;padding-inline:calc(var(--spacing) * 1.5);--tw-leading:1;--tw-font-weight:var(--font-weight-bold);font-size:10px;line-height:1;font-weight:var(--font-weight-bold);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);border-radius:3.40282e38px;justify-content:center;align-items:center;display:inline-flex}.count-badge-info{background-color:var(--pb-info-dim);color:var(--pb-info)}.count-badge-success{background-color:var(--pb-success-dim);color:var(--pb-success)}.count-badge-warning{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.count-badge-error{background-color:var(--pb-error-dim);color:var(--pb-error)}.count-badge-neutral{background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary)}.table-shell{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card)}.table-shell-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4)}.table-shell-header-comfortable{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.table-shell-content{padding:calc(var(--spacing) * 5)}.table-shell-content-comfortable{padding:calc(var(--spacing) * 6)}.table-toolbar{gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media (min-width:64rem){.table-toolbar{flex-direction:row;justify-content:space-between;align-items:flex-end}}:where(.table-toolbar-title-block>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.table-toolbar-eyebrow{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.table-toolbar-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.table-toolbar-title-compact{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.table-toolbar-copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.table-toolbar-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.table-header-row{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.75rem;display:grid}.table-header-aside{justify-self:end}.table-summary{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));flex-wrap:wrap;display:flex}.table-scroll{overflow-x:auto}.table-base{border-collapse:collapse;width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.table-head{text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--pb-text-secondary);text-transform:uppercase}.table-head-row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border)}.table-head-cell{padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);padding-left:var(--pb-table-cell-x);padding-right:var(--pb-table-cell-x)}.table-head-cell-tight{padding-block:calc(var(--spacing) * 2.5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);padding-left:var(--pb-table-cell-x-tight);padding-right:var(--pb-table-cell-x-tight)}.table-head-cell-right{text-align:right}:where(.table-body>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--pb-border)}.table-body-striped>.table-row:nth-child(2n){background-color:var(--pb-table-row-stripe)}.table-row{color:var(--pb-text-secondary)}.table-row-hover>.table-row{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.table-row-hover>.table-row:hover{background-color:var(--pb-table-row-hover)}.table-row-hover>.table-row-detail{transition-property:none}.table-row-hover>.table-row-detail:hover{background-color:#0000}.table-cell{padding-block:calc(var(--spacing) * 3);padding-left:var(--pb-table-cell-x);padding-right:var(--pb-table-cell-x)}.table-cell-tight{padding-block:calc(var(--spacing) * 2.5);padding-left:var(--pb-table-cell-x-tight);padding-right:var(--pb-table-cell-x-tight)}.table-cell-right{text-align:right}.table-cell-muted{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary)}.table-cell-nowrap{white-space:nowrap}.table-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.table-empty{padding:calc(var(--spacing) * 8);text-align:center}.table-empty-copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.chip-btn{border-radius:var(--radius-lg);border-style:var(--tw-border-style);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-width:1px;justify-content:center;align-items:center;display:inline-flex}.chip-btn:disabled{cursor:not-allowed;opacity:.5}.chip-btn{gap:var(--pb-control-gap);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.chip-btn-pill{padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:.2em;letter-spacing:.2em;text-transform:uppercase;border-radius:9999px}.chip-btn-neutral{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary)}@media (hover:hover){.chip-btn-neutral:hover{border-color:var(--pb-border-strong);color:var(--pb-text-primary)}}.chip-btn-selected{color:var(--pb-info);background-color:var(--pb-info-dim);border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-selected{border-color:color-mix(in srgb, var(--pb-info) 26%, transparent)}}.chip-btn-selected:hover{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-selected:hover{background-color:color-mix(in srgb, var(--pb-info) 20%, var(--pb-bg-card))}}.chip-btn-selected:hover{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-selected:hover{border-color:color-mix(in srgb, var(--pb-info) 34%, transparent)}}.chip-btn-info{color:var(--pb-info);background-color:var(--pb-info-dim);border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-info{border-color:color-mix(in srgb, var(--pb-info) 24%, transparent)}}.chip-btn-info:hover{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-info:hover{background-color:color-mix(in srgb, var(--pb-info) 20%, var(--pb-bg-card))}}.chip-btn-info:hover{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-info:hover{border-color:color-mix(in srgb, var(--pb-info) 32%, transparent)}}.chip-btn-success{color:var(--pb-success);background-color:var(--pb-success-dim);border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.chip-btn-success{border-color:color-mix(in srgb, var(--pb-success) 24%, transparent)}}.chip-btn-success:hover{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.chip-btn-success:hover{background-color:color-mix(in srgb, var(--pb-success) 20%, var(--pb-bg-card))}}.chip-btn-success:hover{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.chip-btn-success:hover{border-color:color-mix(in srgb, var(--pb-success) 32%, transparent)}}.chip-btn-warning{color:var(--pb-warning);background-color:var(--pb-warning-dim);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.chip-btn-warning{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.chip-btn-warning:hover{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.chip-btn-warning:hover{background-color:color-mix(in srgb, var(--pb-warning) 20%, var(--pb-bg-card))}}.chip-btn-warning:hover{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.chip-btn-warning:hover{border-color:color-mix(in srgb, var(--pb-warning) 32%, transparent)}}.chip-btn-error{color:var(--pb-error);background-color:var(--pb-error-dim);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.chip-btn-error{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.chip-btn-error:hover{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.chip-btn-error:hover{background-color:color-mix(in srgb, var(--pb-error) 20%, var(--pb-bg-card))}}.chip-btn-error:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.chip-btn-error:hover{border-color:color-mix(in srgb, var(--pb-error) 32%, transparent)}}.card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);border-radius:14px}.card-hover{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:14px}@media (hover:hover){.card-hover:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}}.auth-side-panel{border-right:1px solid var(--pb-border);background:linear-gradient(160deg, var(--pb-bg-surface) 0%, var(--pb-bg-surface) 42%, var(--pb-bg-card) 100%);position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.auth-side-panel{background:linear-gradient(160deg, color-mix(in srgb, var(--pb-bg-surface) 90%, var(--pb-brand) 10%) 0%, var(--pb-bg-surface) 42%, color-mix(in srgb, var(--pb-bg-card) 78%, var(--pb-interactive) 22%) 100%)}}.auth-side-panel:before,.auth-side-panel:after{content:"";pointer-events:none;filter:blur(12px);position:absolute;inset:auto}.auth-side-panel:before{background:radial-gradient(circle, var(--pb-brand) 0%, transparent 64%);width:24rem;height:24rem;top:-10%;left:-8%}@supports (color:color-mix(in lab, red, red)){.auth-side-panel:before{background:radial-gradient(circle, color-mix(in srgb, var(--pb-brand) 26%, transparent) 0%, transparent 64%)}}.auth-side-panel:after{background:radial-gradient(circle, var(--pb-interactive) 0%, transparent 66%);width:26rem;height:26rem;bottom:-14%;right:-12%}@supports (color:color-mix(in lab, red, red)){.auth-side-panel:after{background:radial-gradient(circle, color-mix(in srgb, var(--pb-interactive) 22%, transparent) 0%, transparent 66%)}}.auth-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-overlay);border-radius:1.75rem;position:relative;overflow:hidden}.auth-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.auth-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 40%, transparent), transparent)}}.auth-summary-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.auth-summary-card-emphasis{border-color:var(--pb-brand-border);background-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.auth-summary-card-emphasis{background-color:color-mix(in srgb, var(--pb-brand) 8%, var(--pb-bg-card))}}.auth-kicker{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.login-brand-mobile{justify-content:center;display:flex}.login-hero-stack{flex-direction:column;gap:1rem;max-width:29rem;padding-bottom:.25rem;display:flex}.login-hero-headline{letter-spacing:-.02em;color:var(--pb-text-primary);text-wrap:balance;padding-bottom:.1em;font-family:Syne,sans-serif;font-size:clamp(2.3rem,3.1vw,3.35rem);font-weight:800;line-height:1.08}.login-hero-copy{max-width:25rem;color:var(--pb-text-secondary);padding-bottom:.08em;font-size:.95rem;line-height:1.72}.login-side-footer{color:var(--pb-text-tertiary);flex-wrap:wrap;align-items:center;gap:.625rem 1.25rem;padding-bottom:.08em;font-family:JetBrains Mono,monospace;font-size:.68rem;display:flex}.login-side-footer-item{align-items:center;gap:.45rem;display:inline-flex}.login-side-footer-dot{background:var(--pb-success);width:.4rem;height:.4rem;box-shadow:0 0 4px var(--pb-success);border-radius:9999px}@supports (color:color-mix(in lab, red, red)){.login-side-footer-dot{box-shadow:0 0 4px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.login-side-footer-dot{flex-shrink:0}.login-card-title{letter-spacing:-.02em;color:var(--pb-text-primary);padding-bottom:.08em;font-family:Syne,sans-serif;font-size:1.9rem;font-weight:800;line-height:1.08}.login-card-subtitle{color:var(--pb-text-secondary);padding-bottom:.05em;font-size:.875rem;line-height:1.62}.login-card-hint{color:var(--pb-text-tertiary);padding-bottom:.04em;font-size:.75rem;line-height:1.6}.floating-panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-overlay);overflow:hidden}.add-series-header-metrics{flex-wrap:wrap;align-items:flex-start;gap:1.5rem;display:flex}.add-series-results-shell{position:relative}.add-series-results-loading{z-index:12;background:var(--pb-bg-base);border-radius:.875rem;justify-content:center;align-items:flex-start;padding:1.25rem;display:flex;position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){.add-series-results-loading{background:color-mix(in srgb, var(--pb-bg-base) 55%, transparent)}}.add-series-results-loading{-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);visibility:hidden;pointer-events:none}.add-series-results-loading.htmx-request{opacity:1;visibility:visible}.add-series-results-loading-card{border:1px solid var(--pb-border-default);background:var(--pb-bg-card);border-radius:.875rem;align-items:center;gap:.75rem;max-width:24rem;padding:.875rem 1rem;display:flex}@supports (color:color-mix(in lab, red, red)){.add-series-results-loading-card{background:color-mix(in srgb, var(--pb-bg-card) 94%, transparent)}}.add-series-results-loading-card{color:var(--pb-text-primary);box-shadow:var(--pb-shadow-1)}.add-series-results-loading-card svg{color:var(--pb-interactive);flex-shrink:0}.add-series-results-loading-title{font-size:.82rem;font-weight:800;line-height:1.2}.add-series-results-loading-copy{color:var(--pb-text-secondary);margin-top:.125rem;font-size:.74rem;line-height:1.35}.add-series-preview-notice{border:1px solid var(--pb-interactive);border-radius:.875rem;flex-wrap:wrap;align-items:center;gap:.875rem;padding:.875rem 1rem;display:flex}@supports (color:color-mix(in lab, red, red)){.add-series-preview-notice{border:1px solid color-mix(in srgb, var(--pb-interactive) 32%, var(--pb-border-default))}}.add-series-preview-notice{background:linear-gradient(135deg, var(--pb-interactive), transparent 70%), var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.add-series-preview-notice{background:linear-gradient(135deg, color-mix(in srgb, var(--pb-interactive) 12%, transparent), transparent 70%), var(--pb-bg-card)}}.add-series-preview-notice{color:var(--pb-text-primary);box-shadow:var(--pb-shadow-0)}.add-series-preview-notice-icon{background:var(--pb-interactive);border-radius:999px;flex:none;justify-content:center;align-items:center;width:2rem;height:2rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.add-series-preview-notice-icon{background:color-mix(in srgb, var(--pb-interactive) 14%, transparent)}}.add-series-preview-notice-icon{color:var(--pb-interactive)}.add-series-preview-notice-body{flex:auto;min-width:0}.add-series-preview-notice-title{letter-spacing:.08em;text-transform:uppercase;font-size:.78rem;font-weight:850;line-height:1.2}.add-series-preview-notice-copy{color:var(--pb-text-secondary);margin-top:.125rem;font-size:.8rem;line-height:1.45}.add-series-preview-notice-action{white-space:nowrap;flex:none;margin-left:auto}@media (max-width:640px){.add-series-preview-notice{align-items:flex-start}.add-series-preview-notice-action{justify-content:center;width:100%;margin-left:0}}.add-series-results-list{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.add-series-result-card{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);border-radius:.875rem;align-items:flex-start;gap:.875rem;padding:.875rem;transition:border-color .14s,background-color .14s;display:flex}.add-series-result-card:hover{border-color:var(--pb-border-strong);background:var(--pb-surface-selected)}@supports (color:color-mix(in lab, red, red)){.add-series-result-card:hover{background:color-mix(in srgb, var(--pb-surface-selected) 35%, var(--pb-bg-card))}}.add-series-result-card-static:hover{border-color:var(--pb-border-subtle);background:var(--pb-bg-card)}.add-series-result-cover{aspect-ratio:2/3;border:1px solid var(--pb-border-subtle);background:var(--pb-surface-shell);width:3.5rem;min-width:3.5rem;color:var(--pb-text-tertiary);border-radius:.625rem;overflow:hidden}.add-series-result-cover-empty{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.add-series-result-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.875rem;display:flex}.add-series-result-title{color:var(--pb-text-primary);font-size:.92rem;font-weight:600;line-height:1.25}.add-series-result-meta{color:var(--pb-text-secondary);flex-wrap:wrap;gap:.25rem .75rem;margin-top:.25rem;font-size:.78rem;display:flex}.add-series-result-description{color:var(--pb-text-tertiary);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin-top:.375rem;font-size:.78rem;line-height:1.55;display:-webkit-box;overflow:hidden}.add-series-result-actions{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.add-series-result-add{white-space:nowrap;flex-shrink:0}.add-series-empty-state{text-align:center;border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);border-radius:.875rem;padding:2.5rem 1.25rem}.add-series-empty-title{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text-primary);margin-top:.75rem;font-family:Syne,sans-serif;font-size:.9rem;font-weight:700}.add-series-empty-copy{color:var(--pb-text-secondary);margin-top:.25rem;font-size:.82rem;line-height:1.55}.add-series-modal-title{color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1rem;font-weight:800;line-height:1.1}.add-series-modal-subtitle{color:var(--pb-text-tertiary);font-size:.78rem}.add-series-modal-section-label{border-bottom:1px solid var(--pb-border-subtle);text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);padding-bottom:.375rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700}.add-series-modal-row{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.add-series-modal-row-label{color:var(--pb-text-secondary);font-size:.85rem;font-weight:500}.add-series-modal-preview{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-shell);color:var(--pb-text-secondary);border-radius:.625rem;padding:.625rem .75rem;font-family:JetBrains Mono,monospace;font-size:.72rem;line-height:1.5}.add-series-root-display.input-pb:disabled,.add-series-root-display.input-pb[readonly]{opacity:1;color:var(--pb-text-primary);background:var(--pb-surface-input);cursor:default}.media-result-card{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);display:flex}.media-result-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.media-result-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.media-result-card:hover{box-shadow:var(--pb-shadow-2)}.media-result-card-static:hover{border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1)}.media-result-cover{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card-hover);flex-shrink:0;width:4.75rem;overflow:hidden}.media-result-cover-lg{width:6.25rem}.media-result-cover-inner{aspect-ratio:2/3;width:100%;height:100%}.media-result-empty{width:100%;height:100%;color:var(--pb-text-dim);justify-content:center;align-items:center;display:flex}.media-result-meta{margin-top:var(--spacing);align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);flex-wrap:wrap;display:flex}.media-result-divider{color:var(--pb-text-dim)}.match-file-card{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-direction:column;display:flex}@media (min-width:64rem){.match-file-card{flex-direction:row;align-items:flex-start}}.match-file-card{box-shadow:var(--pb-shadow-1)}.match-file-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.match-file-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.match-file-card:hover{box-shadow:var(--pb-shadow-2)}.match-file-format{height:calc(var(--spacing) * 14);width:calc(var(--spacing) * 14);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:.18em;letter-spacing:.18em;text-transform:uppercase;border-width:1px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.match-file-format-cbz{color:var(--pb-interactive);border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbz{border-color:color-mix(in srgb, var(--pb-interactive) 24%, transparent)}}.match-file-format-cbz{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbz{background-color:color-mix(in srgb, var(--pb-interactive) 12%, transparent)}}.match-file-format-cbr{color:var(--pb-purple);border-color:var(--pb-purple)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbr{border-color:color-mix(in srgb, var(--pb-purple) 24%, transparent)}}.match-file-format-cbr{background-color:var(--pb-purple)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbr{background-color:color-mix(in srgb, var(--pb-purple) 12%, transparent)}}.match-file-format-pdf{color:var(--pb-warning);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.match-file-format-pdf{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.match-file-format-pdf{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.match-file-format-pdf{background-color:color-mix(in srgb, var(--pb-warning) 12%, transparent)}}.match-file-format-other{color:var(--pb-text-secondary);border-color:var(--pb-border);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.match-file-format-other{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.match-file-card-actions{gap:calc(var(--spacing) * 2);flex-direction:column;width:100%;display:flex}@media (min-width:64rem){.match-file-card-actions{align-items:flex-end;width:auto;min-width:11rem}}.library-series-card{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);min-width:0;height:100%;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);flex-direction:column;display:flex;overflow:hidden}.library-series-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.library-series-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 56%, var(--pb-bg-card))}}.library-series-card:hover{box-shadow:var(--pb-shadow-2)}.library-series-card-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-series-card-active{border-color:color-mix(in srgb, var(--pb-interactive) 30%, transparent)}}.library-series-card-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-series-card-active{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.library-series-cover-frame{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);background:linear-gradient(160deg, var(--pb-bg-card-hover) 0%, var(--pb-bg-card) 100%);position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.library-series-cover-frame{background:linear-gradient(160deg, color-mix(in srgb, var(--pb-bg-card-hover) 88%, transparent) 0%, color-mix(in srgb, var(--pb-bg-card) 82%, transparent) 100%)}}.library-series-cover-frame-compact{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border)}.library-series-placeholder{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);width:100%;height:100%;color:var(--pb-text-dim);flex-direction:column;display:flex}.library-series-meta{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.library-series-progress-track{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.library-series-progress-track{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.series-grid-card-meta{container-type:inline-size}.series-grid-card-publisher-year{grid-template-columns:8rem minmax(0,1fr);align-items:start;gap:.25rem .5rem;display:grid}.series-grid-card-publisher{overflow-wrap:anywhere;min-width:0}.series-grid-card-year{text-align:right;justify-self:end}.series-grid-card-progress-track{background-color:var(--pb-text-dim)}@supports (color:color-mix(in lab, red, red)){.series-grid-card-progress-track{background-color:color-mix(in srgb, var(--pb-text-dim) 72%, var(--pb-bg-card) 28%)}}@container (max-width:11rem){.series-grid-card-publisher-year{grid-template-columns:8rem}.series-grid-card-year{text-align:left;justify-self:start}}.admin-workspace-page{gap:calc(var(--spacing) * 4);flex-direction:column;min-height:0;display:flex}.admin-workspace-header{padding-block:0}.admin-workspace-header-copy{max-width:var(--container-3xl);min-width:0}.admin-workspace-tag-row{gap:calc(var(--spacing) * 2);padding-top:var(--spacing);flex-wrap:wrap;display:flex}.admin-workspace-header-aside{min-width:0}.admin-workspace-summary-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);height:100%;padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1)}.admin-workspace-body{gap:calc(var(--spacing) * 7);display:grid}@media (min-width:80rem){.admin-workspace-body{grid-template-columns:10.75rem minmax(0,1fr);align-items:flex-start}}.admin-workspace-body{padding-bottom:var(--pb-page-footer-clearance)}.admin-workspace-rail{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media (min-width:80rem){.admin-workspace-rail{top:calc(var(--spacing) * 24);position:sticky}}:where(.admin-workspace-rail-copy>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.admin-workspace-rail-links{gap:var(--spacing);flex-direction:column;display:flex}.admin-workspace-content{flex:1;min-width:0}:where(.admin-workspace-content>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.admin-workspace-content{width:min(100%,58rem)}.section-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);overflow:hidden}.admin-workspace-content .space-y-6>.section-card:not(:has(~.section-card)){margin-bottom:0!important}.section-card-visible{overflow:visible}.system-version-banner{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);flex-direction:column;padding-block:1.125rem;display:flex;position:relative;overflow:hidden}@media (min-width:64rem){.system-version-banner{flex-direction:row;justify-content:space-between;align-items:center}}.system-version-banner{box-shadow:var(--pb-shadow-1)}.system-version-banner:before{content:"";background:var(--pb-brand);width:3px;position:absolute;top:0;bottom:0;left:0}.system-version-banner-main{align-items:flex-start;gap:calc(var(--spacing) * 4);min-width:0;display:flex}.system-version-banner-icon{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);background:var(--pb-brand-muted);color:var(--pb-brand);border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.system-version-banner-title{letter-spacing:-.02em;color:var(--pb-text-primary);flex-wrap:wrap;align-items:center;gap:.625rem;font-family:Bricolage Grotesque,sans-serif;font-size:1.125rem;font-weight:800;display:flex}.system-version-banner-meta{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.system-version-banner-meta span{font-family:JetBrains Mono,monospace;font-size:.6875rem}.system-version-banner-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;flex-shrink:0;display:flex}:where(.system-detail-list>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.system-detail-list{border-color:var(--pb-border-subtle)}.system-detail-row{justify-content:space-between;align-items:baseline;gap:calc(var(--spacing) * 5);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2.5);display:flex}.system-detail-label{color:var(--pb-text-secondary);flex-shrink:0;font-size:13px}.system-detail-value{color:var(--pb-text-primary);text-align:right;word-break:break-word;font-family:JetBrains Mono,monospace;font-size:.78125rem}.system-detail-value-brand{color:var(--pb-brand)}.system-registry-filename{--tw-leading:calc(var(--spacing) * 5);font-family:JetBrains Mono,Fira Code,monospace;font-size:.8125rem;line-height:calc(var(--spacing) * 5);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.system-links-grid{gap:calc(var(--spacing) * 2);padding:calc(var(--spacing) * 5);grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));display:grid}.system-link-card{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2.5);--tw-font-weight:var(--font-weight-medium);font-size:12.5px;font-weight:var(--font-weight-medium);color:var(--pb-text-primary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background:var(--pb-bg-card-hover);display:flex}@supports (color:color-mix(in lab, red, red)){.system-link-card{background:color-mix(in srgb, var(--pb-bg-card-hover) 20%, transparent)}}.system-link-card:hover{border-color:var(--pb-border-hover);background:var(--pb-bg-card-hover)}.system-link-card svg{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);color:var(--pb-text-dim);flex-shrink:0}.system-link-card-arrow{height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3);color:var(--pb-text-dim);margin-left:auto}.stat-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.stat-card-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.stat-card-value{margin-top:calc(var(--spacing) * 3);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.stat-card-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-secondary)}.info-panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1)}.info-panel-muted{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.info-panel-muted{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.dashboard-briefing-shell{border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-2);background:radial-gradient(circle at top right, var(--pb-interactive) 0%, transparent 38%), radial-gradient(circle at left bottom, var(--pb-brand) 0%, transparent 34%), var(--pb-bg-card);position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.dashboard-briefing-shell{background:radial-gradient(circle at top right, color-mix(in srgb, var(--pb-interactive) 14%, transparent) 0%, transparent 38%), radial-gradient(circle at left bottom, color-mix(in srgb, var(--pb-brand) 10%, transparent) 0%, transparent 34%), var(--pb-bg-card)}}.dashboard-briefing-shell:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.dashboard-briefing-shell:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 44%, transparent), color-mix(in srgb, var(--pb-interactive) 44%, transparent), transparent)}}.dashboard-briefing-meta{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}@media (min-width:64rem){.dashboard-briefing-meta{max-width:13rem}}.dashboard-briefing-meta{box-shadow:var(--pb-shadow-1);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.dashboard-briefing-meta{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 42%, transparent)}}.dashboard-status-pill,.dashboard-priority-badge,.dashboard-watch-tag,.dashboard-scorecard-delta{align-items:center;gap:var(--spacing);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.16em;letter-spacing:.16em;text-transform:uppercase;border-width:1px;border-color:currentColor;border-radius:3.40282e38px;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.dashboard-status-pill,.dashboard-priority-badge,.dashboard-watch-tag,.dashboard-scorecard-delta{border-color:color-mix(in srgb, currentColor 20%, transparent)}}.dashboard-priority-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.dashboard-priority-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}.dashboard-priority-card-v2{align-items:flex-start;gap:calc(var(--spacing) * 4);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:14px;grid-template-columns:1fr auto;transition:box-shadow .14s;display:grid;position:relative;overflow:hidden}.dashboard-priority-card-v2:before{content:"";background-color:#0000;width:6px;position:absolute;inset:0 auto 0 0}.dashboard-priority-card-v2:hover{box-shadow:var(--pb-shadow-2)}.dashboard-priority-card-v2-tone-pill-error:before{background-color:var(--pb-error)}.dashboard-priority-card-v2-tone-pill-warning:before{background-color:var(--pb-warning)}.dashboard-priority-card-v2-tone-pill-info:before{background-color:var(--pb-info)}.dashboard-priority-card-v2-tone-pill-success:before{background-color:var(--pb-success)}.dashboard-priority-card-v2-tone-pill-neutral:before,.dashboard-priority-card-v2-tone-pill-muted:before{background-color:var(--pb-text-dim)}.dashboard-priority-fact{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-base);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);display:inline-flex}.dashboard-priority-card,.dashboard-decision-card{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-width:1px}.dashboard-priority-card-critical,.dashboard-decision-card-critical{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-critical,.dashboard-decision-card-critical{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.dashboard-priority-card-critical,.dashboard-decision-card-critical{background:linear-gradient(135deg, var(--pb-error) 0%, var(--pb-bg-card-hover) 100%)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-critical,.dashboard-decision-card-critical{background:linear-gradient(135deg, color-mix(in srgb, var(--pb-error) 10%, var(--pb-bg-card)) 0%, color-mix(in srgb, var(--pb-bg-card-hover) 92%, transparent) 100%)}}.dashboard-priority-card-high,.dashboard-decision-card-high{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-high,.dashboard-decision-card-high{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.dashboard-priority-card-high,.dashboard-decision-card-high{background:linear-gradient(135deg, var(--pb-warning) 0%, var(--pb-bg-card-hover) 100%)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-high,.dashboard-decision-card-high{background:linear-gradient(135deg, color-mix(in srgb, var(--pb-warning) 10%, var(--pb-bg-card)) 0%, color-mix(in srgb, var(--pb-bg-card-hover) 92%, transparent) 100%)}}.dashboard-priority-card-watch,.dashboard-decision-card-watch{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-watch,.dashboard-decision-card-watch{border-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.dashboard-priority-card-watch,.dashboard-decision-card-watch{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-watch,.dashboard-decision-card-watch{background-color:color-mix(in srgb, var(--pb-warning) 7%, var(--pb-bg-card))}}.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{border-color:color-mix(in srgb, var(--pb-success) 20%, transparent)}}.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{background-color:color-mix(in srgb, var(--pb-success) 7%, var(--pb-bg-card))}}.dashboard-priority-card-info,.dashboard-decision-card-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-info,.dashboard-decision-card-info{border-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.dashboard-priority-card-info,.dashboard-decision-card-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-info,.dashboard-decision-card-info{background-color:color-mix(in srgb, var(--pb-info) 7%, var(--pb-bg-card))}}.dashboard-priority-facts{margin-top:calc(var(--spacing) * 5);gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-top:calc(var(--spacing) * 4);display:grid}@media (min-width:40rem){.dashboard-priority-facts{grid-template-columns:repeat(3,minmax(0,1fr))}}.dashboard-priority-facts dt{--tw-font-weight:var(--font-weight-medium);font-size:10px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.dashboard-priority-facts dd{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-primary)}.dashboard-quiet-note{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-success);background:var(--pb-success);display:flex}@supports (color:color-mix(in lab, red, red)){.dashboard-quiet-note{background:color-mix(in srgb, var(--pb-success) 10%, var(--pb-bg-card))}}.dashboard-quiet-note{border:1px solid var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-quiet-note{border:1px solid color-mix(in srgb, var(--pb-success) 18%, transparent)}}.dashboard-pulse-tile{padding-block:calc(var(--spacing) * 2.5);flex-direction:column;align-items:flex-start;display:flex}.dashboard-pulse-value{margin-top:var(--spacing);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.dashboard-pulse-tile-right{border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--pb-border);padding-left:calc(var(--spacing) * 3)}.dashboard-pulse-tile-bottom{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-top:calc(var(--spacing) * 2.5)}.dashboard-scoreboard-attention-grid{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.dashboard-scoreboard-attention-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.dashboard-scoreboard-attention-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.dashboard-scorecard{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);min-width:0;height:100%;padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-width:1px;flex-direction:column;justify-content:space-between;display:flex}.dashboard-scorecard-critical{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-critical{border-color:color-mix(in srgb, var(--pb-error) 22%, transparent)}}.dashboard-scorecard-critical{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-critical{background-color:color-mix(in srgb, var(--pb-error) 8%, var(--pb-bg-card))}}.dashboard-scorecard-high{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-high{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.dashboard-scorecard-high{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-high{background-color:color-mix(in srgb, var(--pb-warning) 9%, var(--pb-bg-card))}}.dashboard-scorecard-watch{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-watch{border-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.dashboard-scorecard-watch{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-watch{background-color:color-mix(in srgb, var(--pb-warning) 7%, var(--pb-bg-card))}}.dashboard-scorecard-healthy{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-healthy{border-color:color-mix(in srgb, var(--pb-success) 20%, transparent)}}.dashboard-scorecard-healthy{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-healthy{background-color:color-mix(in srgb, var(--pb-success) 7%, var(--pb-bg-card))}}.dashboard-scorecard-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-info{border-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.dashboard-scorecard-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-info{background-color:color-mix(in srgb, var(--pb-info) 7%, var(--pb-bg-card))}}.dashboard-scorecard-title{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.dashboard-scorecard-value{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.dashboard-scorecard-value-watch{color:var(--pb-status-warning)}.dashboard-scorecard-value-critical{color:var(--pb-status-danger)}.dashboard-scorecard-copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.dashboard-healthy-strip{align-items:center;gap:calc(var(--spacing) * 2);column-gap:calc(var(--spacing) * 6);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3.5);flex-wrap:wrap;display:flex}.dashboard-healthy-strip-label{align-items:center;gap:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-success);display:flex}.dashboard-healthy-metric{align-items:baseline;gap:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);font-size:.82rem;display:flex}.dashboard-healthy-metric strong{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--pb-text-primary)}.dashboard-inline-link{align-items:center;gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.dashboard-inline-link:hover{color:var(--pb-interactive-hover)}.dashboard-download-row{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);grid-template-columns:1fr 100px 80px;display:grid}.dashboard-watch-item,.dashboard-exception-row{border-radius:var(--radius-2xl)}.dashboard-watch-item-critical,.dashboard-exception-row-critical{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-critical,.dashboard-exception-row-critical{border-color:color-mix(in srgb, var(--pb-error) 20%, transparent)}}.dashboard-watch-item-critical,.dashboard-exception-row-critical{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-critical,.dashboard-exception-row-critical{background-color:color-mix(in srgb, var(--pb-error) 7%, var(--pb-bg-card))}}.dashboard-watch-item-high,.dashboard-exception-row-high{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-high,.dashboard-exception-row-high{border-color:color-mix(in srgb, var(--pb-warning) 22%, transparent)}}.dashboard-watch-item-high,.dashboard-exception-row-high{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-high,.dashboard-exception-row-high{background-color:color-mix(in srgb, var(--pb-warning) 8%, var(--pb-bg-card))}}.dashboard-watch-item-watch,.dashboard-exception-row-watch{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-watch,.dashboard-exception-row-watch{border-color:color-mix(in srgb, var(--pb-warning) 16%, transparent)}}.dashboard-watch-item-watch,.dashboard-exception-row-watch{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-watch,.dashboard-exception-row-watch{background-color:color-mix(in srgb, var(--pb-warning) 6%, var(--pb-bg-card))}}.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{border-color:color-mix(in srgb, var(--pb-success) 16%, transparent)}}.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{background-color:color-mix(in srgb, var(--pb-success) 6%, var(--pb-bg-card))}}.dashboard-watch-item-info,.dashboard-exception-row-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-info,.dashboard-exception-row-info{border-color:color-mix(in srgb, var(--pb-info) 16%, transparent)}}.dashboard-watch-item-info,.dashboard-exception-row-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-info,.dashboard-exception-row-info{background-color:color-mix(in srgb, var(--pb-info) 6%, var(--pb-bg-card))}}.dashboard-mission-page{gap:calc(var(--spacing) * 4);width:100%;padding-bottom:var(--pb-page-footer-clearance);flex-direction:column;display:flex}.dashboard-mission-control{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.dashboard-mission-control__summary{align-items:flex-start;gap:calc(var(--spacing) * 6);flex-wrap:wrap;flex:1;min-width:0;display:flex}.dashboard-mission-control__title-block{min-width:0}.dashboard-mission-control__title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text);font-family:Syne,sans-serif;font-size:1.5rem;font-weight:800;line-height:1}.dashboard-mission-control__title span{color:var(--pb-brand)}.dashboard-mission-control__subtitle{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.dashboard-mission-control__gauges{align-items:flex-end;gap:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}.dashboard-mission-control__gauges-spacer{flex:none;width:224px;min-width:224px;min-height:72px}.dashboard-gauge{text-align:center;min-width:64px}.dashboard-gauge__ring{width:56px;height:56px;margin-inline:auto;position:relative}.dashboard-gauge__ring svg{width:56px;height:56px;transform:rotate(-90deg)}.dashboard-gauge__track{fill:none;stroke:var(--pb-text-dim)}@supports (color:color-mix(in lab, red, red)){.dashboard-gauge__track{stroke:color-mix(in srgb, var(--pb-text-dim) 14%, transparent)}}.dashboard-gauge__track{stroke-width:4.5px}.dashboard-gauge__fill{fill:none;stroke-width:4.5px;stroke-linecap:round}.dashboard-gauge__value{justify-content:center;align-items:center;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700;display:flex;position:absolute;inset:0}.dashboard-gauge__label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);margin-top:.2rem;font-size:.52rem;font-weight:600;display:block}.dashboard-gauge--success .dashboard-gauge__fill,.dashboard-gauge--success .dashboard-gauge__value{stroke:var(--pb-success);color:var(--pb-success)}.dashboard-gauge--info .dashboard-gauge__fill,.dashboard-gauge--info .dashboard-gauge__value{stroke:var(--pb-info);color:var(--pb-info)}.dashboard-gauge--warning .dashboard-gauge__fill,.dashboard-gauge--warning .dashboard-gauge__value{stroke:var(--pb-warning);color:var(--pb-warning)}.dashboard-gauge--danger .dashboard-gauge__fill,.dashboard-gauge--danger .dashboard-gauge__value{stroke:var(--pb-error);color:var(--pb-error)}.dashboard-gauge--neutral .dashboard-gauge__fill,.dashboard-gauge--neutral .dashboard-gauge__value{stroke:var(--pb-text-dim);color:var(--pb-text-dim)}.dashboard-scoreboard{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.5rem;display:grid}.dashboard-scoreboard__item{gap:calc(var(--spacing) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);min-width:0;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);box-shadow:var(--pb-shadow-1);border-radius:10px;flex-direction:column;display:flex}.dashboard-scoreboard__label,.dashboard-section__label{letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.54rem;font-weight:700}.dashboard-scoreboard__value{color:var(--pb-text);font-family:JetBrains Mono,monospace;font-size:1.05rem;font-weight:700}.dashboard-scoreboard__delta{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.66rem}:where(.dashboard-section>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}.dashboard-table-wrap,.dashboard-activity-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:12px;overflow-x:auto}.dashboard-table{border-collapse:collapse;width:100%}.dashboard-table th{text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);background:var(--pb-bg-shell);border-bottom:1px solid var(--pb-border);padding:.45rem .9rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700}.dashboard-table th.c,.dashboard-table td.c{text-align:center}.dashboard-table th.r,.dashboard-table td.r{text-align:right}.dashboard-table td{border-bottom:1px solid var(--pb-border-subtle);padding:.6rem .9rem;font-size:.82rem;transition:background-color .12s}.dashboard-table tbody tr:last-child td{border-bottom:0}.dashboard-table tbody tr:hover td{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.dashboard-table tbody tr:hover td{background:color-mix(in srgb, var(--pb-interactive) 8%, var(--pb-bg-card))}}.dashboard-table__status-col{width:40px}.dashboard-table__name{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.dashboard-table__detail{margin-top:calc(var(--spacing) * .5);color:var(--pb-text-dim);font-size:.75rem}.table-mono-value,td.table-mono-value,.dashboard-table__mono,td.dashboard-table__mono,.health-table__mono,td.health-table__mono,.series-mission-control-owned,.series-mission-control-type,.utility-tool-table-mono,td.utility-tool-table-mono,.downloads-mono-cell,td.downloads-mono-cell{color:var(--pb-text-secondary);font-variant-numeric:tabular-nums;font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:400;line-height:1.2}.series-mission-control-reading{color:var(--pb-interactive);white-space:nowrap;margin-top:.22rem;font-family:JetBrains Mono,monospace;font-size:.58rem;font-weight:600;line-height:1.2;display:block}.dashboard-table__link{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dashboard-table__link:hover{color:var(--pb-interactive-hover)}.dashboard-led{vertical-align:middle;border-radius:999px;flex-shrink:0;width:8px;height:8px;display:inline-block}.dashboard-led--sys{border:1px solid var(--pb-text);width:10px;height:10px}@supports (color:color-mix(in lab, red, red)){.dashboard-led--sys{border:1px solid color-mix(in srgb, var(--pb-text) 10%, transparent)}}.dashboard-led--green{background:var(--pb-success);box-shadow:0 0 5px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.dashboard-led--amber{background:var(--pb-warning);box-shadow:0 0 5px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-warning) 35%, transparent)}}.dashboard-led--red{background:var(--pb-error);box-shadow:0 0 5px var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--red{box-shadow:0 0 5px color-mix(in srgb, var(--pb-error) 35%, transparent)}}.dashboard-led--blue{background:var(--pb-info);box-shadow:0 0 5px var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--blue{box-shadow:0 0 5px color-mix(in srgb, var(--pb-info) 35%, transparent)}}.dashboard-led--off{background:var(--pb-text-dim);opacity:.4}.dashboard-progress{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.dashboard-progress__track{height:calc(var(--spacing) * 1.5);border-radius:var(--radius-xs);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background:var(--pb-text-dim);flex:1;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.dashboard-progress__track{background:color-mix(in srgb, var(--pb-text-dim) 10%, transparent)}}.dashboard-progress__fill{height:100%;position:relative}.dashboard-progress__fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}.dashboard-progress__fill--green{background:linear-gradient(90deg, var(--pb-success), var(--pb-success))}@supports (color:color-mix(in lab, red, red)){.dashboard-progress__fill--green{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-success) 45%, transparent), var(--pb-success))}}.dashboard-progress__fill--blue{background:linear-gradient(90deg, var(--pb-info), var(--pb-info))}@supports (color:color-mix(in lab, red, red)){.dashboard-progress__fill--blue{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-info) 45%, transparent), var(--pb-info))}}.dashboard-progress__fill--pulse{opacity:.6;width:100%;animation:1.6s ease-in-out infinite pulse}.dashboard-progress__percent,.dashboard-activity-row__time,.dashboard-footer-strip{font-family:JetBrains Mono,monospace}.dashboard-progress__percent{text-align:right;min-width:32px;color:var(--pb-text-sec);font-size:.66rem;font-weight:600}.dashboard-activity-card{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2)}.dashboard-activity-row{align-items:flex-start;gap:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);border-bottom:1px solid var(--pb-border-subtle);display:flex}.dashboard-activity-row:last-child{border-bottom:0}.dashboard-activity-row__dot{margin-top:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 2);width:calc(var(--spacing) * 2);border-radius:3.40282e38px;flex-shrink:0}.dashboard-activity-row__dot--acquired{background:var(--pb-success)}.dashboard-activity-row__dot--imported{background:var(--pb-brand)}.dashboard-activity-row__dot--failed{background:var(--pb-error)}.dashboard-activity-row__body{flex:1}.dashboard-activity-row__summary{--tw-leading:calc(var(--spacing) * 6);font-size:.82rem;line-height:calc(var(--spacing) * 6);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary)}.dashboard-activity-row__detail{--tw-leading:calc(var(--spacing) * 5);font-size:.78rem;line-height:calc(var(--spacing) * 5);color:var(--pb-text-secondary)}.dashboard-activity-row__meta{margin-top:var(--spacing);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.dashboard-activity-row__time{color:var(--pb-text-dim);font-size:.66rem}.dashboard-activity-row__link{--tw-font-weight:var(--font-weight-semibold);font-size:.68rem;font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dashboard-activity-row__link:hover{color:var(--pb-interactive-hover)}.dashboard-activity-card__empty{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-dim)}.dashboard-footer-strip{align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);color:var(--pb-text-dim);box-shadow:var(--pb-shadow-1);border-radius:10px;flex-wrap:wrap;font-size:.72rem;display:flex}.dashboard-footer-strip strong{color:var(--pb-text);font-weight:600}.library-page-shell{flex:1;width:100%;min-height:0;overflow:hidden}.library-format-pills{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.library-scoreboard__item{text-decoration:none;transition:background-color .12s}.library-scoreboard__item--link:hover{background-color:var(--pb-bg-card-hover)}.library-scoreboard__value--info{color:var(--pb-info)}.library-scoreboard__value--success{color:var(--pb-success)}.library-scoreboard__value--warning{color:var(--pb-warning)}.library-matching-banner{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;border-color:var(--pb-warning);border-radius:12px;display:flex}@supports (color:color-mix(in lab, red, red)){.library-matching-banner{border-color:color-mix(in srgb, var(--pb-warning) 26%, transparent)}}.library-matching-banner{background:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.library-matching-banner{background:color-mix(in srgb, var(--pb-warning) 8%, var(--pb-bg-card))}}.library-matching-banner{box-shadow:var(--pb-shadow-1)}.library-matching-banner__body{align-items:center;column-gap:calc(var(--spacing) * 2);row-gap:var(--spacing);flex-wrap:wrap;min-width:0;display:flex}.library-matching-banner__title{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-matching-banner__copy{color:var(--pb-text-secondary)}.library-browser{--library-browser-header-height:calc(1.5rem + .75rem + 1px);border:1px solid var(--pb-border);background:var(--pb-bg-card);min-height:0;box-shadow:var(--pb-shadow-1);border-radius:14px;flex:1 1 0;grid-template-columns:minmax(220px,260px) minmax(0,1fr);display:grid;overflow:hidden}.library-browser__tree{overscroll-behavior:contain;z-index:2;border-right:1px solid var(--pb-border);background:var(--pb-bg-surface);flex-direction:column;min-height:0;display:flex;position:relative;overflow:hidden}.library-browser__tree-list{min-height:0;padding-block:var(--spacing);overscroll-behavior:contain;flex:1;overflow-y:auto}.library-browser__tree-header,.library-browser__toolbar{min-height:var(--library-browser-header-height);height:var(--library-browser-header-height)}.library-browser__tree-header{padding-inline:calc(var(--spacing) * 3);z-index:3;letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);border-bottom:1px solid var(--pb-border);background:var(--pb-bg-surface);align-items:center;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700;display:flex;position:sticky;top:0}@supports (color:color-mix(in lab, red, red)){.library-browser__tree-header{background:color-mix(in srgb, var(--pb-bg-surface) 72%, transparent)}}.library-browser__tree-node{min-height:calc(var(--spacing) * 6);border-radius:var(--radius-md);min-width:0;padding-inline:calc(var(--spacing) * 1.5);padding-block:var(--spacing);color:var(--pb-text-secondary);flex:1;align-items:center;font-size:.73rem;font-weight:400;line-height:1.2;text-decoration:none;display:flex}.library-browser__tree-node--active{background:var(--pb-interactive-dim);color:var(--pb-interactive);font-weight:500}.library-browser__tree-icon{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);color:var(--pb-text-dim);flex-shrink:0;transition:transform .14s,color .14s}.library-browser__tree-icon--folder{color:var(--pb-brand)}.library-browser__tree-icon--expanded{color:var(--pb-interactive);transform:rotate(90deg)}.library-browser__tree-item{padding-left:calc(var(--library-tree-level,0) * .8rem)}.library-browser__tree-row{min-height:calc(var(--spacing) * 6);align-items:center;gap:var(--spacing);min-width:0;padding-inline:calc(var(--spacing) * 2);display:flex}.library-browser__tree-row--context .library-browser__tree-node,.library-browser__tree-row--context .library-browser__tree-toggle,.library-browser__tree-row--context .library-browser__tree-leaf{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser__tree-row--context .library-browser__tree-node,.library-browser__tree-row--context .library-browser__tree-toggle,.library-browser__tree-row--context .library-browser__tree-leaf{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.library-browser__tree-row--context .library-browser__tree-node,.library-browser__tree-row--context .library-browser__tree-toggle,.library-browser__tree-row--context .library-browser__tree-leaf{color:var(--pb-interactive)}.library-browser__tree-toggle,.library-browser__tree-leaf{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);color:var(--pb-text-dim);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.library-browser__tree-toggle{border-radius:4px;transition:background-color .12s,color .12s}.library-browser__tree-toggle:hover{background:var(--pb-bg-card-hover);color:var(--pb-text)}:where(.library-browser__tree-children>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}.library-browser__tree-name{flex:1;min-width:0;display:block}.library-browser__panel{z-index:1;flex-direction:column;min-width:0;min-height:0;display:flex;position:relative;overflow:hidden}.library-browser__toolbar{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 3);border-bottom:1px solid var(--pb-border);background:var(--pb-bg-surface);flex-shrink:0;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser__toolbar{background:color-mix(in srgb, var(--pb-bg-surface) 72%, transparent)}}.library-browser__toolbar-main{align-items:center;gap:calc(var(--spacing) * 1.5);flex:1;min-width:0;display:flex}.library-browser__up-btn{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);border-radius:var(--radius-xs);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-shrink:0;justify-content:center;align-items:center;text-decoration:none;display:inline-flex}.library-browser__up-btn:hover{background:var(--pb-bg-card-hover);color:var(--pb-text)}.library-browser__up-btn.is-disabled{pointer-events:none;opacity:.4}.library-browser__path-block{flex:1;align-items:center;min-width:0;display:flex}.library-browser__path{color:var(--pb-text-sec);white-space:nowrap;text-overflow:ellipsis;font-family:JetBrains Mono,monospace;font-size:.56rem;line-height:1.15;overflow:hidden}.library-browser__table-wrap{overscroll-behavior:contain;min-height:0;box-shadow:none;background:0 0;border:0;border-radius:0;flex:1;position:relative;overflow:auto}.library-browser-table{min-width:760px}.library-browser-table th{padding:.4rem .75rem;font-size:.54rem;font-weight:600}.library-browser-table td{padding:.5rem .75rem;font-size:.76rem;font-weight:400;line-height:1.3}.library-browser__table-row--file,.library-browser__table-row--file td,.library-browser__table-row--file .library-browser__name-label{cursor:pointer}.library-browser__table-row--context{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser__table-row--context{background:color-mix(in srgb, var(--pb-interactive) 8%, transparent)}}.library-context-menu{border-radius:var(--radius-xl);border-style:var(--tw-border-style);min-width:15.5rem;padding-block:calc(var(--spacing) * 1.5);z-index:80;border-width:1px;border-color:var(--pb-border);position:fixed;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.library-context-menu{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-context-menu{background:linear-gradient(180deg, var(--pb-bg-card) 0%, var(--pb-bg-surface) 100%)}@supports (color:color-mix(in lab, red, red)){.library-context-menu{background:linear-gradient(180deg, color-mix(in srgb, var(--pb-bg-card) 94%, transparent) 0%, color-mix(in srgb, var(--pb-bg-surface) 97%, transparent) 100%)}}.library-context-menu{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 18px 48px #0f172a42,0 6px 18px #0f172a29}.library-context-menu__group{display:contents}.library-context-menu__item{align-items:center;gap:calc(var(--spacing) * 3);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--pb-text);display:flex}.library-context-menu__item:hover{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-context-menu__item:hover{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.library-context-menu__item:hover{color:var(--pb-interactive)}.library-context-menu__item--danger:hover{background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-context-menu__item--danger:hover{background:color-mix(in srgb, var(--pb-error) 11%, transparent)}}.library-context-menu__item--danger:hover{color:var(--pb-error)}.library-context-menu__icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:currentColor;flex-shrink:0}.library-context-menu__sep{margin-block:var(--spacing);background:var(--pb-border-subtle);height:1px}@supports (color:color-mix(in lab, red, red)){.library-context-menu__sep{background:color-mix(in srgb, var(--pb-border-subtle) 80%, transparent)}}.modal-panel.library-browser-modal{border-radius:18px;width:min(92vw,48rem);max-height:min(86vh,52rem);overflow:hidden}.modal-panel.library-browser-modal--properties{width:min(84vw,28rem)}.modal-panel.library-browser-modal--compact,.modal-panel.library-delete-modal{width:min(92vw,34rem)}.library-delete-modal__header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 4);border-bottom-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);border-bottom-width:1px;border-color:var(--pb-border);display:flex}.library-delete-modal__eyebrow{letter-spacing:.14em;text-transform:uppercase;color:var(--pb-text-dim);font-size:11px;font-weight:500}.library-delete-modal__title{margin-top:var(--spacing);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-delete-modal__subtitle{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}:where(.library-delete-modal__body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.library-delete-modal__body{padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4)}.library-delete-modal__copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.library-delete-modal__impact{border-radius:var(--radius-lg);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border-hover)}@supports (color:color-mix(in lab, red, red)){.library-delete-modal__impact{border-color:color-mix(in srgb, var(--pb-border-hover) 88%, transparent)}}.library-delete-modal__impact{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-delete-modal__impact{background:color-mix(in srgb, var(--pb-bg-surface) 40%, transparent)}}.library-delete-modal__impact .library-browser-modal__details-table{margin-top:0}.series-delete-modal__options{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}.series-delete-modal__option{align-items:flex-start;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border);display:flex}@supports (color:color-mix(in lab, red, red)){.series-delete-modal__option{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.series-delete-modal__option{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.series-delete-modal__option{background:color-mix(in srgb, var(--pb-bg-surface) 82%, transparent)}}.series-delete-modal__checkbox{margin-top:calc(var(--spacing) * .5);border-color:var(--pb-border-hover);background:var(--pb-bg-card-hover);color:var(--pb-error);border-radius:.25rem}.series-delete-modal__checkbox:focus{--tw-ring-color:var(--pb-error);--tw-ring-offset-width:0px}.series-delete-modal__option-copy{min-width:0}.series-delete-modal__option-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary)}.series-delete-modal__option-text{margin-top:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);line-height:1.5}.library-delete-modal__footer{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3);border-top-width:1px;border-color:var(--pb-border);background:var(--pb-bg-card);display:flex}@supports (color:color-mix(in lab, red, red)){.library-delete-modal__footer{background:color-mix(in srgb, var(--pb-bg-card) 80%, transparent)}}.library-browser-modal__header{align-items:flex-start;gap:calc(var(--spacing) * 4);background:linear-gradient(180deg, var(--pb-bg-surface) 0%, var(--pb-bg-card) 100%)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__header{background:linear-gradient(180deg, color-mix(in srgb, var(--pb-bg-surface) 95%, transparent) 0%, color-mix(in srgb, var(--pb-bg-card) 96%, transparent) 100%)}}.library-browser-modal--properties .library-browser-modal__header,.library-browser-modal--properties .library-browser-modal__body{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.library-browser-modal--properties .modal-footer{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);justify-content:flex-end}.library-browser-modal--form .library-browser-modal__body{padding-block:0;padding-inline:0}.library-browser-modal--form .modal-footer{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3)}.library-browser-modal .modal-footer{justify-content:flex-end;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3);background:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal .modal-footer{background:color-mix(in srgb, var(--pb-bg-card) 80%, transparent)}}.library-browser-modal__title-block{align-items:flex-start;gap:calc(var(--spacing) * 3);min-width:0;display:flex}.library-browser-modal__icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);color:var(--pb-interactive);border-width:1px;border-color:var(--pb-interactive);flex-shrink:0;justify-content:center;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon{border-color:color-mix(in srgb, var(--pb-interactive) 24%, transparent)}}.library-browser-modal__icon{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.library-browser-modal__icon svg{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.library-browser-modal__icon--warning{color:var(--pb-warning);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--warning{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.library-browser-modal__icon--warning{background:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--warning{background:color-mix(in srgb, var(--pb-warning) 10%, transparent)}}.library-browser-modal__icon--danger{color:var(--pb-error);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--danger{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.library-browser-modal__icon--danger{background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--danger{background:color-mix(in srgb, var(--pb-error) 10%, transparent)}}.library-browser-modal--properties .library-browser-modal__icon{height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);border-radius:var(--radius-xl)}.library-browser-modal--properties .library-browser-modal__icon svg{width:1.125rem;height:1.125rem}.library-browser-modal__title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-browser-modal__subtitle{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.library-browser-modal__close{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.library-browser-modal__close:hover{background:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__close:hover{background:color-mix(in srgb, var(--pb-bg-card-hover) 88%, transparent)}}.library-browser-modal__close:hover{color:var(--pb-text)}.library-browser-modal__close:focus-visible{outline:2px solid var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__close:focus-visible{outline:2px solid color-mix(in srgb, var(--pb-interactive) 42%, transparent)}}.library-browser-modal__close:focus-visible{outline-offset:2px}.library-browser-modal__body{min-height:0}.library-browser-modal__form{flex-direction:column;flex:1;min-height:0;display:flex}.library-browser-modal__content{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.library-browser-modal__section-label{margin-bottom:calc(var(--spacing) * 2);padding-bottom:calc(var(--spacing) * 2);letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);border-bottom:1px solid var(--pb-border-subtle);font-family:Syne,sans-serif;font-size:.64rem;font-weight:700;display:block}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__section-label{border-bottom:1px solid color-mix(in srgb, var(--pb-border-subtle) 80%, transparent)}}.library-browser-modal__section{padding-bottom:var(--spacing)}.library-browser-modal__details-table{border-collapse:collapse;table-layout:fixed;width:100%;margin-top:.625rem;font-size:.8rem}.library-browser-modal__details-table--flush{margin-top:0}.library-browser-modal__detail-label{vertical-align:top;width:6.875rem;color:var(--pb-text-dim);font-size:inherit;text-align:left;padding:.25rem .75rem .25rem 0;font-weight:500;line-height:1.35}.library-browser-modal__detail-value{vertical-align:top;min-width:0;color:var(--pb-text);font-size:inherit;overflow-wrap:anywhere;padding:.25rem 0;font-weight:500;line-height:1.4}.library-browser-modal__detail-value--mono{color:var(--pb-text-sec);font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.45}.library-browser-modal__loading{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 6);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__loading{border-color:color-mix(in srgb, var(--pb-border) 86%, transparent)}}.library-browser-modal__loading{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__loading{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser-modal__stats{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(8.5rem,1fr));display:grid}.library-browser-modal__stat{justify-content:space-between;gap:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);min-height:5rem;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border);flex-direction:column;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__stat{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__stat{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__stat{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser-modal__stat-label,.library-browser-modal__field-label{letter-spacing:.11em;text-transform:uppercase;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.62rem;font-weight:700}.library-browser-modal__stat-value,.library-browser-modal__preview-value{min-width:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-primary);overflow-wrap:anywhere;font-weight:500;line-height:1.35}.library-browser-modal__meta{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__meta{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__meta{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__meta{background:color-mix(in srgb, var(--pb-bg-surface) 93%, transparent)}}.library-browser-modal__meta-row{gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);grid-template-columns:minmax(8rem,10rem) minmax(0,1fr);display:grid}.library-browser-modal__meta-row+.library-browser-modal__meta-row{border-top:1px solid var(--pb-border-subtle)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__meta-row+.library-browser-modal__meta-row{border-top:1px solid color-mix(in srgb, var(--pb-border-subtle) 84%, transparent)}}.library-browser-modal__meta-label{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-dim);letter-spacing:.08em;text-transform:uppercase}.library-browser-modal__meta-value{min-width:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);overflow-wrap:anywhere;font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.45}.library-browser-modal__storage{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__storage{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage{background:color-mix(in srgb, var(--pb-bg-surface) 93%, transparent)}}.library-browser-modal__storage-head{margin-bottom:calc(var(--spacing) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.library-browser-modal__storage-bar{height:calc(var(--spacing) * 2.5);background:var(--pb-bg-card-hover);border-radius:3.40282e38px;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage-bar{background:color-mix(in srgb, var(--pb-bg-card-hover) 72%, transparent)}}.library-browser-modal__storage-fill{background:linear-gradient(90deg, var(--pb-interactive) 0%, var(--pb-brand) 100%);border-radius:3.40282e38px;height:100%;display:block}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage-fill{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-interactive) 82%, white) 0%, color-mix(in srgb, var(--pb-brand) 74%, white) 100%)}}.library-browser-modal__field{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.library-browser-modal__settings-rows{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border)}.library-browser-modal__note{padding-inline:calc(var(--spacing) * 6)}.library-browser-modal__body-inset{padding-inline:calc(var(--spacing) * 6);padding-bottom:calc(var(--spacing) * 4)}.library-browser-modal__body-inset--section{padding-bottom:0}.library-browser-modal__body-inset--compact-top{padding-top:calc(var(--spacing) * 3)}.library-browser-modal__body-inset--roomy-bottom{padding-bottom:calc(var(--spacing) * 4)}.library-browser-modal__section-stack{gap:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 4);flex-direction:column;display:flex}.library-browser-modal__section-stack--delete{gap:calc(var(--spacing) * 4)}.library-browser-modal__section-panel{flex-direction:column;display:flex}.library-browser-modal__section-heading-row{padding-inline:calc(var(--spacing) * 6);padding-bottom:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-bottom:1px solid var(--pb-border-subtle);padding-top:0}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__section-heading-row{border-bottom:1px solid color-mix(in srgb, var(--pb-border-subtle) 80%, transparent)}}.library-browser-modal__preview-block{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-block{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__preview-block{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-block{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser-modal__preview-block{overflow-wrap:anywhere;line-height:1.45}.library-browser-modal__preview-block--mono{font-family:JetBrains Mono,monospace;font-size:.76rem}.library-browser-modal__detail-input{width:100%;min-height:2.5rem;font-size:.88rem;display:block}.library-browser-modal__helper,.library-browser-modal__context-value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.library-browser-modal__context-value--mono{color:var(--pb-text-sec);overflow-wrap:anywhere;font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.45}.library-browser-modal__error{border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-error);border-width:1px;border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__error{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.library-browser-modal__error{background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__error{background:color-mix(in srgb, var(--pb-error) 8%, transparent)}}.library-browser-modal__actions{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.library-browser-modal__preview-grid{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));display:grid}.library-browser-modal__preview-card{justify-content:space-between;gap:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);min-height:5.25rem;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border);flex-direction:column;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-card{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__preview-card{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-card{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser__name{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.library-browser__name-icon{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);flex-shrink:0}.library-browser__name-icon--folder{color:var(--pb-brand)}.library-browser__name-icon--file{color:var(--pb-text-dim)}.library-browser__name-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:.76rem;font-weight:400;overflow:hidden}.library-browser__name-label--folder{color:var(--pb-text-primary);font-weight:500;text-decoration:none}.library-browser__name-label--folder:hover{color:var(--pb-interactive)}.library-browser__empty{height:100%;min-height:280px;padding-inline:calc(var(--spacing) * 6);text-align:center;flex-direction:column;justify-content:center;align-items:center;display:flex}.library-browser__empty-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);color:var(--pb-text-dim)}.library-browser__empty-title{margin-top:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-browser__empty-copy{margin-top:calc(var(--spacing) * 2);max-width:var(--container-xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.library-empty-shell{align-items:flex-start;gap:calc(var(--spacing) * 4);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 8);box-shadow:var(--pb-shadow-1);border-radius:14px;flex-direction:column;display:flex}.library-empty-shell__icon-wrap{height:calc(var(--spacing) * 16);width:calc(var(--spacing) * 16);background-color:var(--pb-bg-card-hover);color:var(--pb-text-dim);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.library-empty-shell__icon{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8)}:where(.library-empty-shell__body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.library-empty-shell__copy{max-width:var(--container-2xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7);color:var(--pb-text-secondary)}.library-empty-shell__actions{align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}@media (max-width:1024px){.library-browser{flex:none;grid-template-columns:1fr;height:auto;min-height:520px}.library-browser__tree{border-right:0;border-bottom:1px solid var(--pb-border);max-height:220px}}.health-section__label{letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);margin-block:.625rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700;display:block}.health-component-card__sub,.health-detail-card__stat-value,.health-history-table td{font-family:JetBrains Mono,monospace}:where(.health-section>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}.health-component-grid{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(280px,1fr));display:grid}.health-component-card{cursor:pointer;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 4);padding-bottom:calc(var(--spacing) * 3);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-width:1px;border-radius:14px;flex-direction:column;height:100%;display:flex}.health-component-card:hover{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.health-component-card:hover{background:color-mix(in srgb, var(--pb-interactive) 4%, var(--pb-bg-card))}}.health-component-card:hover{border-color:var(--pb-border-strong)}.health-component-card--success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.health-component-card--success{border-color:color-mix(in srgb, var(--pb-success) 22%, var(--pb-border))}}.health-component-card--warning{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.health-component-card--warning{border-color:color-mix(in srgb, var(--pb-warning) 22%, var(--pb-border))}}.health-component-card--danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.health-component-card--danger{border-color:color-mix(in srgb, var(--pb-error) 22%, var(--pb-border))}}.health-component-card--neutral{border-color:var(--pb-border)}.health-component-card__header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);display:flex}.health-component-card__name{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text);font-family:Syne,sans-serif;font-size:.84rem;font-weight:700}.health-component-card__stats{align-items:flex-start;gap:calc(var(--spacing) * 2);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.health-detail-card__stats{gap:calc(var(--spacing) * 2);display:grid}@media (min-width:40rem){.health-detail-card__stats{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.health-detail-card__stats{grid-template-columns:repeat(4,minmax(0,1fr))}}.health-component-card__stat,.health-detail-card__stat{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);border-width:1px;border-color:var(--pb-border-subtle);border-radius:10px;flex-direction:column;gap:0;display:flex}.health-component-card__stat{background:var(--pb-bg-base)}.health-detail-card__stat{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);background:var(--pb-bg-base)}.health-component-card__stat-label,.health-detail-card__stat-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.46rem;font-weight:700}.health-component-card__stat-value,.health-detail-card__stat-value{--tw-font-weight:var(--font-weight-semibold);font-size:.82rem;font-weight:var(--font-weight-semibold);color:var(--pb-text-primary);display:block}.health-component-card__stat-value{white-space:pre-line;line-height:1.25}.health-component-card__stat-value--danger,.health-detail-card__stat-value--danger{color:var(--pb-error)}.health-component-card__stat-value--warning,.health-detail-card__stat-value--warning{color:var(--pb-warning)}.health-component-card__message{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary);min-height:2.5rem}.health-component-card__sub{color:var(--pb-text-dim);margin-top:auto;font-size:.68rem}:where(.health-detail-shell>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.health-detail-back{align-items:center;gap:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.health-detail-back:hover{color:var(--pb-interactive-hover)}:where(.health-detail-card>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.health-detail-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:14px}.health-detail-card__header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}.health-detail-card__title-row{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;min-width:0;display:flex}.health-detail-card__title{letter-spacing:.03em;text-transform:uppercase;color:var(--pb-text);font-family:Syne,sans-serif;font-size:1rem;font-weight:800}:where(.health-check-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.health-check-row{align-items:center;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);border-radius:10px;grid-template-columns:8px minmax(120px,140px) minmax(0,1fr) auto;display:grid}.health-check-row--database-metric{grid-template-columns:8px minmax(160px,180px) minmax(0,1fr) auto}.health-check-row__name{text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary);justify-self:flex-start}.health-check-row__message{min-width:0;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim);flex:1}.health-check-list--database-metrics .health-check-row__name,.health-check-list--database-metrics .health-check-row__message{text-align:left;justify-self:flex-start}.health-check-list--database-metrics .health-led{justify-self:center}.health-table-wrap{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);border-radius:12px;overflow-x:auto}.health-table{border-collapse:collapse;width:100%}.health-table th{text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);background:var(--pb-bg-shell);border-bottom:1px solid var(--pb-border);padding:.45rem .85rem;font-family:Syne,sans-serif;font-size:.52rem;font-weight:700}.health-table td{border-bottom:1px solid var(--pb-border-subtle);color:var(--pb-text-sec);padding:.55rem .85rem;font-size:.76rem}.health-table tbody tr:last-child td{border-bottom:0}.health-table tbody tr:hover td{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.health-table tbody tr:hover td{background:color-mix(in srgb, var(--pb-interactive) 8%, var(--pb-bg-card))}}.health-table th.c,.health-table td.c{text-align:center}.health-table th.r,.health-table td.r{text-align:right}.health-table__status-col{width:40px}.health-table__name{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.health-table__detail{margin-top:calc(var(--spacing) * .5);color:var(--pb-text-dim);font-size:.72rem}.health-led{border-radius:999px;flex-shrink:0;width:8px;height:8px;display:inline-block}.health-led--header{width:10px;height:10px}.health-led--green{background:var(--pb-success);box-shadow:0 0 5px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.health-led--green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.health-led--amber{background:var(--pb-warning);box-shadow:0 0 5px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.health-led--amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-warning) 35%, transparent)}}.health-led--red{background:var(--pb-error);box-shadow:0 0 5px var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.health-led--red{box-shadow:0 0 5px color-mix(in srgb, var(--pb-error) 35%, transparent)}}.health-led--off{background:var(--pb-text-dim);opacity:.4}.health-footer-strip{align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:var(--spacing);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);color:var(--pb-text-dim);border-radius:10px;flex-wrap:wrap;font-family:JetBrains Mono,monospace;font-size:.72rem;display:flex}.health-footer-strip strong{color:var(--pb-text);font-weight:600}.step-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1)}.step-badge{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-info);background-color:var(--pb-info);border-radius:3.40282e38px;justify-content:center;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.step-badge{background-color:color-mix(in srgb, var(--pb-info) 14%, transparent)}}.step-badge{border:1px solid var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.step-badge{border:1px solid color-mix(in srgb, var(--pb-info) 28%, transparent)}}.action-card{align-items:flex-start;gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--pb-bg-card-hover);display:flex}@supports (color:color-mix(in lab, red, red)){.action-card{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 40%, transparent)}}.action-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.action-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.action-card-icon{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:var(--radius-xl);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-shrink:0;justify-content:center;align-items:center;display:flex}.action-card-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.action-card-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim)}.utility-launch-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);height:100%;padding:calc(var(--spacing) * 5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;box-shadow:var(--pb-shadow-1);flex-direction:column;transition-duration:.15s;display:flex}.utility-launch-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.utility-launch-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.utility-launch-card:hover{box-shadow:var(--pb-shadow-2)}.utility-launch-icon-brand{background-color:var(--pb-brand-dim);color:var(--pb-brand)}.utility-launch-icon-interactive{background-color:var(--pb-interactive-dim);color:var(--pb-interactive)}.utility-launch-icon-info{background-color:var(--pb-info-dim);color:var(--pb-info)}.utility-launch-icon-success{background-color:var(--pb-success-dim);color:var(--pb-success)}.utility-launch-icon-warning{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.utility-launch-kicker{margin-top:calc(var(--spacing) * 4);--tw-font-weight:var(--font-weight-medium);font-size:10px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-launch-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-launch-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-launch-footer{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);padding-top:calc(var(--spacing) * 4);margin-top:auto;display:flex}.utility-launch-tag{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.utility-launch-link{align-items:center;gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.utility-launch-link:hover{color:var(--pb-interactive-hover)}.utility-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.utility-hero-card{padding-inline:calc(var(--spacing) * 7)}}.utility-hero-card{box-shadow:var(--pb-shadow-2)}.utility-top-link{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);border-radius:3.40282e38px;align-items:center;display:inline-flex}.utility-top-link:hover{border-color:var(--pb-border-hover);color:var(--pb-text);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.utility-top-link:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.utility-top-link-muted{color:var(--pb-text-dim)}.utility-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.utility-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 42%, transparent), transparent)}}.utility-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.utility-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.utility-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.utility-hero-title{margin-top:calc(var(--spacing) * 2);font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-hero-copy{margin-top:calc(var(--spacing) * 2);max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-hero-highlights{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:48rem){.utility-hero-highlights{grid-template-columns:repeat(3,minmax(0,1fr))}}.utility-hero-highlight{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.utility-hero-highlight-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-hero-highlight-value{margin-top:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-hero-highlight-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-hero-actions{gap:calc(var(--spacing) * 3);display:grid}.utility-hero-action{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1)}.utility-hero-action:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.utility-hero-action:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.utility-hero-action-primary{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.utility-hero-action-primary{border-color:color-mix(in srgb, var(--pb-interactive) 26%, transparent)}}.utility-hero-action-primary{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.utility-hero-action-primary{background-color:color-mix(in srgb, var(--pb-interactive) 8%, var(--pb-bg-card))}}.utility-hero-action-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-hero-action-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-hero-action-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.utility-hero-notes{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:80rem){.utility-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.utility-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.utility-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-hero-note-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utilities-page{width:100%;max-width:1100px;padding-bottom:var(--pb-page-footer-clearance);margin-inline:auto}.utilities-shell{width:100%}:where(.utilities-content>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.utilities-header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.utilities-header-copy{align-items:flex-start;gap:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.utilities-header-title{letter-spacing:.04em;text-transform:uppercase;font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1}.utilities-header-title span{color:var(--pb-brand)}.utilities-header-subtitle{color:var(--pb-text-dim);margin-top:4px;font-size:.78rem;font-weight:400;line-height:1.5}.utilities-gauges{align-items:flex-end;gap:calc(var(--spacing) * 3.5);display:flex}.utilities-gauge{text-align:center}.utilities-gauge-ring{height:calc(var(--spacing) * 12);width:calc(var(--spacing) * 12);border-style:var(--tw-border-style);background-color:var(--pb-bg-card);border-width:4px;border-radius:3.40282e38px;justify-content:center;align-items:center;margin-inline:auto;display:flex;position:relative}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring{background-color:color-mix(in srgb, var(--pb-bg-card) 70%, var(--pb-bg-base))}}.utilities-gauge-ring-success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring-success{border-color:color-mix(in srgb, var(--pb-success) 60%, var(--pb-border))}}.utilities-gauge-ring-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring-info{border-color:color-mix(in srgb, var(--pb-info) 60%, var(--pb-border))}}.utilities-gauge-ring-ink{border-color:var(--pb-text)}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring-ink{border-color:color-mix(in srgb, var(--pb-text) 22%, var(--pb-border))}}.utilities-gauge-value{font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:700}.utilities-gauge-label{--tw-font-weight:var(--font-weight-medium);font-size:.52rem;font-weight:var(--font-weight-medium);color:var(--pb-text-dim);text-transform:uppercase;letter-spacing:.1em;margin-top:2px;display:block}.utilities-tabs{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);border-radius:10px;align-self:center;display:inline-flex;overflow:hidden}.utilities-tab{padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-bold);font-family:Syne,sans-serif;font-size:.78rem;font-weight:var(--font-weight-bold);--tw-tracking:.08em;letter-spacing:.08em;color:var(--pb-text-dim);text-transform:uppercase;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.utilities-tab+.utilities-tab{border-left:1px solid var(--pb-border)}.utilities-tab:hover{color:var(--pb-text);background-color:var(--pb-text)}@supports (color:color-mix(in lab, red, red)){.utilities-tab:hover{background-color:color-mix(in srgb, var(--pb-text) 3%, transparent)}}.utilities-tab.is-active{background-color:var(--pb-interactive);color:var(--pb-text-inverse)}.utilities-section-label{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-bottom:calc(var(--spacing) * 1.5);--tw-font-weight:var(--font-weight-bold);font-family:Syne,sans-serif;font-size:.62rem;font-weight:var(--font-weight-bold);--tw-tracking:.12em;letter-spacing:.12em;color:var(--pb-text-dim);text-transform:uppercase}.utilities-tool-grid{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.75rem;display:grid}.utilities-tool-grid-wide{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}.utility-launch-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);height:100%;padding:calc(var(--spacing) * 5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;box-shadow:var(--pb-shadow-1);border-radius:14px;flex-direction:column;transition-duration:.15s;display:flex;overflow:hidden}.utility-launch-card:hover{border-color:var(--pb-border-strong);box-shadow:var(--pb-shadow-2);background-color:var(--pb-bg-card);transform:translateY(-2px)}.utility-launch-header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);display:flex}.utility-launch-arrow{color:var(--pb-text-dim);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}.utility-launch-card:hover .utility-launch-arrow{color:var(--pb-interactive);transform:translate(2px)}.utility-launch-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:10px;justify-content:center;align-items:center;display:flex}.utility-launch-title{margin-top:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-extrabold);font-family:Syne,sans-serif;font-size:.9rem;font-weight:var(--font-weight-extrabold);--tw-tracking:.04em;letter-spacing:.04em;color:var(--pb-text-primary);text-transform:uppercase}.utility-launch-copy{margin-top:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);flex:1;font-size:.78rem;font-weight:400;line-height:1.5}.utility-launch-footer{margin-top:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.utility-launch-tag{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-family:JetBrains Mono,monospace;font-weight:var(--font-weight-medium);color:var(--pb-text-dim);text-transform:uppercase;letter-spacing:.06em;align-items:center;font-size:.6rem;display:inline-flex}.utility-tool-page{width:100%;max-width:1100px;margin-inline:auto}:where(.utility-tool-header-block>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.utility-tool-back-link{align-items:center;gap:var(--spacing);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));font-size:.82rem;display:inline-flex}@media (hover:hover){.utility-tool-back-link:hover{color:var(--pb-text-primary)}}.utility-tool-header-shell{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 4);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:14px;flex-wrap:wrap;display:flex}.utility-tool-header-left{align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.utility-tool-header-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:10px;justify-content:center;align-items:center;display:flex}.utility-tool-header-icon-converter{background-color:var(--pb-interactive-dim);color:var(--pb-interactive)}.utility-tool-header-icon-mass-convert{background-color:var(--pb-success-dim);color:var(--pb-success)}.utility-tool-header-icon-integrity{background-color:var(--pb-info-dim);color:var(--pb-info)}.utility-tool-header-icon-rename,.utility-tool-header-icon-db-check{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.utility-tool-header-icon-export{background-color:var(--pb-brand-dim);color:var(--pb-brand)}.utility-tool-header-title{--tw-font-weight:var(--font-weight-extrabold);font-family:Syne,sans-serif;font-size:1.1rem;font-weight:var(--font-weight-extrabold);--tw-tracking:.03em;letter-spacing:.03em;color:var(--pb-text-primary);text-transform:uppercase}.utility-tool-header-title span{color:var(--pb-brand)}.utility-tool-header-subtitle{margin-top:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.utility-tool-header-tag{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-semibold);font-family:JetBrains Mono,monospace;font-size:.62rem;font-weight:var(--font-weight-semibold);--tw-tracking:.08em;letter-spacing:.08em;color:var(--pb-text-dim);text-transform:uppercase;align-items:center;display:inline-flex}.system-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.system-hero-card{padding-inline:calc(var(--spacing) * 7)}}.system-hero-card{box-shadow:var(--pb-shadow-2)}.system-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.system-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 42%, transparent), transparent)}}.system-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.system-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.system-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.system-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.system-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.system-hero-aside{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.system-hero-aside{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.system-hero-aside{grid-template-columns:repeat(1,minmax(0,1fr))}}.system-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.system-hero-note-emphasis{border-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.system-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-brand) 20%, transparent)}}.system-hero-note-emphasis{background-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.system-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-brand) 8%, var(--pb-bg-card))}}.system-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.system-hero-note-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.system-hero-note-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.system-shell-body{gap:calc(var(--spacing) * 6);display:grid}@media (min-width:80rem){.system-shell-body{grid-template-columns:280px minmax(0,1fr)}}@media (min-width:96rem){.system-shell-body{grid-template-columns:300px minmax(0,1fr)}}.system-rail-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;overflow:hidden}.system-rail-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 5)}.system-rail-eyebrow{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.system-rail-title{margin-top:var(--spacing);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.system-rail-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}:where(.system-rail-links>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.system-rail-links{padding:calc(var(--spacing) * 3)}.system-rail-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.system-rail-footer{background-color:color-mix(in srgb, var(--pb-bg-card) 55%, transparent)}}.system-tab-card{align-items:flex-start;gap:calc(var(--spacing) * 3);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;border-width:1px;transition-duration:.15s;display:flex}.system-tab-card-inactive{border-color:var(--pb-border);background-color:var(--pb-bg-surface)}.system-tab-card-inactive:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.system-tab-card-inactive:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.system-tab-card-inactive:hover{box-shadow:var(--pb-shadow-1)}.system-tab-card-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.system-tab-card-active{border-color:color-mix(in srgb, var(--pb-interactive) 30%, transparent)}}.system-tab-card-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.system-tab-card-active{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.system-tab-card-active{box-shadow:var(--pb-shadow-1)}.system-tab-icon{margin-top:calc(var(--spacing) * .5);height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:var(--radius-2xl);flex-shrink:0;justify-content:center;align-items:center;display:flex}.system-tab-copy{flex:1;min-width:0}.system-tab-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.system-tab-description{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-secondary);display:block}.system-tab-arrow{margin-top:var(--spacing);color:var(--pb-text-dim);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}.system-tab-card:hover .system-tab-arrow,.system-tab-card-active .system-tab-arrow{color:var(--pb-interactive);transform:translate(.125rem)}.system-focus-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;position:relative;overflow:hidden}.system-focus-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.system-focus-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-interactive) 34%, transparent), transparent)}}.system-focus-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.system-focus-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:flex-start}}.system-focus-meta{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim);flex-wrap:wrap;display:flex}.system-focus-chip{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.system-focus-actions{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.system-focus-actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.system-focus-actions{grid-template-columns:repeat(1,minmax(0,1fr))}}.review-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.review-hero-card{padding-inline:calc(var(--spacing) * 7)}}.review-hero-card{box-shadow:var(--pb-shadow-2)}.review-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-info), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.review-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-info) 42%, transparent), transparent)}}.review-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.review-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.review-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.review-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.review-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.review-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.review-hero-notes{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.review-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.review-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.review-hero-note-emphasis{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-info) 20%, transparent)}}.review-hero-note-emphasis{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-info) 10%, var(--pb-bg-card))}}.review-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.review-hero-note-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.review-workspace-tabs{align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:1.25rem;flex-wrap:wrap;display:flex}@media (min-width:40rem){.review-workspace-tabs{padding-inline:calc(var(--spacing) * 5)}}.review-workspace-tabs{box-shadow:var(--pb-shadow-1)}.review-tab-chip-active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary);border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-tab-chip-active{border-color:color-mix(in srgb, var(--pb-info) 34%, transparent)}}.review-tab-chip-active{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-tab-chip-active{background-color:color-mix(in srgb, var(--pb-info) 12%, var(--pb-bg-card))}}.review-tab-chip-active{box-shadow:var(--pb-shadow-1)}.review-tab-chip-inactive{color:var(--pb-text-dim)}.review-focus-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;position:relative;overflow:hidden}.review-focus-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.review-focus-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 34%, transparent), transparent)}}.review-focus-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.review-focus-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:flex-start}}.review-focus-chip{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.review-focus-actions{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.review-focus-actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.review-focus-actions{grid-template-columns:repeat(1,minmax(0,1fr))}}.review-note-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:1.25rem}.workflow-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.workflow-hero-card{padding-inline:calc(var(--spacing) * 7)}}.workflow-hero-card{box-shadow:var(--pb-shadow-2)}.workflow-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.workflow-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-interactive) 40%, transparent), transparent)}}.workflow-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.workflow-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.workflow-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.workflow-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.workflow-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.workflow-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.workflow-hero-notes{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.workflow-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.workflow-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.workflow-hero-note-emphasis{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-interactive) 20%, transparent)}}.workflow-hero-note-emphasis{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.workflow-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.workflow-hero-note-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.series-page-shell{gap:calc(var(--spacing) * 6);flex-direction:column;min-height:100%;display:flex}.series-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.series-hero-card{padding-inline:calc(var(--spacing) * 7)}}.series-hero-card{box-shadow:var(--pb-shadow-2)}.series-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.series-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 34%, transparent), color-mix(in srgb, var(--pb-interactive) 34%, transparent), transparent)}}.series-hero-grid{gap:calc(var(--spacing) * 5);display:grid}@media (min-width:96rem){.series-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.8fr);align-items:flex-start}}.series-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.series-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.series-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7);color:var(--pb-text-secondary)}.series-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.series-hero-notes{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:96rem){.series-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.series-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.series-hero-note-emphasis{border-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.series-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-brand) 22%, transparent)}}.series-hero-note-emphasis{background-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.series-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-brand) 8%, var(--pb-bg-card))}}.series-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.series-hero-note-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.series-hero-note-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.page-context-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:1.25rem}.page-context-back-link{align-items:center;gap:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.page-context-back-link:hover{color:var(--pb-text-primary)}.page-context-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.page-context-grid{grid-template-columns:minmax(0,1.35fr) minmax(280px,.9fr);align-items:flex-start}}.page-context-breadcrumbs{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);flex-wrap:wrap;display:flex}.page-context-link{align-items:center;gap:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.page-context-link:hover{color:var(--pb-text-primary)}.page-context-separator{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);color:var(--pb-text-dim)}.page-context-summary{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.page-context-chip-row{gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.page-context-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);padding:calc(var(--spacing) * 4);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.page-context-note{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 40%, transparent)}}.page-context-note-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.page-context-note-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.detail-hero-shell{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.detail-hero-shell{padding-inline:calc(var(--spacing) * 7)}}.detail-hero-shell{box-shadow:var(--pb-shadow-2)}.detail-hero-shell:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.detail-hero-shell:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 40%, transparent), transparent)}}.detail-hero-grid{gap:calc(var(--spacing) * 6);display:grid}@media (min-width:80rem){.detail-hero-grid{grid-template-columns:220px minmax(0,1fr) 320px}}.detail-cover-panel{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card-hover);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:1.5rem}.detail-summary-panel{min-width:0}:where(.detail-summary-panel>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.detail-header-stack>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.detail-status-row{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.detail-body-copy{max-width:56rem}.detail-stat-grid{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.detail-stat-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.detail-stat-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.detail-info-grid{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.detail-info-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.detail-info-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}:where(.detail-aside>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.detail-action-panel{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.25rem}.series-domain-page{width:100%;max-width:1400px;padding-bottom:var(--pb-page-footer-clearance);margin-inline:auto}.series-domain-breadcrumb-row{flex-wrap:wrap;align-items:center;gap:.75rem;display:flex}.series-domain-back-link{color:var(--pb-interactive);align-items:center;gap:.25rem;font-size:.82rem;transition:color .14s;display:inline-flex}.series-domain-back-link:hover{color:var(--pb-interactive-hover)}.series-domain-breadcrumbs{color:var(--pb-text-tertiary);flex-wrap:wrap;align-items:center;gap:.35rem;font-size:.82rem;display:flex}.series-domain-breadcrumbs a{color:inherit}.series-domain-breadcrumbs a:hover{color:var(--pb-interactive)}.series-domain-breadcrumb-sep{font-size:.7rem}.series-domain-breadcrumbs .current{color:var(--pb-text-primary);font-weight:600}.detail-hero-shell{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);box-shadow:var(--pb-shadow-1);border-radius:14px;overflow:hidden}.detail-hero-shell:before{display:none}.series-domain-hero-inner{grid-template-columns:130px minmax(0,1fr) minmax(13rem,16rem);align-items:start;gap:1.25rem;display:grid}.story-arc-detail-hero-inner{grid-template-columns:130px minmax(0,1fr) minmax(18rem,19rem)}.issue-domain-hero-inner{grid-template-columns:120px minmax(0,1fr) minmax(13rem,16rem)}.series-domain-cover-column{flex-shrink:0;width:130px}.issue-domain-cover-column{width:120px}.series-domain-cover-frame{aspect-ratio:2/3;border:1px solid var(--pb-border-default);background:linear-gradient(155deg, var(--pb-surface-card) 0%, var(--pb-surface-app) 100%);color:var(--pb-text-tertiary);border-radius:12px;justify-content:center;align-items:center;transition:box-shadow .2s;display:flex;overflow:hidden}.series-domain-cover-frame:hover{box-shadow:var(--pb-shadow-2)}.issue-domain-cover-frame{width:120px}.series-domain-cover-image{object-fit:cover;object-position:top;cursor:pointer;width:100%;height:100%}.series-domain-cover-placeholder{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.series-domain-hero-title{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text-primary);overflow-wrap:anywhere;font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1.05}.series-domain-hero-title span{color:var(--pb-brand)}.series-domain-hero-title-link{color:inherit;text-underline-offset:.18em;text-decoration:none;text-decoration-thickness:2px;transition:color .14s,text-decoration-color .14s;display:inline}.series-domain-hero-title-link:hover,.series-domain-hero-title-link:focus-visible{color:var(--pb-interactive);text-decoration-line:underline;-webkit-text-decoration-color:var(--pb-interactive);-webkit-text-decoration-color:var(--pb-interactive);text-decoration-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-domain-hero-title-link:hover,.series-domain-hero-title-link:focus-visible{-webkit-text-decoration-color:color-mix(in srgb, var(--pb-interactive) 58%, transparent);-webkit-text-decoration-color:color-mix(in srgb, var(--pb-interactive) 58%, transparent);text-decoration-color:color-mix(in srgb, var(--pb-interactive) 58%, transparent)}}.series-domain-hero-title-link:hover,.series-domain-hero-title-link:focus-visible{outline:none}.series-domain-hero-title-sm{font-size:1.4rem}.series-domain-hero-description{max-width:35rem;color:var(--pb-text-secondary);margin-top:.5rem;font-size:.85rem;line-height:1.7}.series-domain-hero-subtitle{color:var(--pb-text-secondary);margin-top:.25rem;font-size:.88rem}.series-domain-status-row{flex-wrap:wrap;align-items:center;gap:.375rem;margin-top:.75rem;display:flex}.series-domain-led{border-radius:999px;width:8px;height:8px;display:inline-block}.series-domain-led-on{background:var(--pb-status-success);box-shadow:0 0 5px var(--pb-status-success), 0 0 10px var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.series-domain-led-on{box-shadow:0 0 5px color-mix(in srgb, var(--pb-status-success) 35%, transparent), 0 0 10px color-mix(in srgb, var(--pb-status-success) 12%, transparent)}}.series-domain-led-off{background:var(--pb-text-tertiary);opacity:.35}.app-progress{align-items:center;gap:.625rem;display:flex}.app-progress-track{background:var(--pb-text-secondary);border-radius:2px;flex:1;height:8px}@supports (color:color-mix(in lab, red, red)){.app-progress-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.app-progress-track{border:1px solid var(--pb-border-subtle);position:relative;overflow:hidden}.app-progress-fill{background:linear-gradient(90deg, var(--pb-status-success), var(--pb-status-success));height:100%}@supports (color:color-mix(in lab, red, red)){.app-progress-fill{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-success) 45%, transparent), var(--pb-status-success))}}.app-progress-fill{position:relative}.app-progress-fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}.app-progress-fill--interactive{background:linear-gradient(90deg, var(--pb-interactive), var(--pb-interactive))}@supports (color:color-mix(in lab, red, red)){.app-progress-fill--interactive{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-interactive) 45%, transparent), var(--pb-interactive))}}.app-progress-fill--error{background:linear-gradient(90deg, var(--pb-error), var(--pb-error))}@supports (color:color-mix(in lab, red, red)){.app-progress-fill--error{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-error) 45%, transparent), var(--pb-error))}}.app-progress-value{text-align:right;min-width:40px;color:var(--pb-status-success);font-family:JetBrains Mono,monospace;font-size:.82rem;font-weight:700}.app-progress-value--interactive{color:var(--pb-interactive)}.app-progress-value--error{color:var(--pb-error)}.app-progress-value-stack{flex-direction:column;align-items:flex-end;line-height:1.05;display:flex}.app-progress-value-secondary{letter-spacing:.01em;color:var(--pb-text-dim);white-space:nowrap;font-family:DM Sans,sans-serif;font-size:.62rem;font-weight:600}.series-domain-info-grid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.625rem;margin-top:1rem;display:grid}.series-domain-info-box{background:var(--pb-surface-app);border:1px solid var(--pb-border-subtle);border-radius:10px;padding:.75rem .875rem}.series-domain-info-box-wide{grid-column:span 2}.series-domain-info-label{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.series-domain-info-value{color:var(--pb-text-secondary);margin-top:.25rem;font-size:.85rem}.series-domain-path{word-break:break-all;font-size:.75rem}.series-domain-actions-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;padding:1.125rem 1.5rem}.series-domain-actions-inner{flex-wrap:wrap;align-items:center;gap:1rem;display:flex}.series-domain-actions-panel{border-left:1px solid var(--pb-border-subtle);flex-direction:column;gap:.75rem;min-width:0;padding-left:1.25rem;display:flex}.series-domain-actions-panel-wide{min-width:18rem}.series-domain-actions-title{text-transform:uppercase;letter-spacing:.08em;color:var(--pb-text-primary);white-space:nowrap;font-family:Syne,sans-serif;font-size:.88rem;font-weight:800}.series-domain-actions-title span{color:var(--pb-brand)}.series-domain-actions-divider{background:var(--pb-border-default);align-self:stretch;width:1px}.series-domain-actions-buttons{flex-wrap:wrap;flex:1;align-items:center;gap:.5rem;display:flex}.series-domain-actions-panel .series-domain-actions-buttons{flex-direction:column;flex:initial;align-items:center;gap:.55rem}.series-domain-actions-panel .series-domain-actions-buttons>a,.series-domain-actions-panel .series-domain-actions-buttons>button,.series-domain-actions-panel .series-domain-actions-buttons .series-domain-inline-toggle{width:100%}.series-domain-actions-panel .series-domain-actions-buttons>a,.series-domain-actions-panel .series-domain-actions-buttons>button{gap:var(--pb-control-gap);justify-content:center}.series-domain-actions-panel .series-domain-actions-buttons .series-domain-inline-toggle{justify-content:space-between}.series-domain-action-form{width:100%}.series-domain-action-form>button{justify-content:center;width:100%}.series-domain-inline-toggle{border:1px solid var(--pb-border-default);background:var(--pb-surface-card);border-radius:10px;align-items:center;gap:.75rem;height:40px;padding:0 .75rem;display:inline-flex}.series-domain-inline-toggle-label{color:var(--pb-text-primary);font-size:.82rem;font-weight:600}.series-domain-section-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;padding:1rem 1.25rem}.series-domain-section-title{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:.82rem;font-weight:700}:where(.series-domain-alt-panel>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.series-domain-alt-title{text-transform:uppercase;letter-spacing:.12em;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.72rem;font-weight:700}.series-domain-alt-strip{flex-direction:column;align-items:flex-start;gap:.5rem;display:flex}.series-domain-alt-list{flex-wrap:wrap;align-items:flex-start;gap:.35rem;min-height:0;display:flex}.series-domain-alt-pill{gap:.35rem}.series-domain-alt-pill-remove{color:var(--pb-text-tertiary);margin-left:.1rem;transition:color .14s}.series-domain-alt-pill-remove:hover{color:var(--pb-status-danger)}.series-domain-alt-form{flex-wrap:wrap;align-items:center;gap:.5rem;width:100%;display:flex}.series-domain-alt-input{border:1px solid var(--pb-border-default);background:var(--pb-surface-input);width:180px;color:var(--pb-text-primary);border-radius:8px;padding:.45rem .75rem;font-size:.78rem}.series-domain-alt-input:focus{border-color:var(--pb-interactive);outline:none}.series-domain-alt-add{padding:.3rem .75rem;font-size:.78rem}.series-domain-issues-wrap{overflow:visible}.series-domain-issues-card{padding-top:.875rem}.series-domain-issues-toolbar{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.75rem;display:flex}.series-domain-issues-toolbar-left{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.series-domain-issues-title{color:var(--pb-text-primary);font-size:.88rem;font-weight:700}.series-domain-issues-summary{flex-wrap:wrap;align-items:center;gap:.35rem;display:flex}.series-domain-issues-summary .pill{font-size:.62rem}.series-domain-issues-select.dropdown-select{--pb-control-min-height:34px;--pb-control-font-size:.78rem;width:auto}.series-domain-issues-select .dropdown-select-trigger{background:var(--pb-surface-input);padding-block:.4rem;padding-inline:.75rem .65rem}.series-domain-issues-progress{align-items:center;gap:.625rem;margin-top:.9rem;display:flex}.series-domain-issues-progress-track{background:var(--pb-text-secondary);border-radius:2px;flex:1;height:8px}@supports (color:color-mix(in lab, red, red)){.series-domain-issues-progress-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.series-domain-issues-progress-track{border:1px solid var(--pb-border-subtle);overflow:hidden}.series-domain-issues-progress-fill{background:linear-gradient(90deg, var(--pb-status-success), var(--pb-status-success));height:100%}@supports (color:color-mix(in lab, red, red)){.series-domain-issues-progress-fill{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-success) 45%, transparent), var(--pb-status-success))}}.series-domain-issues-progress-label{text-align:right;min-width:36px;color:var(--pb-status-success);font-family:JetBrains Mono,monospace;font-size:.78rem;font-weight:700}.series-domain-issues-table-wrap{margin-top:.875rem;overflow-x:auto}.series-domain-issues-table{border-collapse:collapse;width:100%}.series-domain-issues-table th,.series-domain-issues-table td{border-bottom:1px solid var(--pb-border-subtle);padding:.75rem .875rem}.series-domain-issues-table thead th{text-align:left;text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);background:var(--pb-surface-shell);border-bottom:2px solid var(--pb-border-default);font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.series-domain-issues-table tbody tr:hover td{background:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.series-domain-issues-table tbody tr:hover td{background:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.series-domain-table-head.r{text-align:right}.series-domain-issue-num{white-space:nowrap;color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.84rem}.series-domain-issue-title-cell{min-width:240px;color:var(--pb-text-secondary);font-size:.84rem}.series-domain-issue-title-link{color:var(--pb-text-primary);transition:color .14s;display:block}.series-domain-issue-title-link:hover{color:var(--pb-interactive)}.series-domain-issue-date{white-space:nowrap;color:var(--pb-text-tertiary);font-size:.8rem}.series-domain-issue-reading{min-width:9.5rem;color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.7rem}.series-domain-issue-reading-mobile{color:var(--pb-interactive);margin-top:.3rem;font-family:JetBrains Mono,monospace;font-size:.65rem}.series-domain-reading-progress{gap:.28rem;max-width:8.5rem;display:grid}.series-domain-reading-progress-track{background:var(--pb-border-default);border-radius:999px;height:3px;display:block;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.series-domain-reading-progress-track{background:color-mix(in srgb, var(--pb-border-default) 68%, transparent)}}.series-domain-reading-progress-fill{border-radius:inherit;background:var(--pb-interactive);height:100%;display:block}.series-domain-reading-queue{color:var(--pb-text-tertiary);margin-top:.3rem;font-family:DM Sans,sans-serif;font-size:.62rem;display:block}.series-domain-issue-status{white-space:nowrap}.series-domain-issue-actions-cell{text-align:right}.series-domain-issue-actions{align-items:center;gap:.25rem;display:inline-flex}.series-domain-issue-action-btn{width:28px;height:28px;color:var(--pb-text-tertiary);background:0 0;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.series-domain-issue-action-btn:hover{color:var(--pb-interactive);background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-domain-issue-action-btn:hover{background:color-mix(in srgb, var(--pb-interactive) 6%, transparent)}}.series-domain-reading-menu{z-index:60;border:1px solid var(--pb-border-default);background:var(--pb-surface-raised);min-width:10.5rem;box-shadow:var(--pb-shadow-2);border-radius:10px;display:grid;position:absolute;top:calc(100% + .3rem);right:0;overflow:hidden}.series-domain-reading-menu button{text-align:left;min-height:2.5rem;color:var(--pb-text-secondary);padding:.55rem .75rem;font-size:.72rem;font-weight:600}.series-domain-reading-menu button:hover,.series-domain-reading-menu button:focus-visible{color:var(--pb-text-primary);background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-domain-reading-menu button:hover,.series-domain-reading-menu button:focus-visible{background:color-mix(in srgb, var(--pb-interactive) 9%, transparent)}}.series-domain-table-footer{margin-top:.75rem}.series-domain-copy-block{color:var(--pb-text-secondary);margin-top:.75rem}.issue-domain-stat-strip{grid-template-columns:repeat(4,minmax(0,1fr));gap:.625rem;margin-top:1rem;display:grid}.issue-domain-stat-box{background:var(--pb-surface-shell);border:1px solid var(--pb-border-subtle);border-radius:12px;padding:.8rem .9rem}.issue-domain-stat-label{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.issue-domain-stat-value{color:var(--pb-text-primary);margin-top:.3rem;font-family:JetBrains Mono,monospace;font-size:.85rem}.issue-domain-stat-value a{color:var(--pb-interactive)}.issue-domain-creators-grid{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem;display:grid}.issue-domain-creator-card{background:var(--pb-surface-app);border:1px solid var(--pb-border-subtle);border-radius:10px;padding:.8rem .9rem}.issue-domain-creator-name{color:var(--pb-text-primary);font-size:.85rem;font-weight:600}.issue-domain-creator-role{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);margin-top:.2rem;font-size:.68rem}.issue-domain-file-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:.75rem;margin-top:.75rem;display:grid}.issue-domain-file-name{font-size:.78rem}.issue-domain-file-path-box{background:var(--pb-surface-app);border:1px solid var(--pb-border-subtle);border-radius:12px;justify-content:space-between;align-items:flex-start;gap:.75rem;margin-top:.75rem;padding:.875rem 1rem;display:flex}.issue-domain-file-path{word-break:break-all;color:var(--pb-text-secondary);margin-top:.3rem;font-family:JetBrains Mono,monospace;font-size:.75rem;line-height:1.7}.issue-domain-copy-btn{border:1px solid var(--pb-border-default);background:var(--pb-surface-card);width:30px;height:30px;color:var(--pb-text-tertiary);border-radius:8px;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.issue-domain-copy-btn:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.series-domain-telemetry-strip{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);color:var(--pb-text-tertiary);box-shadow:var(--pb-shadow-1);border-radius:12px;flex-wrap:wrap;align-items:center;gap:.35rem 1rem;padding:.65rem 1rem;font-family:JetBrains Mono,monospace;font-size:.7rem;display:flex}.series-domain-telemetry-strip strong{color:var(--pb-text-primary);font-weight:700}.series-page-shell{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.series-registry-header{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:1.25rem;display:flex}.series-registry-header-left{flex-wrap:wrap;align-items:flex-start;gap:1.5rem;display:flex}.series-registry-title{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1}.series-registry-title span{color:var(--pb-brand)}.series-registry-subtitle{color:var(--pb-text-tertiary);letter-spacing:.01em;margin-top:.25rem;font-size:.78rem}.series-registry-gauges{align-items:flex-end;gap:1rem;display:flex}.series-registry-gauge{text-align:center}.series-registry-gauge-ring{width:56px;height:56px;margin:0 auto;position:relative}.series-registry-gauge-ring svg{transform:rotate(-90deg)}.series-registry-gauge-bg{fill:none;stroke:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.series-registry-gauge-bg{stroke:color-mix(in srgb, var(--pb-text-secondary) 10%, transparent)}}.series-registry-gauge-bg{stroke-width:4.5px}.series-registry-gauge-fill{fill:none;stroke-width:4.5px;stroke-linecap:round;transition:stroke-dashoffset .6s}.series-registry-gauge-fill-success{stroke:var(--pb-status-success)}.series-registry-gauge-fill-info{stroke:var(--pb-status-info)}.series-registry-gauge-fill-warning{stroke:var(--pb-status-warning)}.series-registry-gauge-fill-danger{stroke:var(--pb-status-danger)}.series-registry-gauge-fill-default{stroke:var(--pb-text-secondary)}.series-registry-gauge-value{justify-content:center;align-items:center;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700;display:flex;position:absolute;inset:0}.series-registry-gauge-label{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);margin-top:.25rem;font-size:.6rem;font-weight:600}.series-registry-actions{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.series-toolbar-shell{z-index:20;position:sticky;top:0}.series-toolbar-frame{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);overflow:visible}.series-toolbar-body{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.series-toolbar-browse{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.series-toolbar-primary{flex-wrap:wrap;flex:1;align-items:flex-end;gap:.5rem;min-width:0;display:flex}.series-toolbar-actions{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.series-toolbar-field{flex-shrink:0;display:block}.series-toolbar-label{text-transform:uppercase;letter-spacing:.08em;color:var(--pb-text-tertiary);margin-bottom:.25rem;font-size:.68rem;font-weight:700;display:block}.series-registry-search{width:220px;max-width:100%}.add-series-search-field{width:275px}.series-registry-search.search-field{border-color:var(--pb-border-default);background:var(--pb-surface-input);min-height:38px;box-shadow:none;border-radius:8px}.series-registry-search .search-field-input{font-size:.82rem}.series-registry-dropdown.dropdown-select{--pb-control-min-height:38px;--pb-control-font-size:.78rem;--pb-control-radius:8px;width:fit-content;max-width:100%}.series-registry-dropdown .dropdown-select-trigger{border-color:var(--pb-border-default);background:var(--pb-surface-input);box-shadow:none;padding-inline:.75rem}.series-registry-dropdown .dropdown-select-trigger-label{color:var(--pb-text-primary);font-weight:500}.series-registry-dropdown .dropdown-select-panel{border-radius:10px}.series-toolbar-view-block{flex-direction:column;display:inline-flex}.series-view-toggle{border:1px solid var(--pb-border-default);background:0 0;border-radius:8px;display:inline-flex;overflow:visible}.series-view-toggle-item:first-child{border-top-left-radius:7px;border-bottom-left-radius:7px}.series-view-toggle-item:last-child{border-top-right-radius:7px;border-bottom-right-radius:7px}.series-view-toggle-item{width:38px;height:34px;color:var(--pb-text-tertiary);background:0 0;border:none;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.series-view-toggle-item+.series-view-toggle-item{border-left:1px solid var(--pb-border-default)}.series-view-toggle-item:hover{background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.series-view-toggle-item:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.series-view-toggle-item:hover{color:var(--pb-text-primary)}html[data-series-view=list] .series-view-toggle-item[data-series-view-option=list],html[data-series-view=grid] .series-view-toggle-item[data-series-view-option=grid],html[data-story-arc-view=list] .series-view-toggle-item[data-story-arc-view-option=list],html[data-story-arc-view=grid] .series-view-toggle-item[data-story-arc-view-option=grid],html[data-whats-new-view=list] .whats-new-view-toggle-item[data-whats-new-view-option=list],html[data-whats-new-view=compact] .whats-new-view-toggle-item[data-whats-new-view-option=compact]{background:var(--pb-interactive);color:var(--pb-text-inverse)}.whats-new-pane-toggle{background:var(--pb-surface-input)}.whats-new-pane-toggle-item{min-height:34px;color:var(--pb-text-tertiary);letter-spacing:.06em;text-transform:uppercase;background:0 0;border:none;justify-content:center;align-items:center;padding:0 .875rem;font-size:.78rem;font-weight:700;transition:all .14s;display:inline-flex}.whats-new-pane-toggle-item:first-child{border-top-left-radius:7px;border-bottom-left-radius:7px}.whats-new-pane-toggle-item:last-child{border-top-right-radius:7px;border-bottom-right-radius:7px}.whats-new-pane-toggle-item+.whats-new-pane-toggle-item{border-left:1px solid var(--pb-border-default)}.whats-new-pane-toggle-item:hover{background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.whats-new-pane-toggle-item:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.whats-new-pane-toggle-item:hover{color:var(--pb-text-primary)}.whats-new-pane-toggle-item.is-active,.whats-new-pane-toggle-item[aria-current=page]{background:var(--pb-interactive);color:var(--pb-text-inverse)}.whats-new-title-block{min-height:4.55rem}.whats-new-cache-badge{min-height:34px;color:var(--pb-text-inverse);align-items:center;display:inline-flex}.whats-new-cache-badge-success{background:#1f6f4a}.whats-new-cache-badge-error{background:#9f2f24}.whats-new-week-nav{border:1px solid var(--pb-border-default);background:var(--pb-surface-input);border-radius:10px;justify-content:center;justify-self:center;align-items:center;gap:.375rem;width:fit-content;max-width:min(100%,34rem);margin-inline:auto;padding:.375rem;display:inline-flex}.whats-new-week-nav-button{border:1px solid var(--pb-border-default);background:var(--pb-surface-card);min-width:32px;min-height:32px;color:var(--pb-text-secondary);border-radius:8px;justify-content:center;align-items:center;font-size:.9rem;font-weight:800;transition:all .14s;display:inline-flex}.whats-new-week-nav-button:hover{border-color:var(--pb-border-hover);background:var(--pb-interactive-selected);color:var(--pb-text-primary)}.whats-new-week-nav-button.is-disabled{opacity:.45;cursor:not-allowed}.whats-new-week-dropdown.dropdown-select{--pb-control-min-height:32px;--pb-control-font-size:.78rem;--pb-control-radius:8px;flex:0 auto;width:fit-content;max-width:min(52vw,14rem)}.whats-new-week-dropdown .dropdown-select-trigger{border-color:var(--pb-border-default);background:var(--pb-surface-card);min-width:9.5rem;max-width:14rem;box-shadow:none;padding-inline:.625rem}.whats-new-week-dropdown .dropdown-select-panel{border-radius:10px;min-width:11.5rem;max-width:min(90vw,16rem)}.whats-new-week-dropdown .dropdown-select-trigger-label,.whats-new-week-dropdown .dropdown-select-option-label{font-weight:700}.whats-new-week-position{color:var(--pb-text-tertiary);letter-spacing:.08em;text-transform:uppercase;flex:none;padding-inline:.25rem .375rem;font-size:.68rem;font-weight:700}@media (max-width:640px){.whats-new-week-nav{max-width:100%}.whats-new-week-dropdown.dropdown-select{max-width:min(58vw,13rem)}.whats-new-week-position{text-align:center;width:100%}}.whats-new-results-stack,.whats-new-panel{gap:1rem;display:grid}.whats-new-stale-banner{border:1px solid var(--pb-status-warning);justify-content:space-between;align-items:center;gap:1rem;display:flex}@supports (color:color-mix(in lab, red, red)){.whats-new-stale-banner{border:1px solid color-mix(in srgb, var(--pb-status-warning) 40%, transparent)}}.whats-new-stale-banner{background:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.whats-new-stale-banner{background:color-mix(in srgb, var(--pb-status-warning) 10%, transparent)}}.whats-new-stale-banner{color:var(--pb-text-primary);border-radius:12px;margin-bottom:1rem;padding:.75rem 1rem;font-size:.82rem}.whats-new-stale-copy{min-width:0}.whats-new-stale-actions{flex-direction:column;flex:none;align-items:flex-end;gap:.375rem;display:flex}.whats-new-stale-message{max-width:24rem;color:var(--pb-text-secondary);text-align:right;font-size:.72rem;line-height:1.35}@media (max-width:640px){.whats-new-stale-banner{flex-direction:column;align-items:stretch}.whats-new-stale-actions{align-items:flex-start}.whats-new-stale-message{text-align:left}}.whats-new-release-summary{margin-bottom:0}.whats-new-release-table-slot{min-width:0}html[data-whats-new-view=compact] .whats-new-release-cover,html[data-whats-new-view=compact] .whats-new-pulls-col{display:none}html[data-whats-new-view=compact] .whats-new-release-title-cell{gap:0}.select-btn{border:1px solid var(--pb-border-default);min-height:38px;color:var(--pb-text-secondary);background:0 0;border-radius:8px;justify-content:center;align-items:center;gap:.375rem;padding:.375rem .875rem;font-family:DM Sans,sans-serif;font-size:.78rem;font-weight:600;transition:all .14s;display:inline-flex}.select-btn:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.select-btn:disabled{cursor:not-allowed;opacity:.45}.select-btn-success{color:var(--pb-status-success);border-color:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.select-btn-success{border-color:color-mix(in srgb, var(--pb-status-success) 30%, transparent)}}.select-btn-success{background:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.select-btn-success{background:color-mix(in srgb, var(--pb-status-success) 8%, transparent)}}.select-btn-warning{color:var(--pb-status-warning);border-color:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.select-btn-warning{border-color:color-mix(in srgb, var(--pb-status-warning) 30%, transparent)}}.select-btn-warning{background:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.select-btn-warning{background:color-mix(in srgb, var(--pb-status-warning) 8%, transparent)}}.select-btn-danger{color:var(--pb-status-danger);border-color:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){.select-btn-danger{border-color:color-mix(in srgb, var(--pb-status-danger) 30%, transparent)}}.select-btn-danger{background:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){.select-btn-danger{background:color-mix(in srgb, var(--pb-status-danger) 8%, transparent)}}.series-selection-shell{flex-direction:column;gap:.625rem;display:flex}.series-selection-inline{align-items:center;display:inline-flex}.series-selection-count{color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.78rem;font-weight:600}.series-selection-controls-row{flex-wrap:wrap;justify-content:space-between;gap:.75rem;display:flex}.series-selection-bulk,.series-selection-actions{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.series-results-shell{gap:calc(var(--spacing) * 4);min-height:16rem;padding-bottom:var(--pb-page-footer-clearance);flex-direction:column;flex:none;display:flex}.series-results-body-shell{min-height:14rem}:where(:is(.series-mission-control-shell,.series-collector-wall-shell)>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.series-mission-control-table-wrap{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;overflow:auto hidden}.series-mission-control-table{border-collapse:collapse;width:100%}.series-mission-control-table th{text-align:left;text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);background:var(--pb-surface-shell);border-bottom:2px solid var(--pb-border-default);padding:.5625rem .875rem;font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.series-mission-control-table td{border-bottom:1px solid var(--pb-border-subtle);padding:.6875rem .875rem;font-size:.82rem;transition:background .1s}.series-mission-control-table tbody tr:last-child td{border-bottom:none}.series-mission-control-table tbody tr:not(.table-detail-row):hover td{background:var(--pb-surface-selected)}.series-mission-control-table th.c,.series-mission-control-table td.c{text-align:center}.series-mission-control-table th.r,.series-mission-control-table td.r{text-align:right}.series-mission-control-row-selected td{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-mission-control-row-selected td{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.series-led{border-radius:50%;width:8px;height:8px;display:inline-block}.series-led-green{background:var(--pb-status-success);box-shadow:0 0 5px var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.series-led-green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-status-success) 35%, transparent)}}.series-led-amber{background:var(--pb-status-warning);box-shadow:0 0 5px var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.series-led-amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-status-warning) 35%, transparent)}}.series-led-off{background:var(--pb-text-tertiary);opacity:.35}.series-mission-control-name{color:var(--pb-text-primary);font-weight:700;transition:color .14s}.series-mission-control-name:hover{color:var(--pb-interactive)}.series-mission-control-publisher{color:var(--pb-text-tertiary);margin-top:1px;font-size:.72rem}.series-mission-control-year{color:var(--pb-text-tertiary);font-size:.78rem}.series-mission-control-bar-cell{width:220px}.series-mission-control-bar{align-items:center;gap:.5rem;display:flex}.series-mission-control-bar-track{background:var(--pb-text-secondary);border-radius:2px;flex:1;height:8px}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.series-mission-control-bar-track{border:1px solid var(--pb-border-subtle);position:relative;overflow:hidden}.series-mission-control-bar-fill{height:100%;position:relative}.series-mission-control-bar-fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}.series-mission-control-bar-fill-green{background:linear-gradient(90deg, var(--pb-status-success), var(--pb-status-success))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-green{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-success) 45%, transparent), var(--pb-status-success))}}.series-mission-control-bar-fill-amber{background:linear-gradient(90deg, var(--pb-status-warning), var(--pb-status-warning))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-amber{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-warning) 45%, transparent), var(--pb-status-warning))}}.series-mission-control-bar-fill-red{background:linear-gradient(90deg, var(--pb-status-danger), var(--pb-status-danger))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-red{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-danger) 45%, transparent), var(--pb-status-danger))}}.series-mission-control-bar-fill-blue{background:linear-gradient(90deg, var(--pb-status-info), var(--pb-status-info))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-blue{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-info) 45%, transparent), var(--pb-status-info))}}.series-mission-control-bar-pct{text-align:right;font-variant-numeric:tabular-nums;min-width:36px;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700}.series-mission-control-bar-pct-green{color:var(--pb-status-success)}.series-mission-control-bar-pct-amber{color:var(--pb-status-warning)}.series-mission-control-bar-pct-red{color:var(--pb-status-danger)}.series-mission-control-bar-pct-blue{color:var(--pb-status-info)}.series-mission-control-actions{justify-content:flex-end;gap:.25rem;display:flex}.series-mission-control-action-btn{border:1px solid var(--pb-border-subtle);width:30px;height:30px;color:var(--pb-text-tertiary);background:0 0;border-radius:8px;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.series-mission-control-action-btn:hover{border-color:var(--pb-interactive);color:var(--pb-interactive);background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-mission-control-action-btn:hover{background:color-mix(in srgb, var(--pb-interactive) 4%, transparent)}}.series-mission-control-action-btn.disabled{opacity:.25;pointer-events:none}.series-mission-control-action-btn svg{width:14px;height:14px}.series-mission-control-footer{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);color:var(--pb-text-tertiary);border-radius:10px;flex-wrap:wrap;align-items:center;gap:.375rem 1.5rem;padding:.625rem 1rem;font-family:JetBrains Mono,monospace;font-size:.7rem;display:flex}.series-mission-control-footer strong{color:var(--pb-text-primary);font-weight:600}.series-collector-wall-grid{grid-template-columns:repeat(auto-fill,minmax(172px,1fr));gap:24px 18px;display:grid}.series-wall-card{min-width:0;transition:transform .22s;position:relative}.series-wall-card:hover{transform:translateY(-5px)}.series-wall-card-selected .series-wall-cover-wrap{outline:2px solid var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-wall-card-selected .series-wall-cover-wrap{outline:2px solid color-mix(in srgb, var(--pb-interactive) 28%, transparent)}}.series-wall-card-selected .series-wall-cover-wrap{outline-offset:2px}.series-wall-cover-wrap{aspect-ratio:2/3;border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;transition:box-shadow .22s;position:relative;overflow:hidden}.series-wall-card:hover .series-wall-cover-wrap{box-shadow:var(--pb-shadow-2), 0 0 20px var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-wall-card:hover .series-wall-cover-wrap{box-shadow:var(--pb-shadow-2), 0 0 20px color-mix(in srgb, var(--pb-interactive) 6%, transparent)}}.series-wall-cover-placeholder{width:100%;height:100%;color:var(--pb-text-tertiary);background:linear-gradient(155deg, var(--pb-surface-card) 0%, var(--pb-surface-app) 100%);justify-content:center;align-items:center;display:flex}.series-monitor-badge{width:1.5rem;height:1.5rem;color:var(--pb-text-inverse);background:var(--pb-brand-signal);border:2px solid var(--pb-surface-card);border-radius:999px;flex:none;justify-content:center;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.series-monitor-badge{border:2px solid color-mix(in srgb, var(--pb-surface-card) 92%, white)}}.series-monitor-badge{box-shadow:0 0 0 2px #0000006b,0 10px 22px #00000057}.series-monitor-badge svg{flex:none;width:.9rem;height:.9rem}.series-monitor-badge.is-paused{color:var(--pb-text-secondary);background:var(--pb-surface-raised);border-color:var(--pb-border-default);box-shadow:0 0 0 1px var(--pb-border-subtle)}.series-wall-monitor-dot{z-index:3;position:absolute;top:9px;left:9px}.story-arc-wall-review{z-index:4;border:1px solid var(--pb-border-default);background:var(--pb-surface-card);border-radius:999px;justify-content:center;align-items:center;gap:.2rem;min-width:1.75rem;height:1.75rem;padding-inline:.38rem;display:inline-flex;position:absolute;top:9px;right:9px}@supports (color:color-mix(in lab, red, red)){.story-arc-wall-review{background:color-mix(in srgb, var(--pb-surface-card) 92%, transparent)}}.story-arc-wall-review{box-shadow:var(--pb-shadow-1);font-family:JetBrains Mono,monospace;font-size:.65rem;font-weight:700}.story-arc-wall-review svg{width:.8rem;height:.8rem}.story-arc-wall-review-warning{color:var(--pb-warning);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.story-arc-wall-review-warning{border-color:color-mix(in srgb, var(--pb-warning) 48%, transparent)}}.story-arc-wall-review-error{color:var(--pb-error);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.story-arc-wall-review-error{border-color:color-mix(in srgb, var(--pb-error) 48%, transparent)}}.story-arc-review-indicator{border:1px solid var(--pb-border-default);border-radius:999px;justify-content:center;align-items:center;gap:.28rem;min-width:2.5rem;min-height:1.75rem;padding-inline:.45rem;font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:700;display:inline-flex}.story-arc-review-indicator svg{width:.82rem;height:.82rem}.story-arc-review-indicator-success{color:var(--pb-success);background:var(--pb-success-dim);border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.story-arc-review-indicator-success{border-color:color-mix(in srgb, var(--pb-success) 32%, transparent)}}.story-arc-review-indicator-warning{color:var(--pb-warning);background:var(--pb-warning-dim);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.story-arc-review-indicator-warning{border-color:color-mix(in srgb, var(--pb-warning) 32%, transparent)}}.story-arc-review-indicator-error{color:var(--pb-error);background:var(--pb-error-dim);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.story-arc-review-indicator-error{border-color:color-mix(in srgb, var(--pb-error) 32%, transparent)}}.series-wall-catalog-state-badge{z-index:4;border:1px solid var(--pb-warning);background:var(--pb-warning);border-radius:999px;justify-content:center;align-items:center;max-width:calc(100% - 18px);display:inline-flex;position:absolute;top:46px;right:9px}@supports (color:color-mix(in lab, red, red)){.series-wall-catalog-state-badge{background:color-mix(in srgb, var(--pb-warning) 92%, black)}}.series-wall-catalog-state-badge{color:var(--pb-text-inverse);letter-spacing:.04em;text-align:center;text-transform:uppercase;padding:.2rem .5rem;font-size:.58rem;font-weight:800;line-height:1.15;box-shadow:0 8px 18px #00000057}.series-wall-catalog-state-badge.is-failed{border-color:var(--pb-error);background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.series-wall-catalog-state-badge.is-failed{background:color-mix(in srgb, var(--pb-error) 92%, black)}}.pull-list-monitor-toggle{width:28px;height:28px;box-shadow:none;cursor:pointer;border-width:1px;border-radius:6px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s}.pull-list-monitor-toggle svg{width:12px;height:12px}.pull-list-monitor-toggle:hover{border-color:var(--pb-brand-signal)}@supports (color:color-mix(in lab, red, red)){.pull-list-monitor-toggle:hover{border-color:color-mix(in srgb, var(--pb-brand-signal) 56%, transparent)}}.pull-list-monitor-toggle:hover{background:var(--pb-brand-signal);color:var(--pb-text-inverse);box-shadow:0 0 0 2px var(--pb-brand-signal)}@supports (color:color-mix(in lab, red, red)){.pull-list-monitor-toggle:hover{box-shadow:0 0 0 2px color-mix(in srgb, var(--pb-brand-signal) 14%, transparent)}}.pull-list-monitor-toggle:focus-visible{outline:2px solid var(--pb-focus-outline);outline-offset:3px}.series-wall-selection-control{z-index:4;border:1px solid var(--pb-border-default);background:var(--pb-surface-card);border-radius:10px;position:absolute;top:10px;right:10px}@supports (color:color-mix(in lab, red, red)){.series-wall-selection-control{background:color-mix(in srgb, var(--pb-surface-card) 92%, transparent)}}.series-wall-selection-control{box-shadow:var(--pb-shadow-1);padding:.25rem}.series-wall-ring{z-index:2;width:44px;height:44px;position:absolute;bottom:-6px;right:-6px}.series-wall-ring svg{transform:rotate(-90deg)}.series-wall-ring-bg{fill:none;stroke:var(--pb-surface-card);stroke-width:3.5px}.series-wall-ring-fill{fill:none;stroke-width:3.5px;stroke-linecap:round;transition:stroke-dashoffset .6s}.series-wall-ring-fill-green{stroke:var(--pb-status-success)}.series-wall-ring-fill-amber{stroke:var(--pb-status-warning)}.series-wall-ring-fill-red{stroke:var(--pb-status-danger)}.series-wall-ring-center{background:var(--pb-surface-card);color:var(--pb-text-primary);border-radius:999px;justify-content:center;align-items:center;margin:5px;font-family:JetBrains Mono,monospace;font-size:.58rem;font-weight:700;display:flex;position:absolute;inset:0}.series-wall-overlay{--series-wall-overlay-edge:#fff;z-index:3;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);box-shadow:inset 0 0 0 4.5px var(--series-wall-overlay-edge);opacity:0;pointer-events:none;background:#1e1a17e0;border-radius:18px;flex-direction:column;justify-content:flex-end;padding:.875rem;transition:opacity .2s;display:flex;position:absolute;inset:-4px}[data-theme=light] .series-wall-overlay{--series-wall-overlay-edge:#1e1a17}@media (prefers-color-scheme:light){:root:not([data-theme]) .series-wall-overlay{--series-wall-overlay-edge:#1e1a17}}.series-wall-card:hover .series-wall-overlay{opacity:1}.series-wall-overlay-title{color:var(--pb-text-primary);font-size:.82rem;font-weight:700;line-height:1.3}.series-wall-overlay-meta{color:var(--pb-text-tertiary);gap:.25rem;margin-top:.45rem;font-size:.68rem;display:grid}.series-wall-overlay-meta-line,.series-wall-overlay-stat{grid-template-columns:minmax(4.25rem,max-content) minmax(0,1fr);align-items:baseline;column-gap:.5rem;line-height:1.25;display:grid}.series-wall-overlay-meta-label{color:var(--pb-text-tertiary);letter-spacing:.08em;text-transform:uppercase;font-family:JetBrains Mono,monospace;font-size:.56rem;font-weight:700}.series-wall-overlay-meta-value{overflow-wrap:anywhere;min-width:0;color:var(--pb-text-secondary)}.series-wall-overlay-stats{color:var(--pb-text-secondary);gap:.25rem;margin-top:.35rem;font-size:.68rem;display:grid}.series-wall-overlay-stats strong{color:var(--pb-text-primary);font-weight:600}.series-wall-overlay-actions{pointer-events:auto;gap:6px;margin-top:10px;display:flex}.series-wall-overlay-btn{color:var(--pb-text-secondary);text-align:center;background:#ffffff0f;border:1px solid #cbd5e140;border-radius:8px;flex:1;justify-content:center;padding:7px;font-family:DM Sans,sans-serif;font-size:.72rem;font-weight:600;transition:all .14s;display:inline-flex}.series-wall-overlay-btn:hover{background:var(--pb-interactive);color:var(--pb-text-primary);border-color:var(--pb-interactive)}.series-wall-card-title{text-overflow:ellipsis;white-space:nowrap;color:var(--pb-text-primary);margin-top:10px;font-size:.82rem;font-weight:600;line-height:1.3;display:block;overflow:hidden}.series-empty-state{justify-content:center;align-items:center;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 12);text-align:center;box-shadow:var(--pb-shadow-1);border-radius:1.25rem;flex-direction:column;display:flex}.series-empty-state-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.series-empty-state-copy{max-width:var(--container-xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}@media (max-width:1180px){.series-domain-hero-inner{grid-template-columns:130px minmax(0,1fr)}.issue-domain-hero-inner{grid-template-columns:120px minmax(0,1fr)}.series-domain-actions-panel{border-top:1px solid var(--pb-border-subtle);border-left:0;grid-column:1/-1;padding-top:1rem;padding-left:0}.series-domain-actions-panel .series-domain-actions-buttons{grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));align-items:stretch;display:grid}}@media (max-width:900px){.issue-domain-stat-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.issue-domain-file-grid{grid-template-columns:1fr}}@media (max-width:640px){.detail-hero-shell,.series-domain-actions-card,.series-domain-section-card{padding:1rem}.series-domain-cover-column,.issue-domain-cover-column,.issue-domain-cover-frame{width:104px}.series-domain-hero-inner,.issue-domain-hero-inner{grid-template-columns:104px minmax(0,1fr)}.series-domain-hero-title{font-size:1.2rem}.series-domain-actions-inner{align-items:flex-start}.series-domain-actions-divider{display:none}.series-domain-actions-panel .series-domain-actions-buttons,.issue-domain-stat-strip{grid-template-columns:1fr}.series-domain-info-box-wide{grid-column:auto}.series-domain-issues-toolbar{flex-direction:column;align-items:stretch}}[data-series-page][data-series-toolbar-mode=browse] [data-testid=series-select-toolbar],[data-series-page][data-series-toolbar-mode=select] [data-testid=series-browse-toolbar]{display:none}[data-series-page][data-series-toolbar-mode=select] [data-testid=series-select-toolbar]{display:flex}[data-select-toolbar-page][data-toolbar-mode=browse] [data-select-toolbar],[data-select-toolbar-page][data-toolbar-mode=select] [data-browse-toolbar]{display:none}[data-select-toolbar-page][data-toolbar-mode=select] [data-select-toolbar]{display:flex}[data-series-page][data-series-toolbar-mode=browse] [data-series-selection-cell]{display:none}[data-series-page][data-series-toolbar-mode=select] [data-series-selection-cell]{display:table-cell}[data-series-page][data-series-toolbar-mode=browse] [data-series-selection-control]{display:none}[data-series-page][data-series-toolbar-mode=select] [data-series-selection-control]{display:block}[data-series-page][data-series-toolbar-mode=select] [data-series-selection-control=grid]{display:inline-flex}#page-footer-dock:empty{display:none}body:has(#page-footer-dock:not(:empty)) #content{padding-bottom:var(--pb-page-footer-clearance)}body:has(#page-footer-dock:not(:empty)) #content:has(.admin-workspace-page,.dashboard-mission-page,.downloads-view,.series-domain-page,.series-results-shell,.utilities-page){padding-bottom:0}#content:has(.admin-workspace-page){overflow-anchor:none}.page-dock-inner{--page-dock-height-status:2rem;--page-dock-height-pagination:2.5rem;max-width:85rem;height:var(--page-dock-height-status);flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:.75rem;margin:0 auto;padding:0 1rem;display:flex;overflow:hidden}.page-dock-inner:has(.page-dock-pagination){height:var(--page-dock-height-pagination)}.page-footer-clearance{height:var(--pb-page-footer-clearance);flex:0 0 var(--pb-page-footer-clearance);pointer-events:none}.page-dock-inner-status-only{justify-content:flex-end}.page-dock-pagination{flex:none}.page-dock-pagination nav{gap:.125rem}.page-dock-pagination nav>a,.page-dock-pagination nav>button,.page-dock-pagination nav>span{text-align:center;border-radius:.375rem;min-width:1.75rem;padding:.25rem .625rem;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:600;line-height:1}.page-dock-pagination nav>a,.page-dock-pagination nav>button{appearance:none;color:var(--pb-text-secondary);cursor:pointer;background:0 0;border:0;margin:0}.page-dock-pagination nav>span.bg-pb-interactive{border-radius:.375rem}.page-dock-status{min-width:0;color:var(--pb-text-tertiary);scrollbar-width:none;flex-wrap:nowrap;align-items:center;gap:1rem;font-family:JetBrains Mono,monospace;font-size:.65rem;display:flex;overflow-x:auto}.page-dock-status::-webkit-scrollbar{display:none}.page-dock-status-item{white-space:nowrap;align-items:center;gap:.25rem;display:inline-flex}.page-dock-status-value{color:var(--pb-text-primary);font-weight:600}.page-dock-status-label{text-transform:uppercase;letter-spacing:.04em}.page-dock-led{border-radius:999px;width:6px;height:6px;margin-right:2px;display:inline-block}.page-dock-led-green{background:var(--pb-status-success);box-shadow:0 0 4px var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.page-dock-led-green{box-shadow:0 0 4px color-mix(in srgb, var(--pb-status-success) 35%, transparent)}}.page-dock-led-amber{background:var(--pb-status-warning);box-shadow:0 0 4px var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.page-dock-led-amber{box-shadow:0 0 4px color-mix(in srgb, var(--pb-status-warning) 35%, transparent)}}.page-dock-led-off{background:var(--pb-text-tertiary);opacity:.35}@media (max-width:767px){.page-dock-inner{padding-inline:0}.page-dock-status{gap:.75rem}}.workflow-tabs-shell{align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:1.25rem;flex-wrap:wrap;display:flex}@media (min-width:40rem){.workflow-tabs-shell{padding-inline:calc(var(--spacing) * 5)}}.workflow-tabs-shell{box-shadow:var(--pb-shadow-1)}.workflow-tab-chip-active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary);border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-tab-chip-active{border-color:color-mix(in srgb, var(--pb-interactive) 34%, transparent)}}.workflow-tab-chip-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-tab-chip-active{background-color:color-mix(in srgb, var(--pb-interactive) 12%, var(--pb-bg-card))}}.workflow-tab-chip-active{box-shadow:var(--pb-shadow-1)}.workflow-tab-chip-inactive{color:var(--pb-text-dim)}.workflow-focus-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;position:relative;overflow:hidden}.workflow-focus-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.workflow-focus-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 34%, transparent), transparent)}}.workflow-focus-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.workflow-focus-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:flex-start}}.workflow-focus-chip{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.workflow-focus-actions{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.workflow-focus-actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.workflow-focus-actions{grid-template-columns:repeat(1,minmax(0,1fr))}}:where(.workflow-shell-region>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.workflow-shell-region{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);border-radius:1.5rem;overflow:visible}@media (min-width:40rem){.workflow-shell-region{padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 5)}}.workflow-shell-region{box-shadow:var(--pb-shadow-1)}.utility-workspace-shell{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;overflow:hidden}.utility-workspace-shell-visible{overflow:visible}:where(.utility-workspace-body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.utility-workspace-body{padding:calc(var(--spacing) * 6)}@media (min-width:40rem){.utility-workspace-body{padding:calc(var(--spacing) * 7)}}:where(.utility-tool-body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.utility-tool-body{padding:calc(var(--spacing) * 5)}@media (min-width:40rem){.utility-tool-body{padding:calc(var(--spacing) * 5)}}.utility-workspace-shell .info-panel{border-color:var(--pb-border-subtle);background-color:var(--pb-bg-base);box-shadow:none}.utility-workspace-shell .info-panel-muted{background-color:var(--pb-bg-base)}.utility-empty-state{border-radius:var(--radius-xl);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--pb-border-subtle);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 8);text-align:center;background-color:var(--pb-bg-base)}.utility-path-pill{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);display:flex}.utility-token{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.utility-step-grid{gap:calc(var(--spacing) * 3);display:grid}.utility-step-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);width:100%;padding:calc(var(--spacing) * 4);display:block}.utility-step-card-inline{align-items:center;gap:.75rem;display:flex}.utility-step-card-locked{opacity:.78}.utility-step-num{color:var(--pb-text-tertiary);min-width:1.25rem;font-family:JetBrains Mono,monospace;font-size:.62rem;font-weight:700}.utility-step-name{color:var(--pb-text-primary);flex:auto;min-width:0;font-size:.82rem;font-weight:600}.utility-step-tag{color:var(--pb-text-tertiary);white-space:nowrap;font-family:JetBrains Mono,monospace;font-size:.62rem}.utility-step-card-inline .utility-step-num,.utility-step-card-inline .utility-step-tag{color:var(--pb-text-secondary)}.utility-step-check{flex-shrink:0;margin-top:0}.utility-scan-mode-grid{gap:calc(var(--spacing) * 2);display:grid}@media (min-width:40rem){.utility-scan-mode-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-scan-mode{justify-content:center;align-items:flex-start;gap:var(--spacing);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);min-height:72px;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-direction:column;display:flex}.utility-scan-mode:hover{border-color:var(--pb-border-hover)}.utility-scan-mode-active{border-color:var(--pb-interactive);background-color:var(--pb-surface-selected)}.utility-scan-mode-label{--tw-font-weight:var(--font-weight-semibold);font-size:.82rem;font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.utility-scan-mode-copy{--tw-leading:calc(var(--spacing) * 5);font-size:.72rem;line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim)}.utility-scope-chip-row{flex-wrap:wrap;gap:.375rem;display:flex}.utility-scope-chip{border:1px solid var(--pb-border-default);color:var(--pb-text-secondary);background:0 0;border-radius:.5rem;justify-content:center;align-items:center;padding:.4375rem 1rem;font-size:.78rem;font-weight:600;transition:border-color .14s,background-color .14s,color .14s;display:inline-flex}.utility-scope-chip:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.utility-scope-chip-active{background:var(--pb-interactive);color:var(--pb-text-inverse);border-color:var(--pb-interactive)}.utility-scope-chip-active:hover{color:var(--pb-text-inverse)}.utility-export-chip-row{flex-wrap:wrap;gap:.375rem;display:flex}.utility-export-chip{border:1px solid var(--pb-border-default);color:var(--pb-text-secondary);background:0 0;border-radius:999px;justify-content:center;align-items:center;padding:.4375rem .875rem;font-family:DM Sans,sans-serif;font-size:.76rem;font-weight:500;line-height:1.2;transition:border-color .14s,background-color .14s,color .14s;display:inline-flex}.utility-export-chip:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.utility-export-chip-active{border-color:var(--pb-interactive);background:var(--pb-interactive);color:var(--pb-text-inverse)}.utility-export-chip-active:hover{color:var(--pb-text-inverse)}.utility-tool-section-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);margin-bottom:.5rem;font-family:Syne,sans-serif;font-size:.62rem;font-weight:700}.utility-tool-section-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;display:flex}.utility-tool-field-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.58rem;font-weight:700;display:block}.utility-export-group-grid{gap:.75rem;display:grid}@media (min-width:1024px){.utility-export-group-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-export-group-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);border-radius:12px;flex-direction:column;gap:.5rem;padding:.875rem;display:flex}.utility-export-group-title{color:var(--pb-text-primary);font-size:.72rem;font-weight:600}.utility-export-group-fields{flex-direction:column;gap:.375rem;display:flex}.utility-export-group-fields .utility-step-card{padding:.5rem .75rem}.utility-export-field-label{color:var(--pb-text-primary);font-size:.8rem;font-weight:500;line-height:1.25}.utility-export-field-meta{justify-content:space-between;align-items:baseline;gap:.875rem;width:100%;min-width:0;display:flex}.utility-export-field-sub{color:var(--pb-text-tertiary);text-align:right;white-space:nowrap;text-overflow:ellipsis;flex:0 auto;min-width:0;font-family:JetBrains Mono,monospace;font-size:.66rem;line-height:1.2;overflow:hidden}.utility-export-options-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);border-radius:10px;padding:1rem}.utility-export-inline-label{color:var(--pb-text-primary);font-size:.72rem;font-weight:600}.utility-export-multi-grid{gap:.5rem;display:grid}@media (min-width:640px){.utility-export-multi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-export-summary-grid{gap:.625rem;display:grid}@media (min-width:640px){.utility-export-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-export-summary-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);border-radius:10px;padding:.875rem 1rem}.utility-export-summary-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.58rem;font-weight:700}.utility-export-summary-value{color:var(--pb-text-primary);margin-top:.35rem;font-family:JetBrains Mono,monospace;font-size:.92rem;font-weight:600}.utility-tool-select.dropdown-select{--pb-control-font-size:.82rem;--pb-control-line-height:1.25rem;--pb-control-radius:8px;width:100%}.utility-tool-select .dropdown-select-trigger{border-color:var(--pb-border-default);background:var(--pb-surface-input);min-height:0;box-shadow:none;padding:.5rem .75rem}.utility-tool-select .dropdown-select-trigger-label{color:var(--pb-text-primary);font-weight:400}.utility-tool-select .dropdown-select-chevron{color:var(--pb-text-secondary)}.utility-tool-output-field{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);color:var(--pb-text-secondary);border-radius:8px;padding:.5rem .75rem;font-family:DM Sans,sans-serif;font-size:.82rem;line-height:1.25rem}.utility-tool-browse-button{gap:.375rem;min-height:0;padding:.375rem .875rem;font-size:.78rem;font-weight:600;line-height:1.5}.utility-tool-browse-button svg{width:.875rem;height:.875rem}.utility-tool-table-wrap{border:1px solid var(--pb-border-subtle);border-radius:10px;overflow:hidden}.utility-tool-table{border-collapse:collapse;width:100%}.utility-tool-table th{text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);background:var(--pb-surface-shell);border-bottom:1px solid var(--pb-border-default);padding:.4375rem .75rem;font-family:Syne,sans-serif;font-size:.52rem;font-weight:700}.utility-tool-table th.r,.utility-tool-table td.r{text-align:right}.utility-tool-table td{color:var(--pb-text-secondary);border-bottom:1px solid var(--pb-border-subtle);padding:.5rem .75rem;font-size:.78rem}.utility-tool-table tbody tr:last-child td{border-bottom:none}.utility-tool-table tbody tr:hover td{background:var(--pb-surface-selected)}.utility-tool-table-output{color:var(--pb-status-success)}.utility-tool-action-footer{border-top:1px solid var(--pb-border-subtle);justify-content:flex-end;align-items:center;gap:.625rem;padding:.75rem 0 0;display:flex}.utility-tool-action-footer .settings-footer-actions{gap:.625rem;margin-left:auto}.utility-tool-action-footer .btn-ghost,.utility-tool-action-footer .btn-primary{min-height:0;padding:.5625rem 1.125rem;font-size:.82rem;line-height:1.25rem}.utility-tool-action-footer-card{margin-top:.25rem}.utility-step-card-active{border-color:var(--pb-interactive);background-color:var(--pb-surface-selected)}.utility-step-card-static{border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card)}.utility-empty-state-compact{padding-top:1rem;padding-bottom:1rem}.utility-template-table-type{color:var(--pb-text-primary);font-size:.78rem;font-weight:600}.utility-template-meta-token{color:var(--pb-text-tertiary);font-family:JetBrains Mono,monospace;font-size:.72rem}.utility-template-meta-token strong{color:var(--pb-text-secondary);font-weight:600}.utilities-queue-table-wrap{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:14px;overflow:hidden}.utilities-queue-table{border-collapse:collapse;width:100%}.utilities-queue-table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;--tw-font-weight:var(--font-weight-bold);font-family:Syne,sans-serif;font-size:.58rem;font-weight:var(--font-weight-bold);--tw-tracking:.1em;letter-spacing:.1em;color:var(--pb-text-dim);text-transform:uppercase}.utilities-queue-table th.c,.utilities-queue-table td.c{text-align:center}.utilities-queue-table th.r,.utilities-queue-table td.r{text-align:right}.utilities-queue-table td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border-subtle);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:.82rem}.utilities-queue-table tbody:last-child tr:last-child td{border-bottom:0}.utilities-queue-row:hover td{background-color:var(--pb-surface-selected)}.utilities-queue-row>td{vertical-align:middle}.utilities-queue-led{height:calc(var(--spacing) * 2);width:calc(var(--spacing) * 2);background-color:var(--pb-text-dim);border-radius:3.40282e38px;display:inline-block}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led{background-color:color-mix(in srgb, var(--pb-text-dim) 45%, transparent)}}.utilities-queue-led-green{background-color:var(--pb-success);box-shadow:0 0 8px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led-green{box-shadow:0 0 8px color-mix(in srgb, var(--pb-success) 30%, transparent)}}.utilities-queue-led-amber{background-color:var(--pb-warning);box-shadow:0 0 8px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led-amber{box-shadow:0 0 8px color-mix(in srgb, var(--pb-warning) 28%, transparent)}}.utilities-queue-led-blue{background-color:var(--pb-info);box-shadow:0 0 8px var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led-blue{box-shadow:0 0 8px color-mix(in srgb, var(--pb-info) 28%, transparent)}}.utilities-queue-job-name{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary)}.utilities-queue-job-type{margin-top:calc(var(--spacing) * .5);color:var(--pb-text-dim);font-size:.72rem}.utilities-queue-job-meta{margin-top:var(--spacing);color:var(--pb-text-secondary);font-size:.68rem}.utilities-queue-items{--tw-font-weight:var(--font-weight-semibold);font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:var(--font-weight-semibold);color:var(--pb-text-secondary)}.utilities-queue-progress-cell .series-mission-control-bar{gap:.625rem}.utilities-queue-progress-cell .series-mission-control-bar-track{min-width:120px}.utilities-queue-time{color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.72rem}.utilities-queue-actions{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 1.5);display:flex}.utilities-queue-act-btn{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:8px;justify-content:center;align-items:center;display:inline-flex}.utilities-queue-act-btn:hover{border-color:var(--pb-border-strong);color:var(--pb-text);background-color:var(--pb-bg-card-hover)}.utilities-queue-act-btn-danger{color:var(--pb-error)}.utilities-queue-act-btn-danger:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-act-btn-danger:hover{border-color:color-mix(in srgb, var(--pb-error) 36%, transparent)}}.utilities-queue-act-btn-danger:hover{background-color:var(--pb-error-dim);color:var(--pb-error)}.utilities-queue-detail-cell{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-detail-cell{background-color:color-mix(in srgb, var(--pb-bg-surface) 72%, transparent)}}.utilities-queue-detail-summary{align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:var(--spacing);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);color:var(--pb-text-dim);flex-wrap:wrap;font-size:.72rem;display:flex}.utilities-queue-detail-summary strong{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.utilities-queue-detail-label{--tw-tracking:.08em;letter-spacing:.08em;text-transform:uppercase}.utilities-queue-empty{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 12);text-align:center;flex-direction:column;display:flex}.utilities-queue-empty-compact{padding-block:calc(var(--spacing) * 8)}.utilities-queue-empty-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.utilities-queue-empty-copy{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.queue-job-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);overflow:hidden}.queue-job-card-expanded{border-color:var(--pb-border-hover);box-shadow:var(--pb-shadow-2)}.queue-progress-track{height:calc(var(--spacing) * 1.5);background-color:var(--pb-bg-card-hover);border-radius:3.40282e38px;width:100%;overflow:hidden}.queue-progress-fill{height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.5s;background-color:var(--pb-success);border-radius:3.40282e38px;transition-duration:.5s}.queue-progress-fill-paused{background-color:var(--pb-warning)}.selection-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1)}.selection-card:not(.selection-card-active):hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.selection-card:not(.selection-card-active):hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 55%, transparent)}}.selection-card-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.selection-card-active{border-color:color-mix(in srgb, var(--pb-interactive) 42%, transparent)}}.selection-card-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.selection-card-active{background-color:color-mix(in srgb, var(--pb-interactive) 14%, var(--pb-bg-card))}}.selection-card-active{box-shadow:var(--pb-shadow-2)}.selection-card-icon{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:var(--radius-xl);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-shrink:0;justify-content:center;align-items:center;display:flex}.selection-card-icon-active{color:var(--pb-interactive);background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.selection-card-icon-active{background-color:color-mix(in srgb, var(--pb-interactive) 14%, transparent)}}.settings-theme-choice{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.settings-theme-choice-icon{height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);border-radius:var(--radius-lg)}.settings-media-preview-panel{background-color:var(--pb-surface-selected);border-color:var(--pb-border-hover)}@supports (color:color-mix(in lab, red, red)){.settings-media-preview-panel{border-color:color-mix(in srgb, var(--pb-border-hover) 85%, transparent)}}.settings-media-preview-panel{overflow-wrap:anywhere;min-height:4.625rem;transition:opacity .16s,border-color .16s}.settings-media-preview-panel.is-loading{opacity:.68;border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.settings-media-preview-panel.is-loading{border-color:color-mix(in srgb, var(--pb-interactive) 30%, var(--pb-border-hover))}}.workflow-step{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);box-shadow:var(--pb-shadow-1)}.workflow-step-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active{border-color:color-mix(in srgb, var(--pb-interactive) 32%, transparent)}}.workflow-step-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.workflow-step-complete{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete{border-color:color-mix(in srgb, var(--pb-success) 28%, transparent)}}.workflow-step-complete{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete{background-color:color-mix(in srgb, var(--pb-success) 10%, var(--pb-bg-card))}}.workflow-step-badge{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);border-style:var(--tw-border-style);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);border-width:1px;border-color:var(--pb-border-hover);color:var(--pb-text-secondary);background-color:var(--pb-bg-card);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.workflow-step-active .workflow-step-badge{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active .workflow-step-badge{border-color:color-mix(in srgb, var(--pb-interactive) 32%, transparent)}}.workflow-step-active .workflow-step-badge{color:var(--pb-interactive);background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active .workflow-step-badge{background-color:color-mix(in srgb, var(--pb-interactive) 12%, transparent)}}.workflow-step-complete .workflow-step-badge{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete .workflow-step-badge{border-color:color-mix(in srgb, var(--pb-success) 28%, transparent)}}.workflow-step-complete .workflow-step-badge{color:var(--pb-success);background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete .workflow-step-badge{background-color:color-mix(in srgb, var(--pb-success) 12%, transparent)}}.workflow-step-label{--tw-font-weight:var(--font-weight-medium);font-size:10px;font-weight:var(--font-weight-medium);--tw-tracking:.12em;letter-spacing:.12em;color:var(--pb-text-dim);text-transform:uppercase}.workflow-step-title{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.checklist-item{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);display:flex}.section-body{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.section-header{gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);flex-direction:column;display:flex}@media (min-width:40rem){.section-header{flex-direction:row;justify-content:space-between;align-items:flex-start}}.section-header-copy{min-width:0}:where(.section-header-copy>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.section-header-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;flex-shrink:0;display:flex}.section-eyebrow{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.section-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-primary);text-transform:uppercase}.section-title-plain{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.section-description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.settings-connection-list{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}.settings-connection-card{cursor:pointer;border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);text-align:left;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.settings-connection-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.settings-connection-card-muted{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card-muted{background-color:color-mix(in srgb, var(--pb-bg-surface) 70%, transparent)}}.settings-connection-card-muted{opacity:.78}.settings-connection-card .downloads-action-btn.is-danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card .downloads-action-btn.is-danger{border-color:color-mix(in srgb, var(--pb-error) 28%, var(--pb-border-subtle))}}.settings-connection-card .downloads-action-btn.is-danger{background:var(--pb-error-dim);color:var(--pb-error)}.settings-connection-card .downloads-action-btn.is-danger:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card .downloads-action-btn.is-danger:hover{border-color:color-mix(in srgb, var(--pb-error) 44%, transparent)}}.settings-connection-card .downloads-action-btn.is-danger:hover{background:var(--pb-error-dim)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card .downloads-action-btn.is-danger:hover{background:color-mix(in srgb, var(--pb-error-dim) 86%, transparent)}}.settings-connection-card .downloads-action-btn.is-danger:hover{color:var(--pb-error)}.admin-nav-link{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-lg);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-size:.8125rem;font-weight:var(--font-weight-medium);color:var(--pb-text-dim);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-width:1px;border-color:#0000;display:flex}.admin-nav-link:hover{color:var(--pb-text-secondary);background-color:var(--pb-surface-selected)}.admin-nav-link-active{color:var(--pb-interactive);background-color:var(--pb-surface-selected)}.admin-nav-link-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);flex-shrink:0;justify-content:center;align-items:center;display:flex}.admin-nav-link-active .admin-nav-link-icon{color:var(--pb-interactive)}.admin-nav-link-copy{min-width:0}.admin-nav-link-title{text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);font-size:.8125rem;font-weight:var(--font-weight-medium);overflow:hidden}:where(.settings-rows>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--pb-border)}.settings-row{gap:calc(var(--spacing) * 4);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}@media (min-width:40rem){.settings-row{grid-template-columns:220px minmax(0,1fr);align-items:flex-start}}.settings-row-meta{padding-top:var(--spacing)}.settings-row-label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);display:block}.settings-row-help{margin-top:calc(var(--spacing) * .5);--tw-leading:calc(var(--spacing) * 5);font-size:11px;line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim)}.settings-row-content{min-width:0}.settings-rows-align-end .settings-row-content{flex-direction:column;align-items:flex-end;display:flex}.settings-footer{gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media (min-width:40rem){.settings-footer{flex-direction:row;justify-content:space-between;align-items:center}}.settings-footer{background-color:var(--pb-bg-surface);overflow-anchor:none}@media (min-width:40rem){.settings-footer-end{justify-content:flex-end}}.settings-footer-copy{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.settings-footer-actions{align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}.settings-footer-actions .btn-primary,.settings-footer-actions .btn-ghost,.settings-footer-actions .btn-danger,.settings-footer-actions .btn-warning{min-height:calc(var(--spacing) * 10);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.field-note{margin-top:calc(var(--spacing) * 1.5);align-items:flex-start;gap:calc(var(--spacing) * 1.5);--tw-leading:calc(var(--spacing) * 5);font-size:11px;line-height:calc(var(--spacing) * 5);display:flex}.field-note code{color:var(--pb-text-secondary)}.field-note-icon{margin-top:calc(var(--spacing) * .5);flex-shrink:0;width:.75rem;height:.75rem}.field-note-warning{color:var(--pb-warning)}.field-note-info{color:var(--pb-info)}.field-note-danger{color:var(--pb-error)}.field-note-success{color:var(--pb-success)}.alert-banner{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);border-width:1px;flex-direction:column;display:flex}@media (min-width:40rem){.alert-banner{flex-direction:row;justify-content:space-between;align-items:flex-start}}.alert-banner{box-shadow:var(--pb-shadow-1)}.alert-banner-main{align-items:flex-start;gap:calc(var(--spacing) * 4);min-width:0;display:flex}.alert-banner-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:var(--radius-lg);flex-shrink:0;justify-content:center;align-items:center;display:flex}.alert-banner-copy{min-width:0}:where(.alert-banner-copy>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.alert-banner-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.alert-banner-description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.alert-banner-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;flex-shrink:0;display:flex}.alert-banner-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.alert-banner-info{background-color:color-mix(in srgb, var(--pb-info) 10%, var(--pb-bg-card))}}.alert-banner-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.alert-banner-info{border-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.alert-banner-info .alert-banner-icon{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.alert-banner-info .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.alert-banner-info .alert-banner-icon{color:var(--pb-info)}.alert-banner-warning{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.alert-banner-warning{background-color:color-mix(in srgb, var(--pb-warning) 10%, var(--pb-bg-card))}}.alert-banner-warning{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.alert-banner-warning{border-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.alert-banner-warning .alert-banner-icon{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.alert-banner-warning .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.alert-banner-warning .alert-banner-icon{color:var(--pb-warning)}.alert-banner-danger{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.alert-banner-danger{background-color:color-mix(in srgb, var(--pb-error) 10%, var(--pb-bg-card))}}.alert-banner-danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.alert-banner-danger{border-color:color-mix(in srgb, var(--pb-error) 18%, transparent)}}.alert-banner-danger .alert-banner-icon{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.alert-banner-danger .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-error) 18%, transparent)}}.alert-banner-danger .alert-banner-icon{color:var(--pb-error)}.alert-banner-success{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.alert-banner-success{background-color:color-mix(in srgb, var(--pb-success) 10%, var(--pb-bg-card))}}.alert-banner-success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.alert-banner-success{border-color:color-mix(in srgb, var(--pb-success) 18%, transparent)}}.alert-banner-success .alert-banner-icon{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.alert-banner-success .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-success) 18%, transparent)}}.alert-banner-success .alert-banner-icon{color:var(--pb-success)}.modal-shell{z-index:50;padding:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 16);justify-content:center;align-items:flex-start;display:flex;position:fixed;inset:0}.modal-backdrop{z-index:0;background-color:var(--pb-bg-overlay);--tw-backdrop-blur:blur(var(--blur-xs));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);position:fixed;inset:0}.modal-panel{z-index:10;border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);width:100%;box-shadow:var(--pb-shadow-overlay);flex-direction:column;display:flex;position:relative;overflow:hidden}.modal-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.modal-body{flex:1;overflow-y:auto}.modal-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);flex-shrink:0;justify-content:space-between;align-items:center;display:flex}.donation-modal-shell{align-items:flex-start;padding-top:4.75rem}.donation-modal-panel{border-radius:1.5rem;width:min(100vw - 2rem,56rem);max-height:calc(100dvh - 6rem)}.donation-modal-header{border-bottom-color:var(--pb-border-subtle);background:radial-gradient(circle at top left, var(--pb-brand-dim), transparent 18rem), var(--pb-bg-card);align-items:flex-start;gap:1rem}.donation-modal-kicker{letter-spacing:.18em;text-transform:uppercase;color:var(--pb-brand);font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:700}.donation-modal-title{color:var(--pb-text);letter-spacing:-.03em;margin-top:.2rem;font-family:Syne,sans-serif;font-size:clamp(1.35rem,2.5vw,2rem);font-weight:800;line-height:1.05}.donation-modal-copy{max-width:38rem;color:var(--pb-text-sec);margin-top:.45rem;font-size:.9rem;line-height:1.55}.donation-modal-body{background:linear-gradient(135deg, transparent, var(--pb-surface-selected)), var(--pb-bg-surface);gap:1rem;padding:1rem;display:grid}.donation-option-card{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:1.25rem;grid-template-columns:minmax(0,1fr) 10.5rem;align-items:stretch;gap:1rem;padding:1rem;display:grid}.donation-option-card--coffee{border-color:#fd0}@supports (color:color-mix(in lab, red, red)){.donation-option-card--coffee{border-color:color-mix(in srgb, #fd0 38%, var(--pb-border-subtle))}}.donation-option-card--liberapay{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.donation-option-card--liberapay{border-color:color-mix(in srgb, var(--pb-success) 32%, var(--pb-border-subtle))}}.donation-option-main{flex-direction:column;justify-content:space-between;gap:1rem;min-width:0;display:flex}.donation-option-eyebrow{letter-spacing:.16em;text-transform:uppercase;color:var(--pb-text-sec);font-family:JetBrains Mono,monospace;font-size:.64rem;font-weight:700}.donation-option-title{color:var(--pb-text);margin-top:.25rem;font-family:Syne,sans-serif;font-size:1.1rem;font-weight:800}.donation-option-copy{max-width:28rem;color:var(--pb-text-sec);margin-top:.35rem;font-size:.82rem;line-height:1.5}.donation-option-button{border-radius:999px;justify-content:center;align-items:center;width:fit-content;padding:.62rem 1rem;font-size:.82rem;font-weight:800;line-height:1;text-decoration:none;transition:transform .16s,box-shadow .16s,filter .16s;display:inline-flex}.donation-option-button:hover,.donation-option-button:focus-visible{filter:brightness(1.02);transform:translateY(-1px)}.donation-option-button:focus-visible{outline:2px solid var(--pb-focus-outline);outline-offset:3px}.donation-option-button--coffee{color:#16130a;background:#fd0;box-shadow:0 10px 20px #fd03}.donation-option-button--liberapay{background:var(--pb-success);color:#f7f1e8;box-shadow:0 10px 20px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.donation-option-button--liberapay{box-shadow:0 10px 20px color-mix(in srgb, var(--pb-success) 20%, transparent)}}[data-theme=dark] .donation-option-button--liberapay{color:#16130a}.donation-qr-frame{border:1px solid var(--pb-border-subtle);background:#fff;border-radius:1rem;place-items:center;min-height:10.5rem;padding:.625rem;display:grid}.donation-qr{object-fit:contain;width:9.25rem;height:9.25rem}@media (max-width:720px){.donation-modal-shell{padding-top:3.25rem}.donation-modal-panel{max-height:calc(100dvh - 4rem)}.donation-option-card{grid-template-columns:1fr}.donation-option-button{width:100%}}.issue-search-modal-panel{border-radius:1.25rem;width:min(100vw - 2rem,78rem);max-width:none;max-height:90vh}.issue-search-modal-header{border-bottom-color:var(--pb-border-subtle);align-items:flex-start;padding:1.125rem 1.5rem}.issue-search-modal-title{letter-spacing:.03em;text-transform:uppercase;color:var(--pb-text);font-family:Syne,sans-serif;font-size:1.05rem;font-weight:800}.issue-search-modal-title span{color:var(--pb-brand)}.issue-search-modal-subtitle{color:var(--pb-text-dim);margin-top:.1875rem;font-size:.78rem}.issue-search-modal-stats{flex-wrap:wrap;gap:.375rem;margin-top:.5rem;display:flex}.issue-search-modal-close{border-radius:var(--radius-lg);--tw-border-style:none;color:var(--pb-text-dim);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;border-style:none;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.issue-search-modal-close:hover{color:var(--pb-text-primary)}}.issue-search-modal-close{width:1.875rem;height:1.875rem}.issue-search-modal-close:hover{background:var(--pb-surface-selected)}.issue-search-modal-body{padding:0}.issue-search-loading-state{min-height:24rem}.issue-search-loading-title{color:var(--pb-text);font-size:.82rem;font-weight:600}.issue-search-loading-copy{color:var(--pb-text-dim);font-size:.72rem}.issue-search-dc-status{border-top:1px solid var(--pb-border-subtle);color:var(--pb-text-dim);align-items:center;gap:.625rem;padding:.75rem 1rem;font-size:.78rem;display:flex}.issue-search-dc-results{border-top:1px solid var(--pb-border-subtle)}.issue-search-dc-results-heading{align-items:center;gap:.5rem;padding:.75rem 1rem;display:flex}.issue-search-modal-footer{border-top-color:var(--pb-border-subtle);justify-content:flex-end;padding:.75rem 1.5rem}.issue-search-modal-footer-meta{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.68rem}.issue-search-modal-footer-meta:empty{display:none}.issue-search-modal-footer-meta strong{color:var(--pb-text);font-weight:700}.issue-search-modal-footer-close{margin-left:auto}.inline-alert{align-items:flex-start;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;display:flex}.inline-alert-icon{margin-top:calc(var(--spacing) * .5);flex-shrink:0}.inline-alert-danger{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.inline-alert-danger{background-color:color-mix(in srgb, var(--pb-error) 12%, var(--pb-bg-card))}}.inline-alert-danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.inline-alert-danger{border-color:color-mix(in srgb, var(--pb-error) 22%, transparent)}}.inline-alert-danger{color:var(--pb-error)}.inline-alert-warning{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.inline-alert-warning{background-color:color-mix(in srgb, var(--pb-warning) 12%, var(--pb-bg-card))}}.inline-alert-warning{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.inline-alert-warning{border-color:color-mix(in srgb, var(--pb-warning) 22%, transparent)}}.inline-alert-warning{color:var(--pb-warning)}.inline-alert-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.inline-alert-info{background-color:color-mix(in srgb, var(--pb-info) 12%, var(--pb-bg-card))}}.inline-alert-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.inline-alert-info{border-color:color-mix(in srgb, var(--pb-info) 22%, transparent)}}.inline-alert-info{color:var(--pb-info)}.inline-alert-success{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.inline-alert-success{background-color:color-mix(in srgb, var(--pb-success) 12%, var(--pb-bg-card))}}.inline-alert-success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.inline-alert-success{border-color:color-mix(in srgb, var(--pb-success) 22%, transparent)}}.inline-alert-success{color:var(--pb-success)}.field-invalid{border-color:var(--pb-error)!important}@supports (color:color-mix(in lab, red, red)){.field-invalid{border-color:color-mix(in srgb, var(--pb-error) 52%, transparent)!important}}.icon-btn{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.icon-btn:hover{border-color:var(--pb-border-strong);color:var(--pb-text-primary)}}.icon-btn:disabled{cursor:not-allowed;opacity:.5}.icon-btn{min-width:var(--pb-control-min-height);width:var(--pb-control-min-height);min-height:var(--pb-control-min-height);border-radius:var(--pb-control-radius);padding:0}.toggle-switch{cursor:pointer;background-color:var(--pb-bg-card-hover);width:2.25rem;height:1.25rem;box-shadow:inset 0 0 0 1px var(--pb-border-hover);border-radius:999px;flex-shrink:0;transition:background-color .18s,box-shadow .18s;display:inline-flex;position:relative}label:has(>.toggle-input+.toggle-switch){position:relative}.toggle-input{clip:rect(0, 0, 0, 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.toggle-input-overlay{opacity:0;clip:auto;clip-path:none;white-space:normal;z-index:1;width:100%;height:100%;margin:0;inset:0}.toggle-switch:after{content:"";background-color:var(--pb-brand);width:1rem;height:1rem;box-shadow:0 1px 2px var(--pb-text-primary);border-radius:999px;position:absolute;top:2px;left:2px}@supports (color:color-mix(in lab, red, red)){.toggle-switch:after{box-shadow:0 1px 2px color-mix(in srgb, var(--pb-text-primary) 14%, transparent)}}.toggle-switch:after{transition:transform .18s}.peer:checked+.toggle-switch{background-color:var(--pb-interactive);box-shadow:inset 0 0 0 1px var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.peer:checked+.toggle-switch{box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--pb-interactive) 34%, transparent)}}.peer:checked+.toggle-switch:after{background-color:var(--pb-surface-shell);transform:translate(1rem)}.peer:focus-visible+.toggle-switch{outline:2px solid var(--pb-focus-outline);outline-offset:3px;box-shadow:0 0 0 4px var(--pb-focus-ring)}.strength-segment{height:calc(var(--spacing) * 1.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;border-radius:3.40282e38px;flex:1;transition-duration:.3s}.strength-segment-neutral{background-color:var(--pb-bg-card-hover)}.strength-segment-danger{background-color:var(--pb-error)}.strength-segment-warning{background-color:var(--pb-warning)}.strength-segment-success{background-color:var(--pb-success)}.input-pb{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-input);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-family:DM Sans,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-primary);border-radius:10px}.input-pb::placeholder{color:var(--pb-text-dim)}.input-pb:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.input-pb:focus{outline-offset:2px;outline:2px solid #0000}}.checkbox-pb{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);border-color:var(--pb-border-hover);background-color:var(--pb-bg-input);color:var(--pb-interactive);border-radius:.25rem}.checkbox-pb:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.select-pb{appearance:none;border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-input);width:100%;padding-block:calc(var(--spacing) * 2);padding-right:calc(var(--spacing) * 8);padding-left:calc(var(--spacing) * 3);font-family:DM Sans,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-primary);border-radius:10px}.select-pb:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.select-pb:focus{outline-offset:2px;outline:2px solid #0000}}.select-pb{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%237A8599' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.25rem}.dropdown-select{min-width:0;position:relative}.dropdown-select-trigger{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);width:100%;color:var(--pb-text-primary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;display:flex}@media (hover:hover){.dropdown-select-trigger:hover{border-color:var(--pb-border-strong)}}.dropdown-select-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.dropdown-select-trigger:focus{outline-offset:2px;outline:2px solid #0000}}.dropdown-select-trigger:disabled{cursor:not-allowed;opacity:.6}.dropdown-select-trigger{gap:var(--pb-control-gap);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.dropdown-select-trigger-label{text-overflow:ellipsis;white-space:nowrap;text-align:left;flex:1;min-width:0;overflow:hidden}.dropdown-select-chevron{color:var(--pb-text-secondary);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));width:var(--pb-control-icon-size);height:var(--pb-control-icon-size);flex-shrink:0}.import-advanced-disclosure[open]>summary .import-advanced-disclosure-chevron{rotate:90deg}.dropdown-select-panel{z-index:80;max-height:calc(var(--spacing) * 60);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card);padding-block:var(--spacing);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-shadow-color:#0003;position:fixed;top:0;left:0;overflow-y:auto}@supports (color:color-mix(in lab, red, red)){.dropdown-select-panel{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.dropdown-select-panel{max-height:var(--pb-dropdown-panel-max-height,15rem);will-change:top, left}.dropdown-select-panel[data-ready=false]{visibility:hidden;pointer-events:none}.dropdown-select-option{text-align:left;width:100%;color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;gap:var(--pb-control-gap);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-panel-item-px);padding-block:var(--pb-control-panel-item-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);display:flex}.dropdown-select-option-label{text-align:left;white-space:nowrap;overflow-wrap:normal;flex:1;min-width:0}.import-reconcile-issue-dropdown .dropdown-select-trigger{align-items:flex-start;height:auto}.import-reconcile-issue-dropdown .dropdown-select-trigger-label{white-space:normal;overflow-wrap:anywhere;text-overflow:clip;overflow:visible}.dropdown-select-panel-wrap .dropdown-select-option{align-items:flex-start}.dropdown-select-panel-wrap .dropdown-select-option-label{white-space:normal;overflow-wrap:anywhere;word-break:normal}.dropdown-select-option:hover,.dropdown-select-option-active{background-color:var(--pb-info-dim);color:var(--pb-text-primary)}.dropdown-select-option-active{transition-duration:0s}.dropdown-select-option-selected{background-color:var(--pb-info-dim);color:var(--pb-text-primary);font-weight:500}.dropdown-select-option-check{width:var(--pb-control-icon-size);height:var(--pb-control-icon-size);flex-shrink:0}.direct-provider-uri-control{position:relative}.direct-provider-uri-input{padding-right:calc(var(--spacing) * 10);font-family:JetBrains Mono,Fira Code,monospace}.direct-provider-uri-toggle{width:calc(var(--spacing) * 10);border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));inset-block:0;justify-content:center;align-items:center;display:flex;position:absolute;right:0}@media (hover:hover){.direct-provider-uri-toggle:hover{color:var(--pb-text-primary)}}.direct-provider-uri-toggle:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.direct-provider-uri-toggle:focus{outline-offset:2px;outline:2px solid #0000}}.direct-provider-uri-toggle:focus{--tw-ring-inset:inset}.direct-provider-uri-options{z-index:90;margin-top:var(--spacing);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card);padding-block:var(--spacing);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-shadow-color:#0003;position:absolute;top:100%;left:0;right:0;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.direct-provider-uri-options{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.direct-provider-uri-option{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media (hover:hover){.direct-provider-uri-option:hover{background-color:var(--pb-info-dim);color:var(--pb-text-primary)}}.direct-provider-uri-option:focus{background-color:var(--pb-info-dim);color:var(--pb-text-primary);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.direct-provider-uri-option:focus{outline-offset:2px;outline:2px solid #0000}}.direct-provider-uri-option-selected{background-color:var(--pb-info-dim);color:var(--pb-text-primary)}.search-field{min-width:0;position:relative}.search-field-icon{pointer-events:none;z-index:10;--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);color:var(--pb-text-secondary);top:50%;left:var(--pb-control-search-icon-left);width:var(--pb-control-icon-size);height:var(--pb-control-icon-size);position:absolute}.search-field-input{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);width:100%;color:var(--pb-text-primary)}.search-field-input::placeholder{color:var(--pb-text-secondary)}.search-field-input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.search-field-input:focus{outline-offset:2px;outline:2px solid #0000}}.search-field-input{min-height:var(--pb-control-min-height);padding-inline-start:var(--pb-control-search-padding-left);padding-inline-end:var(--pb-control-search-padding-right);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius);appearance:none;background-image:none}.search-field-editor{white-space:nowrap;min-height:42px;overflow:hidden}.search-field-editor[data-empty=true]:before{content:attr(data-placeholder);color:var(--pb-text-secondary)}.search-field-input::-webkit-search-decoration{appearance:none;display:none}.search-field-input::-webkit-search-cancel-button{appearance:none;display:none}.search-field-input::-webkit-search-results-button{appearance:none;display:none}.search-field-input::-webkit-search-results-decoration{appearance:none;display:none}.search-field-input::-ms-clear{width:0;height:0;display:none}.search-field-input::-ms-reveal{width:0;height:0;display:none}.search-field-clear{z-index:10;height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);padding:calc(var(--spacing) * .5);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:.25rem;justify-content:center;align-items:center;display:inline-flex;position:absolute;top:50%}@media (hover:hover){.search-field-clear:hover{color:var(--pb-text-primary)}}.search-field-clear{right:var(--pb-control-clear-right)}.search-history-panel{z-index:30;margin-top:var(--spacing);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-shadow-color:#0006;position:absolute;top:100%;left:0;right:0;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.search-history-panel{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.search-history-panel-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);justify-content:space-between;align-items:center;display:flex}.search-history-panel-title{--tw-font-weight:var(--font-weight-semibold);font-size:10px;font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--pb-text-dim);text-transform:uppercase}.search-history-panel-clear{color:var(--pb-text-dim);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));font-size:10px}@media (hover:hover){.search-history-panel-clear:hover{color:var(--pb-error)}}.search-history-list{max-height:280px;padding-block:var(--spacing);overflow-y:auto}.search-history-item{align-items:center;display:flex}.search-history-item-button{align-items:center;gap:calc(var(--spacing) * 2.5);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex:1;display:flex}@media (hover:hover){.search-history-item-button:hover{color:var(--pb-text-primary)}}.search-history-item-button:hover{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.search-history-item-button:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.search-history-item-remove{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);color:var(--pb-text-dim);opacity:0;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.search-history-item-remove:is(:where(.group):hover *){opacity:1}.search-history-item-remove:hover{color:var(--pb-error)}}.touch-target{min-width:44px;min-height:44px}.progress-track{height:var(--spacing);background-color:var(--pb-border);border-radius:3.40282e38px}.progress-fill{background-color:var(--pb-success);height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.progress-fill-partial{background-color:var(--pb-interactive);height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.callout-brand{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--pb-brand-signal-dim);border-width:1px;border-color:var(--pb-brand-signal-border);display:flex}.callout-brand:hover{background-color:var(--pb-brand-signal-hover);border-color:var(--pb-brand-signal-border)}.callout-brand-icon{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);background-color:var(--pb-brand-signal-dim);color:var(--pb-brand-signal);flex-shrink:0}.callout-brand-title{color:var(--pb-brand-signal)}.callout-brand-body{color:var(--pb-brand-signal);opacity:.7}.callout-brand-chevron{color:var(--pb-brand-signal);opacity:.5}}@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{inset-block:0}.-top-3{top:calc(var(--spacing) * -3)}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.top-4{top:calc(var(--spacing) * 4)}.-right-1{right:calc(var(--spacing) * -1)}.-right-3{right:calc(var(--spacing) * -3)}.right-0{right:0}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.-bottom-1{bottom:calc(var(--spacing) * -1)}.left-0{left:0}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[60\]{z-index:60}.z-\[100\]{z-index:100}.z-\[110\]{z-index:110}.col-span-2{grid-column:span 2/span 2}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.\!mt-0{margin-top:0!important}.\!mt-2{margin-top:calc(var(--spacing) * 2)!important}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:var(--spacing)}.\!mb-0{margin-bottom:0!important}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-\[140px\]{margin-left:140px}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.aspect-\[2\/3\]{aspect-ratio:2/3}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-4\.5{height:calc(var(--spacing) * 4.5)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-28{height:calc(var(--spacing) * 28)}.h-32{height:calc(var(--spacing) * 32)}.h-96{height:calc(var(--spacing) * 96)}.h-\[18px\]{height:18px}.h-\[100dvh\]{height:100dvh}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-80{max-height:calc(var(--spacing) * 80)}.max-h-\[28rem\]{max-height:28rem}.max-h-\[62vh\]{max-height:62vh}.max-h-\[70vh\]{max-height:70vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.max-h-\[95vh\]{max-height:95vh}.max-h-\[calc\(100vh-5rem\)\]{max-height:calc(100vh - 5rem)}.\!min-h-8{min-height:calc(var(--spacing) * 8)!important}.\!min-h-10{min-height:calc(var(--spacing) * 10)!important}.min-h-0{min-height:0}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-56{min-height:calc(var(--spacing) * 56)}.min-h-\[36px\]{min-height:36px}.min-h-\[200px\]{min-height:200px}.\!w-8{width:calc(var(--spacing) * 8)!important}.\!w-10{width:calc(var(--spacing) * 10)!important}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-4\.5{width:calc(var(--spacing) * 4.5)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-32{width:calc(var(--spacing) * 32)}.w-36{width:calc(var(--spacing) * 36)}.w-40{width:calc(var(--spacing) * 40)}.w-44{width:calc(var(--spacing) * 44)}.w-48{width:calc(var(--spacing) * 48)}.w-56{width:calc(var(--spacing) * 56)}.w-64{width:calc(var(--spacing) * 64)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-\[10\%\]{width:10%}.w-\[12\%\]{width:12%}.w-\[14\%\]{width:14%}.w-\[18px\]{width:18px}.w-\[22rem\]{width:22rem}.w-\[24\%\]{width:24%}.w-\[26\%\]{width:26%}.w-\[28px\]{width:28px}.w-\[28rem\]{width:28rem}.w-\[30rem\]{width:30rem}.w-\[32px\]{width:32px}.w-\[52px\]{width:52px}.w-\[60px\]{width:60px}.w-\[64px\]{width:64px}.w-\[68px\]{width:68px}.w-\[72px\]{width:72px}.w-\[76px\]{width:76px}.w-\[80px\]{width:80px}.w-\[82px\]{width:82px}.w-\[84px\]{width:84px}.w-\[88px\]{width:88px}.w-\[90px\]{width:90px}.w-\[92px\]{width:92px}.w-\[96px\]{width:96px}.w-\[98px\]{width:98px}.w-\[100px\]{width:100px}.w-\[104px\]{width:104px}.w-\[108px\]{width:108px}.w-\[110px\]{width:110px}.w-\[112px\]{width:112px}.w-\[116px\]{width:116px}.w-\[118px\]{width:118px}.w-\[120px\]{width:120px}.w-\[128px\]{width:128px}.w-\[136px\]{width:136px}.w-\[140px\]{width:140px}.w-\[148px\]{width:148px}.w-\[150px\]{width:150px}.w-\[152px\]{width:152px}.w-\[156px\]{width:156px}.w-\[168px\]{width:168px}.w-\[180px\]{width:180px}.w-\[240px\]{width:240px}.w-auto{width:auto}.w-full{width:100%}.max-w-0{max-width:0}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-\[16rem\]{max-width:16rem}.max-w-\[140px\]{max-width:140px}.max-w-\[150px\]{max-width:150px}.max-w-\[160px\]{max-width:160px}.max-w-\[220px\]{max-width:220px}.max-w-\[240px\]{max-width:240px}.max-w-\[260px\]{max-width:260px}.max-w-\[280px\]{max-width:280px}.max-w-\[300px\]{max-width:300px}.max-w-\[320px\]{max-width:320px}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-7{min-width:calc(var(--spacing) * 7)}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-\[11\.5rem\]{min-width:11.5rem}.min-w-\[20px\]{min-width:20px}.min-w-\[48rem\]{min-width:48rem}.min-w-\[54rem\]{min-width:54rem}.min-w-\[60px\]{min-width:60px}.min-w-\[440px\]{min-width:440px}.min-w-\[620px\]{min-width:620px}.min-w-\[640px\]{min-width:640px}.min-w-\[720px\]{min-width:720px}.min-w-\[760px\]{min-width:760px}.min-w-\[860px\]{min-width:860px}.min-w-\[880px\]{min-width:880px}.min-w-\[920px\]{min-width:920px}.min-w-\[980px\]{min-width:980px}.min-w-\[1040px\]{min-width:1040px}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1{--tw-translate-y:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-2{--tw-translate-y:calc(var(--spacing) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-wait{cursor:wait}.resize{resize:both}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:0}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-1{column-gap:var(--spacing)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-pb-border>:not(:last-child)){border-color:var(--pb-border)}:where(.divide-pb-border-subtle>:not(:last-child)){border-color:var(--pb-border-subtle)}:where(.divide-pb-border\/40>:not(:last-child)){border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){:where(.divide-pb-border\/40>:not(:last-child)){border-color:color-mix(in oklab, var(--pb-border) 40%, transparent)}}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.\!rounded-none{border-radius:0!important}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[var\(--radius-card\)\]{border-radius:var(--radius-card)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.\!border-0{border-style:var(--tw-border-style)!important;border-width:0!important}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-\[3px\]{border-style:var(--tw-border-style);border-width:3px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-pb-warning\/35{border-color:var(--pb-warning)!important}@supports (color:color-mix(in lab, red, red)){.\!border-pb-warning\/35{border-color:color-mix(in oklab, var(--pb-warning) 35%, transparent)!important}}.border-current\/15{border-color:currentColor}@supports (color:color-mix(in lab, red, red)){.border-current\/15{border-color:color-mix(in oklab, currentcolor 15%, transparent)}}.border-pb-border{border-color:var(--pb-border)}.border-pb-border-hover,.border-pb-border-hover\/50{border-color:var(--pb-border-hover)}@supports (color:color-mix(in lab, red, red)){.border-pb-border-hover\/50{border-color:color-mix(in oklab, var(--pb-border-hover) 50%, transparent)}}.border-pb-border-subtle{border-color:var(--pb-border-subtle)}.border-pb-border\/50{border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.border-pb-border\/50{border-color:color-mix(in oklab, var(--pb-border) 50%, transparent)}}.border-pb-border\/70{border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.border-pb-border\/70{border-color:color-mix(in oklab, var(--pb-border) 70%, transparent)}}.border-pb-error,.border-pb-error\/30{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.border-pb-error\/30{border-color:color-mix(in oklab, var(--pb-error) 30%, transparent)}}.border-pb-error\/35{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.border-pb-error\/35{border-color:color-mix(in oklab, var(--pb-error) 35%, transparent)}}.border-pb-error\/40{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.border-pb-error\/40{border-color:color-mix(in oklab, var(--pb-error) 40%, transparent)}}.border-pb-info\/30{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.border-pb-info\/30{border-color:color-mix(in oklab, var(--pb-info) 30%, transparent)}}.border-pb-info\/40{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.border-pb-info\/40{border-color:color-mix(in oklab, var(--pb-info) 40%, transparent)}}.border-pb-interactive,.border-pb-interactive\/25{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.border-pb-interactive\/25{border-color:color-mix(in oklab, var(--pb-interactive) 25%, transparent)}}.border-pb-interactive\/30{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.border-pb-interactive\/30{border-color:color-mix(in oklab, var(--pb-interactive) 30%, transparent)}}.border-pb-interactive\/40{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.border-pb-interactive\/40{border-color:color-mix(in oklab, var(--pb-interactive) 40%, transparent)}}.border-pb-success\/30{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.border-pb-success\/30{border-color:color-mix(in oklab, var(--pb-success) 30%, transparent)}}.border-pb-success\/35{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.border-pb-success\/35{border-color:color-mix(in oklab, var(--pb-success) 35%, transparent)}}.border-pb-success\/40{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.border-pb-success\/40{border-color:color-mix(in oklab, var(--pb-success) 40%, transparent)}}.border-pb-surface{border-color:var(--pb-bg-surface)}.border-pb-warning\/20{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/20{border-color:color-mix(in oklab, var(--pb-warning) 20%, transparent)}}.border-pb-warning\/25{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/25{border-color:color-mix(in oklab, var(--pb-warning) 25%, transparent)}}.border-pb-warning\/30{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/30{border-color:color-mix(in oklab, var(--pb-warning) 30%, transparent)}}.border-pb-warning\/35{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/35{border-color:color-mix(in oklab, var(--pb-warning) 35%, transparent)}}.border-pb-warning\/40{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/40{border-color:color-mix(in oklab, var(--pb-warning) 40%, transparent)}}.border-pb-warning\/50{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/50{border-color:color-mix(in oklab, var(--pb-warning) 50%, transparent)}}.border-transparent{border-color:#0000}.border-t-pb-interactive{border-top-color:var(--pb-interactive)}.\!bg-pb-warning-dim\/45{background-color:var(--pb-warning-dim)!important}@supports (color:color-mix(in lab, red, red)){.\!bg-pb-warning-dim\/45{background-color:color-mix(in oklab, var(--pb-warning-dim) 45%, transparent)!important}}.\!bg-transparent{background-color:#0000!important}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab, red, red)){.bg-black\/80{background-color:color-mix(in oklab, var(--color-black) 80%, transparent)}}.bg-pb-base{background-color:var(--pb-bg-base)}.bg-pb-card{background-color:var(--pb-bg-card)}.bg-pb-card-hover,.bg-pb-card-hover\/20{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/20{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 20%, transparent)}}.bg-pb-card-hover\/25{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/25{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 25%, transparent)}}.bg-pb-card-hover\/35{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/35{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 35%, transparent)}}.bg-pb-card-hover\/40{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/40{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 40%, transparent)}}.bg-pb-card-hover\/45{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/45{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 45%, transparent)}}.bg-pb-card-hover\/50{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/50{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 50%, transparent)}}.bg-pb-card-hover\/60{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/60{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 60%, transparent)}}.bg-pb-card\/40{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/40{background-color:color-mix(in oklab, var(--pb-bg-card) 40%, transparent)}}.bg-pb-card\/50{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/50{background-color:color-mix(in oklab, var(--pb-bg-card) 50%, transparent)}}.bg-pb-card\/60{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/60{background-color:color-mix(in oklab, var(--pb-bg-card) 60%, transparent)}}.bg-pb-card\/70{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/70{background-color:color-mix(in oklab, var(--pb-bg-card) 70%, transparent)}}.bg-pb-card\/80{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/80{background-color:color-mix(in oklab, var(--pb-bg-card) 80%, transparent)}}.bg-pb-card\/85{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/85{background-color:color-mix(in oklab, var(--pb-bg-card) 85%, transparent)}}.bg-pb-card\/95{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/95{background-color:color-mix(in oklab, var(--pb-bg-card) 95%, transparent)}}.bg-pb-error{background-color:var(--pb-error)}.bg-pb-error-dim,.bg-pb-error-dim\/70{background-color:var(--pb-error-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-error-dim\/70{background-color:color-mix(in oklab, var(--pb-error-dim) 70%, transparent)}}.bg-pb-error\/10{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.bg-pb-error\/10{background-color:color-mix(in oklab, var(--pb-error) 10%, transparent)}}.bg-pb-error\/15{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.bg-pb-error\/15{background-color:color-mix(in oklab, var(--pb-error) 15%, transparent)}}.bg-pb-info{background-color:var(--pb-info)}.bg-pb-info-dim{background-color:var(--pb-info-dim)}.bg-pb-info\/10{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.bg-pb-info\/10{background-color:color-mix(in oklab, var(--pb-info) 10%, transparent)}}.bg-pb-input{background-color:var(--pb-bg-input)}.bg-pb-interactive{background-color:var(--pb-interactive)}.bg-pb-interactive-dim{background-color:var(--pb-interactive-dim)}.bg-pb-interactive\/15{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.bg-pb-interactive\/15{background-color:color-mix(in oklab, var(--pb-interactive) 15%, transparent)}}.bg-pb-overlay,.bg-pb-overlay\/45{background-color:var(--pb-bg-overlay)}@supports (color:color-mix(in lab, red, red)){.bg-pb-overlay\/45{background-color:color-mix(in oklab, var(--pb-bg-overlay) 45%, transparent)}}.bg-pb-purple-dim{background-color:var(--pb-purple-dim)}.bg-pb-success{background-color:var(--pb-success)}.bg-pb-success-dim,.bg-pb-success-dim\/70{background-color:var(--pb-success-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success-dim\/70{background-color:color-mix(in oklab, var(--pb-success-dim) 70%, transparent)}}.bg-pb-success\/10{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success\/10{background-color:color-mix(in oklab, var(--pb-success) 10%, transparent)}}.bg-pb-success\/15{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success\/15{background-color:color-mix(in oklab, var(--pb-success) 15%, transparent)}}.bg-pb-surface,.bg-pb-surface\/30{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/30{background-color:color-mix(in oklab, var(--pb-bg-surface) 30%, transparent)}}.bg-pb-surface\/35{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/35{background-color:color-mix(in oklab, var(--pb-bg-surface) 35%, transparent)}}.bg-pb-surface\/40{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/40{background-color:color-mix(in oklab, var(--pb-bg-surface) 40%, transparent)}}.bg-pb-surface\/50{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/50{background-color:color-mix(in oklab, var(--pb-bg-surface) 50%, transparent)}}.bg-pb-surface\/60{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/60{background-color:color-mix(in oklab, var(--pb-bg-surface) 60%, transparent)}}.bg-pb-surface\/80{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/80{background-color:color-mix(in oklab, var(--pb-bg-surface) 80%, transparent)}}.bg-pb-text-dim{background-color:var(--pb-text-dim)}.bg-pb-warning{background-color:var(--pb-warning)}.bg-pb-warning-dim,.bg-pb-warning-dim\/25{background-color:var(--pb-warning-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning-dim\/25{background-color:color-mix(in oklab, var(--pb-warning-dim) 25%, transparent)}}.bg-pb-warning-dim\/45{background-color:var(--pb-warning-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning-dim\/45{background-color:color-mix(in oklab, var(--pb-warning-dim) 45%, transparent)}}.bg-pb-warning\/5{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning\/5{background-color:color-mix(in oklab, var(--pb-warning) 5%, transparent)}}.bg-pb-warning\/10{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning\/10{background-color:color-mix(in oklab, var(--pb-warning) 10%, transparent)}}.bg-slate-950\/70{background-color:#020618b3}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/70{background-color:color-mix(in oklab, var(--color-slate-950) 70%, transparent)}}.bg-transparent{background-color:#0000}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-top{object-position:top}.\!p-0{padding:0!important}.p-0{padding:0}.p-1{padding:var(--spacing)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.\!px-0{padding-inline:0!important}.\!px-2\.5{padding-inline:calc(var(--spacing) * 2.5)!important}.\!px-3{padding-inline:calc(var(--spacing) * 3)!important}.\!px-4{padding-inline:calc(var(--spacing) * 4)!important}.px-0{padding-inline:0}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.\!py-0{padding-block:0!important}.\!py-1\.5{padding-block:calc(var(--spacing) * 1.5)!important}.\!py-2{padding-block:calc(var(--spacing) * 2)!important}.\!py-2\.5{padding-block:calc(var(--spacing) * 2.5)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-\[3px\]{padding-block:3px}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:var(--spacing)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.\!pb-0{padding-bottom:0!important}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.\!text-left{text-align:left!important}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-display{font-family:Bricolage Grotesque,sans-serif}.font-mono{font-family:JetBrains Mono,Fira Code,monospace}.font-sans{font-family:DM Sans,sans-serif}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.78rem\]{font-size:.78rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.88rem\]{font-size:.88rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-7{--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-\[0\.22em\]{--tw-tracking:.22em;letter-spacing:.22em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.\!text-pb-text{color:var(--pb-text-primary)!important}.\!text-pb-warning{color:var(--pb-warning)!important}.text-pb-border{color:var(--pb-border)}.text-pb-brand{color:var(--pb-brand)}.text-pb-error{color:var(--pb-error)}.text-pb-info{color:var(--pb-info)}.text-pb-interactive{color:var(--pb-interactive)}.text-pb-purple{color:var(--pb-purple)}.text-pb-success{color:var(--pb-success)}.text-pb-text{color:var(--pb-text-primary)}.text-pb-text-dim{color:var(--pb-text-dim)}.text-pb-text-inverse{color:var(--pb-text-inverse)}.text-pb-text-sec{color:var(--pb-text-secondary)}.text-pb-warning,.text-pb-warning\/70{color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.text-pb-warning\/70{color:color-mix(in oklab, var(--pb-warning) 70%, transparent)}}.text-red-300{color:var(--color-red-300)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.accent-pb-interactive{accent-color:var(--pb-interactive)}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-\[1px\]{--tw-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xs{--tw-backdrop-blur:blur(var(--blur-xs));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.delay-75{transition-delay:75ms}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:border-pb-text-sec:is(:where(.group):hover *){border-color:var(--pb-text-secondary)}.group-hover\:text-pb-interactive:is(:where(.group):hover *){color:var(--pb-interactive)}.group-hover\:text-pb-text:is(:where(.group):hover *){color:var(--pb-text-primary)}.group-hover\:text-pb-text-sec:is(:where(.group):hover *){color:var(--pb-text-secondary)}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}}.placeholder\:text-pb-text-dim::placeholder{color:var(--pb-text-dim)}.placeholder\:text-pb-text-sec::placeholder{color:var(--pb-text-secondary)}.first\:pt-0:first-child{padding-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:\!border-pb-warning\/45:hover{border-color:var(--pb-warning)!important}@supports (color:color-mix(in lab, red, red)){.hover\:\!border-pb-warning\/45:hover{border-color:color-mix(in oklab, var(--pb-warning) 45%, transparent)!important}}.hover\:border-pb-border-hover:hover{border-color:var(--pb-border-hover)}.hover\:border-pb-border-strong:hover{border-color:var(--pb-border-strong)}.hover\:border-pb-error\/40:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-error\/40:hover{border-color:color-mix(in oklab, var(--pb-error) 40%, transparent)}}.hover\:border-pb-info\/40:hover{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-info\/40:hover{border-color:color-mix(in oklab, var(--pb-info) 40%, transparent)}}.hover\:border-pb-purple\/40:hover{border-color:var(--pb-purple)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-purple\/40:hover{border-color:color-mix(in oklab, var(--pb-purple) 40%, transparent)}}.hover\:border-pb-success\/40:hover{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-success\/40:hover{border-color:color-mix(in oklab, var(--pb-success) 40%, transparent)}}.hover\:border-pb-warning\/40:hover{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-warning\/40:hover{border-color:color-mix(in oklab, var(--pb-warning) 40%, transparent)}}.hover\:\!bg-pb-warning-dim:hover{background-color:var(--pb-warning-dim)!important}.hover\:\!bg-transparent:hover{background-color:#0000!important}.hover\:bg-pb-card:hover{background-color:var(--pb-bg-card)}.hover\:bg-pb-card-hover:hover,.hover\:bg-pb-card-hover\/20:hover{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-card-hover\/20:hover{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 20%, transparent)}}.hover\:bg-pb-card-hover\/45:hover{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-card-hover\/45:hover{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 45%, transparent)}}.hover\:bg-pb-error-dim:hover{background-color:var(--pb-error-dim)}.hover\:bg-pb-error\/25:hover{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-error\/25:hover{background-color:color-mix(in oklab, var(--pb-error) 25%, transparent)}}.hover\:bg-pb-info-dim:hover{background-color:var(--pb-info-dim)}.hover\:bg-pb-interactive-hover:hover{background-color:var(--pb-interactive-hover)}.hover\:bg-pb-interactive\/10:hover{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-interactive\/10:hover{background-color:color-mix(in oklab, var(--pb-interactive) 10%, transparent)}}.hover\:bg-pb-interactive\/25:hover{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-interactive\/25:hover{background-color:color-mix(in oklab, var(--pb-interactive) 25%, transparent)}}.hover\:bg-pb-purple-dim:hover{background-color:var(--pb-purple-dim)}.hover\:bg-pb-success-dim:hover{background-color:var(--pb-success-dim)}.hover\:bg-pb-success\/25:hover{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-success\/25:hover{background-color:color-mix(in oklab, var(--pb-success) 25%, transparent)}}.hover\:bg-pb-warning-dim:hover{background-color:var(--pb-warning-dim)}.hover\:\!text-pb-text:hover{color:var(--pb-text-primary)!important}.hover\:\!text-pb-warning:hover{color:var(--pb-warning)!important}.hover\:text-pb-error:hover{color:var(--pb-error)}.hover\:text-pb-interactive:hover{color:var(--pb-interactive)}.hover\:text-pb-interactive-hover:hover{color:var(--pb-interactive-hover)}.hover\:text-pb-text:hover{color:var(--pb-text-primary)}.hover\:text-pb-text-sec:hover{color:var(--pb-text-secondary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-95:hover{opacity:.95}.hover\:opacity-100:hover{opacity:1}.hover\:brightness-90:hover{--tw-brightness:brightness(90%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-pb-border-hover:focus{--tw-ring-color:var(--pb-border-hover)}.focus\:ring-pb-error:focus{--tw-ring-color:var(--pb-error)}.focus\:ring-pb-interactive:focus{--tw-ring-color:var(--pb-interactive)}.focus\:ring-pb-warning:focus{--tw-ring-color:var(--pb-warning)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-pb-card:focus{--tw-ring-offset-color:var(--pb-bg-card)}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:cursor-wait:disabled{cursor:wait}.disabled\:text-pb-text-dim:disabled{color:var(--pb-text-dim)}.disabled\:opacity-20:disabled{opacity:.2}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-100:disabled{opacity:1}@media (min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:ml-auto{margin-left:auto}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:table-cell{display:table-cell}.sm\:max-h-\[90vh\]{max-height:90vh}.sm\:w-14{width:calc(var(--spacing) * 14)}.sm\:w-44{width:calc(var(--spacing) * 44)}.sm\:w-48{width:calc(var(--spacing) * 48)}.sm\:w-56{width:calc(var(--spacing) * 56)}.sm\:w-auto{width:auto}.sm\:max-w-xs{max-width:var(--container-xs)}.sm\:min-w-\[760px\]{min-width:760px}.sm\:shrink-0{flex-shrink:0}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-\[8rem_minmax\(0\,1fr\)\]{grid-template-columns:8rem minmax(0,1fr)}.sm\:grid-cols-\[9rem_minmax\(0\,1fr\)\]{grid-template-columns:9rem minmax(0,1fr)}.sm\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.4fr\)_auto\]{grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto}.sm\:grid-cols-\[minmax\(0\,3fr\)_minmax\(0\,2fr\)\]{grid-template-columns:minmax(0,3fr) minmax(0,2fr)}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:self-auto{align-self:auto}.sm\:rounded-xl{border-radius:var(--radius-xl)}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-7{padding:calc(var(--spacing) * 7)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:pt-5{padding-top:calc(var(--spacing) * 5)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:pt-7{padding-top:calc(var(--spacing) * 7)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}@media (min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:table-cell{display:table-cell}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-\[1fr_auto_1fr_auto\]{grid-template-columns:1fr auto 1fr auto}.md\:grid-cols-\[minmax\(0\,1fr\)_auto_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr) auto minmax(0,1fr)}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:items-start{align-items:flex-start}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:table-cell{display:table-cell}.lg\:w-52{width:calc(var(--spacing) * 52)}.lg\:w-auto{width:auto}.lg\:max-w-\[11rem\]{max-width:11rem}.lg\:max-w-md{max-width:var(--container-md)}.lg\:min-w-\[920px\]{min-width:920px}.lg\:min-w-\[980px\]{min-width:980px}.lg\:min-w-\[1060px\]{min-width:1060px}.lg\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[minmax\(0\,1\.1fr\)_minmax\(280px\,0\.9fr\)\]{grid-template-columns:minmax(0,1.1fr) minmax(280px,.9fr)}.lg\:grid-cols-\[minmax\(0\,1\.15fr\)_minmax\(280px\,0\.85fr\)\]{grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}.lg\:grid-cols-\[minmax\(0\,1\.25fr\)_minmax\(280px\,0\.75fr\)\]{grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr)}.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(16rem\,0\.65fr\)\]{grid-template-columns:minmax(0,1fr) minmax(16rem,.65fr)}.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(18rem\,24rem\)\]{grid-template-columns:minmax(0,1fr) minmax(18rem,24rem)}.lg\:grid-cols-\[minmax\(0\,280px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,280px) minmax(0,1fr)}.lg\:grid-cols-\[minmax\(12rem\,0\.7fr\)_minmax\(18rem\,1\.3fr\)\]{grid-template-columns:minmax(12rem,.7fr) minmax(18rem,1.3fr)}.lg\:grid-cols-\[repeat\(5\,minmax\(0\,1fr\)\)\]{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:text-right{text-align:right}}@media (min-width:80rem){.xl\:col-span-3{grid-column:span 3/span 3}.xl\:w-32{width:calc(var(--spacing) * 32)}.xl\:w-48{width:calc(var(--spacing) * 48)}.xl\:w-\[34rem\]{width:34rem}.xl\:max-w-sm{max-width:var(--container-sm)}.xl\:min-w-\[30rem\]{min-width:30rem}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:grid-cols-\[minmax\(0\,1\.4fr\)_minmax\(320px\,0\.8fr\)\]{grid-template-columns:minmax(0,1.4fr) minmax(320px,.8fr)}.xl\:grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(17rem\,1fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(17rem,1fr)}.xl\:grid-cols-\[minmax\(0\,1\.55fr\)_minmax\(320px\,0\.85fr\)\]{grid-template-columns:minmax(0,1.55fr) minmax(320px,.85fr)}.xl\:flex-row{flex-direction:row}.xl\:items-end{align-items:flex-end}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:justify-end{justify-content:flex-end}}@media (min-width:96rem){.\32 xl\:grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(320px\,0\.75fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(320px,.75fr)}}}@font-face{font-family:Bricolage Grotesque;font-style:normal;font-weight:800;font-display:swap;src:url(/static/fonts/bricolage-grotesque-800.woff2)format("woff2")}@font-face{font-family:DM Sans;font-style:normal;font-weight:300 700;font-display:swap;src:url(/static/fonts/dm-sans-variable.woff2)format("woff2")}@font-face{font-family:Syne;font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/syne-400.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/syne-500.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:600;font-display:swap;src:url(/static/fonts/syne-600.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/syne-700.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:800;font-display:swap;src:url(/static/fonts/syne-800.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/jetbrains-mono-400.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/jetbrains-mono-500.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:600;font-display:swap;src:url(/static/fonts/jetbrains-mono-600.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/jetbrains-mono-700.ttf)format("truetype")}.no-transitions,.no-transitions *,.no-transitions :before,.no-transitions :after,.boot-no-transitions,.boot-no-transitions *,.boot-no-transitions :before,.boot-no-transitions :after{transition-duration:0s!important}.safe-area-pb{padding-bottom:env(safe-area-inset-bottom,0px)}.skip-link{z-index:100;background:var(--pb-brand);color:var(--pb-text-inverse);border-radius:0 0 .5rem .5rem;padding:.5rem 1rem;font-size:.875rem;font-weight:600;text-decoration:none;transition:top .15s ease-out;position:absolute;top:-100%;left:1rem}.skip-link:focus{outline:2px solid var(--pb-interactive);outline-offset:2px;top:0}input[type=range].range-pb{appearance:none;background:var(--pb-border-hover);cursor:pointer;border-radius:9999px;outline:none;height:6px}input[type=range].range-pb::-webkit-slider-thumb{appearance:none;background:var(--pb-interactive);border:2px solid var(--pb-bg-card);width:18px;height:18px;box-shadow:var(--pb-shadow-1);cursor:pointer;border-radius:50%}input[type=range].range-pb::-moz-range-thumb{background:var(--pb-interactive);border:2px solid var(--pb-bg-card);width:18px;height:18px;box-shadow:var(--pb-shadow-1);cursor:pointer;border-radius:50%}.scrollbar-hidden{scrollbar-width:none;-ms-overflow-style:none}.scrollbar-hidden::-webkit-scrollbar{display:none}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--pb-bg-surface)}::-webkit-scrollbar-thumb{background:var(--pb-border-strong);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--pb-text-dim)}*{scrollbar-width:thin;scrollbar-color:var(--pb-border-strong) var(--pb-bg-surface)}.htmx-indicator{opacity:0;transition:opacity .2s ease-in}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{opacity:1}#content[data-page-swap-phase=leaving],#content[data-page-swap-phase=entering]{pointer-events:none}#content[data-detail-history-hidden=true]{opacity:0;pointer-events:none}@media (prefers-reduced-motion:reduce){html:focus-within{scroll-behavior:auto}*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}#content{transition:none}}@keyframes toast-enter{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}@keyframes toast-exit{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(100%)}}.toast-enter{animation:.3s ease-out forwards toast-enter}.toast-exit{animation:.3s ease-in forwards toast-exit}.sidebar-transition{transition:width .2s ease-in-out,transform .2s ease-in-out}.app-sidebar-backdrop{background:var(--pb-bg-overlay)}@supports (color:color-mix(in lab, red, red)){.app-sidebar-backdrop{background:color-mix(in srgb, var(--pb-bg-overlay) 96%, transparent)}}.app-sidebar-shell{border-right:1px solid var(--pb-border);background:var(--pb-bg-surface);box-shadow:none;overflow:visible}.app-sidebar-brand{background:0 0;padding:1rem 1rem .75rem}.app-sidebar-brand-collapsed{background:0 0;justify-content:center;padding:1rem 0 .75rem;display:flex}.app-sidebar-brand-copy-collapsed{display:none!important}.app-sidebar-brand-card{align-items:center;gap:.75rem;padding:0;transition:color .18s,transform .18s;display:flex}.app-sidebar-brand-card-collapsed{justify-content:center;width:auto;min-width:0;min-height:0;padding:0}.app-sidebar-brand:hover .app-sidebar-brand-card{transform:translateY(-1px)}.app-sidebar-brand-collapsed:hover .app-sidebar-brand-card{transform:none}.app-sidebar-brand-mark{flex-shrink:0;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;display:flex}.app-sidebar-brand-copy{white-space:nowrap;flex-direction:column;min-width:0;display:flex;overflow:hidden}.app-sidebar-brand-title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1.1rem;font-weight:800;line-height:1}.app-sidebar-brand-version{color:var(--pb-text-tertiary);margin-top:2px;font-family:JetBrains Mono,monospace;font-size:.58rem}.app-sidebar-nav{padding:.25rem .625rem 1rem}.app-sidebar-nav-collapsed{padding:.25rem 0 1rem;overflow:visible!important}.app-sidebar-link{color:var(--pb-text-secondary);border:0;border-radius:.5rem;align-items:center;gap:.625rem;margin:1px 0;padding:.4375rem .625rem;font-size:.82rem;font-weight:500;transition:background-color .18s,color .18s,transform .18s;display:flex;position:relative}.app-sidebar-link-collapsed{justify-content:center;gap:0;width:2.75rem;margin-inline:auto;padding:.5rem}.app-sidebar-link-copy-collapsed{display:none!important}.app-sidebar-link:hover{background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.app-sidebar-link:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.app-sidebar-link:hover{color:var(--pb-text-primary)}.app-sidebar-link-collapsed:hover{transform:none}.app-sidebar-link-active{background:var(--pb-selected);color:var(--pb-interactive);font-weight:600}.app-sidebar-link-active:before{content:"";background:var(--pb-interactive);border-radius:0 2px 2px 0;width:3px;position:absolute;top:6px;bottom:6px;left:0}.app-sidebar-link-icon{width:1.25rem;height:1.25rem;color:inherit;flex-shrink:0;justify-content:center;align-items:center;transition:color .18s;display:flex}.app-sidebar-link-copy{flex:1;justify-content:space-between;align-items:center;gap:.75rem;min-width:0;display:flex}.app-sidebar-link-title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:.82rem;font-weight:inherit;overflow:hidden}.app-sidebar-badge-slot{flex-shrink:0;justify-content:flex-end;align-items:center;min-width:1.125rem;display:flex}.app-sidebar-badge-slot .count-badge{border-radius:6px;min-width:18px;min-height:auto;padding:1px 6px;font-family:JetBrains Mono,monospace;font-size:.6rem;font-weight:700;line-height:1.1}.app-sidebar-link-collapsed .app-sidebar-badge-slot{min-width:0;position:absolute;top:2px;right:2px}.app-sidebar-link-collapsed .app-sidebar-badge-slot .count-badge{border-radius:4px;min-width:14px;padding:0 4px;font-size:.52rem}.app-sidebar-section{margin:1rem 0 0}.app-sidebar-section-collapsed{width:3rem;margin:.75rem auto .375rem}.app-sidebar-section-label{letter-spacing:.14em;text-transform:uppercase;color:var(--pb-text-tertiary);align-items:center;gap:.375rem;margin-bottom:.25rem;padding:0 .5rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700;display:flex}.app-sidebar-section-label-collapsed{display:none!important}.app-sidebar-section-label:after{content:"";background:var(--pb-border-subtle);flex:auto;height:1px}.app-sidebar-section-rule{border-color:var(--pb-border-subtle);width:1.5rem;margin:.5rem auto 0}.app-sidebar-toggle{z-index:70;border:1px solid var(--pb-border-hover);background:var(--pb-bg-card);width:1.75rem;height:1.75rem;color:var(--pb-text-secondary);pointer-events:auto;box-shadow:var(--pb-shadow-1);border-radius:999px;justify-content:center;align-items:center;transition:border-color .18s,background-color .18s,color .18s,box-shadow .18s;position:absolute;top:72px;right:-.75rem}.app-sidebar-toggle[data-tip]{position:absolute}.app-sidebar-toggle:hover{border-color:var(--pb-interactive);background:var(--pb-bg-card);color:var(--pb-interactive);box-shadow:var(--pb-shadow-2)}[data-tip]{--pb-tooltip-max-width:min(360px, calc(100vw - 32px), calc(100dvw - 32px));position:relative}.tooltips-overlay-enabled [data-tip]:hover:after,.tooltips-overlay-enabled [data-tip]:focus-visible:after{content:none;display:none}[data-tip]:hover:after,[data-tip]:focus-visible:after{content:attr(data-tip);width:max-content;max-width:var(--pb-tooltip-max-width);background:var(--pb-bg-card);border:1px solid var(--pb-border-hover);color:var(--pb-text-primary);white-space:normal;overflow-wrap:break-word;box-shadow:var(--pb-shadow-overlay);z-index:50;pointer-events:none;border-radius:8px;padding:6px 10px;font-size:11px;line-height:1.4;animation:.15s ease-out tip-in;position:absolute;bottom:calc(100% + 6px);left:0}.app-tooltip-host{z-index:140;pointer-events:none;position:fixed;inset:0}.app-tooltip-overlay{width:max-content;max-width:var(--pb-tooltip-max-width,min(360px, calc(100vw - 32px), calc(100dvw - 32px)));background:var(--pb-bg-card);border:1px solid var(--pb-border-hover);color:var(--pb-text-primary);white-space:normal;overflow-wrap:break-word;box-shadow:var(--pb-shadow-overlay);opacity:0;visibility:hidden;will-change:left, top, opacity;border-radius:8px;padding:6px 10px;font-size:11px;line-height:1.4;transition:opacity .12s ease-out,visibility .12s ease-out;position:absolute;top:0;left:0;transform:translate(0)}.app-tooltip-host[data-visible=true] .app-tooltip-overlay{opacity:1;visibility:visible}[data-search-field-clear][data-tip]{position:absolute}[data-tip-pos=left]:hover:after,[data-tip-pos=left]:focus-visible:after{animation:.15s ease-out tip-in-left;inset:50% calc(100% + 6px) auto auto;transform:translateY(-50%)}[data-tip-pos=right]:hover:after,[data-tip-pos=right]:focus-visible:after{animation:.15s ease-out tip-in-right;inset:50% auto auto calc(100% + 6px);transform:translateY(-50%)}[data-tip-pos=bottom]:hover:after,[data-tip-pos=bottom]:focus-visible:after{animation:.15s ease-out tip-in-bottom;top:calc(100% + 6px);bottom:auto;left:0}[data-tip-size=wide]:hover:after,[data-tip-size=wide]:focus-visible:after{max-width:var(--pb-tooltip-max-width,min(480px, calc(100vw - 32px), calc(100dvw - 32px)))}[data-tip-size=narrow]:hover:after,[data-tip-size=narrow]:focus-visible:after{max-width:var(--pb-tooltip-max-width,min(220px, calc(100vw - 32px), calc(100dvw - 32px)))}.app-header-icon-tip:hover:after,.app-header-icon-tip:focus-visible:after{text-align:center;min-width:7.25rem}.tooltip-wrap{position:relative}.tooltip-panel{z-index:50;visibility:hidden;opacity:0;pointer-events:none;background:var(--pb-bg-card);border:1px solid var(--pb-border-hover);width:max-content;max-width:360px;color:var(--pb-text-primary);white-space:normal;overflow-wrap:break-word;box-shadow:var(--pb-shadow-overlay);border-radius:.5rem;padding:.5rem .75rem;font-size:.75rem;line-height:1.4;transition:opacity .15s ease-out,transform .15s ease-out,visibility .15s ease-out;position:absolute}.tooltip-wrap:hover>.tooltip-panel,.tooltip-wrap:focus-within>.tooltip-panel{visibility:visible;opacity:1}.tooltip-panel-top{bottom:calc(100% + .5rem);left:0;transform:translateY(4px)}.tooltip-wrap:hover>.tooltip-panel-top,.tooltip-wrap:focus-within>.tooltip-panel-top{transform:translateY(0)}.tooltip-panel-left{top:50%;right:calc(100% + .5rem);transform:translate(4px,-50%)}.tooltip-wrap:hover>.tooltip-panel-left,.tooltip-wrap:focus-within>.tooltip-panel-left{transform:translateY(-50%)}.tooltip-panel-right{top:50%;left:calc(100% + .5rem);transform:translate(-4px,-50%)}.tooltip-wrap:hover>.tooltip-panel-right,.tooltip-wrap:focus-within>.tooltip-panel-right{transform:translateY(-50%)}.tooltip-panel-bottom{top:calc(100% + .5rem);left:0;transform:translateY(-4px)}.tooltip-wrap:hover>.tooltip-panel-bottom,.tooltip-wrap:focus-within>.tooltip-panel-bottom{transform:translateY(0)}.tooltip-panel-nowrap{white-space:nowrap}.tooltip-panel-wide{max-width:500px}@keyframes tip-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes tip-in-left{0%{opacity:0;transform:translate(4px,-50%)}to{opacity:1;transform:translateY(-50%)}}@keyframes tip-in-right{0%{opacity:0;transform:translate(-4px,-50%)}to{opacity:1;transform:translateY(-50%)}}@keyframes tip-in-bottom{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.log-terminal{contain:inline-size;background:var(--pb-bg-surface);width:100%;min-width:0;max-width:100%;overflow-x:hidden}@supports (color:color-mix(in lab, red, red)){.log-terminal{background:color-mix(in srgb, var(--pb-bg-surface) 82%, var(--pb-bg-base))}}.log-terminal{background-image:repeating-linear-gradient(0deg, transparent, transparent 2px, var(--pb-border-subtle) 2px, var(--pb-border-subtle) 4px)}@supports (color:color-mix(in lab, red, red)){.log-terminal{background-image:repeating-linear-gradient(0deg, transparent, transparent 2px, color-mix(in srgb, var(--pb-border-subtle) 32%, transparent) 2px, color-mix(in srgb, var(--pb-border-subtle) 32%, transparent) 4px)}}.log-line{box-sizing:border-box;border-left:3px solid #0000;width:100%;min-width:0;max-width:100%;overflow:hidden}.log-line:hover{background:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.log-line:hover{background:color-mix(in srgb, var(--pb-bg-card-hover) 36%, transparent)}}.log-line[data-level=error],.log-line[data-level=critical]{border-left-color:var(--pb-error);background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.log-line[data-level=error],.log-line[data-level=critical]{background:color-mix(in srgb, var(--pb-error) 12%, transparent)}}.log-line[data-level=warning]{border-left-color:var(--pb-warning)}.log-line[data-level=info]{border-left-color:var(--pb-info)}.log-line[data-level=debug]{border-left-color:var(--pb-text-dim)}.log-line.expanded{background:var(--pb-bg-card-hover)!important}@supports (color:color-mix(in lab, red, red)){.log-line.expanded{background:color-mix(in srgb, var(--pb-bg-card-hover) 54%, transparent)!important}}.log-detail{box-sizing:border-box;max-width:calc(100% - 140px);overflow:hidden}.log-path{overflow-wrap:anywhere}.log-badge{letter-spacing:.08em;text-transform:uppercase;text-align:center;border-radius:3px;min-width:38px;padding:1px 5px;font-size:9px;font-weight:700;display:inline-block}.badge-debug{color:var(--pb-text-dim);background:var(--pb-text-dim)}@supports (color:color-mix(in lab, red, red)){.badge-debug{background:color-mix(in srgb, var(--pb-text-dim) 16%, transparent)}}.badge-info{color:var(--pb-info);background:var(--pb-info-dim)}.badge-warning{color:var(--pb-warning);background:var(--pb-warning-dim)}.badge-error{color:var(--pb-error);background:var(--pb-error-dim)}.badge-critical{color:var(--pb-text-inverse);background:var(--pb-error)}.live-dot{background:var(--pb-success);border-radius:50%;width:6px;height:6px;animation:2s ease-in-out infinite live-pulse}@keyframes live-pulse{0%,to{opacity:1;box-shadow:0 0 0 0 color-mix(in srgb, var(--pb-success) 42%, transparent)}50%{opacity:.6;box-shadow:0 0 6px 2px color-mix(in srgb, var(--pb-success) 24%, transparent)}}.json-key{color:var(--pb-info)}.json-str{color:var(--pb-success)}.json-num{color:var(--pb-warning)}.json-bool{color:var(--pb-purple)}.json-null{color:var(--pb-text-dim)}.downloads-page-shell{width:100%}.downloads-view{width:100%;max-width:none;padding:0 0 var(--pb-page-footer-clearance);margin:0}.downloads-header{display:block}.downloads-header-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:1.25rem;width:100%;display:flex}.downloads-header-summary{flex-wrap:wrap;flex:auto;align-items:flex-start;gap:1.5rem;min-width:0;display:flex}.downloads-header-copy{min-width:0}.downloads-header-actions{flex:none;justify-content:flex-end;align-items:flex-start;margin-left:auto;display:flex}.downloads-title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1}.downloads-title span{color:var(--pb-brand)}.downloads-subtitle{color:var(--pb-text-tertiary);letter-spacing:.01em;margin-top:.25rem;font-size:.78rem}.downloads-gauges{align-items:flex-end;gap:1rem;display:flex}.downloads-gauge{text-align:center}.downloads-gauge-ring{width:56px;height:56px;margin:0 auto;position:relative}.downloads-gauge-ring svg{transform:rotate(-90deg)}.downloads-gauge-bg{fill:none;stroke:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.downloads-gauge-bg{stroke:color-mix(in srgb, var(--pb-text-secondary) 10%, transparent)}}.downloads-gauge-bg{stroke-width:4.5px}.downloads-gauge-fill{fill:none;stroke-width:4.5px;stroke-linecap:round;transition:stroke-dashoffset .6s}.downloads-gauge-fill-success{stroke:var(--pb-status-success)}.downloads-gauge-fill-info{stroke:var(--pb-status-info)}.downloads-gauge-fill-warning{stroke:var(--pb-status-warning)}.downloads-gauge-fill-error{stroke:var(--pb-status-danger)}.downloads-gauge-fill-muted{stroke:var(--pb-text-dim)}.downloads-gauge-value{color:var(--pb-text-primary);justify-content:center;align-items:center;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700;display:flex;position:absolute;inset:0}.downloads-gauge-value-success{color:var(--pb-status-success)}.downloads-gauge-value-info{color:var(--pb-status-info)}.downloads-gauge-value-warning{color:var(--pb-status-warning)}.downloads-gauge-value-error{color:var(--pb-status-danger)}.downloads-gauge-value-muted{color:var(--pb-text-dim)}.downloads-gauge-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);margin-top:.25rem;font-size:.6rem;font-weight:600}.downloads-tab-rail{border:1px solid var(--pb-border);background:0 0;border-radius:10px;align-self:flex-start;display:inline-flex;overflow:hidden}.downloads-tab-btn{letter-spacing:.06em;text-transform:uppercase;color:var(--pb-text-dim);background:0 0;border:0;padding:.5rem 1.25rem;font-family:Syne,sans-serif;font-size:.78rem;font-weight:700;transition:background-color .14s,color .14s}.downloads-tab-btn+.downloads-tab-btn{border-left:1px solid var(--pb-border)}.downloads-tab-btn:hover{color:var(--pb-text);background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.downloads-tab-btn:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.downloads-tab-btn.is-active{background:var(--pb-interactive);color:var(--pb-text-inverse)}.downloads-panel-stack{gap:1.25rem;margin-top:.875rem;display:grid}.import-history-page-shell{margin-top:0}.downloads-section{gap:.625rem;display:grid}.downloads-section-label{border-bottom:1px solid var(--pb-border-subtle);letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);padding-bottom:.375rem;font-family:Syne,sans-serif;font-size:.62rem;font-weight:700}.downloads-table-wrap{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);border-radius:14px;position:relative;overflow:visible;box-shadow:0 1px 3px #1e1a170f,0 1px 2px #1e1a170a}.downloads-table-wrap.is-clipped{overflow:hidden}.downloads-table{border-collapse:collapse;width:100%}.downloads-table th{border-bottom:2px solid var(--pb-border);background:var(--pb-bg-surface);text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);padding:.5625rem .875rem;font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.downloads-table td{border-bottom:1px solid var(--pb-border-subtle);color:var(--pb-text);vertical-align:middle;padding:.6875rem .875rem;font-size:.82rem;transition:background-color .1s}.downloads-table tbody:last-child tr:last-child td{border-bottom:0}.downloads-table tr:not(.table-detail-row):hover td{background:var(--pb-surface-selected)}.downloads-table th.is-center,.downloads-table td.is-center{text-align:center}.downloads-table th.is-right,.downloads-table td.is-right{text-align:right}.issue-search-results-table-wrap{max-height:62vh;overflow-y:auto}.issue-search-results-table thead th{z-index:1;position:sticky;top:0}.issue-search-sort-indicator{color:var(--pb-interactive)}.issue-search-release-link{color:var(--pb-text);transition:color .14s}.issue-search-release-link:hover{color:var(--pb-interactive)}.issue-search-release-title{text-overflow:ellipsis;white-space:nowrap;max-width:320px;color:inherit;font-weight:600;display:block;overflow:hidden}.issue-search-release-title-muted{color:var(--pb-text-dim)}.issue-search-release-match{color:var(--pb-text-dim);margin-top:.125rem;font-size:.64rem}.issue-search-action-row{justify-content:flex-end;align-items:center;gap:.25rem;display:inline-flex}.issue-search-rejected-row td{opacity:.45}.issue-search-rejected-label{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.64rem}.issue-search-empty-state{text-align:center;padding:2.5rem 1.25rem}.issue-search-empty-icon{width:2.25rem;height:2.25rem;color:var(--pb-text-dim);margin-inline:auto}.issue-search-empty-title{color:var(--pb-text);margin-top:.625rem;font-size:.85rem;font-weight:600}.issue-search-empty-copy{color:var(--pb-text-sec);margin-top:.25rem;font-size:.78rem}.downloads-release-name{max-width:260px;color:var(--pb-text);font-weight:600;display:block}.downloads-release-meta{color:var(--pb-text-dim);margin-top:.2rem;font-size:.72rem}.downloads-issue-link{color:var(--pb-interactive);transition:color .14s}.downloads-issue-link:hover{color:var(--pb-interactive-hover)}.table-mono-dim,td.table-mono-dim,.downloads-mono-dim-cell,td.downloads-mono-dim-cell{font-family:JetBrains Mono,monospace;font-size:.72rem}.table-mono-dim{color:var(--pb-text-dim)}.downloads-table td.downloads-mono-cell,.downloads-table td.downloads-mono-dim-cell,.downloads-table .downloads-mono-cell,.downloads-table .downloads-mono-dim-cell{font-variant-numeric:tabular-nums;font-family:JetBrains Mono,monospace}.downloads-table td.downloads-mono-cell,.downloads-table .downloads-mono-cell{color:var(--pb-text-secondary);font-size:.75rem;font-weight:400;line-height:1.2}.downloads-table td.downloads-mono-dim-cell,.downloads-table .downloads-mono-dim-cell{color:var(--pb-text-dim);font-size:.72rem}.downloads-muted-text{color:var(--pb-text-dim)}.downloads-progress-cell{align-items:center;gap:.375rem;display:inline-flex}.downloads-progress-track{border:1px solid var(--pb-border-subtle);background:var(--pb-text-secondary);border-radius:2px;width:100px;height:6px;display:inline-block;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.downloads-progress-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.downloads-progress-track{vertical-align:middle}.downloads-progress-fill{height:100%;display:block;position:relative}.downloads-progress-fill.is-blue{background:linear-gradient(90deg, var(--pb-info), var(--pb-info))}@supports (color:color-mix(in lab, red, red)){.downloads-progress-fill.is-blue{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-info) 45%, transparent), var(--pb-info))}}.downloads-progress-fill.is-amber{background:linear-gradient(90deg, var(--pb-warning), var(--pb-warning))}@supports (color:color-mix(in lab, red, red)){.downloads-progress-fill.is-amber{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-warning) 45%, transparent), var(--pb-warning))}}.downloads-progress-fill.is-green{background:linear-gradient(90deg, var(--pb-success), var(--pb-success))}@supports (color:color-mix(in lab, red, red)){.downloads-progress-fill.is-green{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-success) 45%, transparent), var(--pb-success))}}.downloads-progress-fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}@keyframes downloads-progress-indeterminate{0%{transform:translate(-130%)}to{transform:translate(280%)}}.downloads-progress-fill.is-indeterminate,.app-progress-fill.is-indeterminate{will-change:transform;animation:1.25s ease-in-out infinite downloads-progress-indeterminate}@media (prefers-reduced-motion:reduce){.downloads-progress-fill.is-indeterminate,.app-progress-fill.is-indeterminate{opacity:.7;animation:none;transform:translate(60%)}}.downloads-progress-pct{color:var(--pb-info);font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:600}.downloads-progress-pct-warning{color:var(--pb-warning)}.downloads-progress-pct-success{color:var(--pb-success)}.downloads-progress-pct-muted{color:var(--pb-text-dim)}.downloads-led{border-radius:999px;width:8px;height:8px;display:inline-block}.downloads-led-green{background:var(--pb-success);box-shadow:0 0 5px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.downloads-led-green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.downloads-led-blue{background:var(--pb-info);box-shadow:0 0 5px var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.downloads-led-blue{box-shadow:0 0 5px color-mix(in srgb, var(--pb-info) 35%, transparent)}}.downloads-led-amber{background:var(--pb-warning);box-shadow:0 0 5px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.downloads-led-amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-warning) 35%, transparent)}}.downloads-led-off{background:var(--pb-text-dim);opacity:.35}.downloads-action-group{align-items:center;gap:.25rem;display:inline-flex}@media (hover:hover) and (pointer:fine){.downloads-action-group.is-hover-reveal{opacity:0;pointer-events:none;transition:opacity .14s,transform .14s;transform:translateY(2px)}.downloads-table tbody tr:hover .downloads-action-group.is-hover-reveal,.downloads-table tbody tr:focus-within .downloads-action-group.is-hover-reveal{opacity:1;pointer-events:auto;transform:translateY(0)}}.downloads-action-btn{border:1px solid var(--pb-border-subtle);width:28px;height:28px;color:var(--pb-text-dim);background:0 0;border-radius:6px;justify-content:center;align-items:center;transition:border-color .14s,color .14s,background-color .14s;display:inline-flex}.downloads-action-btn svg{width:12px;height:12px}.import-history-action-group{gap:.375rem}.import-history-action-btn{border-radius:8px;flex-shrink:0;width:30px;height:30px}.downloads-action-btn .import-history-action-icon{shape-rendering:geometricprecision;flex-shrink:0;width:12px;height:12px;overflow:visible}.downloads-action-btn .import-history-action-icon--detail{width:14px;height:14px}.downloads-action-btn:hover{border-color:var(--pb-interactive);background:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.downloads-action-btn:hover{background:color-mix(in srgb, var(--pb-info) 10%, transparent)}}.downloads-action-btn:hover{color:var(--pb-interactive)}.downloads-action-btn.is-danger:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.downloads-action-btn.is-danger:hover{border-color:color-mix(in srgb, var(--pb-error) 40%, transparent)}}.downloads-action-btn.is-danger:hover{background:var(--pb-error-dim);color:var(--pb-error)}.downloads-action-btn.is-warn:hover{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.downloads-action-btn.is-warn:hover{border-color:color-mix(in srgb, var(--pb-warning) 40%, transparent)}}.downloads-action-btn.is-warn:hover{background:var(--pb-warning-dim);color:var(--pb-warning)}.downloads-empty-state{text-align:center;padding:2.5rem 1.5rem}.downloads-empty-state.is-compact{padding:1.5rem 1.25rem}.downloads-empty-state.is-history{padding:2rem 1.5rem}.downloads-empty-icon{width:40px;height:40px;color:var(--pb-text-dim);margin:0 auto}.downloads-empty-title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text);margin-top:.75rem;font-family:Syne,sans-serif;font-size:.88rem;font-weight:700}.downloads-empty-copy{color:var(--pb-text-dim);margin-top:.4rem;font-size:.78rem}.intervention-bulk-bar{border-top:1px solid var(--pb-border-subtle);border-bottom:1px solid var(--pb-border-subtle);background:var(--pb-info-bg);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;padding:.625rem .875rem;display:flex}@supports (color:color-mix(in lab, red, red)){.intervention-bulk-bar{background:color-mix(in srgb, var(--pb-info-bg) 40%, var(--pb-bg-surface))}}.intervention-bulk-left{flex-wrap:wrap;align-items:center;gap:.625rem;display:inline-flex}.intervention-bulk-count{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.72rem}.intervention-bulk-actions{flex-wrap:wrap;align-items:center;gap:.5rem;display:inline-flex}.downloads-footer-strip{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);color:var(--pb-text-dim);border-radius:10px;flex-wrap:wrap;align-items:center;gap:.375rem 1.5rem;margin-top:1.25rem;padding:.625rem 1rem;font-family:JetBrains Mono,monospace;font-size:.7rem;display:flex}.downloads-footer-strip strong{color:var(--pb-text);font-weight:600}.downloads-history-toolbar{z-index:2;border:0;border-bottom:1px solid var(--pb-border);border-top-left-radius:inherit;border-top-right-radius:inherit;background:var(--pb-bg-surface);box-shadow:none;border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative}.downloads-toolbar{border-bottom:1px solid var(--pb-border);background:var(--pb-bg-surface);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;padding:.625rem .875rem;display:flex}.downloads-history-toolbar>.downloads-toolbar{border-top-left-radius:inherit;border-top-right-radius:inherit;border-bottom-right-radius:0;border-bottom-left-radius:0}.downloads-toolbar-left{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.downloads-toolbar-search,.downloads-toolbar-select{border:1px solid var(--pb-border);background:var(--pb-bg-card);height:30px;color:var(--pb-text);border-radius:8px;outline:none;font-size:.78rem}.downloads-toolbar-search{width:200px;padding:0 .625rem}.downloads-toolbar-select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2374675B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-position:right 8px center;background-repeat:no-repeat;padding:0 2rem 0 .625rem}[data-theme=dark] .downloads-toolbar-select{background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%239AAABA' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}.downloads-toolbar-search:focus,.downloads-toolbar-select:focus{border-color:var(--pb-interactive)}.downloads-clear-btn{background:var(--pb-error);color:var(--pb-text-inverse);white-space:nowrap;border:0;border-radius:8px;align-items:center;gap:.3125rem;padding:.375rem .75rem;font-size:.75rem;font-weight:600;transition:opacity .14s;display:inline-flex}.downloads-clear-btn svg{width:12px;height:12px}.downloads-clear-btn:hover{opacity:.92}.downloads-sort-btn{width:100%;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;text-align:inherit;text-transform:inherit;justify-content:flex-start;align-items:center;gap:.25rem;transition:color .14s;display:inline-flex}.import-review-cv-year-header{width:5.75rem;min-width:5.75rem}.import-review-cv-year-header .downloads-sort-btn{white-space:nowrap}.import-review-cv-year-header .downloads-sort-chevron{flex-shrink:0}.downloads-table th.is-center .downloads-sort-btn{text-align:center;justify-content:center}.downloads-table th.is-right .downloads-sort-btn{text-align:right;justify-content:flex-end}.downloads-sort-btn:hover,.downloads-sort-btn.is-active{color:var(--pb-text)}.downloads-sort-chevron{opacity:.35;width:12px;height:12px}.downloads-sort-btn.is-active .downloads-sort-chevron{opacity:1}.downloads-sort-chevron.is-desc{transform:rotate(180deg)}.downloads-error-row td{border-bottom:1px solid var(--pb-border-subtle);background:var(--pb-error-dim);padding:.5rem .875rem}@supports (color:color-mix(in lab, red, red)){.downloads-error-row td{background:color-mix(in srgb, var(--pb-error-dim) 50%, var(--pb-bg-card))}}.downloads-error-row td{color:var(--pb-error);font-size:.75rem}.downloads-error-content{align-items:flex-start;gap:.5rem;display:flex}.downloads-error-content svg{flex-shrink:0;width:14px;height:14px;margin-top:1px}.search-history-detail-row td{border-bottom:1px solid var(--pb-border-subtle);background:var(--pb-bg-surface);padding:.75rem .875rem}@supports (color:color-mix(in lab, red, red)){.search-history-detail-row td{background:color-mix(in srgb, var(--pb-bg-surface) 70%, var(--pb-bg-card))}}.search-history-detail-card{border:1px solid var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.search-history-detail-card{border:1px solid color-mix(in srgb, var(--pb-border) 78%, transparent)}}.search-history-detail-card{background:var(--pb-bg-card-hover);border-radius:.75rem}@supports (color:color-mix(in lab, red, red)){.search-history-detail-card{background:color-mix(in srgb, var(--pb-bg-card-hover) 55%, transparent)}}.search-history-detail-card{padding:.625rem .75rem}.search-history-detail-label{letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);font-size:10px;font-weight:600}.search-history-diagnostics-shell{border:1px solid var(--pb-warning);overflow:hidden}@supports (color:color-mix(in lab, red, red)){.search-history-diagnostics-shell{border:1px solid color-mix(in srgb, var(--pb-warning) 30%, transparent)}}.search-history-diagnostics-shell{background:var(--pb-warning-dim);border-radius:.75rem}@supports (color:color-mix(in lab, red, red)){.search-history-diagnostics-shell{background:color-mix(in srgb, var(--pb-warning-dim) 35%, transparent)}}.downloads-pagination{border-top:1px solid var(--pb-border-subtle);padding:.5rem .875rem}@media (max-width:1024px){.downloads-header{gap:1rem}.downloads-table-wrap{overflow-x:auto}.downloads-table-wrap.is-clipped{overflow:auto hidden}.downloads-table{min-width:760px}}@media (max-width:640px){.downloads-header-main{gap:.875rem}.downloads-gauges{gap:.625rem}.downloads-toolbar-search{width:100%}}.comic-reader{--pb-reader-canvas:#070b12;--pb-reader-surface:#0d141ef0;--pb-reader-surface-strong:#0d141efa;--pb-reader-text:#f7f1e8;--pb-reader-text-muted:#cbd5e1;--pb-reader-text-dim:#9aaaba;--pb-reader-interactive:#8fb9ee;--pb-reader-danger:#e38473;--pb-reader-warning:#f2c98d;background:var(--pb-reader-canvas);width:100vw;max-width:none;height:100dvh;max-height:none;color:var(--pb-reader-text);border:0;margin:0;padding:0;position:fixed;inset:0;overflow:hidden}.comic-reader::backdrop{background:var(--pb-reader-canvas)}.comic-reader__shell{background:var(--pb-reader-canvas);width:100%;min-width:0;height:100%;min-height:0;color:var(--pb-reader-text);grid-template-rows:auto minmax(0,1fr) auto;font-family:DM Sans,sans-serif;display:grid;overflow:hidden}.comic-reader__topbar,.comic-reader__controls{z-index:20;background:var(--pb-reader-surface);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-color:#cbd5e129;transition:opacity .14s;position:relative}.comic-reader__topbar{min-height:58px;padding:calc(.45rem + env(safe-area-inset-top,0px)) max(.75rem, env(safe-area-inset-right,0px)) .45rem max(.75rem, env(safe-area-inset-left,0px));border-bottom-width:1px;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:.75rem;display:grid}.comic-reader__identity{min-width:0}.comic-reader__identity h2,.comic-reader__identity p{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.comic-reader__identity h2{color:var(--pb-reader-text);margin:0;font-family:Bricolage Grotesque,sans-serif;font-size:.95rem;font-weight:800;line-height:1.2}.comic-reader__identity p{color:var(--pb-reader-text-dim);margin:.1rem 0 0;font-size:.72rem;line-height:1.2}.comic-reader__top-actions,.comic-reader__issue-navigation,.comic-reader__navigation,.comic-reader__sizing,.comic-reader__state-actions,.comic-reader__help-heading{align-items:center;display:flex}.comic-reader__top-actions,.comic-reader__issue-navigation,.comic-reader__navigation,.comic-reader__sizing,.comic-reader__state-actions{gap:.4rem}.comic-reader__issue-navigation{min-width:0}.comic-reader__issue-navigation .comic-reader__button{gap:.35rem}.comic-reader__button,.comic-reader__zoom-label,.comic-reader__fit-select,.comic-reader__page-jump input{min-height:44px;color:var(--pb-reader-text);font:inherit;background:#f7f1e812;border:1px solid #cbd5e138;border-radius:.55rem}.comic-reader__button,.comic-reader__zoom-label{white-space:nowrap;cursor:pointer;justify-content:center;align-items:center;min-width:44px;padding:.55rem .7rem;font-size:.76rem;font-weight:700;line-height:1;display:inline-flex}.comic-reader__button:hover,.comic-reader__zoom-label:hover,.comic-reader__button[aria-pressed=true]{background:#8fb9ee29;border-color:#8fb9ee8c}.comic-reader__button:focus-visible,.comic-reader__zoom-label:focus-visible,.comic-reader__fit-select:focus-visible,.comic-reader__page-jump input:focus-visible,.comic-reader__viewport:focus-visible{outline:2px solid var(--pb-reader-interactive);outline-offset:-2px}.comic-reader__button:disabled{cursor:default;opacity:.38}.comic-reader__button svg{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.8px;width:19px;height:19px}.comic-reader__button--prominent{background:#8fb9ee1f;border-color:#8fb9ee61}.comic-reader__viewport{z-index:1;overscroll-behavior:contain;background:var(--pb-reader-canvas);scrollbar-color:#9aaaba8c transparent;touch-action:pan-y pinch-zoom;min-width:0;min-height:0;position:relative;overflow:auto}.comic-reader__viewport.is-pannable{touch-action:auto}.comic-reader__page-stage{justify-content:center;align-items:center;width:100%;min-width:100%;height:100%;min-height:100%;padding:.75rem;display:flex}.comic-reader__page{z-index:1;object-fit:contain;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;flex:none;max-width:none;max-height:none;display:block;position:relative;box-shadow:0 8px 30px #00000061}.comic-reader__page--page{width:auto;max-width:100%;height:auto;max-height:100%}.comic-reader__page--width{align-self:flex-start;width:100%;height:auto}.comic-reader__page--height{width:auto;height:100%}.comic-reader__page--actual{align-self:flex-start;width:auto;height:auto}.comic-reader__state,.comic-reader__page-busy{z-index:8;text-align:center;justify-content:center;align-items:center;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.comic-reader__state{width:min(90vw,32rem);color:var(--pb-reader-text-muted);background:#0d141ef5;border:1px solid #cbd5e133;border-radius:.9rem;flex-direction:column;gap:.5rem;padding:1.5rem;box-shadow:0 20px 56px #00000075}.comic-reader__state strong{color:var(--pb-reader-text);font-size:1rem}.comic-reader__state--error{border-color:var(--pb-reader-danger)}@supports (color:color-mix(in lab, red, red)){.comic-reader__state--error{border-color:color-mix(in srgb, var(--pb-reader-danger) 45%, transparent)}}.comic-reader__state-actions{flex-wrap:wrap;justify-content:center;margin-top:.5rem}.comic-reader__completion{z-index:9;right:max(.75rem, env(safe-area-inset-right,0px));bottom:max(.75rem, env(safe-area-inset-bottom,0px));width:min(24rem,100% - 1.5rem);color:var(--pb-reader-text-muted);background:#0d141ef5;border:1px solid #8fb9ee6b;border-radius:.8rem;padding:.85rem;position:absolute;box-shadow:0 16px 42px #0000006b}.comic-reader__completion strong{color:var(--pb-reader-text);font-family:Bricolage Grotesque,sans-serif;font-size:1rem;display:block}.comic-reader__completion p{margin:.35rem 0 0;font-size:.78rem}.comic-reader__page-busy{color:var(--pb-reader-text-muted);background:#0d141ee6;border-radius:999px;padding:.45rem .7rem;font-size:.72rem;top:1rem;transform:translate(-50%)}.comic-reader__spinner{border:2px solid #8fb9ee40;border-top-color:var(--pb-reader-interactive);border-radius:999px;width:1.5rem;height:1.5rem;animation:.7s linear infinite comic-reader-spin}@keyframes comic-reader-spin{to{transform:rotate(360deg)}}.comic-reader__tap-zone{z-index:4;background:0 0;border:0;margin:0;padding:0;position:absolute;top:0;bottom:0}.comic-reader__tap-zone--left{width:30%;left:0}.comic-reader__tap-zone--center{width:40%;left:30%}.comic-reader__tap-zone--right{width:30%;right:0}.comic-reader__controls{min-width:0;padding:.45rem max(.75rem, env(safe-area-inset-right,0px)) calc(.45rem + env(safe-area-inset-bottom,0px)) max(.75rem, env(safe-area-inset-left,0px));border-top-width:1px;justify-content:space-between;align-items:center;gap:.75rem;display:flex}.comic-reader__navigation,.comic-reader__sizing{min-width:0;position:relative}.comic-reader__page-jump{color:var(--pb-reader-text-dim);white-space:nowrap;align-items:center;gap:.35rem;font-size:.75rem;display:inline-flex}.comic-reader__page-jump input{text-align:center;appearance:textfield;width:3.4rem;padding:.4rem}.comic-reader__page-jump input::-webkit-outer-spin-button{appearance:none;margin:0}.comic-reader__page-jump input::-webkit-inner-spin-button{appearance:none;margin:0}.comic-reader__fit-select{color-scheme:dark;max-width:8.5rem;padding:0 1.9rem 0 .65rem;display:none}.comic-reader__zoom-label{min-width:3.75rem;color:var(--pb-reader-text-muted);padding-inline:.45rem}.comic-reader__input-error{border:1px solid var(--pb-reader-danger);width:max-content;max-width:min(20rem,100vw - 1.5rem);padding:.35rem .5rem;position:absolute;bottom:calc(100% + .35rem);left:50%;transform:translate(-50%)}@supports (color:color-mix(in lab, red, red)){.comic-reader__input-error{border:1px solid color-mix(in srgb, var(--pb-reader-danger) 42%, transparent)}}.comic-reader__input-error{background:var(--pb-reader-surface-strong);color:var(--pb-reader-danger);border-radius:.45rem;font-size:.7rem}.comic-reader__help{z-index:30;background:var(--pb-reader-surface-strong);border:1px solid #8fb9ee66;border-radius:.9rem;width:min(92vw,34rem);max-height:min(78dvh,38rem);padding:1rem;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 20px 56px #0009}.comic-reader__help-heading{justify-content:space-between;gap:1rem}.comic-reader__help h3{color:var(--pb-reader-text);margin:0;font-family:Bricolage Grotesque,sans-serif;font-size:1rem}.comic-reader__help dl{gap:.15rem;margin:.75rem 0 0;display:grid}.comic-reader__help dl>div{border-top:1px solid #cbd5e11f;grid-template-columns:minmax(7.5rem,.7fr) minmax(0,1.3fr);gap:.75rem;padding:.45rem 0;display:grid}.comic-reader__help dt{color:var(--pb-reader-interactive);font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:600}.comic-reader__help dd{color:var(--pb-reader-text-muted);margin:0;font-size:.75rem}.comic-reader__save-status{z-index:40;right:max(.75rem, env(safe-area-inset-right,0px));bottom:calc(4.1rem + env(safe-area-inset-bottom,0px));max-width:min(22rem,100vw - 1.5rem);color:var(--pb-reader-warning);background:#231b11f5;border:1px solid #d7a15b66;border-radius:.55rem;margin:0;padding:.45rem .65rem;font-size:.72rem;position:absolute}.comic-reader__shell.is-controls-hidden .comic-reader__topbar,.comic-reader__shell.is-controls-hidden .comic-reader__controls{visibility:hidden;opacity:0;pointer-events:none}@media (max-width:900px){.comic-reader__issue-navigation .comic-reader__button span{clip:rect(0, 0, 0, 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.comic-reader__fit-page,.comic-reader__fit-wide{display:none}.comic-reader__fit-select{display:block}}@media (max-width:600px){.comic-reader__topbar{grid-template-columns:auto minmax(0,1fr) auto;gap:.5rem;min-height:54px}.comic-reader__issue-navigation{grid-area:2/1/auto/-1;justify-content:center}.comic-reader__top-actions{grid-area:1/3}.comic-reader__desktop-action{display:none}.comic-reader__controls{flex-wrap:wrap;justify-content:center;gap:.35rem .6rem}.comic-reader__navigation,.comic-reader__sizing{justify-content:center}.comic-reader__page-stage{padding:.4rem}.comic-reader__help dl>div{grid-template-columns:1fr;gap:.15rem}}@media (max-height:480px){.comic-reader__topbar,.comic-reader__controls{padding-top:.25rem;padding-bottom:.25rem}.comic-reader__topbar{min-height:48px}}@media (prefers-reduced-motion:reduce){.comic-reader__topbar,.comic-reader__controls{transition:none;visibility:visible!important;opacity:1!important;pointer-events:auto!important}.comic-reader__spinner{animation-duration:1.4s}}.reading-workspace{flex-direction:column;gap:1rem;display:flex}.reading-workspace-header,.dashboard-reading-shelf-header{justify-content:space-between;align-items:flex-end;gap:1rem;display:flex}.reading-page-size{align-items:center;gap:.65rem;display:flex}.reading-tabs{border-bottom:1px solid var(--pb-border);gap:.4rem;display:flex;overflow-x:auto}.reading-tab{color:var(--pb-text-tertiary);letter-spacing:.08em;text-transform:uppercase;flex:none;padding:.65rem .85rem;font-family:Syne,sans-serif;font-size:.7rem;font-weight:700;position:relative}.reading-tab:after{content:"";background:0 0;border-radius:999px;height:2px;position:absolute;bottom:-1px;left:.65rem;right:.65rem}.reading-tab:hover,.reading-tab:focus-visible,.reading-tab.is-active{color:var(--pb-interactive)}.reading-tab.is-active:after{background:var(--pb-interactive)}.reading-card-grid,.dashboard-reading-card-grid{grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.85rem;display:grid}.dashboard-reading-card-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.reading-card{border:1px solid var(--pb-border);background:linear-gradient(145deg, var(--pb-interactive), transparent 42%), var(--pb-bg-card);border-radius:12px;grid-template-columns:92px minmax(0,1fr);min-width:0;display:grid;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.reading-card{background:linear-gradient(145deg, color-mix(in srgb, var(--pb-interactive) 5%, transparent), transparent 42%), var(--pb-bg-card)}}.reading-card{box-shadow:var(--pb-shadow-1)}.reading-card:not(.reading-card-dashboard){min-height:15.25rem}.reading-card-cover-link,.reading-card-cover-frame{min-height:138px;display:block}.reading-card-cover-frame{border-right:1px solid var(--pb-border-subtle);background:var(--pb-bg-shell);height:100%;position:relative;overflow:hidden}.reading-card-cover{object-fit:cover;width:100%;height:100%}.reading-card-cover-placeholder{width:100%;height:100%;color:var(--pb-text-dim);justify-content:center;align-items:center;display:flex}.reading-card-cover-placeholder svg{width:2.25rem;height:2.25rem}.reading-card-body{flex-direction:column;gap:.7rem;min-width:0;padding:.85rem;display:flex}.reading-card-series{color:var(--pb-text-primary);text-overflow:ellipsis;white-space:nowrap;font-family:Syne,sans-serif;font-size:.78rem;font-weight:700;display:block;overflow:hidden}.reading-card-series:hover,.reading-card-series:focus-visible{color:var(--pb-interactive)}.reading-card-issue{min-width:0;color:var(--pb-text-tertiary);gap:.4rem;margin-top:.2rem;font-size:.72rem;display:flex}.reading-card-issue>span:first-child,.reading-card-state{font-family:JetBrains Mono,monospace}.reading-card-issue-title{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.reading-card-state{color:var(--pb-text-secondary);font-size:.67rem;font-weight:600}.reading-card-progress{background:var(--pb-border-subtle);border-radius:999px;height:3px;margin-top:.4rem;overflow:hidden}.reading-card-progress span{border-radius:inherit;background:var(--pb-interactive);height:100%;display:block}.reading-card-state-region{min-height:1.75rem}.reading-card-actions{grid-template-rows:repeat(2,minmax(2.25rem,auto));grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch;gap:.4rem;margin-top:auto;display:grid}.reading-card-view-read .reading-card-actions{grid-template-columns:minmax(0,2fr) minmax(0,3fr)}.reading-card-view-read .reading-card-primary-action,.reading-card-view-read .reading-card-completion-action{white-space:nowrap;padding-inline:.5rem}.reading-card-primary-action,.reading-card-completion-action,.reading-card-queue-action{width:100%;min-width:0}.reading-card-queue-action{grid-column:1/-1}.reading-card-queue-action-placeholder{visibility:hidden;pointer-events:none;grid-column:1/-1;min-height:2.25rem}.reading-card-mutation-status{min-height:1rem;color:var(--pb-error);font-size:.68rem}.reading-empty-state{border:1px dashed var(--pb-border);text-align:center;background:var(--pb-bg-card);border-radius:12px;flex-direction:column;justify-content:center;align-items:center;min-height:260px;padding:2.5rem 1.5rem;display:flex}@supports (color:color-mix(in lab, red, red)){.reading-empty-state{background:color-mix(in srgb, var(--pb-bg-card) 78%, transparent)}}.reading-empty-icon{width:2.75rem;height:2.75rem;color:var(--pb-text-dim)}.reading-empty-state h2{color:var(--pb-text-primary);margin-top:.85rem;font-family:Syne,sans-serif;font-size:.95rem;font-weight:700}.reading-empty-state p{max-width:34rem;color:var(--pb-text-tertiary);margin-top:.35rem;font-size:.78rem}.dashboard-reading-shelf{border:1px solid var(--pb-border);background:var(--pb-bg-card);border-radius:12px;padding:.85rem}@supports (color:color-mix(in lab, red, red)){.dashboard-reading-shelf{background:color-mix(in srgb, var(--pb-bg-card) 88%, transparent)}}.dashboard-reading-shelf{box-shadow:var(--pb-shadow-1)}.dashboard-reading-shelf-header h2{color:var(--pb-text-primary);margin-top:.1rem;font-family:Syne,sans-serif;font-size:1rem;font-weight:700}.dashboard-reading-shelf-link{color:var(--pb-interactive);font-size:.72rem;font-weight:700}.dashboard-reading-shelf-link:hover,.dashboard-reading-shelf-link:focus-visible{text-decoration:underline}.reading-card-dashboard{grid-template-columns:76px minmax(0,1fr)}.reading-card-dashboard .reading-card-cover-link,.reading-card-dashboard .reading-card-cover-frame{min-height:124px}.reading-card-dashboard .reading-card-body{gap:.5rem;padding:.7rem}@media (max-width:1180px){.dashboard-reading-card-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width:640px){.reading-workspace-header{flex-direction:column;align-items:flex-start}.reading-card-grid,.dashboard-reading-card-grid{grid-template-columns:1fr}.reading-card{grid-template-columns:82px minmax(0,1fr)}.reading-card-cover-link,.reading-card-cover-frame{min-height:128px}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--color-red-300:oklch(80.8% .114 19.571);--color-slate-950:oklch(12.9% .042 264.695);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-xs:4px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:DM Sans,sans-serif;line-height:1.5}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:JetBrains Mono,Fira Code,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:root,[data-theme=dark]{--pb-surface-app:#101824;--pb-surface-shell:#141e2c;--pb-surface-card:#172231;--pb-surface-raised:#1d2a3d;--pb-surface-input:#132030;--pb-surface-overlay:#070b12b8;--pb-surface-selected:#8fb9ee29;--pb-text-primary:#f7f1e8;--pb-text-secondary:#cbd5e1;--pb-text-tertiary:#9aaaba;--pb-text-inverse:#101824;--pb-border-subtle:#cbd5e11f;--pb-border-default:#cbd5e133;--pb-border-strong:#cbd5e152;--pb-interactive:#8fb9ee;--pb-interactive-hover:#7da9df;--pb-interactive-active:#6a96c8;--pb-interactive-selected:#8fb9ee2e;--pb-brand:#c6a17b;--pb-brand-muted:#c6a17b29;--pb-status-success:#68b88b;--pb-status-warning:#d7a15b;--pb-status-danger:#e38473;--pb-status-info:#8fb9ee;--pb-focus-ring:#8fb9ee47;--pb-focus-outline:#8fb9ee85;--pb-selection:#8fb9ee38;--pb-shadow-0:none;--pb-shadow-1:0 1px 2px #070b1238, 0 10px 24px #070b1229;--pb-shadow-2:0 2px 6px #070b1247, 0 18px 40px #070b1238;--pb-shadow-overlay:0 20px 56px #00000075;--pb-brand-hover:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-hover:color-mix(in srgb, var(--pb-brand) 84%, black)}}:root,[data-theme=dark]{--pb-brand-dim:var(--pb-brand-muted);--pb-brand-border:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-border:color-mix(in srgb, var(--pb-brand) 28%, transparent)}}:root,[data-theme=dark]{--pb-brand-signal:var(--pb-status-warning);--pb-brand-signal-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-signal-dim:color-mix(in srgb, var(--pb-status-warning) 16%, transparent)}}:root,[data-theme=dark]{--pb-brand-signal-hover:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-signal-hover:color-mix(in srgb, var(--pb-status-warning) 22%, transparent)}}:root,[data-theme=dark]{--pb-brand-signal-border:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-brand-signal-border:color-mix(in srgb, var(--pb-status-warning) 28%, transparent)}}:root,[data-theme=dark]{--pb-interactive-dim:var(--pb-interactive-selected);--pb-interactive-border:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-interactive-border:color-mix(in srgb, var(--pb-interactive) 30%, transparent)}}:root,[data-theme=dark]{--pb-success:var(--pb-status-success);--pb-success-dim:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-success-dim:color-mix(in srgb, var(--pb-status-success) 18%, transparent)}}:root,[data-theme=dark]{--pb-warning:var(--pb-status-warning);--pb-warning-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-warning-dim:color-mix(in srgb, var(--pb-status-warning) 20%, transparent)}}:root,[data-theme=dark]{--pb-error:var(--pb-status-danger);--pb-error-dim:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-error-dim:color-mix(in srgb, var(--pb-status-danger) 18%, transparent)}}:root,[data-theme=dark]{--pb-info:var(--pb-status-info);--pb-info-dim:var(--pb-status-info)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-info-dim:color-mix(in srgb, var(--pb-status-info) 18%, transparent)}}:root,[data-theme=dark]{--pb-purple:#a88bda;--pb-purple-dim:#a88bda2e;--pb-bg-base:var(--pb-surface-app);--pb-bg-surface:var(--pb-surface-shell);--pb-bg-card:var(--pb-surface-card);--pb-bg-card-hover:var(--pb-surface-card)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-bg-card-hover:color-mix(in srgb, var(--pb-surface-card) 72%, var(--pb-surface-raised))}}:root,[data-theme=dark]{--pb-bg-input:var(--pb-surface-input);--pb-bg-overlay:var(--pb-surface-overlay);--pb-table-row-stripe:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-table-row-stripe:color-mix(in srgb, var(--pb-bg-card-hover) 68%, transparent)}}:root,[data-theme=dark]{--pb-table-row-hover:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){:root,[data-theme=dark]{--pb-table-row-hover:color-mix(in srgb, var(--pb-bg-card-hover) 88%, transparent)}}:root,[data-theme=dark]{--pb-table-cell-x:1.25rem;--pb-table-cell-x-tight:1rem;--pb-page-footer-clearance:1.5rem;--pb-text-dim:var(--pb-text-tertiary);--pb-border:var(--pb-border-default);--pb-border-hover:var(--pb-border-strong)}[data-theme=light]{--pb-surface-app:#f6f1e8;--pb-surface-shell:#fbf6ee;--pb-surface-card:#fffcf7;--pb-surface-raised:#fff;--pb-surface-input:#fffdfa;--pb-surface-overlay:#1a16136b;--pb-surface-selected:#2f5e8c1f;--pb-text-primary:#1e1a17;--pb-text-secondary:#51473d;--pb-text-tertiary:#74675b;--pb-text-inverse:#f7f1e8;--pb-border-subtle:#51473d24;--pb-border-default:#51473d33;--pb-border-strong:#51473d57;--pb-interactive:#2f5e8c;--pb-interactive-hover:#274f76;--pb-interactive-active:#21435f;--pb-interactive-selected:#2f5e8c24;--pb-brand:#855e3d;--pb-brand-muted:#8c68471f;--pb-status-success:#2e6a4f;--pb-status-warning:#8a5a1f;--pb-status-danger:#b24432;--pb-status-info:#2f5e8c;--pb-focus-ring:#2f5e8c42;--pb-focus-outline:#2f5e8cb8;--pb-selection:#2f5e8c33;--pb-shadow-0:none;--pb-shadow-1:0 1px 2px #1a161314, 0 10px 24px #1a16130f;--pb-shadow-2:0 2px 6px #1a16131a, 0 18px 40px #1a161314;--pb-shadow-overlay:0 20px 56px #1a161333;--pb-brand-hover:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-hover:color-mix(in srgb, var(--pb-brand) 84%, black)}}[data-theme=light]{--pb-brand-dim:var(--pb-brand-muted);--pb-brand-border:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-border:color-mix(in srgb, var(--pb-brand) 28%, transparent)}}[data-theme=light]{--pb-brand-signal:var(--pb-status-warning);--pb-brand-signal-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-signal-dim:color-mix(in srgb, var(--pb-status-warning) 12%, transparent)}}[data-theme=light]{--pb-brand-signal-hover:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-signal-hover:color-mix(in srgb, var(--pb-status-warning) 18%, transparent)}}[data-theme=light]{--pb-brand-signal-border:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-brand-signal-border:color-mix(in srgb, var(--pb-status-warning) 24%, transparent)}}[data-theme=light]{--pb-interactive-dim:var(--pb-interactive-selected);--pb-interactive-border:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-interactive-border:color-mix(in srgb, var(--pb-interactive) 28%, transparent)}}[data-theme=light]{--pb-success:var(--pb-status-success);--pb-success-dim:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-success-dim:color-mix(in srgb, var(--pb-status-success) 12%, transparent)}}[data-theme=light]{--pb-warning:var(--pb-status-warning);--pb-warning-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-warning-dim:color-mix(in srgb, var(--pb-status-warning) 14%, transparent)}}[data-theme=light]{--pb-error:var(--pb-status-danger);--pb-error-dim:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-error-dim:color-mix(in srgb, var(--pb-status-danger) 12%, transparent)}}[data-theme=light]{--pb-info:var(--pb-status-info);--pb-info-dim:var(--pb-status-info)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-info-dim:color-mix(in srgb, var(--pb-status-info) 12%, transparent)}}[data-theme=light]{--pb-purple:#7b5c91;--pb-purple-dim:#7b5c9124;--pb-bg-base:var(--pb-surface-app);--pb-bg-surface:var(--pb-surface-shell);--pb-bg-card:var(--pb-surface-card);--pb-bg-card-hover:var(--pb-surface-card)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-bg-card-hover:color-mix(in srgb, var(--pb-surface-card) 72%, var(--pb-surface-raised))}}[data-theme=light]{--pb-bg-input:var(--pb-surface-input);--pb-bg-overlay:var(--pb-surface-overlay);--pb-table-row-stripe:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-table-row-stripe:color-mix(in srgb, var(--pb-surface-shell) 40%, transparent)}}[data-theme=light]{--pb-table-row-hover:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){[data-theme=light]{--pb-table-row-hover:color-mix(in srgb, var(--pb-surface-shell) 58%, transparent)}}[data-theme=light]{--pb-table-cell-x:1.25rem;--pb-table-cell-x-tight:1rem;--pb-text-dim:var(--pb-text-tertiary);--pb-border:var(--pb-border-default);--pb-border-hover:var(--pb-border-strong)}@media (prefers-color-scheme:light){:root:not([data-theme]){--pb-surface-app:#f6f1e8;--pb-surface-shell:#fbf6ee;--pb-surface-card:#fffcf7;--pb-surface-raised:#fff;--pb-surface-input:#fffdfa;--pb-surface-overlay:#1a16136b;--pb-surface-selected:#2f5e8c1f;--pb-text-primary:#1e1a17;--pb-text-secondary:#51473d;--pb-text-tertiary:#74675b;--pb-text-inverse:#f7f1e8;--pb-border-subtle:#51473d24;--pb-border-default:#51473d33;--pb-border-strong:#51473d57;--pb-interactive:#2f5e8c;--pb-interactive-hover:#274f76;--pb-interactive-active:#21435f;--pb-interactive-selected:#2f5e8c24;--pb-brand:#855e3d;--pb-brand-muted:#8c68471f;--pb-status-success:#2e6a4f;--pb-status-warning:#8a5a1f;--pb-status-danger:#b24432;--pb-status-info:#2f5e8c;--pb-focus-ring:#2f5e8c42;--pb-focus-outline:#2f5e8cb8;--pb-selection:#2f5e8c33;--pb-shadow-0:none;--pb-shadow-1:0 1px 2px #1a161314, 0 10px 24px #1a16130f;--pb-shadow-2:0 2px 6px #1a16131a, 0 18px 40px #1a161314;--pb-shadow-overlay:0 20px 56px #1a161333;--pb-brand-hover:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-hover:color-mix(in srgb, var(--pb-brand) 84%, black)}}:root:not([data-theme]){--pb-brand-dim:var(--pb-brand-muted);--pb-brand-border:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-border:color-mix(in srgb, var(--pb-brand) 28%, transparent)}}:root:not([data-theme]){--pb-brand-signal:var(--pb-status-warning);--pb-brand-signal-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-signal-dim:color-mix(in srgb, var(--pb-status-warning) 12%, transparent)}}:root:not([data-theme]){--pb-brand-signal-hover:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-signal-hover:color-mix(in srgb, var(--pb-status-warning) 18%, transparent)}}:root:not([data-theme]){--pb-brand-signal-border:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-brand-signal-border:color-mix(in srgb, var(--pb-status-warning) 24%, transparent)}}:root:not([data-theme]){--pb-interactive-dim:var(--pb-interactive-selected);--pb-interactive-border:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-interactive-border:color-mix(in srgb, var(--pb-interactive) 28%, transparent)}}:root:not([data-theme]){--pb-success:var(--pb-status-success);--pb-success-dim:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-success-dim:color-mix(in srgb, var(--pb-status-success) 12%, transparent)}}:root:not([data-theme]){--pb-warning:var(--pb-status-warning);--pb-warning-dim:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-warning-dim:color-mix(in srgb, var(--pb-status-warning) 14%, transparent)}}:root:not([data-theme]){--pb-error:var(--pb-status-danger);--pb-error-dim:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-error-dim:color-mix(in srgb, var(--pb-status-danger) 12%, transparent)}}:root:not([data-theme]){--pb-info:var(--pb-status-info);--pb-info-dim:var(--pb-status-info)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-info-dim:color-mix(in srgb, var(--pb-status-info) 12%, transparent)}}:root:not([data-theme]){--pb-purple:#7b5c91;--pb-purple-dim:#7b5c9124;--pb-bg-base:var(--pb-surface-app);--pb-bg-surface:var(--pb-surface-shell);--pb-bg-card:var(--pb-surface-card);--pb-bg-card-hover:var(--pb-surface-card)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-bg-card-hover:color-mix(in srgb, var(--pb-surface-card) 72%, var(--pb-surface-raised))}}:root:not([data-theme]){--pb-bg-input:var(--pb-surface-input);--pb-bg-overlay:var(--pb-surface-overlay);--pb-table-row-stripe:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-table-row-stripe:color-mix(in srgb, var(--pb-surface-shell) 40%, transparent)}}:root:not([data-theme]){--pb-table-row-hover:var(--pb-surface-shell)}@supports (color:color-mix(in lab, red, red)){:root:not([data-theme]){--pb-table-row-hover:color-mix(in srgb, var(--pb-surface-shell) 58%, transparent)}}:root:not([data-theme]){--pb-table-cell-x:1.25rem;--pb-table-cell-x-tight:1rem;--pb-text-dim:var(--pb-text-tertiary);--pb-border:var(--pb-border-default);--pb-border-hover:var(--pb-border-strong)}}html,body{overscroll-behavior:none;overflow:hidden}:focus-visible{outline:2px solid var(--pb-focus-outline);outline-offset:3px;box-shadow:0 0 0 4px var(--pb-focus-ring);border-radius:4px}::selection{background-color:var(--pb-selection)}:where(a[href]:not([aria-disabled=true]),button:not(:disabled):not([aria-disabled=true]),input[type=button]:not(:disabled),input[type=submit]:not(:disabled),input[type=reset]:not(:disabled),input[type=checkbox]:not(:disabled),input[type=radio]:not(:disabled),input[type=file]:not(:disabled),input[type=image]:not(:disabled),select:not(:disabled),summary,[role=button]:not([aria-disabled=true]),[role=link]:not([aria-disabled=true]),label:has(:is(input[type=checkbox],input[type=radio],input[type=file]):not(:disabled))){cursor:pointer}:where(button:disabled,input:disabled,select:disabled,[aria-disabled=true]){cursor:not-allowed}}@layer components{.control-size-sm,.btn-sm,.chip-btn-sm,.icon-btn-sm{--pb-control-min-height:2rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.4375rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:.875rem;--pb-control-panel-item-px:.75rem;--pb-control-panel-item-py:.5rem;--pb-control-search-icon-left:.625rem;--pb-control-search-padding-left:2rem;--pb-control-search-padding-right:1.875rem;--pb-control-clear-right:.5rem}.control-size-md,.btn-md,.chip-btn,.icon-btn,.dropdown-select,.search-field,.btn-primary,.btn-warning,.btn-ghost,.btn-danger{--pb-control-min-height:2.5rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.5rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:1rem;--pb-control-panel-item-px:.75rem;--pb-control-panel-item-py:.5rem;--pb-control-search-icon-left:.75rem;--pb-control-search-padding-left:2.5rem;--pb-control-search-padding-right:2.25rem;--pb-control-clear-right:.625rem}.control-size-lg,.btn-lg,.chip-btn-lg,.icon-btn-lg{--pb-control-min-height:2.875rem;--pb-control-radius:.625rem;--pb-control-gap:.625rem;--pb-control-px:.875rem;--pb-control-py:.625rem;--pb-control-font-size:.9375rem;--pb-control-line-height:1.375rem;--pb-control-icon-size:1rem;--pb-control-panel-item-px:.875rem;--pb-control-panel-item-py:.625rem;--pb-control-search-icon-left:.875rem;--pb-control-search-padding-left:2.75rem;--pb-control-search-padding-right:2.5rem;--pb-control-clear-right:.75rem}.btn-primary.btn-sm,.btn-warning.btn-sm,.btn-ghost.btn-sm,.btn-danger.btn-sm,.btn-primary.control-size-sm,.btn-warning.control-size-sm,.btn-ghost.control-size-sm,.btn-danger.control-size-sm{--pb-control-min-height:2rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.4375rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:.875rem}.btn-primary.btn-md,.btn-warning.btn-md,.btn-ghost.btn-md,.btn-danger.btn-md,.btn-primary.control-size-md,.btn-warning.control-size-md,.btn-ghost.control-size-md,.btn-danger.control-size-md{--pb-control-min-height:2.5rem;--pb-control-radius:.5rem;--pb-control-gap:.5rem;--pb-control-px:.75rem;--pb-control-py:.5rem;--pb-control-font-size:.875rem;--pb-control-line-height:1.25rem;--pb-control-icon-size:1rem}.btn-primary.btn-lg,.btn-warning.btn-lg,.btn-ghost.btn-lg,.btn-danger.btn-lg,.btn-primary.control-size-lg,.btn-warning.control-size-lg,.btn-ghost.control-size-lg,.btn-danger.control-size-lg{--pb-control-min-height:2.875rem;--pb-control-radius:.625rem;--pb-control-gap:.625rem;--pb-control-px:.875rem;--pb-control-py:.625rem;--pb-control-font-size:.9375rem;--pb-control-line-height:1.375rem;--pb-control-icon-size:1rem}.btn-primary{background-color:var(--pb-interactive);--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:10px;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.btn-primary:hover{background-color:var(--pb-interactive-hover)}}.btn-primary:disabled{cursor:not-allowed;opacity:.5}.btn-primary{color:var(--pb-text-inverse);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.header-add-menu{flex-shrink:0;display:inline-flex;position:relative}.header-add-menu__primary{gap:calc(var(--spacing) * 2);white-space:nowrap;border-top-right-radius:0;border-bottom-right-radius:0;padding-right:.625rem}.header-add-menu__trigger{gap:calc(var(--spacing) * 2);white-space:nowrap;border-left:1px solid var(--pb-text-inverse);border-top-left-radius:0;border-bottom-left-radius:0;justify-content:space-between;min-width:7rem;padding-inline:.5rem}@supports (color:color-mix(in lab, red, red)){.header-add-menu__trigger{border-left:1px solid color-mix(in srgb, var(--pb-text-inverse) 26%, transparent)}}.header-add-menu__chevron{width:.875rem;height:.875rem;transition:transform .15s}.header-add-menu__panel{z-index:50;padding:calc(var(--spacing) * 1.5);border:1px solid var(--pb-border);background:var(--pb-bg-card);min-width:17rem;box-shadow:var(--pb-shadow-2);border-radius:.75rem;position:absolute;top:calc(100% + .5rem);left:0}.header-add-menu__item{align-items:center;gap:calc(var(--spacing) * 2.5);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);min-height:2.5rem;color:var(--pb-text);border-radius:.5rem;transition:color .15s,background-color .15s;display:flex}.header-add-menu__item:hover,.header-add-menu__item:focus-visible,.header-add-menu__item--selected{color:var(--pb-interactive);background:var(--pb-interactive-dim)}.header-add-menu__item--disabled{color:var(--pb-text-ter);cursor:not-allowed;opacity:.72}.header-add-menu__item--disabled:hover{color:var(--pb-text-ter);background:0 0}.header-add-menu__item-label{flex:1;min-width:0}.header-add-menu__check{color:var(--pb-success)}.header-add-menu__soon{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);white-space:nowrap;text-transform:uppercase;color:var(--pb-text-ter);flex-shrink:0}.header-add-menu__item svg{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);flex-shrink:0}@media (prefers-reduced-motion:reduce){.header-add-menu__chevron,.header-add-menu__item{transition:none}}.btn-ghost{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;border-radius:10px;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.btn-ghost:hover{border-color:var(--pb-border-strong);color:var(--pb-text-primary)}}.btn-ghost:disabled{cursor:not-allowed;opacity:.5}.btn-ghost{min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.btn-warning{--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:10px;justify-content:center;align-items:center;display:inline-flex}.btn-warning:disabled{cursor:not-allowed;opacity:.5}.btn-warning{background-color:var(--pb-warning);color:var(--pb-text-inverse);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.btn-warning:hover{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.btn-warning:hover{background-color:color-mix(in srgb, var(--pb-warning) 86%, var(--pb-text-primary))}}.btn-danger{justify-content:center;align-items:center;gap:var(--pb-control-gap);background-color:var(--pb-error);--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-weight:var(--font-weight-medium);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:10px;display:inline-flex}@media (hover:hover){.btn-danger:hover{--tw-brightness:brightness(90%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.btn-danger:disabled{cursor:not-allowed;opacity:.5}.btn-danger{color:var(--pb-text-inverse);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.pill,.badge-success,.badge-warning,.badge-error,.badge-info,.badge-neutral,.badge-muted,.badge-purple{justify-content:center;align-items:center;gap:var(--spacing);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);text-align:center;--tw-font-weight:var(--font-weight-medium);font-family:DM Sans,sans-serif;font-size:11px;font-weight:var(--font-weight-medium);border-radius:3.40282e38px;display:inline-flex}.pill-success,.badge-success{background-color:var(--pb-success-dim);color:var(--pb-success)}.pill-warning,.badge-warning{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.pill-error,.badge-error{background-color:var(--pb-error-dim);color:var(--pb-error)}.pill-info,.badge-info{background-color:var(--pb-info-dim);color:var(--pb-info)}.pill-neutral,.badge-neutral{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary)}.pill-muted,.badge-muted{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card-hover);color:var(--pb-text-dim)}.pill-purple,.badge-purple{background-color:var(--pb-purple-dim);color:var(--pb-purple)}.count-badge{min-width:1.25rem;min-height:1.25rem;padding-inline:calc(var(--spacing) * 1.5);--tw-leading:1;--tw-font-weight:var(--font-weight-bold);font-size:10px;line-height:1;font-weight:var(--font-weight-bold);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);border-radius:3.40282e38px;justify-content:center;align-items:center;display:inline-flex}.count-badge-info{background-color:var(--pb-info-dim);color:var(--pb-info)}.count-badge-success{background-color:var(--pb-success-dim);color:var(--pb-success)}.count-badge-warning{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.count-badge-error{background-color:var(--pb-error-dim);color:var(--pb-error)}.count-badge-neutral{background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary)}.table-shell{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card)}.table-shell-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4)}.table-shell-header-comfortable{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.table-shell-content{padding:calc(var(--spacing) * 5)}.table-shell-content-comfortable{padding:calc(var(--spacing) * 6)}.table-toolbar{gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media (min-width:64rem){.table-toolbar{flex-direction:row;justify-content:space-between;align-items:flex-end}}:where(.table-toolbar-title-block>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.table-toolbar-eyebrow{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.table-toolbar-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.table-toolbar-title-compact{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.table-toolbar-copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.table-toolbar-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.table-header-row{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.75rem;display:grid}.table-header-aside{justify-self:end}.table-summary{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));flex-wrap:wrap;display:flex}.table-scroll{overflow-x:auto}.table-base{border-collapse:collapse;width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.table-head{text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--pb-text-secondary);text-transform:uppercase}.table-head-row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border)}.table-head-cell{padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);padding-left:var(--pb-table-cell-x);padding-right:var(--pb-table-cell-x)}.table-head-cell-tight{padding-block:calc(var(--spacing) * 2.5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);padding-left:var(--pb-table-cell-x-tight);padding-right:var(--pb-table-cell-x-tight)}.table-head-cell-right{text-align:right}:where(.table-body>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--pb-border)}.table-body-striped>.table-row:nth-child(2n){background-color:var(--pb-table-row-stripe)}.table-row{color:var(--pb-text-secondary)}.table-row-hover>.table-row{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.table-row-hover>.table-row:hover{background-color:var(--pb-table-row-hover)}.table-row-hover>.table-row-detail{transition-property:none}.table-row-hover>.table-row-detail:hover{background-color:#0000}.table-cell{padding-block:calc(var(--spacing) * 3);padding-left:var(--pb-table-cell-x);padding-right:var(--pb-table-cell-x)}.table-cell-tight{padding-block:calc(var(--spacing) * 2.5);padding-left:var(--pb-table-cell-x-tight);padding-right:var(--pb-table-cell-x-tight)}.table-cell-right{text-align:right}.table-cell-muted{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary)}.table-cell-nowrap{white-space:nowrap}.table-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.table-empty{padding:calc(var(--spacing) * 8);text-align:center}.table-empty-copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.chip-btn{border-radius:var(--radius-lg);border-style:var(--tw-border-style);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-width:1px;justify-content:center;align-items:center;display:inline-flex}.chip-btn:disabled{cursor:not-allowed;opacity:.5}.chip-btn{gap:var(--pb-control-gap);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.chip-btn-pill{padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:.2em;letter-spacing:.2em;text-transform:uppercase;border-radius:9999px}.chip-btn-neutral{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary)}@media (hover:hover){.chip-btn-neutral:hover{border-color:var(--pb-border-strong);color:var(--pb-text-primary)}}.chip-btn-selected{color:var(--pb-info);background-color:var(--pb-info-dim);border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-selected{border-color:color-mix(in srgb, var(--pb-info) 26%, transparent)}}.chip-btn-selected:hover{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-selected:hover{background-color:color-mix(in srgb, var(--pb-info) 20%, var(--pb-bg-card))}}.chip-btn-selected:hover{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-selected:hover{border-color:color-mix(in srgb, var(--pb-info) 34%, transparent)}}.chip-btn-info{color:var(--pb-info);background-color:var(--pb-info-dim);border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-info{border-color:color-mix(in srgb, var(--pb-info) 24%, transparent)}}.chip-btn-info:hover{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-info:hover{background-color:color-mix(in srgb, var(--pb-info) 20%, var(--pb-bg-card))}}.chip-btn-info:hover{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.chip-btn-info:hover{border-color:color-mix(in srgb, var(--pb-info) 32%, transparent)}}.chip-btn-success{color:var(--pb-success);background-color:var(--pb-success-dim);border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.chip-btn-success{border-color:color-mix(in srgb, var(--pb-success) 24%, transparent)}}.chip-btn-success:hover{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.chip-btn-success:hover{background-color:color-mix(in srgb, var(--pb-success) 20%, var(--pb-bg-card))}}.chip-btn-success:hover{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.chip-btn-success:hover{border-color:color-mix(in srgb, var(--pb-success) 32%, transparent)}}.chip-btn-warning{color:var(--pb-warning);background-color:var(--pb-warning-dim);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.chip-btn-warning{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.chip-btn-warning:hover{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.chip-btn-warning:hover{background-color:color-mix(in srgb, var(--pb-warning) 20%, var(--pb-bg-card))}}.chip-btn-warning:hover{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.chip-btn-warning:hover{border-color:color-mix(in srgb, var(--pb-warning) 32%, transparent)}}.chip-btn-error{color:var(--pb-error);background-color:var(--pb-error-dim);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.chip-btn-error{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.chip-btn-error:hover{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.chip-btn-error:hover{background-color:color-mix(in srgb, var(--pb-error) 20%, var(--pb-bg-card))}}.chip-btn-error:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.chip-btn-error:hover{border-color:color-mix(in srgb, var(--pb-error) 32%, transparent)}}.card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);border-radius:14px}.card-hover{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:14px}@media (hover:hover){.card-hover:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}}.auth-side-panel{border-right:1px solid var(--pb-border);background:linear-gradient(160deg, var(--pb-bg-surface) 0%, var(--pb-bg-surface) 42%, var(--pb-bg-card) 100%);position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.auth-side-panel{background:linear-gradient(160deg, color-mix(in srgb, var(--pb-bg-surface) 90%, var(--pb-brand) 10%) 0%, var(--pb-bg-surface) 42%, color-mix(in srgb, var(--pb-bg-card) 78%, var(--pb-interactive) 22%) 100%)}}.auth-side-panel:before,.auth-side-panel:after{content:"";pointer-events:none;filter:blur(12px);position:absolute;inset:auto}.auth-side-panel:before{background:radial-gradient(circle, var(--pb-brand) 0%, transparent 64%);width:24rem;height:24rem;top:-10%;left:-8%}@supports (color:color-mix(in lab, red, red)){.auth-side-panel:before{background:radial-gradient(circle, color-mix(in srgb, var(--pb-brand) 26%, transparent) 0%, transparent 64%)}}.auth-side-panel:after{background:radial-gradient(circle, var(--pb-interactive) 0%, transparent 66%);width:26rem;height:26rem;bottom:-14%;right:-12%}@supports (color:color-mix(in lab, red, red)){.auth-side-panel:after{background:radial-gradient(circle, color-mix(in srgb, var(--pb-interactive) 22%, transparent) 0%, transparent 66%)}}.auth-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-overlay);border-radius:1.75rem;position:relative;overflow:hidden}.auth-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.auth-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 40%, transparent), transparent)}}.auth-summary-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.auth-summary-card-emphasis{border-color:var(--pb-brand-border);background-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.auth-summary-card-emphasis{background-color:color-mix(in srgb, var(--pb-brand) 8%, var(--pb-bg-card))}}.auth-kicker{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.login-brand-mobile{justify-content:center;display:flex}.login-hero-stack{flex-direction:column;gap:1rem;max-width:29rem;padding-bottom:.25rem;display:flex}.login-hero-headline{letter-spacing:-.02em;color:var(--pb-text-primary);text-wrap:balance;padding-bottom:.1em;font-family:Syne,sans-serif;font-size:clamp(2.3rem,3.1vw,3.35rem);font-weight:800;line-height:1.08}.login-hero-copy{max-width:25rem;color:var(--pb-text-secondary);padding-bottom:.08em;font-size:.95rem;line-height:1.72}.login-side-footer{color:var(--pb-text-tertiary);flex-wrap:wrap;align-items:center;gap:.625rem 1.25rem;padding-bottom:.08em;font-family:JetBrains Mono,monospace;font-size:.68rem;display:flex}.login-side-footer-item{align-items:center;gap:.45rem;display:inline-flex}.login-side-footer-dot{background:var(--pb-success);width:.4rem;height:.4rem;box-shadow:0 0 4px var(--pb-success);border-radius:9999px}@supports (color:color-mix(in lab, red, red)){.login-side-footer-dot{box-shadow:0 0 4px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.login-side-footer-dot{flex-shrink:0}.login-card-title{letter-spacing:-.02em;color:var(--pb-text-primary);padding-bottom:.08em;font-family:Syne,sans-serif;font-size:1.9rem;font-weight:800;line-height:1.08}.login-card-subtitle{color:var(--pb-text-secondary);padding-bottom:.05em;font-size:.875rem;line-height:1.62}.login-card-hint{color:var(--pb-text-tertiary);padding-bottom:.04em;font-size:.75rem;line-height:1.6}.floating-panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-overlay);overflow:hidden}.add-series-header-metrics{flex-wrap:wrap;align-items:flex-start;gap:1.5rem;display:flex}.add-series-results-shell{position:relative}.add-series-results-loading{z-index:12;background:var(--pb-bg-base);border-radius:.875rem;justify-content:center;align-items:flex-start;padding:1.25rem;display:flex;position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){.add-series-results-loading{background:color-mix(in srgb, var(--pb-bg-base) 55%, transparent)}}.add-series-results-loading{-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);visibility:hidden;pointer-events:none}.add-series-results-loading.htmx-request{opacity:1;visibility:visible}.add-series-results-loading-card{border:1px solid var(--pb-border-default);background:var(--pb-bg-card);border-radius:.875rem;align-items:center;gap:.75rem;max-width:24rem;padding:.875rem 1rem;display:flex}@supports (color:color-mix(in lab, red, red)){.add-series-results-loading-card{background:color-mix(in srgb, var(--pb-bg-card) 94%, transparent)}}.add-series-results-loading-card{color:var(--pb-text-primary);box-shadow:var(--pb-shadow-1)}.add-series-results-loading-card svg{color:var(--pb-interactive);flex-shrink:0}.add-series-results-loading-title{font-size:.82rem;font-weight:800;line-height:1.2}.add-series-results-loading-copy{color:var(--pb-text-secondary);margin-top:.125rem;font-size:.74rem;line-height:1.35}.add-series-preview-notice{border:1px solid var(--pb-interactive);border-radius:.875rem;flex-wrap:wrap;align-items:center;gap:.875rem;padding:.875rem 1rem;display:flex}@supports (color:color-mix(in lab, red, red)){.add-series-preview-notice{border:1px solid color-mix(in srgb, var(--pb-interactive) 32%, var(--pb-border-default))}}.add-series-preview-notice{background:linear-gradient(135deg, var(--pb-interactive), transparent 70%), var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.add-series-preview-notice{background:linear-gradient(135deg, color-mix(in srgb, var(--pb-interactive) 12%, transparent), transparent 70%), var(--pb-bg-card)}}.add-series-preview-notice{color:var(--pb-text-primary);box-shadow:var(--pb-shadow-0)}.add-series-preview-notice-icon{background:var(--pb-interactive);border-radius:999px;flex:none;justify-content:center;align-items:center;width:2rem;height:2rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.add-series-preview-notice-icon{background:color-mix(in srgb, var(--pb-interactive) 14%, transparent)}}.add-series-preview-notice-icon{color:var(--pb-interactive)}.add-series-preview-notice-body{flex:auto;min-width:0}.add-series-preview-notice-title{letter-spacing:.08em;text-transform:uppercase;font-size:.78rem;font-weight:850;line-height:1.2}.add-series-preview-notice-copy{color:var(--pb-text-secondary);margin-top:.125rem;font-size:.8rem;line-height:1.45}.add-series-preview-notice-action{white-space:nowrap;flex:none;margin-left:auto}@media (max-width:640px){.add-series-preview-notice{align-items:flex-start}.add-series-preview-notice-action{justify-content:center;width:100%;margin-left:0}}.add-series-results-list{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.add-series-result-card{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);border-radius:.875rem;align-items:flex-start;gap:.875rem;padding:.875rem;transition:border-color .14s,background-color .14s;display:flex}.add-series-result-card:hover{border-color:var(--pb-border-strong);background:var(--pb-surface-selected)}@supports (color:color-mix(in lab, red, red)){.add-series-result-card:hover{background:color-mix(in srgb, var(--pb-surface-selected) 35%, var(--pb-bg-card))}}.add-series-result-card-static:hover{border-color:var(--pb-border-subtle);background:var(--pb-bg-card)}.add-series-result-cover{aspect-ratio:2/3;border:1px solid var(--pb-border-subtle);background:var(--pb-surface-shell);width:3.5rem;min-width:3.5rem;color:var(--pb-text-tertiary);border-radius:.625rem;overflow:hidden}.add-series-result-cover-empty{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.add-series-result-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.875rem;display:flex}.add-series-result-title{color:var(--pb-text-primary);font-size:.92rem;font-weight:600;line-height:1.25}.add-series-result-meta{color:var(--pb-text-secondary);flex-wrap:wrap;gap:.25rem .75rem;margin-top:.25rem;font-size:.78rem;display:flex}.add-series-result-description{color:var(--pb-text-tertiary);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin-top:.375rem;font-size:.78rem;line-height:1.55;display:-webkit-box;overflow:hidden}.add-series-result-actions{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.add-series-result-add{white-space:nowrap;flex-shrink:0}.add-series-empty-state{text-align:center;border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);border-radius:.875rem;padding:2.5rem 1.25rem}.add-series-empty-title{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text-primary);margin-top:.75rem;font-family:Syne,sans-serif;font-size:.9rem;font-weight:700}.add-series-empty-copy{color:var(--pb-text-secondary);margin-top:.25rem;font-size:.82rem;line-height:1.55}.add-series-modal-title{color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1rem;font-weight:800;line-height:1.1}.add-series-modal-subtitle{color:var(--pb-text-tertiary);font-size:.78rem}.add-series-modal-section-label{border-bottom:1px solid var(--pb-border-subtle);text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);padding-bottom:.375rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700}.add-series-modal-row{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.add-series-modal-row-label{color:var(--pb-text-secondary);font-size:.85rem;font-weight:500}.add-series-modal-preview{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-shell);color:var(--pb-text-secondary);border-radius:.625rem;padding:.625rem .75rem;font-family:JetBrains Mono,monospace;font-size:.72rem;line-height:1.5}.add-series-root-display.input-pb:disabled,.add-series-root-display.input-pb[readonly]{opacity:1;color:var(--pb-text-primary);background:var(--pb-surface-input);cursor:default}.media-result-card{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);display:flex}.media-result-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.media-result-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.media-result-card:hover{box-shadow:var(--pb-shadow-2)}.media-result-card-static:hover{border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1)}.media-result-cover{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card-hover);flex-shrink:0;width:4.75rem;overflow:hidden}.media-result-cover-lg{width:6.25rem}.media-result-cover-inner{aspect-ratio:2/3;width:100%;height:100%}.media-result-empty{width:100%;height:100%;color:var(--pb-text-dim);justify-content:center;align-items:center;display:flex}.media-result-meta{margin-top:var(--spacing);align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);flex-wrap:wrap;display:flex}.media-result-divider{color:var(--pb-text-dim)}.match-file-card{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-direction:column;display:flex}@media (min-width:64rem){.match-file-card{flex-direction:row;align-items:flex-start}}.match-file-card{box-shadow:var(--pb-shadow-1)}.match-file-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.match-file-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.match-file-card:hover{box-shadow:var(--pb-shadow-2)}.match-file-format{height:calc(var(--spacing) * 14);width:calc(var(--spacing) * 14);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:.18em;letter-spacing:.18em;text-transform:uppercase;border-width:1px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.match-file-format-cbz{color:var(--pb-interactive);border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbz{border-color:color-mix(in srgb, var(--pb-interactive) 24%, transparent)}}.match-file-format-cbz{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbz{background-color:color-mix(in srgb, var(--pb-interactive) 12%, transparent)}}.match-file-format-cbr{color:var(--pb-purple);border-color:var(--pb-purple)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbr{border-color:color-mix(in srgb, var(--pb-purple) 24%, transparent)}}.match-file-format-cbr{background-color:var(--pb-purple)}@supports (color:color-mix(in lab, red, red)){.match-file-format-cbr{background-color:color-mix(in srgb, var(--pb-purple) 12%, transparent)}}.match-file-format-pdf{color:var(--pb-warning);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.match-file-format-pdf{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.match-file-format-pdf{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.match-file-format-pdf{background-color:color-mix(in srgb, var(--pb-warning) 12%, transparent)}}.match-file-format-other{color:var(--pb-text-secondary);border-color:var(--pb-border);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.match-file-format-other{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.match-file-card-actions{gap:calc(var(--spacing) * 2);flex-direction:column;width:100%;display:flex}@media (min-width:64rem){.match-file-card-actions{align-items:flex-end;width:auto;min-width:11rem}}.library-series-card{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);min-width:0;height:100%;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);flex-direction:column;display:flex;overflow:hidden}.library-series-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.library-series-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 56%, var(--pb-bg-card))}}.library-series-card:hover{box-shadow:var(--pb-shadow-2)}.library-series-card-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-series-card-active{border-color:color-mix(in srgb, var(--pb-interactive) 30%, transparent)}}.library-series-card-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-series-card-active{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.library-series-cover-frame{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);background:linear-gradient(160deg, var(--pb-bg-card-hover) 0%, var(--pb-bg-card) 100%);position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.library-series-cover-frame{background:linear-gradient(160deg, color-mix(in srgb, var(--pb-bg-card-hover) 88%, transparent) 0%, color-mix(in srgb, var(--pb-bg-card) 82%, transparent) 100%)}}.library-series-cover-frame-compact{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border)}.library-series-placeholder{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);width:100%;height:100%;color:var(--pb-text-dim);flex-direction:column;display:flex}.library-series-meta{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.library-series-progress-track{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.library-series-progress-track{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.series-grid-card-meta{container-type:inline-size}.series-grid-card-publisher-year{grid-template-columns:8rem minmax(0,1fr);align-items:start;gap:.25rem .5rem;display:grid}.series-grid-card-publisher{overflow-wrap:anywhere;min-width:0}.series-grid-card-year{text-align:right;justify-self:end}.series-grid-card-progress-track{background-color:var(--pb-text-dim)}@supports (color:color-mix(in lab, red, red)){.series-grid-card-progress-track{background-color:color-mix(in srgb, var(--pb-text-dim) 72%, var(--pb-bg-card) 28%)}}@container (max-width:11rem){.series-grid-card-publisher-year{grid-template-columns:8rem}.series-grid-card-year{text-align:left;justify-self:start}}.admin-workspace-page{gap:calc(var(--spacing) * 4);flex-direction:column;min-height:0;display:flex}.admin-workspace-header{padding-block:0}.admin-workspace-header-copy{max-width:var(--container-3xl);min-width:0}.admin-workspace-tag-row{gap:calc(var(--spacing) * 2);padding-top:var(--spacing);flex-wrap:wrap;display:flex}.admin-workspace-header-aside{min-width:0}.admin-workspace-summary-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);height:100%;padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1)}.admin-workspace-body{gap:calc(var(--spacing) * 7);display:grid}@media (min-width:80rem){.admin-workspace-body{grid-template-columns:10.75rem minmax(0,1fr);align-items:flex-start}}.admin-workspace-body{padding-bottom:var(--pb-page-footer-clearance)}.admin-workspace-rail{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media (min-width:80rem){.admin-workspace-rail{top:calc(var(--spacing) * 24);position:sticky}}:where(.admin-workspace-rail-copy>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.admin-workspace-rail-links{gap:var(--spacing);flex-direction:column;display:flex}.admin-workspace-content{flex:1;min-width:0}:where(.admin-workspace-content>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.admin-workspace-content{width:min(100%,58rem)}.section-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);overflow:hidden}.admin-workspace-content .space-y-6>.section-card:not(:has(~.section-card)){margin-bottom:0!important}.section-card-visible{overflow:visible}.system-version-banner{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);flex-direction:column;padding-block:1.125rem;display:flex;position:relative;overflow:hidden}@media (min-width:64rem){.system-version-banner{flex-direction:row;justify-content:space-between;align-items:center}}.system-version-banner{box-shadow:var(--pb-shadow-1)}.system-version-banner:before{content:"";background:var(--pb-brand);width:3px;position:absolute;top:0;bottom:0;left:0}.system-version-banner-main{align-items:flex-start;gap:calc(var(--spacing) * 4);min-width:0;display:flex}.system-version-banner-icon{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);background:var(--pb-brand-muted);color:var(--pb-brand);border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.system-version-banner-title{letter-spacing:-.02em;color:var(--pb-text-primary);flex-wrap:wrap;align-items:center;gap:.625rem;font-family:Bricolage Grotesque,sans-serif;font-size:1.125rem;font-weight:800;display:flex}.system-version-banner-meta{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.system-version-banner-meta span{font-family:JetBrains Mono,monospace;font-size:.6875rem}.system-version-banner-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;flex-shrink:0;display:flex}:where(.system-detail-list>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.system-detail-list{border-color:var(--pb-border-subtle)}.system-detail-row{justify-content:space-between;align-items:baseline;gap:calc(var(--spacing) * 5);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2.5);display:flex}.system-detail-label{color:var(--pb-text-secondary);flex-shrink:0;font-size:13px}.system-detail-value{color:var(--pb-text-primary);text-align:right;word-break:break-word;font-family:JetBrains Mono,monospace;font-size:.78125rem}.system-detail-value-brand{color:var(--pb-brand)}.system-registry-filename{--tw-leading:calc(var(--spacing) * 5);font-family:JetBrains Mono,Fira Code,monospace;font-size:.8125rem;line-height:calc(var(--spacing) * 5);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.system-links-grid{gap:calc(var(--spacing) * 2);padding:calc(var(--spacing) * 5);grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));display:grid}.system-link-card{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2.5);--tw-font-weight:var(--font-weight-medium);font-size:12.5px;font-weight:var(--font-weight-medium);color:var(--pb-text-primary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background:var(--pb-bg-card-hover);display:flex}@supports (color:color-mix(in lab, red, red)){.system-link-card{background:color-mix(in srgb, var(--pb-bg-card-hover) 20%, transparent)}}.system-link-card:hover{border-color:var(--pb-border-hover);background:var(--pb-bg-card-hover)}.system-link-card svg{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);color:var(--pb-text-dim);flex-shrink:0}.system-link-card-arrow{height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3);color:var(--pb-text-dim);margin-left:auto}.stat-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.stat-card-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.stat-card-value{margin-top:calc(var(--spacing) * 3);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.stat-card-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-secondary)}.info-panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1)}.info-panel-muted{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.info-panel-muted{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.dashboard-briefing-shell{border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-2);background:radial-gradient(circle at top right, var(--pb-interactive) 0%, transparent 38%), radial-gradient(circle at left bottom, var(--pb-brand) 0%, transparent 34%), var(--pb-bg-card);position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.dashboard-briefing-shell{background:radial-gradient(circle at top right, color-mix(in srgb, var(--pb-interactive) 14%, transparent) 0%, transparent 38%), radial-gradient(circle at left bottom, color-mix(in srgb, var(--pb-brand) 10%, transparent) 0%, transparent 34%), var(--pb-bg-card)}}.dashboard-briefing-shell:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.dashboard-briefing-shell:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 44%, transparent), color-mix(in srgb, var(--pb-interactive) 44%, transparent), transparent)}}.dashboard-briefing-meta{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}@media (min-width:64rem){.dashboard-briefing-meta{max-width:13rem}}.dashboard-briefing-meta{box-shadow:var(--pb-shadow-1);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.dashboard-briefing-meta{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 42%, transparent)}}.dashboard-status-pill,.dashboard-priority-badge,.dashboard-watch-tag,.dashboard-scorecard-delta{align-items:center;gap:var(--spacing);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.16em;letter-spacing:.16em;text-transform:uppercase;border-width:1px;border-color:currentColor;border-radius:3.40282e38px;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.dashboard-status-pill,.dashboard-priority-badge,.dashboard-watch-tag,.dashboard-scorecard-delta{border-color:color-mix(in srgb, currentColor 20%, transparent)}}.dashboard-priority-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.dashboard-priority-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}.dashboard-priority-card-v2{align-items:flex-start;gap:calc(var(--spacing) * 4);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:14px;grid-template-columns:1fr auto;transition:box-shadow .14s;display:grid;position:relative;overflow:hidden}.dashboard-priority-card-v2:before{content:"";background-color:#0000;width:6px;position:absolute;inset:0 auto 0 0}.dashboard-priority-card-v2:hover{box-shadow:var(--pb-shadow-2)}.dashboard-priority-card-v2-tone-pill-error:before{background-color:var(--pb-error)}.dashboard-priority-card-v2-tone-pill-warning:before{background-color:var(--pb-warning)}.dashboard-priority-card-v2-tone-pill-info:before{background-color:var(--pb-info)}.dashboard-priority-card-v2-tone-pill-success:before{background-color:var(--pb-success)}.dashboard-priority-card-v2-tone-pill-neutral:before,.dashboard-priority-card-v2-tone-pill-muted:before{background-color:var(--pb-text-dim)}.dashboard-priority-fact{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-base);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);display:inline-flex}.dashboard-priority-card,.dashboard-decision-card{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-width:1px}.dashboard-priority-card-critical,.dashboard-decision-card-critical{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-critical,.dashboard-decision-card-critical{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.dashboard-priority-card-critical,.dashboard-decision-card-critical{background:linear-gradient(135deg, var(--pb-error) 0%, var(--pb-bg-card-hover) 100%)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-critical,.dashboard-decision-card-critical{background:linear-gradient(135deg, color-mix(in srgb, var(--pb-error) 10%, var(--pb-bg-card)) 0%, color-mix(in srgb, var(--pb-bg-card-hover) 92%, transparent) 100%)}}.dashboard-priority-card-high,.dashboard-decision-card-high{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-high,.dashboard-decision-card-high{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.dashboard-priority-card-high,.dashboard-decision-card-high{background:linear-gradient(135deg, var(--pb-warning) 0%, var(--pb-bg-card-hover) 100%)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-high,.dashboard-decision-card-high{background:linear-gradient(135deg, color-mix(in srgb, var(--pb-warning) 10%, var(--pb-bg-card)) 0%, color-mix(in srgb, var(--pb-bg-card-hover) 92%, transparent) 100%)}}.dashboard-priority-card-watch,.dashboard-decision-card-watch{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-watch,.dashboard-decision-card-watch{border-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.dashboard-priority-card-watch,.dashboard-decision-card-watch{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-watch,.dashboard-decision-card-watch{background-color:color-mix(in srgb, var(--pb-warning) 7%, var(--pb-bg-card))}}.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{border-color:color-mix(in srgb, var(--pb-success) 20%, transparent)}}.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-healthy,.dashboard-decision-card-healthy{background-color:color-mix(in srgb, var(--pb-success) 7%, var(--pb-bg-card))}}.dashboard-priority-card-info,.dashboard-decision-card-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-info,.dashboard-decision-card-info{border-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.dashboard-priority-card-info,.dashboard-decision-card-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-priority-card-info,.dashboard-decision-card-info{background-color:color-mix(in srgb, var(--pb-info) 7%, var(--pb-bg-card))}}.dashboard-priority-facts{margin-top:calc(var(--spacing) * 5);gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-top:calc(var(--spacing) * 4);display:grid}@media (min-width:40rem){.dashboard-priority-facts{grid-template-columns:repeat(3,minmax(0,1fr))}}.dashboard-priority-facts dt{--tw-font-weight:var(--font-weight-medium);font-size:10px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.dashboard-priority-facts dd{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-primary)}.dashboard-quiet-note{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-success);background:var(--pb-success);display:flex}@supports (color:color-mix(in lab, red, red)){.dashboard-quiet-note{background:color-mix(in srgb, var(--pb-success) 10%, var(--pb-bg-card))}}.dashboard-quiet-note{border:1px solid var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-quiet-note{border:1px solid color-mix(in srgb, var(--pb-success) 18%, transparent)}}.dashboard-pulse-tile{padding-block:calc(var(--spacing) * 2.5);flex-direction:column;align-items:flex-start;display:flex}.dashboard-pulse-value{margin-top:var(--spacing);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.dashboard-pulse-tile-right{border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--pb-border);padding-left:calc(var(--spacing) * 3)}.dashboard-pulse-tile-bottom{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-top:calc(var(--spacing) * 2.5)}.dashboard-scoreboard-attention-grid{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.dashboard-scoreboard-attention-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.dashboard-scoreboard-attention-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.dashboard-scorecard{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);min-width:0;height:100%;padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-width:1px;flex-direction:column;justify-content:space-between;display:flex}.dashboard-scorecard-critical{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-critical{border-color:color-mix(in srgb, var(--pb-error) 22%, transparent)}}.dashboard-scorecard-critical{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-critical{background-color:color-mix(in srgb, var(--pb-error) 8%, var(--pb-bg-card))}}.dashboard-scorecard-high{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-high{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.dashboard-scorecard-high{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-high{background-color:color-mix(in srgb, var(--pb-warning) 9%, var(--pb-bg-card))}}.dashboard-scorecard-watch{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-watch{border-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.dashboard-scorecard-watch{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-watch{background-color:color-mix(in srgb, var(--pb-warning) 7%, var(--pb-bg-card))}}.dashboard-scorecard-healthy{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-healthy{border-color:color-mix(in srgb, var(--pb-success) 20%, transparent)}}.dashboard-scorecard-healthy{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-healthy{background-color:color-mix(in srgb, var(--pb-success) 7%, var(--pb-bg-card))}}.dashboard-scorecard-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-info{border-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.dashboard-scorecard-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-scorecard-info{background-color:color-mix(in srgb, var(--pb-info) 7%, var(--pb-bg-card))}}.dashboard-scorecard-title{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.dashboard-scorecard-value{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.dashboard-scorecard-value-watch{color:var(--pb-status-warning)}.dashboard-scorecard-value-critical{color:var(--pb-status-danger)}.dashboard-scorecard-copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.dashboard-healthy-strip{align-items:center;gap:calc(var(--spacing) * 2);column-gap:calc(var(--spacing) * 6);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3.5);flex-wrap:wrap;display:flex}.dashboard-healthy-strip-label{align-items:center;gap:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-success);display:flex}.dashboard-healthy-metric{align-items:baseline;gap:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);font-size:.82rem;display:flex}.dashboard-healthy-metric strong{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--pb-text-primary)}.dashboard-inline-link{align-items:center;gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.dashboard-inline-link:hover{color:var(--pb-interactive-hover)}.dashboard-download-row{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);grid-template-columns:1fr 100px 80px;display:grid}.dashboard-watch-item,.dashboard-exception-row{border-radius:var(--radius-2xl)}.dashboard-watch-item-critical,.dashboard-exception-row-critical{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-critical,.dashboard-exception-row-critical{border-color:color-mix(in srgb, var(--pb-error) 20%, transparent)}}.dashboard-watch-item-critical,.dashboard-exception-row-critical{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-critical,.dashboard-exception-row-critical{background-color:color-mix(in srgb, var(--pb-error) 7%, var(--pb-bg-card))}}.dashboard-watch-item-high,.dashboard-exception-row-high{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-high,.dashboard-exception-row-high{border-color:color-mix(in srgb, var(--pb-warning) 22%, transparent)}}.dashboard-watch-item-high,.dashboard-exception-row-high{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-high,.dashboard-exception-row-high{background-color:color-mix(in srgb, var(--pb-warning) 8%, var(--pb-bg-card))}}.dashboard-watch-item-watch,.dashboard-exception-row-watch{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-watch,.dashboard-exception-row-watch{border-color:color-mix(in srgb, var(--pb-warning) 16%, transparent)}}.dashboard-watch-item-watch,.dashboard-exception-row-watch{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-watch,.dashboard-exception-row-watch{background-color:color-mix(in srgb, var(--pb-warning) 6%, var(--pb-bg-card))}}.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{border-color:color-mix(in srgb, var(--pb-success) 16%, transparent)}}.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-healthy,.dashboard-exception-row-healthy{background-color:color-mix(in srgb, var(--pb-success) 6%, var(--pb-bg-card))}}.dashboard-watch-item-info,.dashboard-exception-row-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-info,.dashboard-exception-row-info{border-color:color-mix(in srgb, var(--pb-info) 16%, transparent)}}.dashboard-watch-item-info,.dashboard-exception-row-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-watch-item-info,.dashboard-exception-row-info{background-color:color-mix(in srgb, var(--pb-info) 6%, var(--pb-bg-card))}}.dashboard-mission-page{gap:calc(var(--spacing) * 4);width:100%;padding-bottom:var(--pb-page-footer-clearance);flex-direction:column;display:flex}.dashboard-mission-control{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.dashboard-mission-control__summary{align-items:flex-start;gap:calc(var(--spacing) * 6);flex-wrap:wrap;flex:1;min-width:0;display:flex}.dashboard-mission-control__title-block{min-width:0}.dashboard-mission-control__title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text);font-family:Syne,sans-serif;font-size:1.5rem;font-weight:800;line-height:1}.dashboard-mission-control__title span{color:var(--pb-brand)}.dashboard-mission-control__subtitle{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.dashboard-mission-control__gauges{align-items:flex-end;gap:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}.dashboard-mission-control__gauges-spacer{flex:none;width:224px;min-width:224px;min-height:72px}.dashboard-gauge{text-align:center;min-width:64px}.dashboard-gauge__ring{width:56px;height:56px;margin-inline:auto;position:relative}.dashboard-gauge__ring svg{width:56px;height:56px;transform:rotate(-90deg)}.dashboard-gauge__track{fill:none;stroke:var(--pb-text-dim)}@supports (color:color-mix(in lab, red, red)){.dashboard-gauge__track{stroke:color-mix(in srgb, var(--pb-text-dim) 14%, transparent)}}.dashboard-gauge__track{stroke-width:4.5px}.dashboard-gauge__fill{fill:none;stroke-width:4.5px;stroke-linecap:round}.dashboard-gauge__value{justify-content:center;align-items:center;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700;display:flex;position:absolute;inset:0}.dashboard-gauge__label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);margin-top:.2rem;font-size:.52rem;font-weight:600;display:block}.dashboard-gauge--success .dashboard-gauge__fill,.dashboard-gauge--success .dashboard-gauge__value{stroke:var(--pb-success);color:var(--pb-success)}.dashboard-gauge--info .dashboard-gauge__fill,.dashboard-gauge--info .dashboard-gauge__value{stroke:var(--pb-info);color:var(--pb-info)}.dashboard-gauge--warning .dashboard-gauge__fill,.dashboard-gauge--warning .dashboard-gauge__value{stroke:var(--pb-warning);color:var(--pb-warning)}.dashboard-gauge--danger .dashboard-gauge__fill,.dashboard-gauge--danger .dashboard-gauge__value{stroke:var(--pb-error);color:var(--pb-error)}.dashboard-gauge--neutral .dashboard-gauge__fill,.dashboard-gauge--neutral .dashboard-gauge__value{stroke:var(--pb-text-dim);color:var(--pb-text-dim)}.dashboard-scoreboard{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.5rem;display:grid}.dashboard-scoreboard__item{gap:calc(var(--spacing) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);min-width:0;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);box-shadow:var(--pb-shadow-1);border-radius:10px;flex-direction:column;display:flex}.dashboard-scoreboard__label,.dashboard-section__label{letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.54rem;font-weight:700}.dashboard-scoreboard__value{color:var(--pb-text);font-family:JetBrains Mono,monospace;font-size:1.05rem;font-weight:700}.dashboard-scoreboard__delta{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.66rem}:where(.dashboard-section>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}.dashboard-table-wrap,.dashboard-activity-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:12px;overflow-x:auto}.dashboard-table{border-collapse:collapse;width:100%}.dashboard-table th{text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);background:var(--pb-bg-shell);border-bottom:1px solid var(--pb-border);padding:.45rem .9rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700}.dashboard-table th.c,.dashboard-table td.c{text-align:center}.dashboard-table th.r,.dashboard-table td.r{text-align:right}.dashboard-table td{border-bottom:1px solid var(--pb-border-subtle);padding:.6rem .9rem;font-size:.82rem;transition:background-color .12s}.dashboard-table tbody tr:last-child td{border-bottom:0}.dashboard-table tbody tr:hover td{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.dashboard-table tbody tr:hover td{background:color-mix(in srgb, var(--pb-interactive) 8%, var(--pb-bg-card))}}.dashboard-table__status-col{width:40px}.dashboard-table__name{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.dashboard-table__detail{margin-top:calc(var(--spacing) * .5);color:var(--pb-text-dim);font-size:.75rem}.table-mono-value,td.table-mono-value,.dashboard-table__mono,td.dashboard-table__mono,.health-table__mono,td.health-table__mono,.series-mission-control-owned,.series-mission-control-type,.utility-tool-table-mono,td.utility-tool-table-mono,.downloads-mono-cell,td.downloads-mono-cell{color:var(--pb-text-secondary);font-variant-numeric:tabular-nums;font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:400;line-height:1.2}.series-mission-control-reading{color:var(--pb-interactive);white-space:nowrap;margin-top:.22rem;font-family:JetBrains Mono,monospace;font-size:.58rem;font-weight:600;line-height:1.2;display:block}.dashboard-table__link{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dashboard-table__link:hover{color:var(--pb-interactive-hover)}.dashboard-led{vertical-align:middle;border-radius:999px;flex-shrink:0;width:8px;height:8px;display:inline-block}.dashboard-led--sys{border:1px solid var(--pb-text);width:10px;height:10px}@supports (color:color-mix(in lab, red, red)){.dashboard-led--sys{border:1px solid color-mix(in srgb, var(--pb-text) 10%, transparent)}}.dashboard-led--green{background:var(--pb-success);box-shadow:0 0 5px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.dashboard-led--amber{background:var(--pb-warning);box-shadow:0 0 5px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-warning) 35%, transparent)}}.dashboard-led--red{background:var(--pb-error);box-shadow:0 0 5px var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--red{box-shadow:0 0 5px color-mix(in srgb, var(--pb-error) 35%, transparent)}}.dashboard-led--blue{background:var(--pb-info);box-shadow:0 0 5px var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.dashboard-led--blue{box-shadow:0 0 5px color-mix(in srgb, var(--pb-info) 35%, transparent)}}.dashboard-led--off{background:var(--pb-text-dim);opacity:.4}.dashboard-progress{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.dashboard-progress__track{height:calc(var(--spacing) * 1.5);border-radius:var(--radius-xs);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background:var(--pb-text-dim);flex:1;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.dashboard-progress__track{background:color-mix(in srgb, var(--pb-text-dim) 10%, transparent)}}.dashboard-progress__fill{height:100%;position:relative}.dashboard-progress__fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}.dashboard-progress__fill--green{background:linear-gradient(90deg, var(--pb-success), var(--pb-success))}@supports (color:color-mix(in lab, red, red)){.dashboard-progress__fill--green{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-success) 45%, transparent), var(--pb-success))}}.dashboard-progress__fill--blue{background:linear-gradient(90deg, var(--pb-info), var(--pb-info))}@supports (color:color-mix(in lab, red, red)){.dashboard-progress__fill--blue{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-info) 45%, transparent), var(--pb-info))}}.dashboard-progress__fill--pulse{opacity:.6;width:100%;animation:1.6s ease-in-out infinite pulse}.dashboard-progress__percent,.dashboard-activity-row__time,.dashboard-footer-strip{font-family:JetBrains Mono,monospace}.dashboard-progress__percent{text-align:right;min-width:32px;color:var(--pb-text-sec);font-size:.66rem;font-weight:600}.dashboard-activity-card{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2)}.dashboard-activity-row{align-items:flex-start;gap:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);border-bottom:1px solid var(--pb-border-subtle);display:flex}.dashboard-activity-row:last-child{border-bottom:0}.dashboard-activity-row__dot{margin-top:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 2);width:calc(var(--spacing) * 2);border-radius:3.40282e38px;flex-shrink:0}.dashboard-activity-row__dot--acquired{background:var(--pb-success)}.dashboard-activity-row__dot--imported{background:var(--pb-brand)}.dashboard-activity-row__dot--failed{background:var(--pb-error)}.dashboard-activity-row__body{flex:1}.dashboard-activity-row__summary{--tw-leading:calc(var(--spacing) * 6);font-size:.82rem;line-height:calc(var(--spacing) * 6);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary)}.dashboard-activity-row__detail{--tw-leading:calc(var(--spacing) * 5);font-size:.78rem;line-height:calc(var(--spacing) * 5);color:var(--pb-text-secondary)}.dashboard-activity-row__meta{margin-top:var(--spacing);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.dashboard-activity-row__time{color:var(--pb-text-dim);font-size:.66rem}.dashboard-activity-row__link{--tw-font-weight:var(--font-weight-semibold);font-size:.68rem;font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dashboard-activity-row__link:hover{color:var(--pb-interactive-hover)}.dashboard-activity-card__empty{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-dim)}.dashboard-footer-strip{align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);color:var(--pb-text-dim);box-shadow:var(--pb-shadow-1);border-radius:10px;flex-wrap:wrap;font-size:.72rem;display:flex}.dashboard-footer-strip strong{color:var(--pb-text);font-weight:600}.library-page-shell{flex:1;width:100%;min-height:0;overflow:hidden}.library-format-pills{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.library-scoreboard__item{text-decoration:none;transition:background-color .12s}.library-scoreboard__item--link:hover{background-color:var(--pb-bg-card-hover)}.library-scoreboard__value--info{color:var(--pb-info)}.library-scoreboard__value--success{color:var(--pb-success)}.library-scoreboard__value--warning{color:var(--pb-warning)}.library-matching-banner{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;border-color:var(--pb-warning);border-radius:12px;display:flex}@supports (color:color-mix(in lab, red, red)){.library-matching-banner{border-color:color-mix(in srgb, var(--pb-warning) 26%, transparent)}}.library-matching-banner{background:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.library-matching-banner{background:color-mix(in srgb, var(--pb-warning) 8%, var(--pb-bg-card))}}.library-matching-banner{box-shadow:var(--pb-shadow-1)}.library-matching-banner__body{align-items:center;column-gap:calc(var(--spacing) * 2);row-gap:var(--spacing);flex-wrap:wrap;min-width:0;display:flex}.library-matching-banner__title{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-matching-banner__copy{color:var(--pb-text-secondary)}.library-browser{--library-browser-header-height:calc(1.5rem + .75rem + 1px);border:1px solid var(--pb-border);background:var(--pb-bg-card);min-height:0;box-shadow:var(--pb-shadow-1);border-radius:14px;flex:1 1 0;grid-template-columns:minmax(220px,260px) minmax(0,1fr);display:grid;overflow:hidden}.library-browser__tree{overscroll-behavior:contain;z-index:2;border-right:1px solid var(--pb-border);background:var(--pb-bg-surface);flex-direction:column;min-height:0;display:flex;position:relative;overflow:hidden}.library-browser__tree-list{min-height:0;padding-block:var(--spacing);overscroll-behavior:contain;flex:1;overflow-y:auto}.library-browser__tree-header,.library-browser__toolbar{min-height:var(--library-browser-header-height);height:var(--library-browser-header-height)}.library-browser__tree-header{padding-inline:calc(var(--spacing) * 3);z-index:3;letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);border-bottom:1px solid var(--pb-border);background:var(--pb-bg-surface);align-items:center;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700;display:flex;position:sticky;top:0}@supports (color:color-mix(in lab, red, red)){.library-browser__tree-header{background:color-mix(in srgb, var(--pb-bg-surface) 72%, transparent)}}.library-browser__tree-node{min-height:calc(var(--spacing) * 6);border-radius:var(--radius-md);min-width:0;padding-inline:calc(var(--spacing) * 1.5);padding-block:var(--spacing);color:var(--pb-text-secondary);flex:1;align-items:center;font-size:.73rem;font-weight:400;line-height:1.2;text-decoration:none;display:flex}.library-browser__tree-node--active{background:var(--pb-interactive-dim);color:var(--pb-interactive);font-weight:500}.library-browser__tree-icon{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);color:var(--pb-text-dim);flex-shrink:0;transition:transform .14s,color .14s}.library-browser__tree-icon--folder{color:var(--pb-brand)}.library-browser__tree-icon--expanded{color:var(--pb-interactive);transform:rotate(90deg)}.library-browser__tree-item{padding-left:calc(var(--library-tree-level,0) * .8rem)}.library-browser__tree-row{min-height:calc(var(--spacing) * 6);align-items:center;gap:var(--spacing);min-width:0;padding-inline:calc(var(--spacing) * 2);display:flex}.library-browser__tree-row--context .library-browser__tree-node,.library-browser__tree-row--context .library-browser__tree-toggle,.library-browser__tree-row--context .library-browser__tree-leaf{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser__tree-row--context .library-browser__tree-node,.library-browser__tree-row--context .library-browser__tree-toggle,.library-browser__tree-row--context .library-browser__tree-leaf{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.library-browser__tree-row--context .library-browser__tree-node,.library-browser__tree-row--context .library-browser__tree-toggle,.library-browser__tree-row--context .library-browser__tree-leaf{color:var(--pb-interactive)}.library-browser__tree-toggle,.library-browser__tree-leaf{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);color:var(--pb-text-dim);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.library-browser__tree-toggle{border-radius:4px;transition:background-color .12s,color .12s}.library-browser__tree-toggle:hover{background:var(--pb-bg-card-hover);color:var(--pb-text)}:where(.library-browser__tree-children>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}.library-browser__tree-name{flex:1;min-width:0;display:block}.library-browser__panel{z-index:1;flex-direction:column;min-width:0;min-height:0;display:flex;position:relative;overflow:hidden}.library-browser__toolbar{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 3);border-bottom:1px solid var(--pb-border);background:var(--pb-bg-surface);flex-shrink:0;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser__toolbar{background:color-mix(in srgb, var(--pb-bg-surface) 72%, transparent)}}.library-browser__toolbar-main{align-items:center;gap:calc(var(--spacing) * 1.5);flex:1;min-width:0;display:flex}.library-browser__up-btn{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);border-radius:var(--radius-xs);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-shrink:0;justify-content:center;align-items:center;text-decoration:none;display:inline-flex}.library-browser__up-btn:hover{background:var(--pb-bg-card-hover);color:var(--pb-text)}.library-browser__up-btn.is-disabled{pointer-events:none;opacity:.4}.library-browser__path-block{flex:1;align-items:center;min-width:0;display:flex}.library-browser__path{color:var(--pb-text-sec);white-space:nowrap;text-overflow:ellipsis;font-family:JetBrains Mono,monospace;font-size:.56rem;line-height:1.15;overflow:hidden}.library-browser__table-wrap{overscroll-behavior:contain;min-height:0;box-shadow:none;background:0 0;border:0;border-radius:0;flex:1;position:relative;overflow:auto}.library-browser-table{min-width:760px}.library-browser-table th{padding:.4rem .75rem;font-size:.54rem;font-weight:600}.library-browser-table td{padding:.5rem .75rem;font-size:.76rem;font-weight:400;line-height:1.3}.library-browser__table-row--file,.library-browser__table-row--file td,.library-browser__table-row--file .library-browser__name-label{cursor:pointer}.library-browser__table-row--context{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser__table-row--context{background:color-mix(in srgb, var(--pb-interactive) 8%, transparent)}}.library-context-menu{border-radius:var(--radius-xl);border-style:var(--tw-border-style);min-width:15.5rem;padding-block:calc(var(--spacing) * 1.5);z-index:80;border-width:1px;border-color:var(--pb-border);position:fixed;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.library-context-menu{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-context-menu{background:linear-gradient(180deg, var(--pb-bg-card) 0%, var(--pb-bg-surface) 100%)}@supports (color:color-mix(in lab, red, red)){.library-context-menu{background:linear-gradient(180deg, color-mix(in srgb, var(--pb-bg-card) 94%, transparent) 0%, color-mix(in srgb, var(--pb-bg-surface) 97%, transparent) 100%)}}.library-context-menu{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 18px 48px #0f172a42,0 6px 18px #0f172a29}.library-context-menu__group{display:contents}.library-context-menu__item{align-items:center;gap:calc(var(--spacing) * 3);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--pb-text);display:flex}.library-context-menu__item:hover{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-context-menu__item:hover{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.library-context-menu__item:hover{color:var(--pb-interactive)}.library-context-menu__item--danger:hover{background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-context-menu__item--danger:hover{background:color-mix(in srgb, var(--pb-error) 11%, transparent)}}.library-context-menu__item--danger:hover{color:var(--pb-error)}.library-context-menu__icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:currentColor;flex-shrink:0}.library-context-menu__sep{margin-block:var(--spacing);background:var(--pb-border-subtle);height:1px}@supports (color:color-mix(in lab, red, red)){.library-context-menu__sep{background:color-mix(in srgb, var(--pb-border-subtle) 80%, transparent)}}.modal-panel.library-browser-modal{border-radius:18px;width:min(92vw,48rem);max-height:min(86vh,52rem);overflow:hidden}.modal-panel.library-browser-modal--properties{width:min(84vw,28rem)}.modal-panel.library-browser-modal--compact,.modal-panel.library-delete-modal{width:min(92vw,34rem)}.library-delete-modal__header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 4);border-bottom-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);border-bottom-width:1px;border-color:var(--pb-border);display:flex}.library-delete-modal__eyebrow{letter-spacing:.14em;text-transform:uppercase;color:var(--pb-text-dim);font-size:11px;font-weight:500}.library-delete-modal__title{margin-top:var(--spacing);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-delete-modal__subtitle{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}:where(.library-delete-modal__body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.library-delete-modal__body{padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4)}.library-delete-modal__copy{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.library-delete-modal__impact{border-radius:var(--radius-lg);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border-hover)}@supports (color:color-mix(in lab, red, red)){.library-delete-modal__impact{border-color:color-mix(in srgb, var(--pb-border-hover) 88%, transparent)}}.library-delete-modal__impact{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-delete-modal__impact{background:color-mix(in srgb, var(--pb-bg-surface) 40%, transparent)}}.library-delete-modal__impact .library-browser-modal__details-table{margin-top:0}.series-delete-modal__options{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}.series-delete-modal__option{align-items:flex-start;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border);display:flex}@supports (color:color-mix(in lab, red, red)){.series-delete-modal__option{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.series-delete-modal__option{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.series-delete-modal__option{background:color-mix(in srgb, var(--pb-bg-surface) 82%, transparent)}}.series-delete-modal__checkbox{margin-top:calc(var(--spacing) * .5);border-color:var(--pb-border-hover);background:var(--pb-bg-card-hover);color:var(--pb-error);border-radius:.25rem}.series-delete-modal__checkbox:focus{--tw-ring-color:var(--pb-error);--tw-ring-offset-width:0px}.series-delete-modal__option-copy{min-width:0}.series-delete-modal__option-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary)}.series-delete-modal__option-text{margin-top:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);line-height:1.5}.library-delete-modal__footer{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3);border-top-width:1px;border-color:var(--pb-border);background:var(--pb-bg-card);display:flex}@supports (color:color-mix(in lab, red, red)){.library-delete-modal__footer{background:color-mix(in srgb, var(--pb-bg-card) 80%, transparent)}}.library-browser-modal__header{align-items:flex-start;gap:calc(var(--spacing) * 4);background:linear-gradient(180deg, var(--pb-bg-surface) 0%, var(--pb-bg-card) 100%)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__header{background:linear-gradient(180deg, color-mix(in srgb, var(--pb-bg-surface) 95%, transparent) 0%, color-mix(in srgb, var(--pb-bg-card) 96%, transparent) 100%)}}.library-browser-modal--properties .library-browser-modal__header,.library-browser-modal--properties .library-browser-modal__body{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.library-browser-modal--properties .modal-footer{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);justify-content:flex-end}.library-browser-modal--form .library-browser-modal__body{padding-block:0;padding-inline:0}.library-browser-modal--form .modal-footer{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3)}.library-browser-modal .modal-footer{justify-content:flex-end;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3);background:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal .modal-footer{background:color-mix(in srgb, var(--pb-bg-card) 80%, transparent)}}.library-browser-modal__title-block{align-items:flex-start;gap:calc(var(--spacing) * 3);min-width:0;display:flex}.library-browser-modal__icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);color:var(--pb-interactive);border-width:1px;border-color:var(--pb-interactive);flex-shrink:0;justify-content:center;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon{border-color:color-mix(in srgb, var(--pb-interactive) 24%, transparent)}}.library-browser-modal__icon{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.library-browser-modal__icon svg{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.library-browser-modal__icon--warning{color:var(--pb-warning);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--warning{border-color:color-mix(in srgb, var(--pb-warning) 24%, transparent)}}.library-browser-modal__icon--warning{background:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--warning{background:color-mix(in srgb, var(--pb-warning) 10%, transparent)}}.library-browser-modal__icon--danger{color:var(--pb-error);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--danger{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.library-browser-modal__icon--danger{background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__icon--danger{background:color-mix(in srgb, var(--pb-error) 10%, transparent)}}.library-browser-modal--properties .library-browser-modal__icon{height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);border-radius:var(--radius-xl)}.library-browser-modal--properties .library-browser-modal__icon svg{width:1.125rem;height:1.125rem}.library-browser-modal__title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-browser-modal__subtitle{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.library-browser-modal__close{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.library-browser-modal__close:hover{background:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__close:hover{background:color-mix(in srgb, var(--pb-bg-card-hover) 88%, transparent)}}.library-browser-modal__close:hover{color:var(--pb-text)}.library-browser-modal__close:focus-visible{outline:2px solid var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__close:focus-visible{outline:2px solid color-mix(in srgb, var(--pb-interactive) 42%, transparent)}}.library-browser-modal__close:focus-visible{outline-offset:2px}.library-browser-modal__body{min-height:0}.library-browser-modal__form{flex-direction:column;flex:1;min-height:0;display:flex}.library-browser-modal__content{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.library-browser-modal__section-label{margin-bottom:calc(var(--spacing) * 2);padding-bottom:calc(var(--spacing) * 2);letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);border-bottom:1px solid var(--pb-border-subtle);font-family:Syne,sans-serif;font-size:.64rem;font-weight:700;display:block}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__section-label{border-bottom:1px solid color-mix(in srgb, var(--pb-border-subtle) 80%, transparent)}}.library-browser-modal__section{padding-bottom:var(--spacing)}.library-browser-modal__details-table{border-collapse:collapse;table-layout:fixed;width:100%;margin-top:.625rem;font-size:.8rem}.library-browser-modal__details-table--flush{margin-top:0}.library-browser-modal__detail-label{vertical-align:top;width:6.875rem;color:var(--pb-text-dim);font-size:inherit;text-align:left;padding:.25rem .75rem .25rem 0;font-weight:500;line-height:1.35}.library-browser-modal__detail-value{vertical-align:top;min-width:0;color:var(--pb-text);font-size:inherit;overflow-wrap:anywhere;padding:.25rem 0;font-weight:500;line-height:1.4}.library-browser-modal__detail-value--mono{color:var(--pb-text-sec);font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.45}.library-browser-modal__loading{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 6);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__loading{border-color:color-mix(in srgb, var(--pb-border) 86%, transparent)}}.library-browser-modal__loading{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__loading{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser-modal__stats{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(8.5rem,1fr));display:grid}.library-browser-modal__stat{justify-content:space-between;gap:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);min-height:5rem;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border);flex-direction:column;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__stat{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__stat{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__stat{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser-modal__stat-label,.library-browser-modal__field-label{letter-spacing:.11em;text-transform:uppercase;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.62rem;font-weight:700}.library-browser-modal__stat-value,.library-browser-modal__preview-value{min-width:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-primary);overflow-wrap:anywhere;font-weight:500;line-height:1.35}.library-browser-modal__meta{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__meta{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__meta{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__meta{background:color-mix(in srgb, var(--pb-bg-surface) 93%, transparent)}}.library-browser-modal__meta-row{gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);grid-template-columns:minmax(8rem,10rem) minmax(0,1fr);display:grid}.library-browser-modal__meta-row+.library-browser-modal__meta-row{border-top:1px solid var(--pb-border-subtle)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__meta-row+.library-browser-modal__meta-row{border-top:1px solid color-mix(in srgb, var(--pb-border-subtle) 84%, transparent)}}.library-browser-modal__meta-label{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-dim);letter-spacing:.08em;text-transform:uppercase}.library-browser-modal__meta-value{min-width:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);overflow-wrap:anywhere;font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.45}.library-browser-modal__storage{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__storage{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage{background:color-mix(in srgb, var(--pb-bg-surface) 93%, transparent)}}.library-browser-modal__storage-head{margin-bottom:calc(var(--spacing) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.library-browser-modal__storage-bar{height:calc(var(--spacing) * 2.5);background:var(--pb-bg-card-hover);border-radius:3.40282e38px;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage-bar{background:color-mix(in srgb, var(--pb-bg-card-hover) 72%, transparent)}}.library-browser-modal__storage-fill{background:linear-gradient(90deg, var(--pb-interactive) 0%, var(--pb-brand) 100%);border-radius:3.40282e38px;height:100%;display:block}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__storage-fill{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-interactive) 82%, white) 0%, color-mix(in srgb, var(--pb-brand) 74%, white) 100%)}}.library-browser-modal__field{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.library-browser-modal__settings-rows{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border)}.library-browser-modal__note{padding-inline:calc(var(--spacing) * 6)}.library-browser-modal__body-inset{padding-inline:calc(var(--spacing) * 6);padding-bottom:calc(var(--spacing) * 4)}.library-browser-modal__body-inset--section{padding-bottom:0}.library-browser-modal__body-inset--compact-top{padding-top:calc(var(--spacing) * 3)}.library-browser-modal__body-inset--roomy-bottom{padding-bottom:calc(var(--spacing) * 4)}.library-browser-modal__section-stack{gap:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 4);flex-direction:column;display:flex}.library-browser-modal__section-stack--delete{gap:calc(var(--spacing) * 4)}.library-browser-modal__section-panel{flex-direction:column;display:flex}.library-browser-modal__section-heading-row{padding-inline:calc(var(--spacing) * 6);padding-bottom:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-bottom:1px solid var(--pb-border-subtle);padding-top:0}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__section-heading-row{border-bottom:1px solid color-mix(in srgb, var(--pb-border-subtle) 80%, transparent)}}.library-browser-modal__preview-block{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);border-width:1px;border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-block{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__preview-block{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-block{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser-modal__preview-block{overflow-wrap:anywhere;line-height:1.45}.library-browser-modal__preview-block--mono{font-family:JetBrains Mono,monospace;font-size:.76rem}.library-browser-modal__detail-input{width:100%;min-height:2.5rem;font-size:.88rem;display:block}.library-browser-modal__helper,.library-browser-modal__context-value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.library-browser-modal__context-value--mono{color:var(--pb-text-sec);overflow-wrap:anywhere;font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.45}.library-browser-modal__error{border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-error);border-width:1px;border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__error{border-color:color-mix(in srgb, var(--pb-error) 24%, transparent)}}.library-browser-modal__error{background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__error{background:color-mix(in srgb, var(--pb-error) 8%, transparent)}}.library-browser-modal__actions{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.library-browser-modal__preview-grid{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));display:grid}.library-browser-modal__preview-card{justify-content:space-between;gap:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);min-height:5.25rem;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);border-width:1px;border-color:var(--pb-border);flex-direction:column;display:flex}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-card{border-color:color-mix(in srgb, var(--pb-border) 88%, transparent)}}.library-browser-modal__preview-card{background:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.library-browser-modal__preview-card{background:color-mix(in srgb, var(--pb-bg-surface) 94%, transparent)}}.library-browser__name{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.library-browser__name-icon{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);flex-shrink:0}.library-browser__name-icon--folder{color:var(--pb-brand)}.library-browser__name-icon--file{color:var(--pb-text-dim)}.library-browser__name-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:.76rem;font-weight:400;overflow:hidden}.library-browser__name-label--folder{color:var(--pb-text-primary);font-weight:500;text-decoration:none}.library-browser__name-label--folder:hover{color:var(--pb-interactive)}.library-browser__empty{height:100%;min-height:280px;padding-inline:calc(var(--spacing) * 6);text-align:center;flex-direction:column;justify-content:center;align-items:center;display:flex}.library-browser__empty-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);color:var(--pb-text-dim)}.library-browser__empty-title{margin-top:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.library-browser__empty-copy{margin-top:calc(var(--spacing) * 2);max-width:var(--container-xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.library-empty-shell{align-items:flex-start;gap:calc(var(--spacing) * 4);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 8);box-shadow:var(--pb-shadow-1);border-radius:14px;flex-direction:column;display:flex}.library-empty-shell__icon-wrap{height:calc(var(--spacing) * 16);width:calc(var(--spacing) * 16);background-color:var(--pb-bg-card-hover);color:var(--pb-text-dim);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.library-empty-shell__icon{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8)}:where(.library-empty-shell__body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.library-empty-shell__copy{max-width:var(--container-2xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7);color:var(--pb-text-secondary)}.library-empty-shell__actions{align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}@media (max-width:1024px){.library-browser{flex:none;grid-template-columns:1fr;height:auto;min-height:520px}.library-browser__tree{border-right:0;border-bottom:1px solid var(--pb-border);max-height:220px}}.health-section__label{letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);margin-block:.625rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700;display:block}.health-component-card__sub,.health-detail-card__stat-value,.health-history-table td{font-family:JetBrains Mono,monospace}:where(.health-section>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}.health-component-grid{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(280px,1fr));display:grid}.health-component-card{cursor:pointer;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 4);padding-bottom:calc(var(--spacing) * 3);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-width:1px;border-radius:14px;flex-direction:column;height:100%;display:flex}.health-component-card:hover{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.health-component-card:hover{background:color-mix(in srgb, var(--pb-interactive) 4%, var(--pb-bg-card))}}.health-component-card:hover{border-color:var(--pb-border-strong)}.health-component-card--success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.health-component-card--success{border-color:color-mix(in srgb, var(--pb-success) 22%, var(--pb-border))}}.health-component-card--warning{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.health-component-card--warning{border-color:color-mix(in srgb, var(--pb-warning) 22%, var(--pb-border))}}.health-component-card--danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.health-component-card--danger{border-color:color-mix(in srgb, var(--pb-error) 22%, var(--pb-border))}}.health-component-card--neutral{border-color:var(--pb-border)}.health-component-card__header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);display:flex}.health-component-card__name{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text);font-family:Syne,sans-serif;font-size:.84rem;font-weight:700}.health-component-card__stats{align-items:flex-start;gap:calc(var(--spacing) * 2);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.health-detail-card__stats{gap:calc(var(--spacing) * 2);display:grid}@media (min-width:40rem){.health-detail-card__stats{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.health-detail-card__stats{grid-template-columns:repeat(4,minmax(0,1fr))}}.health-component-card__stat,.health-detail-card__stat{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);border-width:1px;border-color:var(--pb-border-subtle);border-radius:10px;flex-direction:column;gap:0;display:flex}.health-component-card__stat{background:var(--pb-bg-base)}.health-detail-card__stat{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);background:var(--pb-bg-base)}.health-component-card__stat-label,.health-detail-card__stat-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.46rem;font-weight:700}.health-component-card__stat-value,.health-detail-card__stat-value{--tw-font-weight:var(--font-weight-semibold);font-size:.82rem;font-weight:var(--font-weight-semibold);color:var(--pb-text-primary);display:block}.health-component-card__stat-value{white-space:pre-line;line-height:1.25}.health-component-card__stat-value--danger,.health-detail-card__stat-value--danger{color:var(--pb-error)}.health-component-card__stat-value--warning,.health-detail-card__stat-value--warning{color:var(--pb-warning)}.health-component-card__message{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary);min-height:2.5rem}.health-component-card__sub{color:var(--pb-text-dim);margin-top:auto;font-size:.68rem}:where(.health-detail-shell>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.health-detail-back{align-items:center;gap:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.health-detail-back:hover{color:var(--pb-interactive-hover)}:where(.health-detail-card>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.health-detail-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:14px}.health-detail-card__header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}.health-detail-card__title-row{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;min-width:0;display:flex}.health-detail-card__title{letter-spacing:.03em;text-transform:uppercase;color:var(--pb-text);font-family:Syne,sans-serif;font-size:1rem;font-weight:800}:where(.health-check-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.health-check-row{align-items:center;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);border-radius:10px;grid-template-columns:8px minmax(120px,140px) minmax(0,1fr) auto;display:grid}.health-check-row--database-metric{grid-template-columns:8px minmax(160px,180px) minmax(0,1fr) auto}.health-check-row__name{text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary);justify-self:flex-start}.health-check-row__message{min-width:0;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim);flex:1}.health-check-list--database-metrics .health-check-row__name,.health-check-list--database-metrics .health-check-row__message{text-align:left;justify-self:flex-start}.health-check-list--database-metrics .health-led{justify-self:center}.health-table-wrap{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);border-radius:12px;overflow-x:auto}.health-table{border-collapse:collapse;width:100%}.health-table th{text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);background:var(--pb-bg-shell);border-bottom:1px solid var(--pb-border);padding:.45rem .85rem;font-family:Syne,sans-serif;font-size:.52rem;font-weight:700}.health-table td{border-bottom:1px solid var(--pb-border-subtle);color:var(--pb-text-sec);padding:.55rem .85rem;font-size:.76rem}.health-table tbody tr:last-child td{border-bottom:0}.health-table tbody tr:hover td{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.health-table tbody tr:hover td{background:color-mix(in srgb, var(--pb-interactive) 8%, var(--pb-bg-card))}}.health-table th.c,.health-table td.c{text-align:center}.health-table th.r,.health-table td.r{text-align:right}.health-table__status-col{width:40px}.health-table__name{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.health-table__detail{margin-top:calc(var(--spacing) * .5);color:var(--pb-text-dim);font-size:.72rem}.health-led{border-radius:999px;flex-shrink:0;width:8px;height:8px;display:inline-block}.health-led--header{width:10px;height:10px}.health-led--green{background:var(--pb-success);box-shadow:0 0 5px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.health-led--green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.health-led--amber{background:var(--pb-warning);box-shadow:0 0 5px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.health-led--amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-warning) 35%, transparent)}}.health-led--red{background:var(--pb-error);box-shadow:0 0 5px var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.health-led--red{box-shadow:0 0 5px color-mix(in srgb, var(--pb-error) 35%, transparent)}}.health-led--off{background:var(--pb-text-dim);opacity:.4}.health-footer-strip{align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:var(--spacing);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);color:var(--pb-text-dim);border-radius:10px;flex-wrap:wrap;font-family:JetBrains Mono,monospace;font-size:.72rem;display:flex}.health-footer-strip strong{color:var(--pb-text);font-weight:600}.step-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1)}.step-badge{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-info);background-color:var(--pb-info);border-radius:3.40282e38px;justify-content:center;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.step-badge{background-color:color-mix(in srgb, var(--pb-info) 14%, transparent)}}.step-badge{border:1px solid var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.step-badge{border:1px solid color-mix(in srgb, var(--pb-info) 28%, transparent)}}.action-card{align-items:flex-start;gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--pb-bg-card-hover);display:flex}@supports (color:color-mix(in lab, red, red)){.action-card{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 40%, transparent)}}.action-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.action-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.action-card-icon{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:var(--radius-xl);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-shrink:0;justify-content:center;align-items:center;display:flex}.action-card-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.action-card-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim)}.utility-launch-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);height:100%;padding:calc(var(--spacing) * 5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;box-shadow:var(--pb-shadow-1);flex-direction:column;transition-duration:.15s;display:flex}.utility-launch-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.utility-launch-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 82%, transparent)}}.utility-launch-card:hover{box-shadow:var(--pb-shadow-2)}.utility-launch-icon-brand{background-color:var(--pb-brand-dim);color:var(--pb-brand)}.utility-launch-icon-interactive{background-color:var(--pb-interactive-dim);color:var(--pb-interactive)}.utility-launch-icon-info{background-color:var(--pb-info-dim);color:var(--pb-info)}.utility-launch-icon-success{background-color:var(--pb-success-dim);color:var(--pb-success)}.utility-launch-icon-warning{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.utility-launch-kicker{margin-top:calc(var(--spacing) * 4);--tw-font-weight:var(--font-weight-medium);font-size:10px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-launch-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-launch-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-launch-footer{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);padding-top:calc(var(--spacing) * 4);margin-top:auto;display:flex}.utility-launch-tag{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.utility-launch-link{align-items:center;gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-interactive);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.utility-launch-link:hover{color:var(--pb-interactive-hover)}.utility-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.utility-hero-card{padding-inline:calc(var(--spacing) * 7)}}.utility-hero-card{box-shadow:var(--pb-shadow-2)}.utility-top-link{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);border-radius:3.40282e38px;align-items:center;display:inline-flex}.utility-top-link:hover{border-color:var(--pb-border-hover);color:var(--pb-text);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.utility-top-link:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.utility-top-link-muted{color:var(--pb-text-dim)}.utility-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.utility-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 42%, transparent), transparent)}}.utility-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.utility-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.utility-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.utility-hero-title{margin-top:calc(var(--spacing) * 2);font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-hero-copy{margin-top:calc(var(--spacing) * 2);max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-hero-highlights{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:48rem){.utility-hero-highlights{grid-template-columns:repeat(3,minmax(0,1fr))}}.utility-hero-highlight{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.utility-hero-highlight-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-hero-highlight-value{margin-top:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-hero-highlight-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-hero-actions{gap:calc(var(--spacing) * 3);display:grid}.utility-hero-action{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1)}.utility-hero-action:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.utility-hero-action:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.utility-hero-action-primary{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.utility-hero-action-primary{border-color:color-mix(in srgb, var(--pb-interactive) 26%, transparent)}}.utility-hero-action-primary{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.utility-hero-action-primary{background-color:color-mix(in srgb, var(--pb-interactive) 8%, var(--pb-bg-card))}}.utility-hero-action-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-hero-action-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.utility-hero-action-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utility-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.utility-hero-notes{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:80rem){.utility-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.utility-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.utility-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.utility-hero-note-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.utilities-page{width:100%;max-width:1100px;padding-bottom:var(--pb-page-footer-clearance);margin-inline:auto}.utilities-shell{width:100%}:where(.utilities-content>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.utilities-header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.utilities-header-copy{align-items:flex-start;gap:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.utilities-header-title{letter-spacing:.04em;text-transform:uppercase;font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1}.utilities-header-title span{color:var(--pb-brand)}.utilities-header-subtitle{color:var(--pb-text-dim);margin-top:4px;font-size:.78rem;font-weight:400;line-height:1.5}.utilities-gauges{align-items:flex-end;gap:calc(var(--spacing) * 3.5);display:flex}.utilities-gauge{text-align:center}.utilities-gauge-ring{height:calc(var(--spacing) * 12);width:calc(var(--spacing) * 12);border-style:var(--tw-border-style);background-color:var(--pb-bg-card);border-width:4px;border-radius:3.40282e38px;justify-content:center;align-items:center;margin-inline:auto;display:flex;position:relative}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring{background-color:color-mix(in srgb, var(--pb-bg-card) 70%, var(--pb-bg-base))}}.utilities-gauge-ring-success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring-success{border-color:color-mix(in srgb, var(--pb-success) 60%, var(--pb-border))}}.utilities-gauge-ring-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring-info{border-color:color-mix(in srgb, var(--pb-info) 60%, var(--pb-border))}}.utilities-gauge-ring-ink{border-color:var(--pb-text)}@supports (color:color-mix(in lab, red, red)){.utilities-gauge-ring-ink{border-color:color-mix(in srgb, var(--pb-text) 22%, var(--pb-border))}}.utilities-gauge-value{font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:700}.utilities-gauge-label{--tw-font-weight:var(--font-weight-medium);font-size:.52rem;font-weight:var(--font-weight-medium);color:var(--pb-text-dim);text-transform:uppercase;letter-spacing:.1em;margin-top:2px;display:block}.utilities-tabs{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);border-radius:10px;align-self:center;display:inline-flex;overflow:hidden}.utilities-tab{padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-bold);font-family:Syne,sans-serif;font-size:.78rem;font-weight:var(--font-weight-bold);--tw-tracking:.08em;letter-spacing:.08em;color:var(--pb-text-dim);text-transform:uppercase;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.utilities-tab+.utilities-tab{border-left:1px solid var(--pb-border)}.utilities-tab:hover{color:var(--pb-text);background-color:var(--pb-text)}@supports (color:color-mix(in lab, red, red)){.utilities-tab:hover{background-color:color-mix(in srgb, var(--pb-text) 3%, transparent)}}.utilities-tab.is-active{background-color:var(--pb-interactive);color:var(--pb-text-inverse)}.utilities-section-label{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-bottom:calc(var(--spacing) * 1.5);--tw-font-weight:var(--font-weight-bold);font-family:Syne,sans-serif;font-size:.62rem;font-weight:var(--font-weight-bold);--tw-tracking:.12em;letter-spacing:.12em;color:var(--pb-text-dim);text-transform:uppercase}.utilities-tool-grid{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.75rem;display:grid}.utilities-tool-grid-wide{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}.utility-launch-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);height:100%;padding:calc(var(--spacing) * 5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;box-shadow:var(--pb-shadow-1);border-radius:14px;flex-direction:column;transition-duration:.15s;display:flex;overflow:hidden}.utility-launch-card:hover{border-color:var(--pb-border-strong);box-shadow:var(--pb-shadow-2);background-color:var(--pb-bg-card);transform:translateY(-2px)}.utility-launch-header{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);display:flex}.utility-launch-arrow{color:var(--pb-text-dim);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}.utility-launch-card:hover .utility-launch-arrow{color:var(--pb-interactive);transform:translate(2px)}.utility-launch-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:10px;justify-content:center;align-items:center;display:flex}.utility-launch-title{margin-top:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-extrabold);font-family:Syne,sans-serif;font-size:.9rem;font-weight:var(--font-weight-extrabold);--tw-tracking:.04em;letter-spacing:.04em;color:var(--pb-text-primary);text-transform:uppercase}.utility-launch-copy{margin-top:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);flex:1;font-size:.78rem;font-weight:400;line-height:1.5}.utility-launch-footer{margin-top:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.utility-launch-tag{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-family:JetBrains Mono,monospace;font-weight:var(--font-weight-medium);color:var(--pb-text-dim);text-transform:uppercase;letter-spacing:.06em;align-items:center;font-size:.6rem;display:inline-flex}.utility-tool-page{width:100%;max-width:1100px;margin-inline:auto}:where(.utility-tool-header-block>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.utility-tool-back-link{align-items:center;gap:var(--spacing);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));font-size:.82rem;display:inline-flex}@media (hover:hover){.utility-tool-back-link:hover{color:var(--pb-text-primary)}}.utility-tool-header-shell{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 4);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:14px;flex-wrap:wrap;display:flex}.utility-tool-header-left{align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.utility-tool-header-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:10px;justify-content:center;align-items:center;display:flex}.utility-tool-header-icon-converter{background-color:var(--pb-interactive-dim);color:var(--pb-interactive)}.utility-tool-header-icon-mass-convert{background-color:var(--pb-success-dim);color:var(--pb-success)}.utility-tool-header-icon-integrity{background-color:var(--pb-info-dim);color:var(--pb-info)}.utility-tool-header-icon-rename,.utility-tool-header-icon-db-check{background-color:var(--pb-warning-dim);color:var(--pb-warning)}.utility-tool-header-icon-export{background-color:var(--pb-brand-dim);color:var(--pb-brand)}.utility-tool-header-title{--tw-font-weight:var(--font-weight-extrabold);font-family:Syne,sans-serif;font-size:1.1rem;font-weight:var(--font-weight-extrabold);--tw-tracking:.03em;letter-spacing:.03em;color:var(--pb-text-primary);text-transform:uppercase}.utility-tool-header-title span{color:var(--pb-brand)}.utility-tool-header-subtitle{margin-top:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.utility-tool-header-tag{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-semibold);font-family:JetBrains Mono,monospace;font-size:.62rem;font-weight:var(--font-weight-semibold);--tw-tracking:.08em;letter-spacing:.08em;color:var(--pb-text-dim);text-transform:uppercase;align-items:center;display:inline-flex}.system-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.system-hero-card{padding-inline:calc(var(--spacing) * 7)}}.system-hero-card{box-shadow:var(--pb-shadow-2)}.system-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.system-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 42%, transparent), transparent)}}.system-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.system-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.system-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.system-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.system-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.system-hero-aside{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.system-hero-aside{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.system-hero-aside{grid-template-columns:repeat(1,minmax(0,1fr))}}.system-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.system-hero-note-emphasis{border-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.system-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-brand) 20%, transparent)}}.system-hero-note-emphasis{background-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.system-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-brand) 8%, var(--pb-bg-card))}}.system-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.system-hero-note-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.system-hero-note-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.system-shell-body{gap:calc(var(--spacing) * 6);display:grid}@media (min-width:80rem){.system-shell-body{grid-template-columns:280px minmax(0,1fr)}}@media (min-width:96rem){.system-shell-body{grid-template-columns:300px minmax(0,1fr)}}.system-rail-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;overflow:hidden}.system-rail-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 5)}.system-rail-eyebrow{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.system-rail-title{margin-top:var(--spacing);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.system-rail-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}:where(.system-rail-links>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.system-rail-links{padding:calc(var(--spacing) * 3)}.system-rail-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.system-rail-footer{background-color:color-mix(in srgb, var(--pb-bg-card) 55%, transparent)}}.system-tab-card{align-items:flex-start;gap:calc(var(--spacing) * 3);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;border-width:1px;transition-duration:.15s;display:flex}.system-tab-card-inactive{border-color:var(--pb-border);background-color:var(--pb-bg-surface)}.system-tab-card-inactive:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.system-tab-card-inactive:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 58%, transparent)}}.system-tab-card-inactive:hover{box-shadow:var(--pb-shadow-1)}.system-tab-card-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.system-tab-card-active{border-color:color-mix(in srgb, var(--pb-interactive) 30%, transparent)}}.system-tab-card-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.system-tab-card-active{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.system-tab-card-active{box-shadow:var(--pb-shadow-1)}.system-tab-icon{margin-top:calc(var(--spacing) * .5);height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:var(--radius-2xl);flex-shrink:0;justify-content:center;align-items:center;display:flex}.system-tab-copy{flex:1;min-width:0}.system-tab-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.system-tab-description{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--pb-text-secondary);display:block}.system-tab-arrow{margin-top:var(--spacing);color:var(--pb-text-dim);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}.system-tab-card:hover .system-tab-arrow,.system-tab-card-active .system-tab-arrow{color:var(--pb-interactive);transform:translate(.125rem)}.system-focus-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;position:relative;overflow:hidden}.system-focus-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.system-focus-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-interactive) 34%, transparent), transparent)}}.system-focus-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.system-focus-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:flex-start}}.system-focus-meta{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim);flex-wrap:wrap;display:flex}.system-focus-chip{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.system-focus-actions{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.system-focus-actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.system-focus-actions{grid-template-columns:repeat(1,minmax(0,1fr))}}.review-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.review-hero-card{padding-inline:calc(var(--spacing) * 7)}}.review-hero-card{box-shadow:var(--pb-shadow-2)}.review-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-info), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.review-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-info) 42%, transparent), transparent)}}.review-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.review-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.review-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.review-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.review-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.review-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.review-hero-notes{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.review-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.review-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.review-hero-note-emphasis{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-info) 20%, transparent)}}.review-hero-note-emphasis{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-info) 10%, var(--pb-bg-card))}}.review-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.review-hero-note-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.review-workspace-tabs{align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:1.25rem;flex-wrap:wrap;display:flex}@media (min-width:40rem){.review-workspace-tabs{padding-inline:calc(var(--spacing) * 5)}}.review-workspace-tabs{box-shadow:var(--pb-shadow-1)}.review-tab-chip-active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary);border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-tab-chip-active{border-color:color-mix(in srgb, var(--pb-info) 34%, transparent)}}.review-tab-chip-active{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.review-tab-chip-active{background-color:color-mix(in srgb, var(--pb-info) 12%, var(--pb-bg-card))}}.review-tab-chip-active{box-shadow:var(--pb-shadow-1)}.review-tab-chip-inactive{color:var(--pb-text-dim)}.review-focus-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;position:relative;overflow:hidden}.review-focus-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.review-focus-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 34%, transparent), transparent)}}.review-focus-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.review-focus-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:flex-start}}.review-focus-chip{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.review-focus-actions{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.review-focus-actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.review-focus-actions{grid-template-columns:repeat(1,minmax(0,1fr))}}.review-note-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:1.25rem}.workflow-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.workflow-hero-card{padding-inline:calc(var(--spacing) * 7)}}.workflow-hero-card{box-shadow:var(--pb-shadow-2)}.workflow-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.workflow-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-interactive) 40%, transparent), transparent)}}.workflow-hero-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.workflow-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.95fr);align-items:flex-start}}.workflow-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.workflow-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.workflow-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.workflow-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.workflow-hero-notes{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.workflow-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.workflow-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.workflow-hero-note-emphasis{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-interactive) 20%, transparent)}}.workflow-hero-note-emphasis{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.workflow-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.workflow-hero-note-copy{margin-top:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.series-page-shell{gap:calc(var(--spacing) * 6);flex-direction:column;min-height:100%;display:flex}.series-hero-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.series-hero-card{padding-inline:calc(var(--spacing) * 7)}}.series-hero-card{box-shadow:var(--pb-shadow-2)}.series-hero-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), var(--pb-interactive), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.series-hero-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 34%, transparent), color-mix(in srgb, var(--pb-interactive) 34%, transparent), transparent)}}.series-hero-grid{gap:calc(var(--spacing) * 5);display:grid}@media (min-width:96rem){.series-hero-grid{grid-template-columns:minmax(0,1.45fr) minmax(320px,.8fr);align-items:flex-start}}.series-hero-eyebrow{--tw-font-weight:var(--font-weight-semibold);font-size:11px;font-weight:var(--font-weight-semibold);--tw-tracking:.24em;letter-spacing:.24em;color:var(--pb-brand);text-transform:uppercase}.series-hero-title{font-family:Bricolage Grotesque,sans-serif;font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--pb-text-primary)}.series-hero-copy{max-width:var(--container-3xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7);color:var(--pb-text-secondary)}.series-hero-notes{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.series-hero-notes{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:96rem){.series-hero-notes{grid-template-columns:repeat(1,minmax(0,1fr))}}.series-hero-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1)}.series-hero-note-emphasis{border-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.series-hero-note-emphasis{border-color:color-mix(in srgb, var(--pb-brand) 22%, transparent)}}.series-hero-note-emphasis{background-color:var(--pb-brand)}@supports (color:color-mix(in lab, red, red)){.series-hero-note-emphasis{background-color:color-mix(in srgb, var(--pb-brand) 8%, var(--pb-bg-card))}}.series-hero-note-kicker{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.16em;letter-spacing:.16em;color:var(--pb-text-dim);text-transform:uppercase}.series-hero-note-title{margin-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.series-hero-note-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.page-context-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:1.25rem}.page-context-back-link{align-items:center;gap:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.page-context-back-link:hover{color:var(--pb-text-primary)}.page-context-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.page-context-grid{grid-template-columns:minmax(0,1.35fr) minmax(280px,.9fr);align-items:flex-start}}.page-context-breadcrumbs{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);flex-wrap:wrap;display:flex}.page-context-link{align-items:center;gap:calc(var(--spacing) * 1.5);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}.page-context-link:hover{color:var(--pb-text-primary)}.page-context-separator{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);color:var(--pb-text-dim)}.page-context-summary{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.page-context-chip-row{gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.page-context-note{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);padding:calc(var(--spacing) * 4);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.page-context-note{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 40%, transparent)}}.page-context-note-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.page-context-note-copy{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.detail-hero-shell{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);border-radius:1.75rem;position:relative;overflow:hidden}@media (min-width:40rem){.detail-hero-shell{padding-inline:calc(var(--spacing) * 7)}}.detail-hero-shell{box-shadow:var(--pb-shadow-2)}.detail-hero-shell:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.detail-hero-shell:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 40%, transparent), transparent)}}.detail-hero-grid{gap:calc(var(--spacing) * 6);display:grid}@media (min-width:80rem){.detail-hero-grid{grid-template-columns:220px minmax(0,1fr) 320px}}.detail-cover-panel{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card-hover);padding:calc(var(--spacing) * 4);box-shadow:var(--pb-shadow-1);border-radius:1.5rem}.detail-summary-panel{min-width:0}:where(.detail-summary-panel>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.detail-header-stack>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.detail-status-row{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.detail-body-copy{max-width:56rem}.detail-stat-grid{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.detail-stat-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.detail-stat-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.detail-info-grid{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.detail-info-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.detail-info-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}:where(.detail-aside>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.detail-action-panel{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.25rem}.series-domain-page{width:100%;max-width:1400px;padding-bottom:var(--pb-page-footer-clearance);margin-inline:auto}.series-domain-breadcrumb-row{flex-wrap:wrap;align-items:center;gap:.75rem;display:flex}.series-domain-back-link{color:var(--pb-interactive);align-items:center;gap:.25rem;font-size:.82rem;transition:color .14s;display:inline-flex}.series-domain-back-link:hover{color:var(--pb-interactive-hover)}.series-domain-breadcrumbs{color:var(--pb-text-tertiary);flex-wrap:wrap;align-items:center;gap:.35rem;font-size:.82rem;display:flex}.series-domain-breadcrumbs a{color:inherit}.series-domain-breadcrumbs a:hover{color:var(--pb-interactive)}.series-domain-breadcrumb-sep{font-size:.7rem}.series-domain-breadcrumbs .current{color:var(--pb-text-primary);font-weight:600}.detail-hero-shell{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 6);box-shadow:var(--pb-shadow-1);border-radius:14px;overflow:hidden}.detail-hero-shell:before{display:none}.series-domain-hero-inner{grid-template-columns:130px minmax(0,1fr) minmax(13rem,16rem);align-items:start;gap:1.25rem;display:grid}.story-arc-detail-hero-inner{grid-template-columns:130px minmax(0,1fr) minmax(18rem,19rem)}.issue-domain-hero-inner{grid-template-columns:120px minmax(0,1fr) minmax(13rem,16rem)}.series-domain-cover-column{flex-shrink:0;width:130px}.issue-domain-cover-column{width:120px}.series-domain-cover-frame{aspect-ratio:2/3;border:1px solid var(--pb-border-default);background:linear-gradient(155deg, var(--pb-surface-card) 0%, var(--pb-surface-app) 100%);color:var(--pb-text-tertiary);border-radius:12px;justify-content:center;align-items:center;transition:box-shadow .2s;display:flex;overflow:hidden}.series-domain-cover-frame:hover{box-shadow:var(--pb-shadow-2)}.issue-domain-cover-frame{width:120px}.series-domain-cover-image{object-fit:cover;object-position:top;cursor:pointer;width:100%;height:100%}.series-domain-cover-placeholder{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.series-domain-hero-title{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text-primary);overflow-wrap:anywhere;font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1.05}.series-domain-hero-title span{color:var(--pb-brand)}.series-domain-hero-title-link{color:inherit;text-underline-offset:.18em;text-decoration:none;text-decoration-thickness:2px;transition:color .14s,text-decoration-color .14s;display:inline}.series-domain-hero-title-link:hover,.series-domain-hero-title-link:focus-visible{color:var(--pb-interactive);text-decoration-line:underline;-webkit-text-decoration-color:var(--pb-interactive);-webkit-text-decoration-color:var(--pb-interactive);text-decoration-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-domain-hero-title-link:hover,.series-domain-hero-title-link:focus-visible{-webkit-text-decoration-color:color-mix(in srgb, var(--pb-interactive) 58%, transparent);-webkit-text-decoration-color:color-mix(in srgb, var(--pb-interactive) 58%, transparent);text-decoration-color:color-mix(in srgb, var(--pb-interactive) 58%, transparent)}}.series-domain-hero-title-link:hover,.series-domain-hero-title-link:focus-visible{outline:none}.series-domain-hero-title-sm{font-size:1.4rem}.series-domain-hero-description{max-width:35rem;color:var(--pb-text-secondary);margin-top:.5rem;font-size:.85rem;line-height:1.7}.series-domain-hero-subtitle{color:var(--pb-text-secondary);margin-top:.25rem;font-size:.88rem}.series-domain-status-row{flex-wrap:wrap;align-items:center;gap:.375rem;margin-top:.75rem;display:flex}.series-domain-led{border-radius:999px;width:8px;height:8px;display:inline-block}.series-domain-led-on{background:var(--pb-status-success);box-shadow:0 0 5px var(--pb-status-success), 0 0 10px var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.series-domain-led-on{box-shadow:0 0 5px color-mix(in srgb, var(--pb-status-success) 35%, transparent), 0 0 10px color-mix(in srgb, var(--pb-status-success) 12%, transparent)}}.series-domain-led-off{background:var(--pb-text-tertiary);opacity:.35}.app-progress{align-items:center;gap:.625rem;display:flex}.app-progress-track{background:var(--pb-text-secondary);border-radius:2px;flex:1;height:8px}@supports (color:color-mix(in lab, red, red)){.app-progress-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.app-progress-track{border:1px solid var(--pb-border-subtle);position:relative;overflow:hidden}.app-progress-fill{background:linear-gradient(90deg, var(--pb-status-success), var(--pb-status-success));height:100%}@supports (color:color-mix(in lab, red, red)){.app-progress-fill{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-success) 45%, transparent), var(--pb-status-success))}}.app-progress-fill{position:relative}.app-progress-fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}.app-progress-fill--interactive{background:linear-gradient(90deg, var(--pb-interactive), var(--pb-interactive))}@supports (color:color-mix(in lab, red, red)){.app-progress-fill--interactive{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-interactive) 45%, transparent), var(--pb-interactive))}}.app-progress-fill--error{background:linear-gradient(90deg, var(--pb-error), var(--pb-error))}@supports (color:color-mix(in lab, red, red)){.app-progress-fill--error{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-error) 45%, transparent), var(--pb-error))}}.app-progress-value{text-align:right;min-width:40px;color:var(--pb-status-success);font-family:JetBrains Mono,monospace;font-size:.82rem;font-weight:700}.app-progress-value--interactive{color:var(--pb-interactive)}.app-progress-value--error{color:var(--pb-error)}.app-progress-value-stack{flex-direction:column;align-items:flex-end;line-height:1.05;display:flex}.app-progress-value-secondary{letter-spacing:.01em;color:var(--pb-text-dim);white-space:nowrap;font-family:DM Sans,sans-serif;font-size:.62rem;font-weight:600}.series-domain-info-grid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.625rem;margin-top:1rem;display:grid}.series-domain-info-box{background:var(--pb-surface-app);border:1px solid var(--pb-border-subtle);border-radius:10px;padding:.75rem .875rem}.series-domain-info-box-wide{grid-column:span 2}.series-domain-info-label{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.series-domain-info-value{color:var(--pb-text-secondary);margin-top:.25rem;font-size:.85rem}.series-domain-path{word-break:break-all;font-size:.75rem}.series-domain-actions-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;padding:1.125rem 1.5rem}.series-domain-actions-inner{flex-wrap:wrap;align-items:center;gap:1rem;display:flex}.series-domain-actions-panel{border-left:1px solid var(--pb-border-subtle);flex-direction:column;gap:.75rem;min-width:0;padding-left:1.25rem;display:flex}.series-domain-actions-panel-wide{min-width:18rem}.series-domain-actions-title{text-transform:uppercase;letter-spacing:.08em;color:var(--pb-text-primary);white-space:nowrap;font-family:Syne,sans-serif;font-size:.88rem;font-weight:800}.series-domain-actions-title span{color:var(--pb-brand)}.series-domain-actions-divider{background:var(--pb-border-default);align-self:stretch;width:1px}.series-domain-actions-buttons{flex-wrap:wrap;flex:1;align-items:center;gap:.5rem;display:flex}.series-domain-actions-panel .series-domain-actions-buttons{flex-direction:column;flex:initial;align-items:center;gap:.55rem}.series-domain-actions-panel .series-domain-actions-buttons>a,.series-domain-actions-panel .series-domain-actions-buttons>button,.series-domain-actions-panel .series-domain-actions-buttons .series-domain-inline-toggle{width:100%}.series-domain-actions-panel .series-domain-actions-buttons>a,.series-domain-actions-panel .series-domain-actions-buttons>button{gap:var(--pb-control-gap);justify-content:center}.series-domain-actions-panel .series-domain-actions-buttons .series-domain-inline-toggle{justify-content:space-between}.series-domain-action-form{width:100%}.series-domain-action-form>button{justify-content:center;width:100%}.series-domain-inline-toggle{border:1px solid var(--pb-border-default);background:var(--pb-surface-card);border-radius:10px;align-items:center;gap:.75rem;height:40px;padding:0 .75rem;display:inline-flex}.series-domain-inline-toggle-label{color:var(--pb-text-primary);font-size:.82rem;font-weight:600}.series-domain-section-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;padding:1rem 1.25rem}.series-domain-section-title{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:.82rem;font-weight:700}:where(.series-domain-alt-panel>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.series-domain-alt-title{text-transform:uppercase;letter-spacing:.12em;color:var(--pb-text-dim);font-family:Syne,sans-serif;font-size:.72rem;font-weight:700}.series-domain-alt-strip{flex-direction:column;align-items:flex-start;gap:.5rem;display:flex}.series-domain-alt-list{flex-wrap:wrap;align-items:flex-start;gap:.35rem;min-height:0;display:flex}.series-domain-alt-pill{gap:.35rem}.series-domain-alt-pill-remove{color:var(--pb-text-tertiary);margin-left:.1rem;transition:color .14s}.series-domain-alt-pill-remove:hover{color:var(--pb-status-danger)}.series-domain-alt-form{flex-wrap:wrap;align-items:center;gap:.5rem;width:100%;display:flex}.series-domain-alt-input{border:1px solid var(--pb-border-default);background:var(--pb-surface-input);width:180px;color:var(--pb-text-primary);border-radius:8px;padding:.45rem .75rem;font-size:.78rem}.series-domain-alt-input:focus{border-color:var(--pb-interactive);outline:none}.series-domain-alt-add{padding:.3rem .75rem;font-size:.78rem}.series-domain-issues-wrap{overflow:visible}.series-domain-issues-card{padding-top:.875rem}.series-domain-issues-toolbar{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.75rem;display:flex}.series-domain-issues-toolbar-left{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.series-domain-issues-title{color:var(--pb-text-primary);font-size:.88rem;font-weight:700}.series-domain-issues-summary{flex-wrap:wrap;align-items:center;gap:.35rem;display:flex}.series-domain-issues-summary .pill{font-size:.62rem}.series-domain-issues-select.dropdown-select{--pb-control-min-height:34px;--pb-control-font-size:.78rem;width:auto}.series-domain-issues-select .dropdown-select-trigger{background:var(--pb-surface-input);padding-block:.4rem;padding-inline:.75rem .65rem}.series-domain-issues-progress{align-items:center;gap:.625rem;margin-top:.9rem;display:flex}.series-domain-issues-progress-track{background:var(--pb-text-secondary);border-radius:2px;flex:1;height:8px}@supports (color:color-mix(in lab, red, red)){.series-domain-issues-progress-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.series-domain-issues-progress-track{border:1px solid var(--pb-border-subtle);overflow:hidden}.series-domain-issues-progress-fill{background:linear-gradient(90deg, var(--pb-status-success), var(--pb-status-success));height:100%}@supports (color:color-mix(in lab, red, red)){.series-domain-issues-progress-fill{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-success) 45%, transparent), var(--pb-status-success))}}.series-domain-issues-progress-label{text-align:right;min-width:36px;color:var(--pb-status-success);font-family:JetBrains Mono,monospace;font-size:.78rem;font-weight:700}.series-domain-issues-table-wrap{margin-top:.875rem;overflow-x:auto}.series-domain-issues-table{border-collapse:collapse;width:100%}.series-domain-issues-table th,.series-domain-issues-table td{border-bottom:1px solid var(--pb-border-subtle);padding:.75rem .875rem}.series-domain-issues-table thead th{text-align:left;text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);background:var(--pb-surface-shell);border-bottom:2px solid var(--pb-border-default);font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.series-domain-issues-table tbody tr:hover td{background:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.series-domain-issues-table tbody tr:hover td{background:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.series-domain-table-head.r{text-align:right}.series-domain-issue-num{white-space:nowrap;color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.84rem}.series-domain-issue-title-cell{min-width:240px;color:var(--pb-text-secondary);font-size:.84rem}.series-domain-issue-title-link{color:var(--pb-text-primary);transition:color .14s;display:block}.series-domain-issue-title-link:hover{color:var(--pb-interactive)}.series-domain-issue-date{white-space:nowrap;color:var(--pb-text-tertiary);font-size:.8rem}.series-domain-issue-reading{min-width:9.5rem;color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.7rem}.series-domain-issue-reading-mobile{color:var(--pb-interactive);margin-top:.3rem;font-family:JetBrains Mono,monospace;font-size:.65rem}.series-domain-reading-progress{gap:.28rem;max-width:8.5rem;display:grid}.series-domain-reading-progress-track{background:var(--pb-border-default);border-radius:999px;height:3px;display:block;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.series-domain-reading-progress-track{background:color-mix(in srgb, var(--pb-border-default) 68%, transparent)}}.series-domain-reading-progress-fill{border-radius:inherit;background:var(--pb-interactive);height:100%;display:block}.series-domain-reading-queue{color:var(--pb-text-tertiary);margin-top:.3rem;font-family:DM Sans,sans-serif;font-size:.62rem;display:block}.series-domain-issue-status{white-space:nowrap}.series-domain-issue-actions-cell{text-align:right}.series-domain-issue-actions{align-items:center;gap:.25rem;display:inline-flex}.series-domain-issue-action-btn{width:28px;height:28px;color:var(--pb-text-tertiary);background:0 0;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.series-domain-issue-action-btn:hover{color:var(--pb-interactive);background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-domain-issue-action-btn:hover{background:color-mix(in srgb, var(--pb-interactive) 6%, transparent)}}.series-domain-reading-menu{z-index:60;border:1px solid var(--pb-border-default);background:var(--pb-surface-raised);min-width:10.5rem;box-shadow:var(--pb-shadow-2);border-radius:10px;display:grid;position:absolute;top:calc(100% + .3rem);right:0;overflow:hidden}.series-domain-reading-menu button{text-align:left;min-height:2.5rem;color:var(--pb-text-secondary);padding:.55rem .75rem;font-size:.72rem;font-weight:600}.series-domain-reading-menu button:hover,.series-domain-reading-menu button:focus-visible{color:var(--pb-text-primary);background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-domain-reading-menu button:hover,.series-domain-reading-menu button:focus-visible{background:color-mix(in srgb, var(--pb-interactive) 9%, transparent)}}.series-domain-table-footer{margin-top:.75rem}.series-domain-copy-block{color:var(--pb-text-secondary);margin-top:.75rem}.issue-domain-stat-strip{grid-template-columns:repeat(4,minmax(0,1fr));gap:.625rem;margin-top:1rem;display:grid}.issue-domain-stat-box{background:var(--pb-surface-shell);border:1px solid var(--pb-border-subtle);border-radius:12px;padding:.8rem .9rem}.issue-domain-stat-label{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.issue-domain-stat-value{color:var(--pb-text-primary);margin-top:.3rem;font-family:JetBrains Mono,monospace;font-size:.85rem}.issue-domain-stat-value a{color:var(--pb-interactive)}.issue-domain-creators-grid{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem;display:grid}.issue-domain-creator-card{background:var(--pb-surface-app);border:1px solid var(--pb-border-subtle);border-radius:10px;padding:.8rem .9rem}.issue-domain-creator-name{color:var(--pb-text-primary);font-size:.85rem;font-weight:600}.issue-domain-creator-role{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);margin-top:.2rem;font-size:.68rem}.issue-domain-file-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:.75rem;margin-top:.75rem;display:grid}.issue-domain-file-name{font-size:.78rem}.issue-domain-file-path-box{background:var(--pb-surface-app);border:1px solid var(--pb-border-subtle);border-radius:12px;justify-content:space-between;align-items:flex-start;gap:.75rem;margin-top:.75rem;padding:.875rem 1rem;display:flex}.issue-domain-file-path{word-break:break-all;color:var(--pb-text-secondary);margin-top:.3rem;font-family:JetBrains Mono,monospace;font-size:.75rem;line-height:1.7}.issue-domain-copy-btn{border:1px solid var(--pb-border-default);background:var(--pb-surface-card);width:30px;height:30px;color:var(--pb-text-tertiary);border-radius:8px;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.issue-domain-copy-btn:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.series-domain-telemetry-strip{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);color:var(--pb-text-tertiary);box-shadow:var(--pb-shadow-1);border-radius:12px;flex-wrap:wrap;align-items:center;gap:.35rem 1rem;padding:.65rem 1rem;font-family:JetBrains Mono,monospace;font-size:.7rem;display:flex}.series-domain-telemetry-strip strong{color:var(--pb-text-primary);font-weight:700}.series-page-shell{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.series-registry-header{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:1.25rem;display:flex}.series-registry-header-left{flex-wrap:wrap;align-items:flex-start;gap:1.5rem;display:flex}.series-registry-title{text-transform:uppercase;letter-spacing:.04em;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1}.series-registry-title span{color:var(--pb-brand)}.series-registry-subtitle{color:var(--pb-text-tertiary);letter-spacing:.01em;margin-top:.25rem;font-size:.78rem}.series-registry-gauges{align-items:flex-end;gap:1rem;display:flex}.series-registry-gauge{text-align:center}.series-registry-gauge-ring{width:56px;height:56px;margin:0 auto;position:relative}.series-registry-gauge-ring svg{transform:rotate(-90deg)}.series-registry-gauge-bg{fill:none;stroke:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.series-registry-gauge-bg{stroke:color-mix(in srgb, var(--pb-text-secondary) 10%, transparent)}}.series-registry-gauge-bg{stroke-width:4.5px}.series-registry-gauge-fill{fill:none;stroke-width:4.5px;stroke-linecap:round;transition:stroke-dashoffset .6s}.series-registry-gauge-fill-success{stroke:var(--pb-status-success)}.series-registry-gauge-fill-info{stroke:var(--pb-status-info)}.series-registry-gauge-fill-warning{stroke:var(--pb-status-warning)}.series-registry-gauge-fill-danger{stroke:var(--pb-status-danger)}.series-registry-gauge-fill-default{stroke:var(--pb-text-secondary)}.series-registry-gauge-value{justify-content:center;align-items:center;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700;display:flex;position:absolute;inset:0}.series-registry-gauge-label{text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);margin-top:.25rem;font-size:.6rem;font-weight:600}.series-registry-actions{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.series-toolbar-shell{z-index:20;position:sticky;top:0}.series-toolbar-frame{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);overflow:visible}.series-toolbar-body{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.series-toolbar-browse{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.series-toolbar-primary{flex-wrap:wrap;flex:1;align-items:flex-end;gap:.5rem;min-width:0;display:flex}.series-toolbar-actions{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.series-toolbar-field{flex-shrink:0;display:block}.series-toolbar-label{text-transform:uppercase;letter-spacing:.08em;color:var(--pb-text-tertiary);margin-bottom:.25rem;font-size:.68rem;font-weight:700;display:block}.series-registry-search{width:220px;max-width:100%}.add-series-search-field{width:275px}.series-registry-search.search-field{border-color:var(--pb-border-default);background:var(--pb-surface-input);min-height:38px;box-shadow:none;border-radius:8px}.series-registry-search .search-field-input{font-size:.82rem}.series-registry-dropdown.dropdown-select{--pb-control-min-height:38px;--pb-control-font-size:.78rem;--pb-control-radius:8px;width:fit-content;max-width:100%}.series-registry-dropdown .dropdown-select-trigger{border-color:var(--pb-border-default);background:var(--pb-surface-input);box-shadow:none;padding-inline:.75rem}.series-registry-dropdown .dropdown-select-trigger-label{color:var(--pb-text-primary);font-weight:500}.series-registry-dropdown .dropdown-select-panel{border-radius:10px}.series-toolbar-view-block{flex-direction:column;display:inline-flex}.series-view-toggle{border:1px solid var(--pb-border-default);background:0 0;border-radius:8px;display:inline-flex;overflow:visible}.series-view-toggle-item:first-child{border-top-left-radius:7px;border-bottom-left-radius:7px}.series-view-toggle-item:last-child{border-top-right-radius:7px;border-bottom-right-radius:7px}.series-view-toggle-item{width:38px;height:34px;color:var(--pb-text-tertiary);background:0 0;border:none;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.series-view-toggle-item+.series-view-toggle-item{border-left:1px solid var(--pb-border-default)}.series-view-toggle-item:hover{background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.series-view-toggle-item:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.series-view-toggle-item:hover{color:var(--pb-text-primary)}html[data-series-view=list] .series-view-toggle-item[data-series-view-option=list],html[data-series-view=grid] .series-view-toggle-item[data-series-view-option=grid],html[data-story-arc-view=list] .series-view-toggle-item[data-story-arc-view-option=list],html[data-story-arc-view=grid] .series-view-toggle-item[data-story-arc-view-option=grid],html[data-whats-new-view=list] .whats-new-view-toggle-item[data-whats-new-view-option=list],html[data-whats-new-view=compact] .whats-new-view-toggle-item[data-whats-new-view-option=compact]{background:var(--pb-interactive);color:var(--pb-text-inverse)}.whats-new-pane-toggle{background:var(--pb-surface-input)}.whats-new-pane-toggle-item{min-height:34px;color:var(--pb-text-tertiary);letter-spacing:.06em;text-transform:uppercase;background:0 0;border:none;justify-content:center;align-items:center;padding:0 .875rem;font-size:.78rem;font-weight:700;transition:all .14s;display:inline-flex}.whats-new-pane-toggle-item:first-child{border-top-left-radius:7px;border-bottom-left-radius:7px}.whats-new-pane-toggle-item:last-child{border-top-right-radius:7px;border-bottom-right-radius:7px}.whats-new-pane-toggle-item+.whats-new-pane-toggle-item{border-left:1px solid var(--pb-border-default)}.whats-new-pane-toggle-item:hover{background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.whats-new-pane-toggle-item:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.whats-new-pane-toggle-item:hover{color:var(--pb-text-primary)}.whats-new-pane-toggle-item.is-active,.whats-new-pane-toggle-item[aria-current=page]{background:var(--pb-interactive);color:var(--pb-text-inverse)}.whats-new-title-block{min-height:4.55rem}.whats-new-cache-badge{min-height:34px;color:var(--pb-text-inverse);align-items:center;display:inline-flex}.whats-new-cache-badge-success{background:#1f6f4a}.whats-new-cache-badge-error{background:#9f2f24}.whats-new-week-nav{border:1px solid var(--pb-border-default);background:var(--pb-surface-input);border-radius:10px;justify-content:center;justify-self:center;align-items:center;gap:.375rem;width:fit-content;max-width:min(100%,34rem);margin-inline:auto;padding:.375rem;display:inline-flex}.whats-new-week-nav-button{border:1px solid var(--pb-border-default);background:var(--pb-surface-card);min-width:32px;min-height:32px;color:var(--pb-text-secondary);border-radius:8px;justify-content:center;align-items:center;font-size:.9rem;font-weight:800;transition:all .14s;display:inline-flex}.whats-new-week-nav-button:hover{border-color:var(--pb-border-hover);background:var(--pb-interactive-selected);color:var(--pb-text-primary)}.whats-new-week-nav-button.is-disabled{opacity:.45;cursor:not-allowed}.whats-new-week-dropdown.dropdown-select{--pb-control-min-height:32px;--pb-control-font-size:.78rem;--pb-control-radius:8px;flex:0 auto;width:fit-content;max-width:min(52vw,14rem)}.whats-new-week-dropdown .dropdown-select-trigger{border-color:var(--pb-border-default);background:var(--pb-surface-card);min-width:9.5rem;max-width:14rem;box-shadow:none;padding-inline:.625rem}.whats-new-week-dropdown .dropdown-select-panel{border-radius:10px;min-width:11.5rem;max-width:min(90vw,16rem)}.whats-new-week-dropdown .dropdown-select-trigger-label,.whats-new-week-dropdown .dropdown-select-option-label{font-weight:700}.whats-new-week-position{color:var(--pb-text-tertiary);letter-spacing:.08em;text-transform:uppercase;flex:none;padding-inline:.25rem .375rem;font-size:.68rem;font-weight:700}@media (max-width:640px){.whats-new-week-nav{max-width:100%}.whats-new-week-dropdown.dropdown-select{max-width:min(58vw,13rem)}.whats-new-week-position{text-align:center;width:100%}}.whats-new-results-stack,.whats-new-panel{gap:1rem;display:grid}.whats-new-stale-banner{border:1px solid var(--pb-status-warning);justify-content:space-between;align-items:center;gap:1rem;display:flex}@supports (color:color-mix(in lab, red, red)){.whats-new-stale-banner{border:1px solid color-mix(in srgb, var(--pb-status-warning) 40%, transparent)}}.whats-new-stale-banner{background:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.whats-new-stale-banner{background:color-mix(in srgb, var(--pb-status-warning) 10%, transparent)}}.whats-new-stale-banner{color:var(--pb-text-primary);border-radius:12px;margin-bottom:1rem;padding:.75rem 1rem;font-size:.82rem}.whats-new-stale-copy{min-width:0}.whats-new-stale-actions{flex-direction:column;flex:none;align-items:flex-end;gap:.375rem;display:flex}.whats-new-stale-message{max-width:24rem;color:var(--pb-text-secondary);text-align:right;font-size:.72rem;line-height:1.35}@media (max-width:640px){.whats-new-stale-banner{flex-direction:column;align-items:stretch}.whats-new-stale-actions{align-items:flex-start}.whats-new-stale-message{text-align:left}}.whats-new-release-summary{margin-bottom:0}.whats-new-release-table-slot{min-width:0}html[data-whats-new-view=compact] .whats-new-release-cover,html[data-whats-new-view=compact] .whats-new-pulls-col{display:none}html[data-whats-new-view=compact] .whats-new-release-title-cell{gap:0}.select-btn{border:1px solid var(--pb-border-default);min-height:38px;color:var(--pb-text-secondary);background:0 0;border-radius:8px;justify-content:center;align-items:center;gap:.375rem;padding:.375rem .875rem;font-family:DM Sans,sans-serif;font-size:.78rem;font-weight:600;transition:all .14s;display:inline-flex}.select-btn:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.select-btn:disabled{cursor:not-allowed;opacity:.45}.select-btn-success{color:var(--pb-status-success);border-color:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.select-btn-success{border-color:color-mix(in srgb, var(--pb-status-success) 30%, transparent)}}.select-btn-success{background:var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.select-btn-success{background:color-mix(in srgb, var(--pb-status-success) 8%, transparent)}}.select-btn-warning{color:var(--pb-status-warning);border-color:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.select-btn-warning{border-color:color-mix(in srgb, var(--pb-status-warning) 30%, transparent)}}.select-btn-warning{background:var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.select-btn-warning{background:color-mix(in srgb, var(--pb-status-warning) 8%, transparent)}}.select-btn-danger{color:var(--pb-status-danger);border-color:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){.select-btn-danger{border-color:color-mix(in srgb, var(--pb-status-danger) 30%, transparent)}}.select-btn-danger{background:var(--pb-status-danger)}@supports (color:color-mix(in lab, red, red)){.select-btn-danger{background:color-mix(in srgb, var(--pb-status-danger) 8%, transparent)}}.series-selection-shell{flex-direction:column;gap:.625rem;display:flex}.series-selection-inline{align-items:center;display:inline-flex}.series-selection-count{color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.78rem;font-weight:600}.series-selection-controls-row{flex-wrap:wrap;justify-content:space-between;gap:.75rem;display:flex}.series-selection-bulk,.series-selection-actions{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.series-results-shell{gap:calc(var(--spacing) * 4);min-height:16rem;padding-bottom:var(--pb-page-footer-clearance);flex-direction:column;flex:none;display:flex}.series-results-body-shell{min-height:14rem}:where(:is(.series-mission-control-shell,.series-collector-wall-shell)>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.series-mission-control-table-wrap{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;overflow:auto hidden}.series-mission-control-table{border-collapse:collapse;width:100%}.series-mission-control-table th{text-align:left;text-transform:uppercase;letter-spacing:.1em;color:var(--pb-text-tertiary);background:var(--pb-surface-shell);border-bottom:2px solid var(--pb-border-default);padding:.5625rem .875rem;font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.series-mission-control-table td{border-bottom:1px solid var(--pb-border-subtle);padding:.6875rem .875rem;font-size:.82rem;transition:background .1s}.series-mission-control-table tbody tr:last-child td{border-bottom:none}.series-mission-control-table tbody tr:not(.table-detail-row):hover td{background:var(--pb-surface-selected)}.series-mission-control-table th.c,.series-mission-control-table td.c{text-align:center}.series-mission-control-table th.r,.series-mission-control-table td.r{text-align:right}.series-mission-control-row-selected td{background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-mission-control-row-selected td{background:color-mix(in srgb, var(--pb-interactive) 10%, transparent)}}.series-led{border-radius:50%;width:8px;height:8px;display:inline-block}.series-led-green{background:var(--pb-status-success);box-shadow:0 0 5px var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.series-led-green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-status-success) 35%, transparent)}}.series-led-amber{background:var(--pb-status-warning);box-shadow:0 0 5px var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.series-led-amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-status-warning) 35%, transparent)}}.series-led-off{background:var(--pb-text-tertiary);opacity:.35}.series-mission-control-name{color:var(--pb-text-primary);font-weight:700;transition:color .14s}.series-mission-control-name:hover{color:var(--pb-interactive)}.series-mission-control-publisher{color:var(--pb-text-tertiary);margin-top:1px;font-size:.72rem}.series-mission-control-year{color:var(--pb-text-tertiary);font-size:.78rem}.series-mission-control-bar-cell{width:220px}.series-mission-control-bar{align-items:center;gap:.5rem;display:flex}.series-mission-control-bar-track{background:var(--pb-text-secondary);border-radius:2px;flex:1;height:8px}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.series-mission-control-bar-track{border:1px solid var(--pb-border-subtle);position:relative;overflow:hidden}.series-mission-control-bar-fill{height:100%;position:relative}.series-mission-control-bar-fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}.series-mission-control-bar-fill-green{background:linear-gradient(90deg, var(--pb-status-success), var(--pb-status-success))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-green{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-success) 45%, transparent), var(--pb-status-success))}}.series-mission-control-bar-fill-amber{background:linear-gradient(90deg, var(--pb-status-warning), var(--pb-status-warning))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-amber{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-warning) 45%, transparent), var(--pb-status-warning))}}.series-mission-control-bar-fill-red{background:linear-gradient(90deg, var(--pb-status-danger), var(--pb-status-danger))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-red{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-danger) 45%, transparent), var(--pb-status-danger))}}.series-mission-control-bar-fill-blue{background:linear-gradient(90deg, var(--pb-status-info), var(--pb-status-info))}@supports (color:color-mix(in lab, red, red)){.series-mission-control-bar-fill-blue{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-status-info) 45%, transparent), var(--pb-status-info))}}.series-mission-control-bar-pct{text-align:right;font-variant-numeric:tabular-nums;min-width:36px;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700}.series-mission-control-bar-pct-green{color:var(--pb-status-success)}.series-mission-control-bar-pct-amber{color:var(--pb-status-warning)}.series-mission-control-bar-pct-red{color:var(--pb-status-danger)}.series-mission-control-bar-pct-blue{color:var(--pb-status-info)}.series-mission-control-actions{justify-content:flex-end;gap:.25rem;display:flex}.series-mission-control-action-btn{border:1px solid var(--pb-border-subtle);width:30px;height:30px;color:var(--pb-text-tertiary);background:0 0;border-radius:8px;justify-content:center;align-items:center;transition:all .14s;display:inline-flex}.series-mission-control-action-btn:hover{border-color:var(--pb-interactive);color:var(--pb-interactive);background:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-mission-control-action-btn:hover{background:color-mix(in srgb, var(--pb-interactive) 4%, transparent)}}.series-mission-control-action-btn.disabled{opacity:.25;pointer-events:none}.series-mission-control-action-btn svg{width:14px;height:14px}.series-mission-control-footer{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);color:var(--pb-text-tertiary);border-radius:10px;flex-wrap:wrap;align-items:center;gap:.375rem 1.5rem;padding:.625rem 1rem;font-family:JetBrains Mono,monospace;font-size:.7rem;display:flex}.series-mission-control-footer strong{color:var(--pb-text-primary);font-weight:600}.series-collector-wall-grid{grid-template-columns:repeat(auto-fill,minmax(172px,1fr));gap:24px 18px;display:grid}.series-wall-card{min-width:0;transition:transform .22s;position:relative}.series-wall-card:hover{transform:translateY(-5px)}.series-wall-card-selected .series-wall-cover-wrap{outline:2px solid var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-wall-card-selected .series-wall-cover-wrap{outline:2px solid color-mix(in srgb, var(--pb-interactive) 28%, transparent)}}.series-wall-card-selected .series-wall-cover-wrap{outline-offset:2px}.series-wall-cover-wrap{aspect-ratio:2/3;border:1px solid var(--pb-border-subtle);background:var(--pb-surface-card);box-shadow:var(--pb-shadow-1);border-radius:14px;transition:box-shadow .22s;position:relative;overflow:hidden}.series-wall-card:hover .series-wall-cover-wrap{box-shadow:var(--pb-shadow-2), 0 0 20px var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.series-wall-card:hover .series-wall-cover-wrap{box-shadow:var(--pb-shadow-2), 0 0 20px color-mix(in srgb, var(--pb-interactive) 6%, transparent)}}.series-wall-cover-placeholder{width:100%;height:100%;color:var(--pb-text-tertiary);background:linear-gradient(155deg, var(--pb-surface-card) 0%, var(--pb-surface-app) 100%);justify-content:center;align-items:center;display:flex}.series-monitor-badge{width:1.5rem;height:1.5rem;color:var(--pb-text-inverse);background:var(--pb-brand-signal);border:2px solid var(--pb-surface-card);border-radius:999px;flex:none;justify-content:center;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.series-monitor-badge{border:2px solid color-mix(in srgb, var(--pb-surface-card) 92%, white)}}.series-monitor-badge{box-shadow:0 0 0 2px #0000006b,0 10px 22px #00000057}.series-monitor-badge svg{flex:none;width:.9rem;height:.9rem}.series-monitor-badge.is-paused{color:var(--pb-text-secondary);background:var(--pb-surface-raised);border-color:var(--pb-border-default);box-shadow:0 0 0 1px var(--pb-border-subtle)}.series-wall-monitor-dot{z-index:3;position:absolute;top:9px;left:9px}.story-arc-wall-review{z-index:4;border:1px solid var(--pb-border-default);background:var(--pb-surface-card);border-radius:999px;justify-content:center;align-items:center;gap:.2rem;min-width:1.75rem;height:1.75rem;padding-inline:.38rem;display:inline-flex;position:absolute;top:9px;right:9px}@supports (color:color-mix(in lab, red, red)){.story-arc-wall-review{background:color-mix(in srgb, var(--pb-surface-card) 92%, transparent)}}.story-arc-wall-review{box-shadow:var(--pb-shadow-1);font-family:JetBrains Mono,monospace;font-size:.65rem;font-weight:700}.story-arc-wall-review svg{width:.8rem;height:.8rem}.story-arc-wall-review-warning{color:var(--pb-warning);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.story-arc-wall-review-warning{border-color:color-mix(in srgb, var(--pb-warning) 48%, transparent)}}.story-arc-wall-review-error{color:var(--pb-error);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.story-arc-wall-review-error{border-color:color-mix(in srgb, var(--pb-error) 48%, transparent)}}.story-arc-review-indicator{border:1px solid var(--pb-border-default);border-radius:999px;justify-content:center;align-items:center;gap:.28rem;min-width:2.5rem;min-height:1.75rem;padding-inline:.45rem;font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:700;display:inline-flex}.story-arc-review-indicator svg{width:.82rem;height:.82rem}.story-arc-review-indicator-success{color:var(--pb-success);background:var(--pb-success-dim);border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.story-arc-review-indicator-success{border-color:color-mix(in srgb, var(--pb-success) 32%, transparent)}}.story-arc-review-indicator-warning{color:var(--pb-warning);background:var(--pb-warning-dim);border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.story-arc-review-indicator-warning{border-color:color-mix(in srgb, var(--pb-warning) 32%, transparent)}}.story-arc-review-indicator-error{color:var(--pb-error);background:var(--pb-error-dim);border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.story-arc-review-indicator-error{border-color:color-mix(in srgb, var(--pb-error) 32%, transparent)}}.series-wall-catalog-state-badge{z-index:4;border:1px solid var(--pb-warning);background:var(--pb-warning);border-radius:999px;justify-content:center;align-items:center;max-width:calc(100% - 18px);display:inline-flex;position:absolute;top:46px;right:9px}@supports (color:color-mix(in lab, red, red)){.series-wall-catalog-state-badge{background:color-mix(in srgb, var(--pb-warning) 92%, black)}}.series-wall-catalog-state-badge{color:var(--pb-text-inverse);letter-spacing:.04em;text-align:center;text-transform:uppercase;padding:.2rem .5rem;font-size:.58rem;font-weight:800;line-height:1.15;box-shadow:0 8px 18px #00000057}.series-wall-catalog-state-badge.is-failed{border-color:var(--pb-error);background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.series-wall-catalog-state-badge.is-failed{background:color-mix(in srgb, var(--pb-error) 92%, black)}}.pull-list-monitor-toggle{width:28px;height:28px;box-shadow:none;cursor:pointer;border-width:1px;border-radius:6px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s}.pull-list-monitor-toggle svg{width:12px;height:12px}.pull-list-monitor-toggle:hover{border-color:var(--pb-brand-signal)}@supports (color:color-mix(in lab, red, red)){.pull-list-monitor-toggle:hover{border-color:color-mix(in srgb, var(--pb-brand-signal) 56%, transparent)}}.pull-list-monitor-toggle:hover{background:var(--pb-brand-signal);color:var(--pb-text-inverse);box-shadow:0 0 0 2px var(--pb-brand-signal)}@supports (color:color-mix(in lab, red, red)){.pull-list-monitor-toggle:hover{box-shadow:0 0 0 2px color-mix(in srgb, var(--pb-brand-signal) 14%, transparent)}}.pull-list-monitor-toggle:focus-visible{outline:2px solid var(--pb-focus-outline);outline-offset:3px}.series-wall-selection-control{z-index:4;border:1px solid var(--pb-border-default);background:var(--pb-surface-card);border-radius:10px;position:absolute;top:10px;right:10px}@supports (color:color-mix(in lab, red, red)){.series-wall-selection-control{background:color-mix(in srgb, var(--pb-surface-card) 92%, transparent)}}.series-wall-selection-control{box-shadow:var(--pb-shadow-1);padding:.25rem}.series-wall-ring{z-index:2;width:44px;height:44px;position:absolute;bottom:-6px;right:-6px}.series-wall-ring svg{transform:rotate(-90deg)}.series-wall-ring-bg{fill:none;stroke:var(--pb-surface-card);stroke-width:3.5px}.series-wall-ring-fill{fill:none;stroke-width:3.5px;stroke-linecap:round;transition:stroke-dashoffset .6s}.series-wall-ring-fill-green{stroke:var(--pb-status-success)}.series-wall-ring-fill-amber{stroke:var(--pb-status-warning)}.series-wall-ring-fill-red{stroke:var(--pb-status-danger)}.series-wall-ring-center{background:var(--pb-surface-card);color:var(--pb-text-primary);border-radius:999px;justify-content:center;align-items:center;margin:5px;font-family:JetBrains Mono,monospace;font-size:.58rem;font-weight:700;display:flex;position:absolute;inset:0}.series-wall-overlay{--series-wall-overlay-edge:#fff;z-index:3;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);box-shadow:inset 0 0 0 4.5px var(--series-wall-overlay-edge);opacity:0;pointer-events:none;background:#1e1a17e0;border-radius:18px;flex-direction:column;justify-content:flex-end;padding:.875rem;transition:opacity .2s;display:flex;position:absolute;inset:-4px}[data-theme=light] .series-wall-overlay{--series-wall-overlay-edge:#1e1a17}@media (prefers-color-scheme:light){:root:not([data-theme]) .series-wall-overlay{--series-wall-overlay-edge:#1e1a17}}.series-wall-card:hover .series-wall-overlay{opacity:1}.series-wall-overlay-title{color:var(--pb-text-primary);font-size:.82rem;font-weight:700;line-height:1.3}.series-wall-overlay-meta{color:var(--pb-text-tertiary);gap:.25rem;margin-top:.45rem;font-size:.68rem;display:grid}.series-wall-overlay-meta-line,.series-wall-overlay-stat{grid-template-columns:minmax(4.25rem,max-content) minmax(0,1fr);align-items:baseline;column-gap:.5rem;line-height:1.25;display:grid}.series-wall-overlay-meta-label{color:var(--pb-text-tertiary);letter-spacing:.08em;text-transform:uppercase;font-family:JetBrains Mono,monospace;font-size:.56rem;font-weight:700}.series-wall-overlay-meta-value{overflow-wrap:anywhere;min-width:0;color:var(--pb-text-secondary)}.series-wall-overlay-stats{color:var(--pb-text-secondary);gap:.25rem;margin-top:.35rem;font-size:.68rem;display:grid}.series-wall-overlay-stats strong{color:var(--pb-text-primary);font-weight:600}.series-wall-overlay-actions{pointer-events:auto;gap:6px;margin-top:10px;display:flex}.series-wall-overlay-btn{color:var(--pb-text-secondary);text-align:center;background:#ffffff0f;border:1px solid #cbd5e140;border-radius:8px;flex:1;justify-content:center;padding:7px;font-family:DM Sans,sans-serif;font-size:.72rem;font-weight:600;transition:all .14s;display:inline-flex}.series-wall-overlay-btn:hover{background:var(--pb-interactive);color:var(--pb-text-primary);border-color:var(--pb-interactive)}.series-wall-card-title{text-overflow:ellipsis;white-space:nowrap;color:var(--pb-text-primary);margin-top:10px;font-size:.82rem;font-weight:600;line-height:1.3;display:block;overflow:hidden}.series-empty-state{justify-content:center;align-items:center;gap:calc(var(--spacing) * 3);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 12);text-align:center;box-shadow:var(--pb-shadow-1);border-radius:1.25rem;flex-direction:column;display:flex}.series-empty-state-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.series-empty-state-copy{max-width:var(--container-xl);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}@media (max-width:1180px){.series-domain-hero-inner{grid-template-columns:130px minmax(0,1fr)}.issue-domain-hero-inner{grid-template-columns:120px minmax(0,1fr)}.series-domain-actions-panel{border-top:1px solid var(--pb-border-subtle);border-left:0;grid-column:1/-1;padding-top:1rem;padding-left:0}.series-domain-actions-panel .series-domain-actions-buttons{grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));align-items:stretch;display:grid}}@media (max-width:900px){.issue-domain-stat-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.issue-domain-file-grid{grid-template-columns:1fr}}@media (max-width:640px){.detail-hero-shell,.series-domain-actions-card,.series-domain-section-card{padding:1rem}.series-domain-cover-column,.issue-domain-cover-column,.issue-domain-cover-frame{width:104px}.series-domain-hero-inner,.issue-domain-hero-inner{grid-template-columns:104px minmax(0,1fr)}.series-domain-hero-title{font-size:1.2rem}.series-domain-actions-inner{align-items:flex-start}.series-domain-actions-divider{display:none}.series-domain-actions-panel .series-domain-actions-buttons,.issue-domain-stat-strip{grid-template-columns:1fr}.series-domain-info-box-wide{grid-column:auto}.series-domain-issues-toolbar{flex-direction:column;align-items:stretch}}[data-series-page][data-series-toolbar-mode=browse] [data-testid=series-select-toolbar],[data-series-page][data-series-toolbar-mode=select] [data-testid=series-browse-toolbar]{display:none}[data-series-page][data-series-toolbar-mode=select] [data-testid=series-select-toolbar]{display:flex}[data-select-toolbar-page][data-toolbar-mode=browse] [data-select-toolbar],[data-select-toolbar-page][data-toolbar-mode=select] [data-browse-toolbar]{display:none}[data-select-toolbar-page][data-toolbar-mode=select] [data-select-toolbar]{display:flex}[data-series-page][data-series-toolbar-mode=browse] [data-series-selection-cell]{display:none}[data-series-page][data-series-toolbar-mode=select] [data-series-selection-cell]{display:table-cell}[data-series-page][data-series-toolbar-mode=browse] [data-series-selection-control]{display:none}[data-series-page][data-series-toolbar-mode=select] [data-series-selection-control]{display:block}[data-series-page][data-series-toolbar-mode=select] [data-series-selection-control=grid]{display:inline-flex}#page-footer-dock:empty{display:none}body:has(#page-footer-dock:not(:empty)) #content{padding-bottom:var(--pb-page-footer-clearance)}body:has(#page-footer-dock:not(:empty)) #content:has(.admin-workspace-page,.dashboard-mission-page,.downloads-view,.series-domain-page,.series-results-shell,.utilities-page){padding-bottom:0}#content:has(.admin-workspace-page){overflow-anchor:none}.page-dock-inner{--page-dock-height-status:2rem;--page-dock-height-pagination:2.5rem;max-width:85rem;height:var(--page-dock-height-status);flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:.75rem;margin:0 auto;padding:0 1rem;display:flex;overflow:hidden}.page-dock-inner:has(.page-dock-pagination){height:var(--page-dock-height-pagination)}.page-footer-clearance{height:var(--pb-page-footer-clearance);flex:0 0 var(--pb-page-footer-clearance);pointer-events:none}.page-dock-inner-status-only{justify-content:flex-end}.page-dock-pagination{flex:none}.page-dock-pagination nav{gap:.125rem}.page-dock-pagination nav>a,.page-dock-pagination nav>button,.page-dock-pagination nav>span{text-align:center;border-radius:.375rem;min-width:1.75rem;padding:.25rem .625rem;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:600;line-height:1}.page-dock-pagination nav>a,.page-dock-pagination nav>button{appearance:none;color:var(--pb-text-secondary);cursor:pointer;background:0 0;border:0;margin:0}.page-dock-pagination nav>span.bg-pb-interactive{border-radius:.375rem}.page-dock-status{min-width:0;color:var(--pb-text-tertiary);scrollbar-width:none;flex-wrap:nowrap;align-items:center;gap:1rem;font-family:JetBrains Mono,monospace;font-size:.65rem;display:flex;overflow-x:auto}.page-dock-status::-webkit-scrollbar{display:none}.page-dock-status-item{white-space:nowrap;align-items:center;gap:.25rem;display:inline-flex}.page-dock-status-value{color:var(--pb-text-primary);font-weight:600}.page-dock-status-label{text-transform:uppercase;letter-spacing:.04em}.page-dock-led{border-radius:999px;width:6px;height:6px;margin-right:2px;display:inline-block}.page-dock-led-green{background:var(--pb-status-success);box-shadow:0 0 4px var(--pb-status-success)}@supports (color:color-mix(in lab, red, red)){.page-dock-led-green{box-shadow:0 0 4px color-mix(in srgb, var(--pb-status-success) 35%, transparent)}}.page-dock-led-amber{background:var(--pb-status-warning);box-shadow:0 0 4px var(--pb-status-warning)}@supports (color:color-mix(in lab, red, red)){.page-dock-led-amber{box-shadow:0 0 4px color-mix(in srgb, var(--pb-status-warning) 35%, transparent)}}.page-dock-led-off{background:var(--pb-text-tertiary);opacity:.35}@media (max-width:767px){.page-dock-inner{padding-inline:0}.page-dock-status{gap:.75rem}}.workflow-tabs-shell{align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:1.25rem;flex-wrap:wrap;display:flex}@media (min-width:40rem){.workflow-tabs-shell{padding-inline:calc(var(--spacing) * 5)}}.workflow-tabs-shell{box-shadow:var(--pb-shadow-1)}.workflow-tab-chip-active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary);border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-tab-chip-active{border-color:color-mix(in srgb, var(--pb-interactive) 34%, transparent)}}.workflow-tab-chip-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-tab-chip-active{background-color:color-mix(in srgb, var(--pb-interactive) 12%, var(--pb-bg-card))}}.workflow-tab-chip-active{box-shadow:var(--pb-shadow-1)}.workflow-tab-chip-inactive{color:var(--pb-text-dim)}.workflow-focus-card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;position:relative;overflow:hidden}.workflow-focus-card:before{content:"";background:linear-gradient(90deg, transparent, var(--pb-brand), transparent);height:1px;position:absolute;inset:0 0 auto}@supports (color:color-mix(in lab, red, red)){.workflow-focus-card:before{background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--pb-brand) 34%, transparent), transparent)}}.workflow-focus-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:80rem){.workflow-focus-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:flex-start}}.workflow-focus-chip{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.workflow-focus-actions{gap:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.workflow-focus-actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.workflow-focus-actions{grid-template-columns:repeat(1,minmax(0,1fr))}}:where(.workflow-shell-region>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.workflow-shell-region{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);border-radius:1.5rem;overflow:visible}@media (min-width:40rem){.workflow-shell-region{padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 5)}}.workflow-shell-region{box-shadow:var(--pb-shadow-1)}.utility-workspace-shell{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:1.5rem;overflow:hidden}.utility-workspace-shell-visible{overflow:visible}:where(.utility-workspace-body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.utility-workspace-body{padding:calc(var(--spacing) * 6)}@media (min-width:40rem){.utility-workspace-body{padding:calc(var(--spacing) * 7)}}:where(.utility-tool-body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.utility-tool-body{padding:calc(var(--spacing) * 5)}@media (min-width:40rem){.utility-tool-body{padding:calc(var(--spacing) * 5)}}.utility-workspace-shell .info-panel{border-color:var(--pb-border-subtle);background-color:var(--pb-bg-base);box-shadow:none}.utility-workspace-shell .info-panel-muted{background-color:var(--pb-bg-base)}.utility-empty-state{border-radius:var(--radius-xl);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--pb-border-subtle);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 8);text-align:center;background-color:var(--pb-bg-base)}.utility-path-pill{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);display:flex}.utility-token{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);border-radius:3.40282e38px;align-items:center;display:inline-flex}.utility-step-grid{gap:calc(var(--spacing) * 3);display:grid}.utility-step-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);width:100%;padding:calc(var(--spacing) * 4);display:block}.utility-step-card-inline{align-items:center;gap:.75rem;display:flex}.utility-step-card-locked{opacity:.78}.utility-step-num{color:var(--pb-text-tertiary);min-width:1.25rem;font-family:JetBrains Mono,monospace;font-size:.62rem;font-weight:700}.utility-step-name{color:var(--pb-text-primary);flex:auto;min-width:0;font-size:.82rem;font-weight:600}.utility-step-tag{color:var(--pb-text-tertiary);white-space:nowrap;font-family:JetBrains Mono,monospace;font-size:.62rem}.utility-step-card-inline .utility-step-num,.utility-step-card-inline .utility-step-tag{color:var(--pb-text-secondary)}.utility-step-check{flex-shrink:0;margin-top:0}.utility-scan-mode-grid{gap:calc(var(--spacing) * 2);display:grid}@media (min-width:40rem){.utility-scan-mode-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-scan-mode{justify-content:center;align-items:flex-start;gap:var(--spacing);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card);min-height:72px;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-direction:column;display:flex}.utility-scan-mode:hover{border-color:var(--pb-border-hover)}.utility-scan-mode-active{border-color:var(--pb-interactive);background-color:var(--pb-surface-selected)}.utility-scan-mode-label{--tw-font-weight:var(--font-weight-semibold);font-size:.82rem;font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.utility-scan-mode-copy{--tw-leading:calc(var(--spacing) * 5);font-size:.72rem;line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim)}.utility-scope-chip-row{flex-wrap:wrap;gap:.375rem;display:flex}.utility-scope-chip{border:1px solid var(--pb-border-default);color:var(--pb-text-secondary);background:0 0;border-radius:.5rem;justify-content:center;align-items:center;padding:.4375rem 1rem;font-size:.78rem;font-weight:600;transition:border-color .14s,background-color .14s,color .14s;display:inline-flex}.utility-scope-chip:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.utility-scope-chip-active{background:var(--pb-interactive);color:var(--pb-text-inverse);border-color:var(--pb-interactive)}.utility-scope-chip-active:hover{color:var(--pb-text-inverse)}.utility-export-chip-row{flex-wrap:wrap;gap:.375rem;display:flex}.utility-export-chip{border:1px solid var(--pb-border-default);color:var(--pb-text-secondary);background:0 0;border-radius:999px;justify-content:center;align-items:center;padding:.4375rem .875rem;font-family:DM Sans,sans-serif;font-size:.76rem;font-weight:500;line-height:1.2;transition:border-color .14s,background-color .14s,color .14s;display:inline-flex}.utility-export-chip:hover{border-color:var(--pb-interactive);color:var(--pb-interactive)}.utility-export-chip-active{border-color:var(--pb-interactive);background:var(--pb-interactive);color:var(--pb-text-inverse)}.utility-export-chip-active:hover{color:var(--pb-text-inverse)}.utility-tool-section-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);margin-bottom:.5rem;font-family:Syne,sans-serif;font-size:.62rem;font-weight:700}.utility-tool-section-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;display:flex}.utility-tool-field-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.58rem;font-weight:700;display:block}.utility-export-group-grid{gap:.75rem;display:grid}@media (min-width:1024px){.utility-export-group-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-export-group-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);border-radius:12px;flex-direction:column;gap:.5rem;padding:.875rem;display:flex}.utility-export-group-title{color:var(--pb-text-primary);font-size:.72rem;font-weight:600}.utility-export-group-fields{flex-direction:column;gap:.375rem;display:flex}.utility-export-group-fields .utility-step-card{padding:.5rem .75rem}.utility-export-field-label{color:var(--pb-text-primary);font-size:.8rem;font-weight:500;line-height:1.25}.utility-export-field-meta{justify-content:space-between;align-items:baseline;gap:.875rem;width:100%;min-width:0;display:flex}.utility-export-field-sub{color:var(--pb-text-tertiary);text-align:right;white-space:nowrap;text-overflow:ellipsis;flex:0 auto;min-width:0;font-family:JetBrains Mono,monospace;font-size:.66rem;line-height:1.2;overflow:hidden}.utility-export-options-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);border-radius:10px;padding:1rem}.utility-export-inline-label{color:var(--pb-text-primary);font-size:.72rem;font-weight:600}.utility-export-multi-grid{gap:.5rem;display:grid}@media (min-width:640px){.utility-export-multi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-export-summary-grid{gap:.625rem;display:grid}@media (min-width:640px){.utility-export-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.utility-export-summary-card{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);border-radius:10px;padding:.875rem 1rem}.utility-export-summary-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);font-family:Syne,sans-serif;font-size:.58rem;font-weight:700}.utility-export-summary-value{color:var(--pb-text-primary);margin-top:.35rem;font-family:JetBrains Mono,monospace;font-size:.92rem;font-weight:600}.utility-tool-select.dropdown-select{--pb-control-font-size:.82rem;--pb-control-line-height:1.25rem;--pb-control-radius:8px;width:100%}.utility-tool-select .dropdown-select-trigger{border-color:var(--pb-border-default);background:var(--pb-surface-input);min-height:0;box-shadow:none;padding:.5rem .75rem}.utility-tool-select .dropdown-select-trigger-label{color:var(--pb-text-primary);font-weight:400}.utility-tool-select .dropdown-select-chevron{color:var(--pb-text-secondary)}.utility-tool-output-field{border:1px solid var(--pb-border-subtle);background:var(--pb-surface-app);color:var(--pb-text-secondary);border-radius:8px;padding:.5rem .75rem;font-family:DM Sans,sans-serif;font-size:.82rem;line-height:1.25rem}.utility-tool-browse-button{gap:.375rem;min-height:0;padding:.375rem .875rem;font-size:.78rem;font-weight:600;line-height:1.5}.utility-tool-browse-button svg{width:.875rem;height:.875rem}.utility-tool-table-wrap{border:1px solid var(--pb-border-subtle);border-radius:10px;overflow:hidden}.utility-tool-table{border-collapse:collapse;width:100%}.utility-tool-table th{text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);background:var(--pb-surface-shell);border-bottom:1px solid var(--pb-border-default);padding:.4375rem .75rem;font-family:Syne,sans-serif;font-size:.52rem;font-weight:700}.utility-tool-table th.r,.utility-tool-table td.r{text-align:right}.utility-tool-table td{color:var(--pb-text-secondary);border-bottom:1px solid var(--pb-border-subtle);padding:.5rem .75rem;font-size:.78rem}.utility-tool-table tbody tr:last-child td{border-bottom:none}.utility-tool-table tbody tr:hover td{background:var(--pb-surface-selected)}.utility-tool-table-output{color:var(--pb-status-success)}.utility-tool-action-footer{border-top:1px solid var(--pb-border-subtle);justify-content:flex-end;align-items:center;gap:.625rem;padding:.75rem 0 0;display:flex}.utility-tool-action-footer .settings-footer-actions{gap:.625rem;margin-left:auto}.utility-tool-action-footer .btn-ghost,.utility-tool-action-footer .btn-primary{min-height:0;padding:.5625rem 1.125rem;font-size:.82rem;line-height:1.25rem}.utility-tool-action-footer-card{margin-top:.25rem}.utility-step-card-active{border-color:var(--pb-interactive);background-color:var(--pb-surface-selected)}.utility-step-card-static{border-color:var(--pb-border-subtle);background-color:var(--pb-bg-card)}.utility-empty-state-compact{padding-top:1rem;padding-bottom:1rem}.utility-template-table-type{color:var(--pb-text-primary);font-size:.78rem;font-weight:600}.utility-template-meta-token{color:var(--pb-text-tertiary);font-family:JetBrains Mono,monospace;font-size:.72rem}.utility-template-meta-token strong{color:var(--pb-text-secondary);font-weight:600}.utilities-queue-table-wrap{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:14px;overflow:hidden}.utilities-queue-table{border-collapse:collapse;width:100%}.utilities-queue-table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;--tw-font-weight:var(--font-weight-bold);font-family:Syne,sans-serif;font-size:.58rem;font-weight:var(--font-weight-bold);--tw-tracking:.1em;letter-spacing:.1em;color:var(--pb-text-dim);text-transform:uppercase}.utilities-queue-table th.c,.utilities-queue-table td.c{text-align:center}.utilities-queue-table th.r,.utilities-queue-table td.r{text-align:right}.utilities-queue-table td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border-subtle);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:.82rem}.utilities-queue-table tbody:last-child tr:last-child td{border-bottom:0}.utilities-queue-row:hover td{background-color:var(--pb-surface-selected)}.utilities-queue-row>td{vertical-align:middle}.utilities-queue-led{height:calc(var(--spacing) * 2);width:calc(var(--spacing) * 2);background-color:var(--pb-text-dim);border-radius:3.40282e38px;display:inline-block}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led{background-color:color-mix(in srgb, var(--pb-text-dim) 45%, transparent)}}.utilities-queue-led-green{background-color:var(--pb-success);box-shadow:0 0 8px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led-green{box-shadow:0 0 8px color-mix(in srgb, var(--pb-success) 30%, transparent)}}.utilities-queue-led-amber{background-color:var(--pb-warning);box-shadow:0 0 8px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led-amber{box-shadow:0 0 8px color-mix(in srgb, var(--pb-warning) 28%, transparent)}}.utilities-queue-led-blue{background-color:var(--pb-info);box-shadow:0 0 8px var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-led-blue{box-shadow:0 0 8px color-mix(in srgb, var(--pb-info) 28%, transparent)}}.utilities-queue-job-name{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-primary)}.utilities-queue-job-type{margin-top:calc(var(--spacing) * .5);color:var(--pb-text-dim);font-size:.72rem}.utilities-queue-job-meta{margin-top:var(--spacing);color:var(--pb-text-secondary);font-size:.68rem}.utilities-queue-items{--tw-font-weight:var(--font-weight-semibold);font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:var(--font-weight-semibold);color:var(--pb-text-secondary)}.utilities-queue-progress-cell .series-mission-control-bar{gap:.625rem}.utilities-queue-progress-cell .series-mission-control-bar-track{min-width:120px}.utilities-queue-time{color:var(--pb-text-secondary);font-family:JetBrains Mono,monospace;font-size:.72rem}.utilities-queue-actions{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 1.5);display:flex}.utilities-queue-act-btn{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:8px;justify-content:center;align-items:center;display:inline-flex}.utilities-queue-act-btn:hover{border-color:var(--pb-border-strong);color:var(--pb-text);background-color:var(--pb-bg-card-hover)}.utilities-queue-act-btn-danger{color:var(--pb-error)}.utilities-queue-act-btn-danger:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-act-btn-danger:hover{border-color:color-mix(in srgb, var(--pb-error) 36%, transparent)}}.utilities-queue-act-btn-danger:hover{background-color:var(--pb-error-dim);color:var(--pb-error)}.utilities-queue-detail-cell{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.utilities-queue-detail-cell{background-color:color-mix(in srgb, var(--pb-bg-surface) 72%, transparent)}}.utilities-queue-detail-summary{align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:var(--spacing);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);color:var(--pb-text-dim);flex-wrap:wrap;font-size:.72rem;display:flex}.utilities-queue-detail-summary strong{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.utilities-queue-detail-label{--tw-tracking:.08em;letter-spacing:.08em;text-transform:uppercase}.utilities-queue-empty{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 12);text-align:center;flex-direction:column;display:flex}.utilities-queue-empty-compact{padding-block:calc(var(--spacing) * 8)}.utilities-queue-empty-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary)}.utilities-queue-empty-copy{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.queue-job-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1);overflow:hidden}.queue-job-card-expanded{border-color:var(--pb-border-hover);box-shadow:var(--pb-shadow-2)}.queue-progress-track{height:calc(var(--spacing) * 1.5);background-color:var(--pb-bg-card-hover);border-radius:3.40282e38px;width:100%;overflow:hidden}.queue-progress-fill{height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.5s;background-color:var(--pb-success);border-radius:3.40282e38px;transition-duration:.5s}.queue-progress-fill-paused{background-color:var(--pb-warning)}.selection-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));box-shadow:var(--pb-shadow-1)}.selection-card:not(.selection-card-active):hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.selection-card:not(.selection-card-active):hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 55%, transparent)}}.selection-card-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.selection-card-active{border-color:color-mix(in srgb, var(--pb-interactive) 42%, transparent)}}.selection-card-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.selection-card-active{background-color:color-mix(in srgb, var(--pb-interactive) 14%, var(--pb-bg-card))}}.selection-card-active{box-shadow:var(--pb-shadow-2)}.selection-card-icon{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:var(--radius-xl);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-shrink:0;justify-content:center;align-items:center;display:flex}.selection-card-icon-active{color:var(--pb-interactive);background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.selection-card-icon-active{background-color:color-mix(in srgb, var(--pb-interactive) 14%, transparent)}}.settings-theme-choice{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.settings-theme-choice-icon{height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);border-radius:var(--radius-lg)}.settings-media-preview-panel{background-color:var(--pb-surface-selected);border-color:var(--pb-border-hover)}@supports (color:color-mix(in lab, red, red)){.settings-media-preview-panel{border-color:color-mix(in srgb, var(--pb-border-hover) 85%, transparent)}}.settings-media-preview-panel{overflow-wrap:anywhere;min-height:4.625rem;transition:opacity .16s,border-color .16s}.settings-media-preview-panel.is-loading{opacity:.68;border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.settings-media-preview-panel.is-loading{border-color:color-mix(in srgb, var(--pb-interactive) 30%, var(--pb-border-hover))}}.workflow-step{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);box-shadow:var(--pb-shadow-1)}.workflow-step-active{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active{border-color:color-mix(in srgb, var(--pb-interactive) 32%, transparent)}}.workflow-step-active{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active{background-color:color-mix(in srgb, var(--pb-interactive) 10%, var(--pb-bg-card))}}.workflow-step-complete{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete{border-color:color-mix(in srgb, var(--pb-success) 28%, transparent)}}.workflow-step-complete{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete{background-color:color-mix(in srgb, var(--pb-success) 10%, var(--pb-bg-card))}}.workflow-step-badge{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);border-style:var(--tw-border-style);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);border-width:1px;border-color:var(--pb-border-hover);color:var(--pb-text-secondary);background-color:var(--pb-bg-card);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.workflow-step-active .workflow-step-badge{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active .workflow-step-badge{border-color:color-mix(in srgb, var(--pb-interactive) 32%, transparent)}}.workflow-step-active .workflow-step-badge{color:var(--pb-interactive);background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.workflow-step-active .workflow-step-badge{background-color:color-mix(in srgb, var(--pb-interactive) 12%, transparent)}}.workflow-step-complete .workflow-step-badge{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete .workflow-step-badge{border-color:color-mix(in srgb, var(--pb-success) 28%, transparent)}}.workflow-step-complete .workflow-step-badge{color:var(--pb-success);background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.workflow-step-complete .workflow-step-badge{background-color:color-mix(in srgb, var(--pb-success) 12%, transparent)}}.workflow-step-label{--tw-font-weight:var(--font-weight-medium);font-size:10px;font-weight:var(--font-weight-medium);--tw-tracking:.12em;letter-spacing:.12em;color:var(--pb-text-dim);text-transform:uppercase}.workflow-step-title{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.checklist-item{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-secondary);display:flex}.section-body{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.section-header{gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);flex-direction:column;display:flex}@media (min-width:40rem){.section-header{flex-direction:row;justify-content:space-between;align-items:flex-start}}.section-header-copy{min-width:0}:where(.section-header-copy>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.section-header-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;flex-shrink:0;display:flex}.section-eyebrow{--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-dim);text-transform:uppercase}.section-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:.14em;letter-spacing:.14em;color:var(--pb-text-primary);text-transform:uppercase}.section-title-plain{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.section-description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.settings-connection-list{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}.settings-connection-card{cursor:pointer;border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding:calc(var(--spacing) * 5);text-align:left;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.settings-connection-card:hover{border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.settings-connection-card-muted{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card-muted{background-color:color-mix(in srgb, var(--pb-bg-surface) 70%, transparent)}}.settings-connection-card-muted{opacity:.78}.settings-connection-card .downloads-action-btn.is-danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card .downloads-action-btn.is-danger{border-color:color-mix(in srgb, var(--pb-error) 28%, var(--pb-border-subtle))}}.settings-connection-card .downloads-action-btn.is-danger{background:var(--pb-error-dim);color:var(--pb-error)}.settings-connection-card .downloads-action-btn.is-danger:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card .downloads-action-btn.is-danger:hover{border-color:color-mix(in srgb, var(--pb-error) 44%, transparent)}}.settings-connection-card .downloads-action-btn.is-danger:hover{background:var(--pb-error-dim)}@supports (color:color-mix(in lab, red, red)){.settings-connection-card .downloads-action-btn.is-danger:hover{background:color-mix(in srgb, var(--pb-error-dim) 86%, transparent)}}.settings-connection-card .downloads-action-btn.is-danger:hover{color:var(--pb-error)}.admin-nav-link{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-lg);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-size:.8125rem;font-weight:var(--font-weight-medium);color:var(--pb-text-dim);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-width:1px;border-color:#0000;display:flex}.admin-nav-link:hover{color:var(--pb-text-secondary);background-color:var(--pb-surface-selected)}.admin-nav-link-active{color:var(--pb-interactive);background-color:var(--pb-surface-selected)}.admin-nav-link-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);flex-shrink:0;justify-content:center;align-items:center;display:flex}.admin-nav-link-active .admin-nav-link-icon{color:var(--pb-interactive)}.admin-nav-link-copy{min-width:0}.admin-nav-link-title{text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);font-size:.8125rem;font-weight:var(--font-weight-medium);overflow:hidden}:where(.settings-rows>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--pb-border)}.settings-row{gap:calc(var(--spacing) * 4);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}@media (min-width:40rem){.settings-row{grid-template-columns:220px minmax(0,1fr);align-items:flex-start}}.settings-row-meta{padding-top:var(--spacing)}.settings-row-label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--pb-text-secondary);display:block}.settings-row-help{margin-top:calc(var(--spacing) * .5);--tw-leading:calc(var(--spacing) * 5);font-size:11px;line-height:calc(var(--spacing) * 5);color:var(--pb-text-dim)}.settings-row-content{min-width:0}.settings-rows-align-end .settings-row-content{flex-direction:column;align-items:flex-end;display:flex}.settings-footer{gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media (min-width:40rem){.settings-footer{flex-direction:row;justify-content:space-between;align-items:center}}.settings-footer{background-color:var(--pb-bg-surface);overflow-anchor:none}@media (min-width:40rem){.settings-footer-end{justify-content:flex-end}}.settings-footer-copy{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--pb-text-dim)}.settings-footer-actions{align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}.settings-footer-actions .btn-primary,.settings-footer-actions .btn-ghost,.settings-footer-actions .btn-danger,.settings-footer-actions .btn-warning{min-height:calc(var(--spacing) * 10);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.field-note{margin-top:calc(var(--spacing) * 1.5);align-items:flex-start;gap:calc(var(--spacing) * 1.5);--tw-leading:calc(var(--spacing) * 5);font-size:11px;line-height:calc(var(--spacing) * 5);display:flex}.field-note code{color:var(--pb-text-secondary)}.field-note-icon{margin-top:calc(var(--spacing) * .5);flex-shrink:0;width:.75rem;height:.75rem}.field-note-warning{color:var(--pb-warning)}.field-note-info{color:var(--pb-info)}.field-note-danger{color:var(--pb-error)}.field-note-success{color:var(--pb-success)}.alert-banner{gap:calc(var(--spacing) * 4);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);border-width:1px;flex-direction:column;display:flex}@media (min-width:40rem){.alert-banner{flex-direction:row;justify-content:space-between;align-items:flex-start}}.alert-banner{box-shadow:var(--pb-shadow-1)}.alert-banner-main{align-items:flex-start;gap:calc(var(--spacing) * 4);min-width:0;display:flex}.alert-banner-icon{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);border-radius:var(--radius-lg);flex-shrink:0;justify-content:center;align-items:center;display:flex}.alert-banner-copy{min-width:0}:where(.alert-banner-copy>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.alert-banner-title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--pb-text-primary)}.alert-banner-description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--pb-text-secondary)}.alert-banner-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;flex-shrink:0;display:flex}.alert-banner-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.alert-banner-info{background-color:color-mix(in srgb, var(--pb-info) 10%, var(--pb-bg-card))}}.alert-banner-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.alert-banner-info{border-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.alert-banner-info .alert-banner-icon{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.alert-banner-info .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-info) 18%, transparent)}}.alert-banner-info .alert-banner-icon{color:var(--pb-info)}.alert-banner-warning{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.alert-banner-warning{background-color:color-mix(in srgb, var(--pb-warning) 10%, var(--pb-bg-card))}}.alert-banner-warning{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.alert-banner-warning{border-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.alert-banner-warning .alert-banner-icon{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.alert-banner-warning .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-warning) 18%, transparent)}}.alert-banner-warning .alert-banner-icon{color:var(--pb-warning)}.alert-banner-danger{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.alert-banner-danger{background-color:color-mix(in srgb, var(--pb-error) 10%, var(--pb-bg-card))}}.alert-banner-danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.alert-banner-danger{border-color:color-mix(in srgb, var(--pb-error) 18%, transparent)}}.alert-banner-danger .alert-banner-icon{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.alert-banner-danger .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-error) 18%, transparent)}}.alert-banner-danger .alert-banner-icon{color:var(--pb-error)}.alert-banner-success{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.alert-banner-success{background-color:color-mix(in srgb, var(--pb-success) 10%, var(--pb-bg-card))}}.alert-banner-success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.alert-banner-success{border-color:color-mix(in srgb, var(--pb-success) 18%, transparent)}}.alert-banner-success .alert-banner-icon{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.alert-banner-success .alert-banner-icon{background-color:color-mix(in srgb, var(--pb-success) 18%, transparent)}}.alert-banner-success .alert-banner-icon{color:var(--pb-success)}.modal-shell{z-index:50;padding:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 16);justify-content:center;align-items:flex-start;display:flex;position:fixed;inset:0}.modal-backdrop{z-index:0;background-color:var(--pb-bg-overlay);--tw-backdrop-blur:blur(var(--blur-xs));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);position:fixed;inset:0}.modal-panel{z-index:10;border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);width:100%;box-shadow:var(--pb-shadow-overlay);flex-direction:column;display:flex;position:relative;overflow:hidden}.modal-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.modal-body{flex:1;overflow-y:auto}.modal-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--pb-border);background-color:var(--pb-bg-card);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 4);flex-shrink:0;justify-content:space-between;align-items:center;display:flex}.donation-modal-shell{align-items:flex-start;padding-top:4.75rem}.donation-modal-panel{border-radius:1.5rem;width:min(100vw - 2rem,56rem);max-height:calc(100dvh - 6rem)}.donation-modal-header{border-bottom-color:var(--pb-border-subtle);background:radial-gradient(circle at top left, var(--pb-brand-dim), transparent 18rem), var(--pb-bg-card);align-items:flex-start;gap:1rem}.donation-modal-kicker{letter-spacing:.18em;text-transform:uppercase;color:var(--pb-brand);font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:700}.donation-modal-title{color:var(--pb-text);letter-spacing:-.03em;margin-top:.2rem;font-family:Syne,sans-serif;font-size:clamp(1.35rem,2.5vw,2rem);font-weight:800;line-height:1.05}.donation-modal-copy{max-width:38rem;color:var(--pb-text-sec);margin-top:.45rem;font-size:.9rem;line-height:1.55}.donation-modal-body{background:linear-gradient(135deg, transparent, var(--pb-surface-selected)), var(--pb-bg-surface);gap:1rem;padding:1rem;display:grid}.donation-option-card{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);box-shadow:var(--pb-shadow-1);border-radius:1.25rem;grid-template-columns:minmax(0,1fr) 10.5rem;align-items:stretch;gap:1rem;padding:1rem;display:grid}.donation-option-card--coffee{border-color:#fd0}@supports (color:color-mix(in lab, red, red)){.donation-option-card--coffee{border-color:color-mix(in srgb, #fd0 38%, var(--pb-border-subtle))}}.donation-option-card--liberapay{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.donation-option-card--liberapay{border-color:color-mix(in srgb, var(--pb-success) 32%, var(--pb-border-subtle))}}.donation-option-main{flex-direction:column;justify-content:space-between;gap:1rem;min-width:0;display:flex}.donation-option-eyebrow{letter-spacing:.16em;text-transform:uppercase;color:var(--pb-text-sec);font-family:JetBrains Mono,monospace;font-size:.64rem;font-weight:700}.donation-option-title{color:var(--pb-text);margin-top:.25rem;font-family:Syne,sans-serif;font-size:1.1rem;font-weight:800}.donation-option-copy{max-width:28rem;color:var(--pb-text-sec);margin-top:.35rem;font-size:.82rem;line-height:1.5}.donation-option-button{border-radius:999px;justify-content:center;align-items:center;width:fit-content;padding:.62rem 1rem;font-size:.82rem;font-weight:800;line-height:1;text-decoration:none;transition:transform .16s,box-shadow .16s,filter .16s;display:inline-flex}.donation-option-button:hover,.donation-option-button:focus-visible{filter:brightness(1.02);transform:translateY(-1px)}.donation-option-button:focus-visible{outline:2px solid var(--pb-focus-outline);outline-offset:3px}.donation-option-button--coffee{color:#16130a;background:#fd0;box-shadow:0 10px 20px #fd03}.donation-option-button--liberapay{background:var(--pb-success);color:#f7f1e8;box-shadow:0 10px 20px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.donation-option-button--liberapay{box-shadow:0 10px 20px color-mix(in srgb, var(--pb-success) 20%, transparent)}}[data-theme=dark] .donation-option-button--liberapay{color:#16130a}.donation-qr-frame{border:1px solid var(--pb-border-subtle);background:#fff;border-radius:1rem;place-items:center;min-height:10.5rem;padding:.625rem;display:grid}.donation-qr{object-fit:contain;width:9.25rem;height:9.25rem}@media (max-width:720px){.donation-modal-shell{padding-top:3.25rem}.donation-modal-panel{max-height:calc(100dvh - 4rem)}.donation-option-card{grid-template-columns:1fr}.donation-option-button{width:100%}}.issue-search-modal-panel{border-radius:1.25rem;width:min(100vw - 2rem,78rem);max-width:none;max-height:90vh}.issue-search-modal-header{border-bottom-color:var(--pb-border-subtle);align-items:flex-start;padding:1.125rem 1.5rem}.issue-search-modal-title{letter-spacing:.03em;text-transform:uppercase;color:var(--pb-text);font-family:Syne,sans-serif;font-size:1.05rem;font-weight:800}.issue-search-modal-title span{color:var(--pb-brand)}.issue-search-modal-subtitle{color:var(--pb-text-dim);margin-top:.1875rem;font-size:.78rem}.issue-search-modal-stats{flex-wrap:wrap;gap:.375rem;margin-top:.5rem;display:flex}.issue-search-modal-close{border-radius:var(--radius-lg);--tw-border-style:none;color:var(--pb-text-dim);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;border-style:none;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.issue-search-modal-close:hover{color:var(--pb-text-primary)}}.issue-search-modal-close{width:1.875rem;height:1.875rem}.issue-search-modal-close:hover{background:var(--pb-surface-selected)}.issue-search-modal-body{padding:0}.issue-search-loading-state{min-height:24rem}.issue-search-loading-title{color:var(--pb-text);font-size:.82rem;font-weight:600}.issue-search-loading-copy{color:var(--pb-text-dim);font-size:.72rem}.issue-search-dc-status{border-top:1px solid var(--pb-border-subtle);color:var(--pb-text-dim);align-items:center;gap:.625rem;padding:.75rem 1rem;font-size:.78rem;display:flex}.issue-search-dc-results{border-top:1px solid var(--pb-border-subtle)}.issue-search-dc-results-heading{align-items:center;gap:.5rem;padding:.75rem 1rem;display:flex}.issue-search-modal-footer{border-top-color:var(--pb-border-subtle);justify-content:flex-end;padding:.75rem 1.5rem}.issue-search-modal-footer-meta{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.68rem}.issue-search-modal-footer-meta:empty{display:none}.issue-search-modal-footer-meta strong{color:var(--pb-text);font-weight:700}.issue-search-modal-footer-close{margin-left:auto}.inline-alert{align-items:flex-start;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;display:flex}.inline-alert-icon{margin-top:calc(var(--spacing) * .5);flex-shrink:0}.inline-alert-danger{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.inline-alert-danger{background-color:color-mix(in srgb, var(--pb-error) 12%, var(--pb-bg-card))}}.inline-alert-danger{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.inline-alert-danger{border-color:color-mix(in srgb, var(--pb-error) 22%, transparent)}}.inline-alert-danger{color:var(--pb-error)}.inline-alert-warning{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.inline-alert-warning{background-color:color-mix(in srgb, var(--pb-warning) 12%, var(--pb-bg-card))}}.inline-alert-warning{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.inline-alert-warning{border-color:color-mix(in srgb, var(--pb-warning) 22%, transparent)}}.inline-alert-warning{color:var(--pb-warning)}.inline-alert-info{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.inline-alert-info{background-color:color-mix(in srgb, var(--pb-info) 12%, var(--pb-bg-card))}}.inline-alert-info{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.inline-alert-info{border-color:color-mix(in srgb, var(--pb-info) 22%, transparent)}}.inline-alert-info{color:var(--pb-info)}.inline-alert-success{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.inline-alert-success{background-color:color-mix(in srgb, var(--pb-success) 12%, var(--pb-bg-card))}}.inline-alert-success{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.inline-alert-success{border-color:color-mix(in srgb, var(--pb-success) 22%, transparent)}}.inline-alert-success{color:var(--pb-success)}.field-invalid{border-color:var(--pb-error)!important}@supports (color:color-mix(in lab, red, red)){.field-invalid{border-color:color-mix(in srgb, var(--pb-error) 52%, transparent)!important}}.icon-btn{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.icon-btn:hover{border-color:var(--pb-border-strong);color:var(--pb-text-primary)}}.icon-btn:disabled{cursor:not-allowed;opacity:.5}.icon-btn{min-width:var(--pb-control-min-height);width:var(--pb-control-min-height);min-height:var(--pb-control-min-height);border-radius:var(--pb-control-radius);padding:0}.toggle-switch{cursor:pointer;background-color:var(--pb-bg-card-hover);width:2.25rem;height:1.25rem;box-shadow:inset 0 0 0 1px var(--pb-border-hover);border-radius:999px;flex-shrink:0;transition:background-color .18s,box-shadow .18s;display:inline-flex;position:relative}label:has(>.toggle-input+.toggle-switch){position:relative}.toggle-input{clip:rect(0, 0, 0, 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.toggle-input-overlay{opacity:0;clip:auto;clip-path:none;white-space:normal;z-index:1;width:100%;height:100%;margin:0;inset:0}.toggle-switch:after{content:"";background-color:var(--pb-brand);width:1rem;height:1rem;box-shadow:0 1px 2px var(--pb-text-primary);border-radius:999px;position:absolute;top:2px;left:2px}@supports (color:color-mix(in lab, red, red)){.toggle-switch:after{box-shadow:0 1px 2px color-mix(in srgb, var(--pb-text-primary) 14%, transparent)}}.toggle-switch:after{transition:transform .18s}.peer:checked+.toggle-switch{background-color:var(--pb-interactive);box-shadow:inset 0 0 0 1px var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.peer:checked+.toggle-switch{box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--pb-interactive) 34%, transparent)}}.peer:checked+.toggle-switch:after{background-color:var(--pb-surface-shell);transform:translate(1rem)}.peer:focus-visible+.toggle-switch{outline:2px solid var(--pb-focus-outline);outline-offset:3px;box-shadow:0 0 0 4px var(--pb-focus-ring)}.strength-segment{height:calc(var(--spacing) * 1.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;border-radius:3.40282e38px;flex:1;transition-duration:.3s}.strength-segment-neutral{background-color:var(--pb-bg-card-hover)}.strength-segment-danger{background-color:var(--pb-error)}.strength-segment-warning{background-color:var(--pb-warning)}.strength-segment-success{background-color:var(--pb-success)}.input-pb{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-input);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-family:DM Sans,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-primary);border-radius:10px}.input-pb::placeholder{color:var(--pb-text-dim)}.input-pb:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.input-pb:focus{outline-offset:2px;outline:2px solid #0000}}.checkbox-pb{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);border-color:var(--pb-border-hover);background-color:var(--pb-bg-input);color:var(--pb-interactive);border-radius:.25rem}.checkbox-pb:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.select-pb{appearance:none;border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-input);width:100%;padding-block:calc(var(--spacing) * 2);padding-right:calc(var(--spacing) * 8);padding-left:calc(var(--spacing) * 3);font-family:DM Sans,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-primary);border-radius:10px}.select-pb:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.select-pb:focus{outline-offset:2px;outline:2px solid #0000}}.select-pb{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%237A8599' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.25rem}.dropdown-select{min-width:0;position:relative}.dropdown-select-trigger{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);width:100%;color:var(--pb-text-primary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;display:flex}@media (hover:hover){.dropdown-select-trigger:hover{border-color:var(--pb-border-strong)}}.dropdown-select-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.dropdown-select-trigger:focus{outline-offset:2px;outline:2px solid #0000}}.dropdown-select-trigger:disabled{cursor:not-allowed;opacity:.6}.dropdown-select-trigger{gap:var(--pb-control-gap);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-px);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius)}.dropdown-select-trigger-label{text-overflow:ellipsis;white-space:nowrap;text-align:left;flex:1;min-width:0;overflow:hidden}.dropdown-select-chevron{color:var(--pb-text-secondary);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));width:var(--pb-control-icon-size);height:var(--pb-control-icon-size);flex-shrink:0}.import-advanced-disclosure[open]>summary .import-advanced-disclosure-chevron{rotate:90deg}.dropdown-select-panel{z-index:80;max-height:calc(var(--spacing) * 60);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card);padding-block:var(--spacing);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-shadow-color:#0003;position:fixed;top:0;left:0;overflow-y:auto}@supports (color:color-mix(in lab, red, red)){.dropdown-select-panel{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.dropdown-select-panel{max-height:var(--pb-dropdown-panel-max-height,15rem);will-change:top, left}.dropdown-select-panel[data-ready=false]{visibility:hidden;pointer-events:none}.dropdown-select-option{text-align:left;width:100%;color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;gap:var(--pb-control-gap);min-height:var(--pb-control-min-height);padding-inline:var(--pb-control-panel-item-px);padding-block:var(--pb-control-panel-item-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);display:flex}.dropdown-select-option-label{text-align:left;white-space:nowrap;overflow-wrap:normal;flex:1;min-width:0}.import-reconcile-issue-dropdown .dropdown-select-trigger{align-items:flex-start;height:auto}.import-reconcile-issue-dropdown .dropdown-select-trigger-label{white-space:normal;overflow-wrap:anywhere;text-overflow:clip;overflow:visible}.dropdown-select-panel-wrap .dropdown-select-option{align-items:flex-start}.dropdown-select-panel-wrap .dropdown-select-option-label{white-space:normal;overflow-wrap:anywhere;word-break:normal}.dropdown-select-option:hover,.dropdown-select-option-active{background-color:var(--pb-info-dim);color:var(--pb-text-primary)}.dropdown-select-option-active{transition-duration:0s}.dropdown-select-option-selected{background-color:var(--pb-info-dim);color:var(--pb-text-primary);font-weight:500}.dropdown-select-option-check{width:var(--pb-control-icon-size);height:var(--pb-control-icon-size);flex-shrink:0}.direct-provider-uri-control{position:relative}.direct-provider-uri-input{padding-right:calc(var(--spacing) * 10);font-family:JetBrains Mono,Fira Code,monospace}.direct-provider-uri-toggle{width:calc(var(--spacing) * 10);border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));inset-block:0;justify-content:center;align-items:center;display:flex;position:absolute;right:0}@media (hover:hover){.direct-provider-uri-toggle:hover{color:var(--pb-text-primary)}}.direct-provider-uri-toggle:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.direct-provider-uri-toggle:focus{outline-offset:2px;outline:2px solid #0000}}.direct-provider-uri-toggle:focus{--tw-ring-inset:inset}.direct-provider-uri-options{z-index:90;margin-top:var(--spacing);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card);padding-block:var(--spacing);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-shadow-color:#0003;position:absolute;top:100%;left:0;right:0;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.direct-provider-uri-options{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.direct-provider-uri-option{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media (hover:hover){.direct-provider-uri-option:hover{background-color:var(--pb-info-dim);color:var(--pb-text-primary)}}.direct-provider-uri-option:focus{background-color:var(--pb-info-dim);color:var(--pb-text-primary);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.direct-provider-uri-option:focus{outline-offset:2px;outline:2px solid #0000}}.direct-provider-uri-option-selected{background-color:var(--pb-info-dim);color:var(--pb-text-primary)}.search-field{min-width:0;position:relative}.search-field-icon{pointer-events:none;z-index:10;--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);color:var(--pb-text-secondary);top:50%;left:var(--pb-control-search-icon-left);width:var(--pb-control-icon-size);height:var(--pb-control-icon-size);position:absolute}.search-field-input{border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card-hover);width:100%;color:var(--pb-text-primary)}.search-field-input::placeholder{color:var(--pb-text-secondary)}.search-field-input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--pb-interactive);--tw-outline-style:none;border-color:#0000;outline-style:none}@media (forced-colors:active){.search-field-input:focus{outline-offset:2px;outline:2px solid #0000}}.search-field-input{min-height:var(--pb-control-min-height);padding-inline-start:var(--pb-control-search-padding-left);padding-inline-end:var(--pb-control-search-padding-right);padding-block:var(--pb-control-py);font-size:var(--pb-control-font-size);line-height:var(--pb-control-line-height);border-radius:var(--pb-control-radius);appearance:none;background-image:none}.search-field-editor{white-space:nowrap;min-height:42px;overflow:hidden}.search-field-editor[data-empty=true]:before{content:attr(data-placeholder);color:var(--pb-text-secondary)}.search-field-input::-webkit-search-decoration{appearance:none;display:none}.search-field-input::-webkit-search-cancel-button{appearance:none;display:none}.search-field-input::-webkit-search-results-button{appearance:none;display:none}.search-field-input::-webkit-search-results-decoration{appearance:none;display:none}.search-field-input::-ms-clear{width:0;height:0;display:none}.search-field-input::-ms-reveal{width:0;height:0;display:none}.search-field-clear{z-index:10;height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);padding:calc(var(--spacing) * .5);color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:.25rem;justify-content:center;align-items:center;display:inline-flex;position:absolute;top:50%}@media (hover:hover){.search-field-clear:hover{color:var(--pb-text-primary)}}.search-field-clear{right:var(--pb-control-clear-right)}.search-history-panel{z-index:30;margin-top:var(--spacing);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--pb-border-hover);background-color:var(--pb-bg-card);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-shadow-color:#0006;position:absolute;top:100%;left:0;right:0;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.search-history-panel{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.search-history-panel-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--pb-border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);justify-content:space-between;align-items:center;display:flex}.search-history-panel-title{--tw-font-weight:var(--font-weight-semibold);font-size:10px;font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--pb-text-dim);text-transform:uppercase}.search-history-panel-clear{color:var(--pb-text-dim);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));font-size:10px}@media (hover:hover){.search-history-panel-clear:hover{color:var(--pb-error)}}.search-history-list{max-height:280px;padding-block:var(--spacing);overflow-y:auto}.search-history-item{align-items:center;display:flex}.search-history-item-button{align-items:center;gap:calc(var(--spacing) * 2.5);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--pb-text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex:1;display:flex}@media (hover:hover){.search-history-item-button:hover{color:var(--pb-text-primary)}}.search-history-item-button:hover{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.search-history-item-button:hover{background-color:color-mix(in srgb, var(--pb-bg-card-hover) 60%, transparent)}}.search-history-item-remove{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);color:var(--pb-text-dim);opacity:0;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.search-history-item-remove:is(:where(.group):hover *){opacity:1}.search-history-item-remove:hover{color:var(--pb-error)}}.touch-target{min-width:44px;min-height:44px}.progress-track{height:var(--spacing);background-color:var(--pb-border);border-radius:3.40282e38px}.progress-fill{background-color:var(--pb-success);height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.progress-fill-partial{background-color:var(--pb-interactive);height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.callout-brand{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 4);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--pb-brand-signal-dim);border-width:1px;border-color:var(--pb-brand-signal-border);display:flex}.callout-brand:hover{background-color:var(--pb-brand-signal-hover);border-color:var(--pb-brand-signal-border)}.callout-brand-icon{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);background-color:var(--pb-brand-signal-dim);color:var(--pb-brand-signal);flex-shrink:0}.callout-brand-title{color:var(--pb-brand-signal)}.callout-brand-body{color:var(--pb-brand-signal);opacity:.7}.callout-brand-chevron{color:var(--pb-brand-signal);opacity:.5}}@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-auto{inset:auto}.inset-y-0{inset-block:0}.-top-3{top:calc(var(--spacing) * -3)}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.top-4{top:calc(var(--spacing) * 4)}.-right-1{right:calc(var(--spacing) * -1)}.-right-3{right:calc(var(--spacing) * -3)}.right-0{right:0}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.-bottom-1{bottom:calc(var(--spacing) * -1)}.left-0{left:0}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[60\]{z-index:60}.z-\[100\]{z-index:100}.z-\[110\]{z-index:110}.col-span-2{grid-column:span 2/span 2}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.\!mt-0{margin-top:0!important}.\!mt-2{margin-top:calc(var(--spacing) * 2)!important}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:var(--spacing)}.\!mb-0{margin-bottom:0!important}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-\[140px\]{margin-left:140px}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.aspect-\[2\/3\]{aspect-ratio:2/3}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-4\.5{height:calc(var(--spacing) * 4.5)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-28{height:calc(var(--spacing) * 28)}.h-32{height:calc(var(--spacing) * 32)}.h-96{height:calc(var(--spacing) * 96)}.h-\[18px\]{height:18px}.h-\[100dvh\]{height:100dvh}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-80{max-height:calc(var(--spacing) * 80)}.max-h-\[28rem\]{max-height:28rem}.max-h-\[62vh\]{max-height:62vh}.max-h-\[70vh\]{max-height:70vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.max-h-\[95vh\]{max-height:95vh}.max-h-\[calc\(100dvh-6rem\)\]{max-height:calc(100dvh - 6rem)}.max-h-\[calc\(100vh-5rem\)\]{max-height:calc(100vh - 5rem)}.\!min-h-8{min-height:calc(var(--spacing) * 8)!important}.\!min-h-10{min-height:calc(var(--spacing) * 10)!important}.min-h-0{min-height:0}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-56{min-height:calc(var(--spacing) * 56)}.min-h-\[36px\]{min-height:36px}.min-h-\[200px\]{min-height:200px}.\!w-8{width:calc(var(--spacing) * 8)!important}.\!w-10{width:calc(var(--spacing) * 10)!important}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-4\.5{width:calc(var(--spacing) * 4.5)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-32{width:calc(var(--spacing) * 32)}.w-36{width:calc(var(--spacing) * 36)}.w-40{width:calc(var(--spacing) * 40)}.w-44{width:calc(var(--spacing) * 44)}.w-48{width:calc(var(--spacing) * 48)}.w-56{width:calc(var(--spacing) * 56)}.w-64{width:calc(var(--spacing) * 64)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-\[10\%\]{width:10%}.w-\[12\%\]{width:12%}.w-\[14\%\]{width:14%}.w-\[18px\]{width:18px}.w-\[22rem\]{width:22rem}.w-\[24\%\]{width:24%}.w-\[26\%\]{width:26%}.w-\[28px\]{width:28px}.w-\[28rem\]{width:28rem}.w-\[30\%\]{width:30%}.w-\[30rem\]{width:30rem}.w-\[32px\]{width:32px}.w-\[34\%\]{width:34%}.w-\[35\%\]{width:35%}.w-\[52px\]{width:52px}.w-\[60px\]{width:60px}.w-\[64px\]{width:64px}.w-\[68px\]{width:68px}.w-\[72px\]{width:72px}.w-\[76px\]{width:76px}.w-\[80px\]{width:80px}.w-\[82px\]{width:82px}.w-\[84px\]{width:84px}.w-\[88px\]{width:88px}.w-\[90px\]{width:90px}.w-\[92px\]{width:92px}.w-\[96px\]{width:96px}.w-\[98px\]{width:98px}.w-\[100px\]{width:100px}.w-\[104px\]{width:104px}.w-\[108px\]{width:108px}.w-\[110px\]{width:110px}.w-\[112px\]{width:112px}.w-\[116px\]{width:116px}.w-\[118px\]{width:118px}.w-\[120px\]{width:120px}.w-\[128px\]{width:128px}.w-\[136px\]{width:136px}.w-\[140px\]{width:140px}.w-\[148px\]{width:148px}.w-\[150px\]{width:150px}.w-\[152px\]{width:152px}.w-\[156px\]{width:156px}.w-\[168px\]{width:168px}.w-\[180px\]{width:180px}.w-\[240px\]{width:240px}.w-auto{width:auto}.w-full{width:100%}.max-w-0{max-width:0}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-\[16rem\]{max-width:16rem}.max-w-\[140px\]{max-width:140px}.max-w-\[150px\]{max-width:150px}.max-w-\[160px\]{max-width:160px}.max-w-\[220px\]{max-width:220px}.max-w-\[240px\]{max-width:240px}.max-w-\[260px\]{max-width:260px}.max-w-\[280px\]{max-width:280px}.max-w-\[300px\]{max-width:300px}.max-w-\[320px\]{max-width:320px}.max-w-\[560px\]{max-width:560px}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-7{min-width:calc(var(--spacing) * 7)}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-\[11\.5rem\]{min-width:11.5rem}.min-w-\[20px\]{min-width:20px}.min-w-\[48rem\]{min-width:48rem}.min-w-\[54rem\]{min-width:54rem}.min-w-\[60px\]{min-width:60px}.min-w-\[440px\]{min-width:440px}.min-w-\[620px\]{min-width:620px}.min-w-\[640px\]{min-width:640px}.min-w-\[720px\]{min-width:720px}.min-w-\[760px\]{min-width:760px}.min-w-\[820px\]{min-width:820px}.min-w-\[860px\]{min-width:860px}.min-w-\[880px\]{min-width:880px}.min-w-\[920px\]{min-width:920px}.min-w-\[980px\]{min-width:980px}.min-w-\[1040px\]{min-width:1040px}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.basis-\[360px\]{flex-basis:360px}.basis-\[420px\]{flex-basis:420px}.table-fixed{table-layout:fixed}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1{--tw-translate-y:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-2{--tw-translate-y:calc(var(--spacing) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-wait{cursor:wait}.resize{resize:both}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.grid-cols-\[auto_minmax\(0\,1fr\)\]{grid-template-columns:auto minmax(0,1fr)}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:0}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-1{column-gap:var(--spacing)}.gap-x-1\.5{column-gap:calc(var(--spacing) * 1.5)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-pb-border>:not(:last-child)){border-color:var(--pb-border)}:where(.divide-pb-border-subtle>:not(:last-child)){border-color:var(--pb-border-subtle)}:where(.divide-pb-border\/40>:not(:last-child)){border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){:where(.divide-pb-border\/40>:not(:last-child)){border-color:color-mix(in oklab, var(--pb-border) 40%, transparent)}}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.\!rounded-full{border-radius:3.40282e38px!important}.\!rounded-none{border-radius:0!important}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[var\(--radius-card\)\]{border-radius:var(--radius-card)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.\!border-0{border-style:var(--tw-border-style)!important;border-width:0!important}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-\[3px\]{border-style:var(--tw-border-style);border-width:3px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-pb-warning\/35{border-color:var(--pb-warning)!important}@supports (color:color-mix(in lab, red, red)){.\!border-pb-warning\/35{border-color:color-mix(in oklab, var(--pb-warning) 35%, transparent)!important}}.border-current\/15{border-color:currentColor}@supports (color:color-mix(in lab, red, red)){.border-current\/15{border-color:color-mix(in oklab, currentcolor 15%, transparent)}}.border-pb-border{border-color:var(--pb-border)}.border-pb-border-hover,.border-pb-border-hover\/50{border-color:var(--pb-border-hover)}@supports (color:color-mix(in lab, red, red)){.border-pb-border-hover\/50{border-color:color-mix(in oklab, var(--pb-border-hover) 50%, transparent)}}.border-pb-border-subtle{border-color:var(--pb-border-subtle)}.border-pb-border\/50{border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.border-pb-border\/50{border-color:color-mix(in oklab, var(--pb-border) 50%, transparent)}}.border-pb-border\/70{border-color:var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.border-pb-border\/70{border-color:color-mix(in oklab, var(--pb-border) 70%, transparent)}}.border-pb-error,.border-pb-error\/30{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.border-pb-error\/30{border-color:color-mix(in oklab, var(--pb-error) 30%, transparent)}}.border-pb-error\/35{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.border-pb-error\/35{border-color:color-mix(in oklab, var(--pb-error) 35%, transparent)}}.border-pb-error\/40{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.border-pb-error\/40{border-color:color-mix(in oklab, var(--pb-error) 40%, transparent)}}.border-pb-info\/30{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.border-pb-info\/30{border-color:color-mix(in oklab, var(--pb-info) 30%, transparent)}}.border-pb-info\/40{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.border-pb-info\/40{border-color:color-mix(in oklab, var(--pb-info) 40%, transparent)}}.border-pb-interactive,.border-pb-interactive\/25{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.border-pb-interactive\/25{border-color:color-mix(in oklab, var(--pb-interactive) 25%, transparent)}}.border-pb-interactive\/30{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.border-pb-interactive\/30{border-color:color-mix(in oklab, var(--pb-interactive) 30%, transparent)}}.border-pb-interactive\/40{border-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.border-pb-interactive\/40{border-color:color-mix(in oklab, var(--pb-interactive) 40%, transparent)}}.border-pb-success\/30{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.border-pb-success\/30{border-color:color-mix(in oklab, var(--pb-success) 30%, transparent)}}.border-pb-success\/35{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.border-pb-success\/35{border-color:color-mix(in oklab, var(--pb-success) 35%, transparent)}}.border-pb-success\/40{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.border-pb-success\/40{border-color:color-mix(in oklab, var(--pb-success) 40%, transparent)}}.border-pb-surface{border-color:var(--pb-bg-surface)}.border-pb-warning\/20{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/20{border-color:color-mix(in oklab, var(--pb-warning) 20%, transparent)}}.border-pb-warning\/25{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/25{border-color:color-mix(in oklab, var(--pb-warning) 25%, transparent)}}.border-pb-warning\/30{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/30{border-color:color-mix(in oklab, var(--pb-warning) 30%, transparent)}}.border-pb-warning\/35{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/35{border-color:color-mix(in oklab, var(--pb-warning) 35%, transparent)}}.border-pb-warning\/40{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/40{border-color:color-mix(in oklab, var(--pb-warning) 40%, transparent)}}.border-pb-warning\/50{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.border-pb-warning\/50{border-color:color-mix(in oklab, var(--pb-warning) 50%, transparent)}}.border-transparent{border-color:#0000}.border-t-pb-interactive{border-top-color:var(--pb-interactive)}.\!bg-pb-warning-dim\/45{background-color:var(--pb-warning-dim)!important}@supports (color:color-mix(in lab, red, red)){.\!bg-pb-warning-dim\/45{background-color:color-mix(in oklab, var(--pb-warning-dim) 45%, transparent)!important}}.\!bg-transparent{background-color:#0000!important}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab, red, red)){.bg-black\/80{background-color:color-mix(in oklab, var(--color-black) 80%, transparent)}}.bg-pb-base{background-color:var(--pb-bg-base)}.bg-pb-card{background-color:var(--pb-bg-card)}.bg-pb-card-hover,.bg-pb-card-hover\/20{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/20{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 20%, transparent)}}.bg-pb-card-hover\/25{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/25{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 25%, transparent)}}.bg-pb-card-hover\/35{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/35{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 35%, transparent)}}.bg-pb-card-hover\/40{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/40{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 40%, transparent)}}.bg-pb-card-hover\/45{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/45{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 45%, transparent)}}.bg-pb-card-hover\/50{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/50{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 50%, transparent)}}.bg-pb-card-hover\/60{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card-hover\/60{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 60%, transparent)}}.bg-pb-card\/40{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/40{background-color:color-mix(in oklab, var(--pb-bg-card) 40%, transparent)}}.bg-pb-card\/50{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/50{background-color:color-mix(in oklab, var(--pb-bg-card) 50%, transparent)}}.bg-pb-card\/60{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/60{background-color:color-mix(in oklab, var(--pb-bg-card) 60%, transparent)}}.bg-pb-card\/70{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/70{background-color:color-mix(in oklab, var(--pb-bg-card) 70%, transparent)}}.bg-pb-card\/80{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/80{background-color:color-mix(in oklab, var(--pb-bg-card) 80%, transparent)}}.bg-pb-card\/85{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/85{background-color:color-mix(in oklab, var(--pb-bg-card) 85%, transparent)}}.bg-pb-card\/95{background-color:var(--pb-bg-card)}@supports (color:color-mix(in lab, red, red)){.bg-pb-card\/95{background-color:color-mix(in oklab, var(--pb-bg-card) 95%, transparent)}}.bg-pb-error{background-color:var(--pb-error)}.bg-pb-error-dim,.bg-pb-error-dim\/70{background-color:var(--pb-error-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-error-dim\/70{background-color:color-mix(in oklab, var(--pb-error-dim) 70%, transparent)}}.bg-pb-error\/10{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.bg-pb-error\/10{background-color:color-mix(in oklab, var(--pb-error) 10%, transparent)}}.bg-pb-error\/15{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.bg-pb-error\/15{background-color:color-mix(in oklab, var(--pb-error) 15%, transparent)}}.bg-pb-info{background-color:var(--pb-info)}.bg-pb-info-dim{background-color:var(--pb-info-dim)}.bg-pb-info\/10{background-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.bg-pb-info\/10{background-color:color-mix(in oklab, var(--pb-info) 10%, transparent)}}.bg-pb-input{background-color:var(--pb-bg-input)}.bg-pb-interactive{background-color:var(--pb-interactive)}.bg-pb-interactive-dim{background-color:var(--pb-interactive-dim)}.bg-pb-interactive\/15{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.bg-pb-interactive\/15{background-color:color-mix(in oklab, var(--pb-interactive) 15%, transparent)}}.bg-pb-overlay,.bg-pb-overlay\/45{background-color:var(--pb-bg-overlay)}@supports (color:color-mix(in lab, red, red)){.bg-pb-overlay\/45{background-color:color-mix(in oklab, var(--pb-bg-overlay) 45%, transparent)}}.bg-pb-purple-dim{background-color:var(--pb-purple-dim)}.bg-pb-success{background-color:var(--pb-success)}.bg-pb-success-dim,.bg-pb-success-dim\/70{background-color:var(--pb-success-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success-dim\/70{background-color:color-mix(in oklab, var(--pb-success-dim) 70%, transparent)}}.bg-pb-success\/5{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success\/5{background-color:color-mix(in oklab, var(--pb-success) 5%, transparent)}}.bg-pb-success\/10{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success\/10{background-color:color-mix(in oklab, var(--pb-success) 10%, transparent)}}.bg-pb-success\/15{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.bg-pb-success\/15{background-color:color-mix(in oklab, var(--pb-success) 15%, transparent)}}.bg-pb-surface,.bg-pb-surface\/30{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/30{background-color:color-mix(in oklab, var(--pb-bg-surface) 30%, transparent)}}.bg-pb-surface\/40{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/40{background-color:color-mix(in oklab, var(--pb-bg-surface) 40%, transparent)}}.bg-pb-surface\/50{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/50{background-color:color-mix(in oklab, var(--pb-bg-surface) 50%, transparent)}}.bg-pb-surface\/60{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/60{background-color:color-mix(in oklab, var(--pb-bg-surface) 60%, transparent)}}.bg-pb-surface\/80{background-color:var(--pb-bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-pb-surface\/80{background-color:color-mix(in oklab, var(--pb-bg-surface) 80%, transparent)}}.bg-pb-text-dim{background-color:var(--pb-text-dim)}.bg-pb-warning{background-color:var(--pb-warning)}.bg-pb-warning-dim,.bg-pb-warning-dim\/25{background-color:var(--pb-warning-dim)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning-dim\/25{background-color:color-mix(in oklab, var(--pb-warning-dim) 25%, transparent)}}.bg-pb-warning\/5{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning\/5{background-color:color-mix(in oklab, var(--pb-warning) 5%, transparent)}}.bg-pb-warning\/10{background-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.bg-pb-warning\/10{background-color:color-mix(in oklab, var(--pb-warning) 10%, transparent)}}.bg-slate-950\/70{background-color:#020618b3}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/70{background-color:color-mix(in oklab, var(--color-slate-950) 70%, transparent)}}.bg-transparent{background-color:#0000}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-top{object-position:top}.\!p-0{padding:0!important}.p-0{padding:0}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.\!px-0{padding-inline:0!important}.\!px-2\.5{padding-inline:calc(var(--spacing) * 2.5)!important}.\!px-3{padding-inline:calc(var(--spacing) * 3)!important}.\!px-4{padding-inline:calc(var(--spacing) * 4)!important}.px-0{padding-inline:0}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.\!py-0{padding-block:0!important}.\!py-1\.5{padding-block:calc(var(--spacing) * 1.5)!important}.\!py-2{padding-block:calc(var(--spacing) * 2)!important}.\!py-2\.5{padding-block:calc(var(--spacing) * 2.5)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-\[3px\]{padding-block:3px}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:var(--spacing)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.\!pb-0{padding-bottom:0!important}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.\!text-left{text-align:left!important}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-display{font-family:Bricolage Grotesque,sans-serif}.font-mono{font-family:JetBrains Mono,Fira Code,monospace}.font-sans{font-family:DM Sans,sans-serif}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.78rem\]{font-size:.78rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.88rem\]{font-size:.88rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[22px\]{font-size:22px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-7{--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[18px\]{--tw-leading:18px;line-height:18px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-\[0\.22em\]{--tw-tracking:.22em;letter-spacing:.22em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-pretty{text-wrap:pretty}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.\!text-pb-text{color:var(--pb-text-primary)!important}.\!text-pb-warning{color:var(--pb-warning)!important}.text-current{color:currentColor}.text-pb-border{color:var(--pb-border)}.text-pb-brand{color:var(--pb-brand)}.text-pb-error{color:var(--pb-error)}.text-pb-info{color:var(--pb-info)}.text-pb-interactive{color:var(--pb-interactive)}.text-pb-purple{color:var(--pb-purple)}.text-pb-success{color:var(--pb-success)}.text-pb-text{color:var(--pb-text-primary)}.text-pb-text-dim{color:var(--pb-text-dim)}.text-pb-text-inverse{color:var(--pb-text-inverse)}.text-pb-text-sec{color:var(--pb-text-secondary)}.text-pb-warning,.text-pb-warning\/70{color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.text-pb-warning\/70{color:color-mix(in oklab, var(--pb-warning) 70%, transparent)}}.text-red-300{color:var(--color-red-300)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.accent-pb-interactive{accent-color:var(--pb-interactive)}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-\[1px\]{--tw-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xs{--tw-backdrop-blur:blur(var(--blur-xs));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.delay-75{transition-delay:75ms}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:border-pb-text-sec:is(:where(.group):hover *){border-color:var(--pb-text-secondary)}.group-hover\:text-pb-interactive:is(:where(.group):hover *){color:var(--pb-interactive)}.group-hover\:text-pb-text:is(:where(.group):hover *){color:var(--pb-text-primary)}.group-hover\:text-pb-text-sec:is(:where(.group):hover *){color:var(--pb-text-secondary)}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}}.placeholder\:text-pb-text-dim::placeholder{color:var(--pb-text-dim)}.placeholder\:text-pb-text-sec::placeholder{color:var(--pb-text-secondary)}.first\:pt-0:first-child{padding-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:\!border-pb-warning\/45:hover{border-color:var(--pb-warning)!important}@supports (color:color-mix(in lab, red, red)){.hover\:\!border-pb-warning\/45:hover{border-color:color-mix(in oklab, var(--pb-warning) 45%, transparent)!important}}.hover\:border-pb-border-hover:hover{border-color:var(--pb-border-hover)}.hover\:border-pb-border-strong:hover{border-color:var(--pb-border-strong)}.hover\:border-pb-error\/40:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-error\/40:hover{border-color:color-mix(in oklab, var(--pb-error) 40%, transparent)}}.hover\:border-pb-info\/40:hover{border-color:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-info\/40:hover{border-color:color-mix(in oklab, var(--pb-info) 40%, transparent)}}.hover\:border-pb-purple\/40:hover{border-color:var(--pb-purple)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-purple\/40:hover{border-color:color-mix(in oklab, var(--pb-purple) 40%, transparent)}}.hover\:border-pb-success\/40:hover{border-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-success\/40:hover{border-color:color-mix(in oklab, var(--pb-success) 40%, transparent)}}.hover\:border-pb-warning\/40:hover{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.hover\:border-pb-warning\/40:hover{border-color:color-mix(in oklab, var(--pb-warning) 40%, transparent)}}.hover\:\!bg-pb-warning-dim:hover{background-color:var(--pb-warning-dim)!important}.hover\:\!bg-transparent:hover{background-color:#0000!important}.hover\:bg-pb-card:hover{background-color:var(--pb-bg-card)}.hover\:bg-pb-card-hover:hover,.hover\:bg-pb-card-hover\/20:hover{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-card-hover\/20:hover{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 20%, transparent)}}.hover\:bg-pb-card-hover\/45:hover{background-color:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-card-hover\/45:hover{background-color:color-mix(in oklab, var(--pb-bg-card-hover) 45%, transparent)}}.hover\:bg-pb-error-dim:hover{background-color:var(--pb-error-dim)}.hover\:bg-pb-error\/25:hover{background-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-error\/25:hover{background-color:color-mix(in oklab, var(--pb-error) 25%, transparent)}}.hover\:bg-pb-info-dim:hover{background-color:var(--pb-info-dim)}.hover\:bg-pb-interactive-hover:hover{background-color:var(--pb-interactive-hover)}.hover\:bg-pb-interactive\/10:hover{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-interactive\/10:hover{background-color:color-mix(in oklab, var(--pb-interactive) 10%, transparent)}}.hover\:bg-pb-interactive\/25:hover{background-color:var(--pb-interactive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-interactive\/25:hover{background-color:color-mix(in oklab, var(--pb-interactive) 25%, transparent)}}.hover\:bg-pb-purple-dim:hover{background-color:var(--pb-purple-dim)}.hover\:bg-pb-success-dim:hover{background-color:var(--pb-success-dim)}.hover\:bg-pb-success\/25:hover{background-color:var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pb-success\/25:hover{background-color:color-mix(in oklab, var(--pb-success) 25%, transparent)}}.hover\:bg-pb-warning-dim:hover{background-color:var(--pb-warning-dim)}.hover\:\!text-pb-text:hover{color:var(--pb-text-primary)!important}.hover\:\!text-pb-warning:hover{color:var(--pb-warning)!important}.hover\:text-pb-error:hover{color:var(--pb-error)}.hover\:text-pb-interactive:hover{color:var(--pb-interactive)}.hover\:text-pb-interactive-hover:hover{color:var(--pb-interactive-hover)}.hover\:text-pb-text:hover{color:var(--pb-text-primary)}.hover\:text-pb-text-sec:hover{color:var(--pb-text-secondary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-95:hover{opacity:.95}.hover\:opacity-100:hover{opacity:1}.hover\:brightness-90:hover{--tw-brightness:brightness(90%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-pb-border-hover:focus{--tw-ring-color:var(--pb-border-hover)}.focus\:ring-pb-error:focus{--tw-ring-color:var(--pb-error)}.focus\:ring-pb-interactive:focus{--tw-ring-color:var(--pb-interactive)}.focus\:ring-pb-warning:focus{--tw-ring-color:var(--pb-warning)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-pb-card:focus{--tw-ring-offset-color:var(--pb-bg-card)}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-pb-interactive:focus-visible{outline-color:var(--pb-interactive)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:cursor-wait:disabled{cursor:wait}.disabled\:text-pb-text-dim:disabled{color:var(--pb-text-dim)}.disabled\:opacity-20:disabled{opacity:.2}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-100:disabled{opacity:1}@media (prefers-reduced-motion:reduce){.motion-reduce\:animate-none{animation:none}}@media (min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:ml-auto{margin-left:auto}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:table-cell{display:table-cell}.sm\:max-h-\[90vh\]{max-height:90vh}.sm\:w-14{width:calc(var(--spacing) * 14)}.sm\:w-44{width:calc(var(--spacing) * 44)}.sm\:w-48{width:calc(var(--spacing) * 48)}.sm\:w-56{width:calc(var(--spacing) * 56)}.sm\:w-auto{width:auto}.sm\:max-w-xs{max-width:var(--container-xs)}.sm\:min-w-\[760px\]{min-width:760px}.sm\:shrink-0{flex-shrink:0}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.4fr\)_auto\]{grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto}.sm\:grid-cols-\[minmax\(0\,3fr\)_minmax\(0\,2fr\)\]{grid-template-columns:minmax(0,3fr) minmax(0,2fr)}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:self-auto{align-self:auto}.sm\:rounded-xl{border-radius:var(--radius-xl)}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-7{padding:calc(var(--spacing) * 7)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:pt-5{padding-top:calc(var(--spacing) * 5)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:pt-7{padding-top:calc(var(--spacing) * 7)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}@media (min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:table-cell{display:table-cell}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-\[1fr_auto_1fr_auto\]{grid-template-columns:1fr auto 1fr auto}.md\:grid-cols-\[minmax\(0\,1fr\)_auto_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr) auto minmax(0,1fr)}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:items-start{align-items:flex-start}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:table-cell{display:table-cell}.lg\:w-52{width:calc(var(--spacing) * 52)}.lg\:w-auto{width:auto}.lg\:max-w-\[11rem\]{max-width:11rem}.lg\:max-w-md{max-width:var(--container-md)}.lg\:min-w-\[920px\]{min-width:920px}.lg\:min-w-\[980px\]{min-width:980px}.lg\:min-w-\[1060px\]{min-width:1060px}.lg\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[minmax\(0\,1\.1fr\)_minmax\(280px\,0\.9fr\)\]{grid-template-columns:minmax(0,1.1fr) minmax(280px,.9fr)}.lg\:grid-cols-\[minmax\(0\,1\.15fr\)_minmax\(280px\,0\.85fr\)\]{grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}.lg\:grid-cols-\[minmax\(0\,1\.25fr\)_minmax\(280px\,0\.75fr\)\]{grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr)}.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(16rem\,0\.65fr\)\]{grid-template-columns:minmax(0,1fr) minmax(16rem,.65fr)}.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(18rem\,24rem\)\]{grid-template-columns:minmax(0,1fr) minmax(18rem,24rem)}.lg\:grid-cols-\[minmax\(0\,2fr\)_minmax\(0\,3fr\)\]{grid-template-columns:minmax(0,2fr) minmax(0,3fr)}.lg\:grid-cols-\[minmax\(0\,280px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,280px) minmax(0,1fr)}.lg\:grid-cols-\[minmax\(12rem\,0\.7fr\)_minmax\(18rem\,1\.3fr\)\]{grid-template-columns:minmax(12rem,.7fr) minmax(18rem,1.3fr)}.lg\:grid-cols-\[repeat\(5\,minmax\(0\,1fr\)\)\]{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:text-right{text-align:right}}@media (min-width:80rem){.xl\:col-span-3{grid-column:span 3/span 3}.xl\:w-32{width:calc(var(--spacing) * 32)}.xl\:w-48{width:calc(var(--spacing) * 48)}.xl\:w-\[34rem\]{width:34rem}.xl\:max-w-sm{max-width:var(--container-sm)}.xl\:min-w-\[30rem\]{min-width:30rem}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:grid-cols-\[minmax\(0\,1\.4fr\)_minmax\(320px\,0\.8fr\)\]{grid-template-columns:minmax(0,1.4fr) minmax(320px,.8fr)}.xl\:grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(17rem\,1fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(17rem,1fr)}.xl\:grid-cols-\[minmax\(0\,1\.55fr\)_minmax\(320px\,0\.85fr\)\]{grid-template-columns:minmax(0,1.55fr) minmax(320px,.85fr)}.xl\:flex-row{flex-direction:row}.xl\:items-end{align-items:flex-end}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:justify-end{justify-content:flex-end}}@media (min-width:96rem){.\32 xl\:grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(320px\,0\.75fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(320px,.75fr)}}}@font-face{font-family:Bricolage Grotesque;font-style:normal;font-weight:800;font-display:swap;src:url(/static/fonts/bricolage-grotesque-800.woff2)format("woff2")}@font-face{font-family:DM Sans;font-style:normal;font-weight:300 700;font-display:swap;src:url(/static/fonts/dm-sans-variable.woff2)format("woff2")}@font-face{font-family:Syne;font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/syne-400.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/syne-500.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:600;font-display:swap;src:url(/static/fonts/syne-600.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/syne-700.ttf)format("truetype")}@font-face{font-family:Syne;font-style:normal;font-weight:800;font-display:swap;src:url(/static/fonts/syne-800.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/jetbrains-mono-400.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/jetbrains-mono-500.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:600;font-display:swap;src:url(/static/fonts/jetbrains-mono-600.ttf)format("truetype")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/jetbrains-mono-700.ttf)format("truetype")}.no-transitions,.no-transitions *,.no-transitions :before,.no-transitions :after,.boot-no-transitions,.boot-no-transitions *,.boot-no-transitions :before,.boot-no-transitions :after{transition-duration:0s!important}.safe-area-pb{padding-bottom:env(safe-area-inset-bottom,0px)}.skip-link{z-index:100;background:var(--pb-brand);color:var(--pb-text-inverse);border-radius:0 0 .5rem .5rem;padding:.5rem 1rem;font-size:.875rem;font-weight:600;text-decoration:none;transition:top .15s ease-out;position:absolute;top:-100%;left:1rem}.skip-link:focus{outline:2px solid var(--pb-interactive);outline-offset:2px;top:0}input[type=range].range-pb{appearance:none;background:var(--pb-border-hover);cursor:pointer;border-radius:9999px;outline:none;height:6px}input[type=range].range-pb::-webkit-slider-thumb{appearance:none;background:var(--pb-interactive);border:2px solid var(--pb-bg-card);width:18px;height:18px;box-shadow:var(--pb-shadow-1);cursor:pointer;border-radius:50%}input[type=range].range-pb::-moz-range-thumb{background:var(--pb-interactive);border:2px solid var(--pb-bg-card);width:18px;height:18px;box-shadow:var(--pb-shadow-1);cursor:pointer;border-radius:50%}.scrollbar-hidden{scrollbar-width:none;-ms-overflow-style:none}.scrollbar-hidden::-webkit-scrollbar{display:none}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--pb-bg-surface)}::-webkit-scrollbar-thumb{background:var(--pb-border-strong);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--pb-text-dim)}*{scrollbar-width:thin;scrollbar-color:var(--pb-border-strong) var(--pb-bg-surface)}.htmx-indicator{opacity:0;transition:opacity .2s ease-in}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{opacity:1}#content[data-page-swap-phase=leaving],#content[data-page-swap-phase=entering]{pointer-events:none}#content[data-detail-history-hidden=true]{opacity:0;pointer-events:none}@media (prefers-reduced-motion:reduce){html:focus-within{scroll-behavior:auto}*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}#content{transition:none}}@keyframes toast-enter{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}@keyframes toast-exit{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(100%)}}.toast-enter{animation:.3s ease-out forwards toast-enter}.toast-exit{animation:.3s ease-in forwards toast-exit}.sidebar-transition{transition:width .2s ease-in-out,transform .2s ease-in-out}.app-sidebar-backdrop{background:var(--pb-bg-overlay)}@supports (color:color-mix(in lab, red, red)){.app-sidebar-backdrop{background:color-mix(in srgb, var(--pb-bg-overlay) 96%, transparent)}}.app-sidebar-shell{border-right:1px solid var(--pb-border);background:var(--pb-bg-surface);box-shadow:none;overflow:visible}.app-sidebar-brand{background:0 0;padding:1rem 1rem .75rem}.app-sidebar-brand-collapsed{background:0 0;justify-content:center;padding:1rem 0 .75rem;display:flex}.app-sidebar-brand-copy-collapsed{display:none!important}.app-sidebar-brand-card{align-items:center;gap:.75rem;padding:0;transition:color .18s,transform .18s;display:flex}.app-sidebar-brand-card-collapsed{justify-content:center;width:auto;min-width:0;min-height:0;padding:0}.app-sidebar-brand:hover .app-sidebar-brand-card{transform:translateY(-1px)}.app-sidebar-brand-collapsed:hover .app-sidebar-brand-card{transform:none}.app-sidebar-brand-mark{flex-shrink:0;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;display:flex}.app-sidebar-brand-copy{white-space:nowrap;flex-direction:column;min-width:0;display:flex;overflow:hidden}.app-sidebar-brand-title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1.1rem;font-weight:800;line-height:1}.app-sidebar-brand-version{color:var(--pb-text-tertiary);margin-top:2px;font-family:JetBrains Mono,monospace;font-size:.58rem}.app-sidebar-nav{padding:.25rem .625rem 1rem}.app-sidebar-nav-collapsed{padding:.25rem 0 1rem;overflow:visible!important}.app-sidebar-link{color:var(--pb-text-secondary);border:0;border-radius:.5rem;align-items:center;gap:.625rem;margin:1px 0;padding:.4375rem .625rem;font-size:.82rem;font-weight:500;transition:background-color .18s,color .18s,transform .18s;display:flex;position:relative}.app-sidebar-link-collapsed{justify-content:center;gap:0;width:2.75rem;margin-inline:auto;padding:.5rem}.app-sidebar-link-copy-collapsed{display:none!important}.app-sidebar-link:hover{background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.app-sidebar-link:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.app-sidebar-link:hover{color:var(--pb-text-primary)}.app-sidebar-link-collapsed:hover{transform:none}.app-sidebar-link-active{background:var(--pb-selected);color:var(--pb-interactive);font-weight:600}.app-sidebar-link-active:before{content:"";background:var(--pb-interactive);border-radius:0 2px 2px 0;width:3px;position:absolute;top:6px;bottom:6px;left:0}.app-sidebar-link-icon{width:1.25rem;height:1.25rem;color:inherit;flex-shrink:0;justify-content:center;align-items:center;transition:color .18s;display:flex}.app-sidebar-link-copy{flex:1;justify-content:space-between;align-items:center;gap:.75rem;min-width:0;display:flex}.app-sidebar-link-title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:.82rem;font-weight:inherit;overflow:hidden}.app-sidebar-badge-slot{flex-shrink:0;justify-content:flex-end;align-items:center;min-width:1.125rem;display:flex}.app-sidebar-badge-slot .count-badge{border-radius:6px;min-width:18px;min-height:auto;padding:1px 6px;font-family:JetBrains Mono,monospace;font-size:.6rem;font-weight:700;line-height:1.1}.app-sidebar-link-collapsed .app-sidebar-badge-slot{min-width:0;position:absolute;top:2px;right:2px}.app-sidebar-link-collapsed .app-sidebar-badge-slot .count-badge{border-radius:4px;min-width:14px;padding:0 4px;font-size:.52rem}.app-sidebar-section{margin:1rem 0 0}.app-sidebar-section-collapsed{width:3rem;margin:.75rem auto .375rem}.app-sidebar-section-label{letter-spacing:.14em;text-transform:uppercase;color:var(--pb-text-tertiary);align-items:center;gap:.375rem;margin-bottom:.25rem;padding:0 .5rem;font-family:Syne,sans-serif;font-size:.54rem;font-weight:700;display:flex}.app-sidebar-section-label-collapsed{display:none!important}.app-sidebar-section-label:after{content:"";background:var(--pb-border-subtle);flex:auto;height:1px}.app-sidebar-section-rule{border-color:var(--pb-border-subtle);width:1.5rem;margin:.5rem auto 0}.app-sidebar-toggle{z-index:70;border:1px solid var(--pb-border-hover);background:var(--pb-bg-card);width:1.75rem;height:1.75rem;color:var(--pb-text-secondary);pointer-events:auto;box-shadow:var(--pb-shadow-1);border-radius:999px;justify-content:center;align-items:center;transition:border-color .18s,background-color .18s,color .18s,box-shadow .18s;position:absolute;top:72px;right:-.75rem}.app-sidebar-toggle[data-tip]{position:absolute}.app-sidebar-toggle:hover{border-color:var(--pb-interactive);background:var(--pb-bg-card);color:var(--pb-interactive);box-shadow:var(--pb-shadow-2)}[data-tip]{--pb-tooltip-max-width:min(360px, calc(100vw - 32px), calc(100dvw - 32px));position:relative}.tooltips-overlay-enabled [data-tip]:hover:after,.tooltips-overlay-enabled [data-tip]:focus-visible:after{content:none;display:none}[data-tip]:hover:after,[data-tip]:focus-visible:after{content:attr(data-tip);width:max-content;max-width:var(--pb-tooltip-max-width);background:var(--pb-bg-card);border:1px solid var(--pb-border-hover);color:var(--pb-text-primary);white-space:normal;overflow-wrap:break-word;box-shadow:var(--pb-shadow-overlay);z-index:50;pointer-events:none;border-radius:8px;padding:6px 10px;font-size:11px;line-height:1.4;animation:.15s ease-out tip-in;position:absolute;bottom:calc(100% + 6px);left:0}.app-tooltip-host{z-index:140;pointer-events:none;position:fixed;inset:0}.app-tooltip-overlay{width:max-content;max-width:var(--pb-tooltip-max-width,min(360px, calc(100vw - 32px), calc(100dvw - 32px)));background:var(--pb-bg-card);border:1px solid var(--pb-border-hover);color:var(--pb-text-primary);white-space:normal;overflow-wrap:break-word;box-shadow:var(--pb-shadow-overlay);opacity:0;visibility:hidden;will-change:left, top, opacity;border-radius:8px;padding:6px 10px;font-size:11px;line-height:1.4;transition:opacity .12s ease-out,visibility .12s ease-out;position:absolute;top:0;left:0;transform:translate(0)}.app-tooltip-host[data-visible=true] .app-tooltip-overlay{opacity:1;visibility:visible}[data-search-field-clear][data-tip]{position:absolute}[data-tip-pos=left]:hover:after,[data-tip-pos=left]:focus-visible:after{animation:.15s ease-out tip-in-left;inset:50% calc(100% + 6px) auto auto;transform:translateY(-50%)}[data-tip-pos=right]:hover:after,[data-tip-pos=right]:focus-visible:after{animation:.15s ease-out tip-in-right;inset:50% auto auto calc(100% + 6px);transform:translateY(-50%)}[data-tip-pos=bottom]:hover:after,[data-tip-pos=bottom]:focus-visible:after{animation:.15s ease-out tip-in-bottom;top:calc(100% + 6px);bottom:auto;left:0}[data-tip-size=wide]:hover:after,[data-tip-size=wide]:focus-visible:after{max-width:var(--pb-tooltip-max-width,min(480px, calc(100vw - 32px), calc(100dvw - 32px)))}[data-tip-size=narrow]:hover:after,[data-tip-size=narrow]:focus-visible:after{max-width:var(--pb-tooltip-max-width,min(220px, calc(100vw - 32px), calc(100dvw - 32px)))}.app-header-icon-tip:hover:after,.app-header-icon-tip:focus-visible:after{text-align:center;min-width:7.25rem}.tooltip-wrap{position:relative}.tooltip-panel{z-index:50;visibility:hidden;opacity:0;pointer-events:none;background:var(--pb-bg-card);border:1px solid var(--pb-border-hover);width:max-content;max-width:360px;color:var(--pb-text-primary);white-space:normal;overflow-wrap:break-word;box-shadow:var(--pb-shadow-overlay);border-radius:.5rem;padding:.5rem .75rem;font-size:.75rem;line-height:1.4;transition:opacity .15s ease-out,transform .15s ease-out,visibility .15s ease-out;position:absolute}.tooltip-wrap:hover>.tooltip-panel,.tooltip-wrap:focus-within>.tooltip-panel{visibility:visible;opacity:1}.tooltip-panel-top{bottom:calc(100% + .5rem);left:0;transform:translateY(4px)}.tooltip-wrap:hover>.tooltip-panel-top,.tooltip-wrap:focus-within>.tooltip-panel-top{transform:translateY(0)}.tooltip-panel-left{top:50%;right:calc(100% + .5rem);transform:translate(4px,-50%)}.tooltip-wrap:hover>.tooltip-panel-left,.tooltip-wrap:focus-within>.tooltip-panel-left{transform:translateY(-50%)}.tooltip-panel-right{top:50%;left:calc(100% + .5rem);transform:translate(-4px,-50%)}.tooltip-wrap:hover>.tooltip-panel-right,.tooltip-wrap:focus-within>.tooltip-panel-right{transform:translateY(-50%)}.tooltip-panel-bottom{top:calc(100% + .5rem);left:0;transform:translateY(-4px)}.tooltip-wrap:hover>.tooltip-panel-bottom,.tooltip-wrap:focus-within>.tooltip-panel-bottom{transform:translateY(0)}.tooltip-panel-nowrap{white-space:nowrap}.tooltip-panel-wide{max-width:500px}@keyframes tip-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes tip-in-left{0%{opacity:0;transform:translate(4px,-50%)}to{opacity:1;transform:translateY(-50%)}}@keyframes tip-in-right{0%{opacity:0;transform:translate(-4px,-50%)}to{opacity:1;transform:translateY(-50%)}}@keyframes tip-in-bottom{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.log-terminal{contain:inline-size;background:var(--pb-bg-surface);width:100%;min-width:0;max-width:100%;overflow-x:hidden}@supports (color:color-mix(in lab, red, red)){.log-terminal{background:color-mix(in srgb, var(--pb-bg-surface) 82%, var(--pb-bg-base))}}.log-terminal{background-image:repeating-linear-gradient(0deg, transparent, transparent 2px, var(--pb-border-subtle) 2px, var(--pb-border-subtle) 4px)}@supports (color:color-mix(in lab, red, red)){.log-terminal{background-image:repeating-linear-gradient(0deg, transparent, transparent 2px, color-mix(in srgb, var(--pb-border-subtle) 32%, transparent) 2px, color-mix(in srgb, var(--pb-border-subtle) 32%, transparent) 4px)}}.log-line{box-sizing:border-box;border-left:3px solid #0000;width:100%;min-width:0;max-width:100%;overflow:hidden}.log-line:hover{background:var(--pb-bg-card-hover)}@supports (color:color-mix(in lab, red, red)){.log-line:hover{background:color-mix(in srgb, var(--pb-bg-card-hover) 36%, transparent)}}.log-line[data-level=error],.log-line[data-level=critical]{border-left-color:var(--pb-error);background:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.log-line[data-level=error],.log-line[data-level=critical]{background:color-mix(in srgb, var(--pb-error) 12%, transparent)}}.log-line[data-level=warning]{border-left-color:var(--pb-warning)}.log-line[data-level=info]{border-left-color:var(--pb-info)}.log-line[data-level=debug]{border-left-color:var(--pb-text-dim)}.log-line.expanded{background:var(--pb-bg-card-hover)!important}@supports (color:color-mix(in lab, red, red)){.log-line.expanded{background:color-mix(in srgb, var(--pb-bg-card-hover) 54%, transparent)!important}}.log-detail{box-sizing:border-box;max-width:calc(100% - 140px);overflow:hidden}.log-path{overflow-wrap:anywhere}.log-badge{letter-spacing:.08em;text-transform:uppercase;text-align:center;border-radius:3px;min-width:38px;padding:1px 5px;font-size:9px;font-weight:700;display:inline-block}.badge-debug{color:var(--pb-text-dim);background:var(--pb-text-dim)}@supports (color:color-mix(in lab, red, red)){.badge-debug{background:color-mix(in srgb, var(--pb-text-dim) 16%, transparent)}}.badge-info{color:var(--pb-info);background:var(--pb-info-dim)}.badge-warning{color:var(--pb-warning);background:var(--pb-warning-dim)}.badge-error{color:var(--pb-error);background:var(--pb-error-dim)}.badge-critical{color:var(--pb-text-inverse);background:var(--pb-error)}.live-dot{background:var(--pb-success);border-radius:50%;width:6px;height:6px;animation:2s ease-in-out infinite live-pulse}@keyframes live-pulse{0%,to{opacity:1;box-shadow:0 0 0 0 color-mix(in srgb, var(--pb-success) 42%, transparent)}50%{opacity:.6;box-shadow:0 0 6px 2px color-mix(in srgb, var(--pb-success) 24%, transparent)}}.json-key{color:var(--pb-info)}.json-str{color:var(--pb-success)}.json-num{color:var(--pb-warning)}.json-bool{color:var(--pb-purple)}.json-null{color:var(--pb-text-dim)}.downloads-page-shell{width:100%}.downloads-view{width:100%;max-width:none;padding:0 0 var(--pb-page-footer-clearance);margin:0}.downloads-header{display:block}.downloads-header-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:1.25rem;width:100%;display:flex}.downloads-header-summary{flex-wrap:wrap;flex:auto;align-items:flex-start;gap:1.5rem;min-width:0;display:flex}.downloads-header-copy{min-width:0}.downloads-header-actions{flex:none;justify-content:flex-end;align-items:flex-start;margin-left:auto;display:flex}.downloads-title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text-primary);font-family:Syne,sans-serif;font-size:1.6rem;font-weight:800;line-height:1}.downloads-title span{color:var(--pb-brand)}.downloads-subtitle{color:var(--pb-text-tertiary);letter-spacing:.01em;margin-top:.25rem;font-size:.78rem}.downloads-gauges{align-items:flex-end;gap:1rem;display:flex}.downloads-gauge{text-align:center}.downloads-gauge-ring{width:56px;height:56px;margin:0 auto;position:relative}.downloads-gauge-ring svg{transform:rotate(-90deg)}.downloads-gauge-bg{fill:none;stroke:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.downloads-gauge-bg{stroke:color-mix(in srgb, var(--pb-text-secondary) 10%, transparent)}}.downloads-gauge-bg{stroke-width:4.5px}.downloads-gauge-fill{fill:none;stroke-width:4.5px;stroke-linecap:round;transition:stroke-dashoffset .6s}.downloads-gauge-fill-success{stroke:var(--pb-status-success)}.downloads-gauge-fill-info{stroke:var(--pb-status-info)}.downloads-gauge-fill-warning{stroke:var(--pb-status-warning)}.downloads-gauge-fill-error{stroke:var(--pb-status-danger)}.downloads-gauge-fill-muted{stroke:var(--pb-text-dim)}.downloads-gauge-value{color:var(--pb-text-primary);justify-content:center;align-items:center;font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:700;display:flex;position:absolute;inset:0}.downloads-gauge-value-success{color:var(--pb-status-success)}.downloads-gauge-value-info{color:var(--pb-status-info)}.downloads-gauge-value-warning{color:var(--pb-status-warning)}.downloads-gauge-value-error{color:var(--pb-status-danger)}.downloads-gauge-value-muted{color:var(--pb-text-dim)}.downloads-gauge-label{letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-tertiary);margin-top:.25rem;font-size:.6rem;font-weight:600}.downloads-tab-rail{border:1px solid var(--pb-border);background:0 0;border-radius:10px;align-self:flex-start;display:inline-flex;overflow:hidden}.downloads-tab-btn{letter-spacing:.06em;text-transform:uppercase;color:var(--pb-text-dim);background:0 0;border:0;padding:.5rem 1.25rem;font-family:Syne,sans-serif;font-size:.78rem;font-weight:700;transition:background-color .14s,color .14s}.downloads-tab-btn+.downloads-tab-btn{border-left:1px solid var(--pb-border)}.downloads-tab-btn:hover{color:var(--pb-text);background:var(--pb-text-secondary)}@supports (color:color-mix(in lab, red, red)){.downloads-tab-btn:hover{background:color-mix(in srgb, var(--pb-text-secondary) 4%, transparent)}}.downloads-tab-btn.is-active{background:var(--pb-interactive);color:var(--pb-text-inverse)}.downloads-panel-stack{gap:1.25rem;margin-top:.875rem;display:grid}.import-history-page-shell{margin-top:0}.downloads-section{gap:.625rem;display:grid}.downloads-section-label{border-bottom:1px solid var(--pb-border-subtle);letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);padding-bottom:.375rem;font-family:Syne,sans-serif;font-size:.62rem;font-weight:700}.downloads-table-wrap{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);border-radius:14px;position:relative;overflow:visible;box-shadow:0 1px 3px #1e1a170f,0 1px 2px #1e1a170a}.downloads-table-wrap.is-clipped{overflow:hidden}.downloads-table{border-collapse:collapse;width:100%}.downloads-table th{border-bottom:2px solid var(--pb-border);background:var(--pb-bg-surface);text-align:left;letter-spacing:.1em;text-transform:uppercase;color:var(--pb-text-dim);padding:.5625rem .875rem;font-family:Syne,sans-serif;font-size:.6rem;font-weight:700}.downloads-table td{border-bottom:1px solid var(--pb-border-subtle);color:var(--pb-text);vertical-align:middle;padding:.6875rem .875rem;font-size:.82rem;transition:background-color .1s}.downloads-table tbody:last-child tr:last-child td{border-bottom:0}.downloads-table tr:not(.table-detail-row):hover td{background:var(--pb-surface-selected)}.downloads-table th.is-center,.downloads-table td.is-center{text-align:center}.downloads-table th.is-right,.downloads-table td.is-right{text-align:right}.issue-search-results-table-wrap{max-height:62vh;overflow-y:auto}.issue-search-results-table thead th{z-index:1;position:sticky;top:0}.issue-search-sort-indicator{color:var(--pb-interactive)}.issue-search-release-link{color:var(--pb-text);transition:color .14s}.issue-search-release-link:hover{color:var(--pb-interactive)}.issue-search-release-title{text-overflow:ellipsis;white-space:nowrap;max-width:320px;color:inherit;font-weight:600;display:block;overflow:hidden}.issue-search-release-title-muted{color:var(--pb-text-dim)}.issue-search-release-match{color:var(--pb-text-dim);margin-top:.125rem;font-size:.64rem}.issue-search-action-row{justify-content:flex-end;align-items:center;gap:.25rem;display:inline-flex}.issue-search-rejected-row td{opacity:.45}.issue-search-rejected-label{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.64rem}.issue-search-empty-state{text-align:center;padding:2.5rem 1.25rem}.issue-search-empty-icon{width:2.25rem;height:2.25rem;color:var(--pb-text-dim);margin-inline:auto}.issue-search-empty-title{color:var(--pb-text);margin-top:.625rem;font-size:.85rem;font-weight:600}.issue-search-empty-copy{color:var(--pb-text-sec);margin-top:.25rem;font-size:.78rem}.downloads-release-name{max-width:260px;color:var(--pb-text);font-weight:600;display:block}.downloads-release-meta{color:var(--pb-text-dim);margin-top:.2rem;font-size:.72rem}.downloads-issue-link{color:var(--pb-interactive);transition:color .14s}.downloads-issue-link:hover{color:var(--pb-interactive-hover)}.table-mono-dim,td.table-mono-dim,.downloads-mono-dim-cell,td.downloads-mono-dim-cell{font-family:JetBrains Mono,monospace;font-size:.72rem}.table-mono-dim{color:var(--pb-text-dim)}.downloads-table td.downloads-mono-cell,.downloads-table td.downloads-mono-dim-cell,.downloads-table .downloads-mono-cell,.downloads-table .downloads-mono-dim-cell{font-variant-numeric:tabular-nums;font-family:JetBrains Mono,monospace}.downloads-table td.downloads-mono-cell,.downloads-table .downloads-mono-cell{color:var(--pb-text-secondary);font-size:.75rem;font-weight:400;line-height:1.2}.downloads-table td.downloads-mono-dim-cell,.downloads-table .downloads-mono-dim-cell{color:var(--pb-text-dim);font-size:.72rem}.downloads-muted-text{color:var(--pb-text-dim)}.downloads-progress-cell{align-items:center;gap:.375rem;display:inline-flex}.downloads-progress-track{border:1px solid var(--pb-border-subtle);background:var(--pb-text-secondary);border-radius:2px;width:100px;height:6px;display:inline-block;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.downloads-progress-track{background:color-mix(in srgb, var(--pb-text-secondary) 6%, transparent)}}.downloads-progress-track{vertical-align:middle}.downloads-progress-fill{height:100%;display:block;position:relative}.downloads-progress-fill.is-blue{background:linear-gradient(90deg, var(--pb-info), var(--pb-info))}@supports (color:color-mix(in lab, red, red)){.downloads-progress-fill.is-blue{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-info) 45%, transparent), var(--pb-info))}}.downloads-progress-fill.is-amber{background:linear-gradient(90deg, var(--pb-warning), var(--pb-warning))}@supports (color:color-mix(in lab, red, red)){.downloads-progress-fill.is-amber{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-warning) 45%, transparent), var(--pb-warning))}}.downloads-progress-fill.is-green{background:linear-gradient(90deg, var(--pb-success), var(--pb-success))}@supports (color:color-mix(in lab, red, red)){.downloads-progress-fill.is-green{background:linear-gradient(90deg, color-mix(in srgb, var(--pb-success) 45%, transparent), var(--pb-success))}}.downloads-progress-fill:after{content:"";background:repeating-linear-gradient(90deg,#0000 0 2px,#ffffff1f 2px 4px);position:absolute;inset:0}@keyframes downloads-progress-indeterminate{0%{transform:translate(-130%)}to{transform:translate(280%)}}.downloads-progress-fill.is-indeterminate,.app-progress-fill.is-indeterminate{will-change:transform;animation:1.25s ease-in-out infinite downloads-progress-indeterminate}@media (prefers-reduced-motion:reduce){.downloads-progress-fill.is-indeterminate,.app-progress-fill.is-indeterminate{opacity:.7;animation:none;transform:translate(60%)}}.downloads-progress-pct{color:var(--pb-info);font-family:JetBrains Mono,monospace;font-size:.68rem;font-weight:600}.downloads-progress-pct-warning{color:var(--pb-warning)}.downloads-progress-pct-success{color:var(--pb-success)}.downloads-progress-pct-muted{color:var(--pb-text-dim)}.downloads-led{border-radius:999px;width:8px;height:8px;display:inline-block}.downloads-led-green{background:var(--pb-success);box-shadow:0 0 5px var(--pb-success)}@supports (color:color-mix(in lab, red, red)){.downloads-led-green{box-shadow:0 0 5px color-mix(in srgb, var(--pb-success) 35%, transparent)}}.downloads-led-blue{background:var(--pb-info);box-shadow:0 0 5px var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.downloads-led-blue{box-shadow:0 0 5px color-mix(in srgb, var(--pb-info) 35%, transparent)}}.downloads-led-amber{background:var(--pb-warning);box-shadow:0 0 5px var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.downloads-led-amber{box-shadow:0 0 5px color-mix(in srgb, var(--pb-warning) 35%, transparent)}}.downloads-led-off{background:var(--pb-text-dim);opacity:.35}.downloads-action-group{align-items:center;gap:.25rem;display:inline-flex}@media (hover:hover) and (pointer:fine){.downloads-action-group.is-hover-reveal{opacity:0;pointer-events:none;transition:opacity .14s,transform .14s;transform:translateY(2px)}.downloads-table tbody tr:hover .downloads-action-group.is-hover-reveal,.downloads-table tbody tr:focus-within .downloads-action-group.is-hover-reveal{opacity:1;pointer-events:auto;transform:translateY(0)}}.downloads-action-btn{border:1px solid var(--pb-border-subtle);width:28px;height:28px;color:var(--pb-text-dim);background:0 0;border-radius:6px;justify-content:center;align-items:center;transition:border-color .14s,color .14s,background-color .14s;display:inline-flex}.downloads-action-btn svg{width:12px;height:12px}.import-history-action-group{gap:.375rem}.import-history-action-btn{border-radius:8px;flex-shrink:0;width:30px;height:30px}.downloads-action-btn .import-history-action-icon{shape-rendering:geometricprecision;flex-shrink:0;width:12px;height:12px;overflow:visible}.downloads-action-btn .import-history-action-icon--detail{width:14px;height:14px}.downloads-action-btn:hover{border-color:var(--pb-interactive);background:var(--pb-info)}@supports (color:color-mix(in lab, red, red)){.downloads-action-btn:hover{background:color-mix(in srgb, var(--pb-info) 10%, transparent)}}.downloads-action-btn:hover{color:var(--pb-interactive)}.downloads-action-btn.is-danger:hover{border-color:var(--pb-error)}@supports (color:color-mix(in lab, red, red)){.downloads-action-btn.is-danger:hover{border-color:color-mix(in srgb, var(--pb-error) 40%, transparent)}}.downloads-action-btn.is-danger:hover{background:var(--pb-error-dim);color:var(--pb-error)}.downloads-action-btn.is-warn:hover{border-color:var(--pb-warning)}@supports (color:color-mix(in lab, red, red)){.downloads-action-btn.is-warn:hover{border-color:color-mix(in srgb, var(--pb-warning) 40%, transparent)}}.downloads-action-btn.is-warn:hover{background:var(--pb-warning-dim);color:var(--pb-warning)}.downloads-empty-state{text-align:center;padding:2.5rem 1.5rem}.downloads-empty-state.is-compact{padding:1.5rem 1.25rem}.downloads-empty-state.is-history{padding:2rem 1.5rem}.downloads-empty-icon{width:40px;height:40px;color:var(--pb-text-dim);margin:0 auto}.downloads-empty-title{letter-spacing:.04em;text-transform:uppercase;color:var(--pb-text);margin-top:.75rem;font-family:Syne,sans-serif;font-size:.88rem;font-weight:700}.downloads-empty-copy{color:var(--pb-text-dim);margin-top:.4rem;font-size:.78rem}.intervention-bulk-bar{border-top:1px solid var(--pb-border-subtle);border-bottom:1px solid var(--pb-border-subtle);background:var(--pb-info-bg);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;padding:.625rem .875rem;display:flex}@supports (color:color-mix(in lab, red, red)){.intervention-bulk-bar{background:color-mix(in srgb, var(--pb-info-bg) 40%, var(--pb-bg-surface))}}.intervention-bulk-left{flex-wrap:wrap;align-items:center;gap:.625rem;display:inline-flex}.intervention-bulk-count{color:var(--pb-text-dim);font-family:JetBrains Mono,monospace;font-size:.72rem}.intervention-bulk-actions{flex-wrap:wrap;align-items:center;gap:.5rem;display:inline-flex}.downloads-footer-strip{border:1px solid var(--pb-border-subtle);background:var(--pb-bg-card);color:var(--pb-text-dim);border-radius:10px;flex-wrap:wrap;align-items:center;gap:.375rem 1.5rem;margin-top:1.25rem;padding:.625rem 1rem;font-family:JetBrains Mono,monospace;font-size:.7rem;display:flex}.downloads-footer-strip strong{color:var(--pb-text);font-weight:600}.downloads-history-toolbar{z-index:2;border:0;border-bottom:1px solid var(--pb-border);border-top-left-radius:inherit;border-top-right-radius:inherit;background:var(--pb-bg-surface);box-shadow:none;border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative}.downloads-toolbar{border-bottom:1px solid var(--pb-border);background:var(--pb-bg-surface);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;padding:.625rem .875rem;display:flex}.downloads-history-toolbar>.downloads-toolbar{border-top-left-radius:inherit;border-top-right-radius:inherit;border-bottom-right-radius:0;border-bottom-left-radius:0}.downloads-toolbar-left{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.downloads-toolbar-search,.downloads-toolbar-select{border:1px solid var(--pb-border);background:var(--pb-bg-card);height:30px;color:var(--pb-text);border-radius:8px;outline:none;font-size:.78rem}.downloads-toolbar-search{width:200px;padding:0 .625rem}.downloads-toolbar-select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2374675B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-position:right 8px center;background-repeat:no-repeat;padding:0 2rem 0 .625rem}[data-theme=dark] .downloads-toolbar-select{background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%239AAABA' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}.downloads-toolbar-search:focus,.downloads-toolbar-select:focus{border-color:var(--pb-interactive)}.downloads-clear-btn{background:var(--pb-error);color:var(--pb-text-inverse);white-space:nowrap;border:0;border-radius:8px;align-items:center;gap:.3125rem;padding:.375rem .75rem;font-size:.75rem;font-weight:600;transition:opacity .14s;display:inline-flex}.downloads-clear-btn svg{width:12px;height:12px}.downloads-clear-btn:hover{opacity:.92}.downloads-sort-btn{width:100%;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;text-align:inherit;text-transform:inherit;justify-content:flex-start;align-items:center;gap:.25rem;transition:color .14s;display:inline-flex}.import-review-cv-year-header{width:5.75rem;min-width:5.75rem}.import-review-cv-year-header .downloads-sort-btn{white-space:nowrap}.import-review-cv-year-header .downloads-sort-chevron{flex-shrink:0}.downloads-table th.is-center .downloads-sort-btn{text-align:center;justify-content:center}.downloads-table th.is-right .downloads-sort-btn{text-align:right;justify-content:flex-end}.downloads-sort-btn:hover,.downloads-sort-btn.is-active{color:var(--pb-text)}.downloads-sort-chevron{opacity:.35;width:12px;height:12px}.downloads-sort-btn.is-active .downloads-sort-chevron{opacity:1}.downloads-sort-chevron.is-desc{transform:rotate(180deg)}.downloads-error-row td{border-bottom:1px solid var(--pb-border-subtle);background:var(--pb-error-dim);padding:.5rem .875rem}@supports (color:color-mix(in lab, red, red)){.downloads-error-row td{background:color-mix(in srgb, var(--pb-error-dim) 50%, var(--pb-bg-card))}}.downloads-error-row td{color:var(--pb-error);font-size:.75rem}.downloads-error-content{align-items:flex-start;gap:.5rem;display:flex}.downloads-error-content svg{flex-shrink:0;width:14px;height:14px;margin-top:1px}.search-history-detail-row td{border-bottom:1px solid var(--pb-border-subtle);background:var(--pb-bg-surface);padding:.75rem .875rem}@supports (color:color-mix(in lab, red, red)){.search-history-detail-row td{background:color-mix(in srgb, var(--pb-bg-surface) 70%, var(--pb-bg-card))}}.search-history-detail-card{border:1px solid var(--pb-border)}@supports (color:color-mix(in lab, red, red)){.search-history-detail-card{border:1px solid color-mix(in srgb, var(--pb-border) 78%, transparent)}}.search-history-detail-card{background:var(--pb-bg-card-hover);border-radius:.75rem}@supports (color:color-mix(in lab, red, red)){.search-history-detail-card{background:color-mix(in srgb, var(--pb-bg-card-hover) 55%, transparent)}}.search-history-detail-card{padding:.625rem .75rem}.search-history-detail-label{letter-spacing:.12em;text-transform:uppercase;color:var(--pb-text-dim);font-size:10px;font-weight:600}.search-history-diagnostics-shell{border:1px solid var(--pb-warning);overflow:hidden}@supports (color:color-mix(in lab, red, red)){.search-history-diagnostics-shell{border:1px solid color-mix(in srgb, var(--pb-warning) 30%, transparent)}}.search-history-diagnostics-shell{background:var(--pb-warning-dim);border-radius:.75rem}@supports (color:color-mix(in lab, red, red)){.search-history-diagnostics-shell{background:color-mix(in srgb, var(--pb-warning-dim) 35%, transparent)}}.downloads-pagination{border-top:1px solid var(--pb-border-subtle);padding:.5rem .875rem}@media (max-width:1024px){.downloads-header{gap:1rem}.downloads-table-wrap{overflow-x:auto}.downloads-table-wrap.is-clipped{overflow:auto hidden}.downloads-table{min-width:760px}}@media (max-width:640px){.downloads-header-main{gap:.875rem}.downloads-gauges{gap:.625rem}.downloads-toolbar-search{width:100%}}.comic-reader{--pb-reader-canvas:#070b12;--pb-reader-surface:#0d141ef0;--pb-reader-surface-strong:#0d141efa;--pb-reader-text:#f7f1e8;--pb-reader-text-muted:#cbd5e1;--pb-reader-text-dim:#9aaaba;--pb-reader-interactive:#8fb9ee;--pb-reader-danger:#e38473;--pb-reader-warning:#f2c98d;background:var(--pb-reader-canvas);width:100vw;max-width:none;height:100dvh;max-height:none;color:var(--pb-reader-text);border:0;margin:0;padding:0;position:fixed;inset:0;overflow:hidden}.comic-reader::backdrop{background:var(--pb-reader-canvas)}.comic-reader__shell{background:var(--pb-reader-canvas);width:100%;min-width:0;height:100%;min-height:0;color:var(--pb-reader-text);grid-template-rows:auto minmax(0,1fr) auto;font-family:DM Sans,sans-serif;display:grid;overflow:hidden}.comic-reader__topbar,.comic-reader__controls{z-index:20;background:var(--pb-reader-surface);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-color:#cbd5e129;transition:opacity .14s;position:relative}.comic-reader__topbar{min-height:58px;padding:calc(.45rem + env(safe-area-inset-top,0px)) max(.75rem, env(safe-area-inset-right,0px)) .45rem max(.75rem, env(safe-area-inset-left,0px));border-bottom-width:1px;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:.75rem;display:grid}.comic-reader__identity{min-width:0}.comic-reader__identity h2,.comic-reader__identity p{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.comic-reader__identity h2{color:var(--pb-reader-text);margin:0;font-family:Bricolage Grotesque,sans-serif;font-size:.95rem;font-weight:800;line-height:1.2}.comic-reader__identity p{color:var(--pb-reader-text-dim);margin:.1rem 0 0;font-size:.72rem;line-height:1.2}.comic-reader__top-actions,.comic-reader__issue-navigation,.comic-reader__navigation,.comic-reader__sizing,.comic-reader__state-actions,.comic-reader__help-heading{align-items:center;display:flex}.comic-reader__top-actions,.comic-reader__issue-navigation,.comic-reader__navigation,.comic-reader__sizing,.comic-reader__state-actions{gap:.4rem}.comic-reader__issue-navigation{min-width:0}.comic-reader__issue-navigation .comic-reader__button{gap:.35rem}.comic-reader__button,.comic-reader__zoom-label,.comic-reader__fit-select,.comic-reader__page-jump input{min-height:44px;color:var(--pb-reader-text);font:inherit;background:#f7f1e812;border:1px solid #cbd5e138;border-radius:.55rem}.comic-reader__button,.comic-reader__zoom-label{white-space:nowrap;cursor:pointer;justify-content:center;align-items:center;min-width:44px;padding:.55rem .7rem;font-size:.76rem;font-weight:700;line-height:1;display:inline-flex}.comic-reader__button:hover,.comic-reader__zoom-label:hover,.comic-reader__button[aria-pressed=true]{background:#8fb9ee29;border-color:#8fb9ee8c}.comic-reader__button:focus-visible,.comic-reader__zoom-label:focus-visible,.comic-reader__fit-select:focus-visible,.comic-reader__page-jump input:focus-visible,.comic-reader__viewport:focus-visible{outline:2px solid var(--pb-reader-interactive);outline-offset:-2px}.comic-reader__button:disabled{cursor:default;opacity:.38}.comic-reader__button svg{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.8px;width:19px;height:19px}.comic-reader__button--prominent{background:#8fb9ee1f;border-color:#8fb9ee61}.comic-reader__viewport{z-index:1;overscroll-behavior:contain;background:var(--pb-reader-canvas);scrollbar-color:#9aaaba8c transparent;touch-action:pan-y pinch-zoom;min-width:0;min-height:0;position:relative;overflow:auto}.comic-reader__viewport.is-pannable{touch-action:auto}.comic-reader__page-stage{justify-content:center;align-items:center;width:100%;min-width:100%;height:100%;min-height:100%;padding:.75rem;display:flex}.comic-reader__page{z-index:1;object-fit:contain;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;flex:none;max-width:none;max-height:none;display:block;position:relative;box-shadow:0 8px 30px #00000061}.comic-reader__page--page{width:auto;max-width:100%;height:auto;max-height:100%}.comic-reader__page--width{align-self:flex-start;width:100%;height:auto}.comic-reader__page--height{width:auto;height:100%}.comic-reader__page--actual{align-self:flex-start;width:auto;height:auto}.comic-reader__state,.comic-reader__page-busy{z-index:8;text-align:center;justify-content:center;align-items:center;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.comic-reader__state{width:min(90vw,32rem);color:var(--pb-reader-text-muted);background:#0d141ef5;border:1px solid #cbd5e133;border-radius:.9rem;flex-direction:column;gap:.5rem;padding:1.5rem;box-shadow:0 20px 56px #00000075}.comic-reader__state strong{color:var(--pb-reader-text);font-size:1rem}.comic-reader__state--error{border-color:var(--pb-reader-danger)}@supports (color:color-mix(in lab, red, red)){.comic-reader__state--error{border-color:color-mix(in srgb, var(--pb-reader-danger) 45%, transparent)}}.comic-reader__state-actions{flex-wrap:wrap;justify-content:center;margin-top:.5rem}.comic-reader__completion{z-index:9;right:max(.75rem, env(safe-area-inset-right,0px));bottom:max(.75rem, env(safe-area-inset-bottom,0px));width:min(24rem,100% - 1.5rem);color:var(--pb-reader-text-muted);background:#0d141ef5;border:1px solid #8fb9ee6b;border-radius:.8rem;padding:.85rem;position:absolute;box-shadow:0 16px 42px #0000006b}.comic-reader__completion strong{color:var(--pb-reader-text);font-family:Bricolage Grotesque,sans-serif;font-size:1rem;display:block}.comic-reader__completion p{margin:.35rem 0 0;font-size:.78rem}.comic-reader__page-busy{color:var(--pb-reader-text-muted);background:#0d141ee6;border-radius:999px;padding:.45rem .7rem;font-size:.72rem;top:1rem;transform:translate(-50%)}.comic-reader__spinner{border:2px solid #8fb9ee40;border-top-color:var(--pb-reader-interactive);border-radius:999px;width:1.5rem;height:1.5rem;animation:.7s linear infinite comic-reader-spin}@keyframes comic-reader-spin{to{transform:rotate(360deg)}}.comic-reader__tap-zone{z-index:4;background:0 0;border:0;margin:0;padding:0;position:absolute;top:0;bottom:0}.comic-reader__tap-zone--left{width:30%;left:0}.comic-reader__tap-zone--center{width:40%;left:30%}.comic-reader__tap-zone--right{width:30%;right:0}.comic-reader__controls{min-width:0;padding:.45rem max(.75rem, env(safe-area-inset-right,0px)) calc(.45rem + env(safe-area-inset-bottom,0px)) max(.75rem, env(safe-area-inset-left,0px));border-top-width:1px;justify-content:space-between;align-items:center;gap:.75rem;display:flex}.comic-reader__navigation,.comic-reader__sizing{min-width:0;position:relative}.comic-reader__page-jump{color:var(--pb-reader-text-dim);white-space:nowrap;align-items:center;gap:.35rem;font-size:.75rem;display:inline-flex}.comic-reader__page-jump input{text-align:center;appearance:textfield;width:3.4rem;padding:.4rem}.comic-reader__page-jump input::-webkit-outer-spin-button{appearance:none;margin:0}.comic-reader__page-jump input::-webkit-inner-spin-button{appearance:none;margin:0}.comic-reader__fit-select{color-scheme:dark;max-width:8.5rem;padding:0 1.9rem 0 .65rem;display:none}.comic-reader__zoom-label{min-width:3.75rem;color:var(--pb-reader-text-muted);padding-inline:.45rem}.comic-reader__input-error{border:1px solid var(--pb-reader-danger);width:max-content;max-width:min(20rem,100vw - 1.5rem);padding:.35rem .5rem;position:absolute;bottom:calc(100% + .35rem);left:50%;transform:translate(-50%)}@supports (color:color-mix(in lab, red, red)){.comic-reader__input-error{border:1px solid color-mix(in srgb, var(--pb-reader-danger) 42%, transparent)}}.comic-reader__input-error{background:var(--pb-reader-surface-strong);color:var(--pb-reader-danger);border-radius:.45rem;font-size:.7rem}.comic-reader__help{z-index:30;background:var(--pb-reader-surface-strong);border:1px solid #8fb9ee66;border-radius:.9rem;width:min(92vw,34rem);max-height:min(78dvh,38rem);padding:1rem;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 20px 56px #0009}.comic-reader__help-heading{justify-content:space-between;gap:1rem}.comic-reader__help h3{color:var(--pb-reader-text);margin:0;font-family:Bricolage Grotesque,sans-serif;font-size:1rem}.comic-reader__help dl{gap:.15rem;margin:.75rem 0 0;display:grid}.comic-reader__help dl>div{border-top:1px solid #cbd5e11f;grid-template-columns:minmax(7.5rem,.7fr) minmax(0,1.3fr);gap:.75rem;padding:.45rem 0;display:grid}.comic-reader__help dt{color:var(--pb-reader-interactive);font-family:JetBrains Mono,monospace;font-size:.72rem;font-weight:600}.comic-reader__help dd{color:var(--pb-reader-text-muted);margin:0;font-size:.75rem}.comic-reader__save-status{z-index:40;right:max(.75rem, env(safe-area-inset-right,0px));bottom:calc(4.1rem + env(safe-area-inset-bottom,0px));max-width:min(22rem,100vw - 1.5rem);color:var(--pb-reader-warning);background:#231b11f5;border:1px solid #d7a15b66;border-radius:.55rem;margin:0;padding:.45rem .65rem;font-size:.72rem;position:absolute}.comic-reader__shell.is-controls-hidden .comic-reader__topbar,.comic-reader__shell.is-controls-hidden .comic-reader__controls{visibility:hidden;opacity:0;pointer-events:none}@media (max-width:900px){.comic-reader__issue-navigation .comic-reader__button span{clip:rect(0, 0, 0, 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.comic-reader__fit-page,.comic-reader__fit-wide{display:none}.comic-reader__fit-select{display:block}}@media (max-width:600px){.comic-reader__topbar{grid-template-columns:auto minmax(0,1fr) auto;gap:.5rem;min-height:54px}.comic-reader__issue-navigation{grid-area:2/1/auto/-1;justify-content:center}.comic-reader__top-actions{grid-area:1/3}.comic-reader__desktop-action{display:none}.comic-reader__controls{flex-wrap:wrap;justify-content:center;gap:.35rem .6rem}.comic-reader__navigation,.comic-reader__sizing{justify-content:center}.comic-reader__page-stage{padding:.4rem}.comic-reader__help dl>div{grid-template-columns:1fr;gap:.15rem}}@media (max-height:480px){.comic-reader__topbar,.comic-reader__controls{padding-top:.25rem;padding-bottom:.25rem}.comic-reader__topbar{min-height:48px}}@media (prefers-reduced-motion:reduce){.comic-reader__topbar,.comic-reader__controls{transition:none;visibility:visible!important;opacity:1!important;pointer-events:auto!important}.comic-reader__spinner{animation-duration:1.4s}}.reading-workspace{flex-direction:column;gap:1rem;display:flex}.reading-workspace-header,.dashboard-reading-shelf-header{justify-content:space-between;align-items:flex-end;gap:1rem;display:flex}.reading-page-size{align-items:center;gap:.65rem;display:flex}.reading-tabs{border-bottom:1px solid var(--pb-border);gap:.4rem;display:flex;overflow-x:auto}.reading-tab{color:var(--pb-text-tertiary);letter-spacing:.08em;text-transform:uppercase;flex:none;padding:.65rem .85rem;font-family:Syne,sans-serif;font-size:.7rem;font-weight:700;position:relative}.reading-tab:after{content:"";background:0 0;border-radius:999px;height:2px;position:absolute;bottom:-1px;left:.65rem;right:.65rem}.reading-tab:hover,.reading-tab:focus-visible,.reading-tab.is-active{color:var(--pb-interactive)}.reading-tab.is-active:after{background:var(--pb-interactive)}.reading-card-grid,.dashboard-reading-card-grid{grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.85rem;display:grid}.dashboard-reading-card-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.reading-card{border:1px solid var(--pb-border);background:linear-gradient(145deg, var(--pb-interactive), transparent 42%), var(--pb-bg-card);border-radius:12px;grid-template-columns:92px minmax(0,1fr);min-width:0;display:grid;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.reading-card{background:linear-gradient(145deg, color-mix(in srgb, var(--pb-interactive) 5%, transparent), transparent 42%), var(--pb-bg-card)}}.reading-card{box-shadow:var(--pb-shadow-1)}.reading-card:not(.reading-card-dashboard){min-height:15.25rem}.reading-card-cover-link,.reading-card-cover-frame{min-height:138px;display:block}.reading-card-cover-frame{border-right:1px solid var(--pb-border-subtle);background:var(--pb-bg-shell);height:100%;position:relative;overflow:hidden}.reading-card-cover{object-fit:cover;width:100%;height:100%}.reading-card-cover-placeholder{width:100%;height:100%;color:var(--pb-text-dim);justify-content:center;align-items:center;display:flex}.reading-card-cover-placeholder svg{width:2.25rem;height:2.25rem}.reading-card-body{flex-direction:column;gap:.7rem;min-width:0;padding:.85rem;display:flex}.reading-card-series{color:var(--pb-text-primary);text-overflow:ellipsis;white-space:nowrap;font-family:Syne,sans-serif;font-size:.78rem;font-weight:700;display:block;overflow:hidden}.reading-card-series:hover,.reading-card-series:focus-visible{color:var(--pb-interactive)}.reading-card-issue{min-width:0;color:var(--pb-text-tertiary);gap:.4rem;margin-top:.2rem;font-size:.72rem;display:flex}.reading-card-issue>span:first-child,.reading-card-state{font-family:JetBrains Mono,monospace}.reading-card-issue-title{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.reading-card-state{color:var(--pb-text-secondary);font-size:.67rem;font-weight:600}.reading-card-progress{background:var(--pb-border-subtle);border-radius:999px;height:3px;margin-top:.4rem;overflow:hidden}.reading-card-progress span{border-radius:inherit;background:var(--pb-interactive);height:100%;display:block}.reading-card-state-region{min-height:1.75rem}.reading-card-actions{grid-template-rows:repeat(2,minmax(2.25rem,auto));grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch;gap:.4rem;margin-top:auto;display:grid}.reading-card-view-read .reading-card-actions{grid-template-columns:minmax(0,2fr) minmax(0,3fr)}.reading-card-view-read .reading-card-primary-action,.reading-card-view-read .reading-card-completion-action{white-space:nowrap;padding-inline:.5rem}.reading-card-primary-action,.reading-card-completion-action,.reading-card-queue-action{width:100%;min-width:0}.reading-card-queue-action{grid-column:1/-1}.reading-card-queue-action-placeholder{visibility:hidden;pointer-events:none;grid-column:1/-1;min-height:2.25rem}.reading-card-mutation-status{min-height:1rem;color:var(--pb-error);font-size:.68rem}.reading-empty-state{border:1px dashed var(--pb-border);text-align:center;background:var(--pb-bg-card);border-radius:12px;flex-direction:column;justify-content:center;align-items:center;min-height:260px;padding:2.5rem 1.5rem;display:flex}@supports (color:color-mix(in lab, red, red)){.reading-empty-state{background:color-mix(in srgb, var(--pb-bg-card) 78%, transparent)}}.reading-empty-icon{width:2.75rem;height:2.75rem;color:var(--pb-text-dim)}.reading-empty-state h2{color:var(--pb-text-primary);margin-top:.85rem;font-family:Syne,sans-serif;font-size:.95rem;font-weight:700}.reading-empty-state p{max-width:34rem;color:var(--pb-text-tertiary);margin-top:.35rem;font-size:.78rem}.dashboard-reading-shelf{border:1px solid var(--pb-border);background:var(--pb-bg-card);border-radius:12px;padding:.85rem}@supports (color:color-mix(in lab, red, red)){.dashboard-reading-shelf{background:color-mix(in srgb, var(--pb-bg-card) 88%, transparent)}}.dashboard-reading-shelf{box-shadow:var(--pb-shadow-1)}.dashboard-reading-shelf-header h2{color:var(--pb-text-primary);margin-top:.1rem;font-family:Syne,sans-serif;font-size:1rem;font-weight:700}.dashboard-reading-shelf-link{color:var(--pb-interactive);font-size:.72rem;font-weight:700}.dashboard-reading-shelf-link:hover,.dashboard-reading-shelf-link:focus-visible{text-decoration:underline}.reading-card-dashboard{grid-template-columns:76px minmax(0,1fr)}.reading-card-dashboard .reading-card-cover-link,.reading-card-dashboard .reading-card-cover-frame{min-height:124px}.reading-card-dashboard .reading-card-body{gap:.5rem;padding:.7rem}@media (max-width:1180px){.dashboard-reading-card-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width:640px){.reading-workspace-header{flex-direction:column;align-items:flex-start}.reading-card-grid,.dashboard-reading-card-grid{grid-template-columns:1fr}.reading-card{grid-template-columns:82px minmax(0,1fr)}.reading-card-cover-link,.reading-card-cover-frame{min-height:128px}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/src/pullbox/ui/static/js/pullbox.js b/src/pullbox/ui/static/js/pullbox.js index d8f04d44..b2ff0f2a 100644 --- a/src/pullbox/ui/static/js/pullbox.js +++ b/src/pullbox/ui/static/js/pullbox.js @@ -1024,7 +1024,8 @@ function readImportReviewStatusCounts(shell) { if (!shell || typeof shell.getAttribute !== "function") { return {}; } - var raw = shell.getAttribute("data-import-review-status-counts") || "{}"; + var countSource = shell.querySelector("#import-review-state") || shell; + var raw = countSource.getAttribute("data-import-review-status-counts") || "{}"; try { var parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? parsed : {}; @@ -1124,7 +1125,7 @@ function captureImportReviewViewport(shell, preferredElement) { } var capturedRowKeys = Object.create(null); - var captureRow = function (row) { + var captureRow = function (row, keepAnchor) { if (!row || !shell.contains(row)) { return; } @@ -1133,10 +1134,12 @@ function captureImportReviewViewport(shell, preferredElement) { return; } capturedRowKeys[rowKey] = true; - state.anchors.push({ - key: rowKey, - top: row.getBoundingClientRect().top, - }); + if (keepAnchor !== false) { + state.anchors.push({ + key: rowKey, + top: row.getBoundingClientRect().top, + }); + } if (row.querySelector("[data-import-review-expand-action][aria-expanded='true']")) { state.expandedRows.push({ key: rowKey, @@ -1156,10 +1159,8 @@ function captureImportReviewViewport(shell, preferredElement) { var rows = shell.querySelectorAll("[data-import-review-series-row]"); for (var i = 0; i < rows.length; i += 1) { var rect = rows[i].getBoundingClientRect(); - if (rect.bottom < 0 || rect.top > viewportHeight) { - continue; - } - captureRow(rows[i]); + // A dialog can cover or scroll past an expanded row; keep its disclosure state. + captureRow(rows[i], !(rect.bottom < 0 || rect.top > viewportHeight)); } return state; } @@ -1224,6 +1225,11 @@ var pendingImportReviewViewportState = null; document.body.addEventListener("htmx:beforeSwap", function (event) { var target = event && event.detail ? event.detail.target : null; if (target && target.id === "import-step-review-shell") { + if (target.hasAttribute("data-import-review-workspace") && !event.detail.isError) { + event.detail.shouldSwap = false; + applyImportReviewWorkspace(event.detail.xhr.responseText, target); + return; + } var requestElement = event.detail.requestConfig && event.detail.requestConfig.elt ? event.detail.requestConfig.elt @@ -1232,6 +1238,49 @@ document.body.addEventListener("htmx:beforeSwap", function (event) { } }); +function applyImportReviewWorkspace(html, shell) { + var wrapper = document.createElement("div"); + wrapper.innerHTML = html.trim(); + var nextShell = wrapper.querySelector("#import-step-review-shell"); + if (!nextShell || !nextShell.hasAttribute("data-import-review-workspace")) { + throw new Error("Import review refresh returned an unexpected response."); + } + var state = captureImportReviewViewport(shell, document.activeElement); + restoreImportReviewExpansionState(state, nextShell); + var regions = nextShell.querySelectorAll("[data-import-review-region]"); + var update = function () { + for (var i = 0; i < regions.length; i += 1) { + var current = document.getElementById(regions[i].id); + if (!current || !shell.contains(current)) { continue; } + Idiomorph.morph(current, regions[i], { + morphStyle: "outerHTML", + callbacks: { + beforeNodeMorphed: function (before, after) { + return !before.isEqualNode(after); + }, + beforeNodeRemoved: function (node) { + if (window.Alpine && node.nodeType === 1) { Alpine.destroyTree(node); } + return true; + }, + }, + }); + } + if (window.Alpine) { Alpine.initTree(shell); } + }; + if (window.Alpine) { Alpine.mutateDom(update); } else { update(); } + htmx.process(shell); + var data = window.Alpine ? Alpine.$data(shell) : null; + var nextState = shell.querySelector("#import-review-state"); + if (data && nextState) { + data.splitSeriesRequiresPreferredRoot = nextState.getAttribute("data-requires-preferred-root") === "true"; + data.applyReviewSummary(JSON.parse(nextState.getAttribute("data-review-summary") || "{}")); + data.rehydrateAfterShellSwap(); + } + _syncFooterDockFromResponse(html); + restoreImportReviewViewport(state, shell); + return shell; +} + function loadImportReviewShell(url) { var shell = document.getElementById("import-step-review-shell"); if (!shell || !url) { @@ -1275,6 +1324,9 @@ function loadImportReviewShell(url) { if (typeof Idiomorph === "undefined" || typeof Idiomorph.morph !== "function") { throw new Error("Import review refresh is unavailable."); } + if (currentShell.hasAttribute("data-import-review-workspace")) { + return applyImportReviewWorkspace(html, currentShell); + } var viewportState = captureImportReviewViewport(currentShell); destroyAlpineTree(currentShell); Idiomorph.morph(currentShell, nextShell, { morphStyle: "outerHTML" }); @@ -2116,6 +2168,49 @@ function toggleImportReviewRow(button) { window.toggleImportReviewRow = toggleImportReviewRow; +function handleImportReviewTabKey(event) { + var tabs = Array.from(event.currentTarget.querySelectorAll('[role="tab"]')); + var index = tabs.indexOf(event.target); + if (index < 0) { return; } + if (event.key === 'ArrowRight') { index = (index + 1) % tabs.length; } + else if (event.key === 'ArrowLeft') { index = (index + tabs.length - 1) % tabs.length; } + else if (event.key === 'Home') { index = 0; } + else if (event.key === 'End') { index = tabs.length - 1; } + else { return; } + event.preventDefault(); + tabs[index].focus({ preventScroll: true }); + tabs[index].click(); +} + +function positionImportReviewMenu(menu, event) { + if (event.newState !== 'open') { return; } + var trigger = document.querySelector('[popovertarget="' + menu.id + '"]'); + if (!trigger) { return; } + var rect = trigger.getBoundingClientRect(); + menu.style.left = Math.max(8, Math.min(window.innerWidth - 232, rect.right - 224)) + 'px'; + menu.style.top = Math.max(8, Math.min(window.innerHeight - 190, rect.bottom + 4)) + 'px'; +} + +window.handleImportReviewTabKey = handleImportReviewTabKey; +window.positionImportReviewMenu = positionImportReviewMenu; + +function trapImportReviewDialog(event, dialog) { + var controls = Array.from(dialog.querySelectorAll('button:not([disabled]), a[href], input:not([disabled]), [tabindex="0"]')).filter(function (el) { return el.getClientRects().length; }); + if (!controls.length) { event.preventDefault(); dialog.focus(); return; } + var first = controls[0], last = controls[controls.length - 1]; + if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) { event.preventDefault(); last.focus(); } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } +} + +function closeImportReviewPreview() { + var shell = document.getElementById('import-step-review-shell'); + var data = shell && window.Alpine ? Alpine.$data(shell) : null; + if (data) { data.refreshSeriesReview(); } +} + +window.trapImportReviewDialog = trapImportReviewDialog; +window.closeImportReviewPreview = closeImportReviewPreview; + function readImportConflictCommitState(jobId) { function normalizeCommittedPages(pages) { if (!pages || typeof pages !== "object" || Array.isArray(pages)) { @@ -2202,6 +2297,7 @@ function importCvSearchModalData(config) { query: cfg.query || "", jobId: cfg.jobId, seriesId: cfg.seriesId, + fileId: cfg.fileId || null, selecting: false, close: function (force) { @@ -2232,7 +2328,7 @@ function importCvSearchModalData(config) { "/series/" + this.seriesId + "/cv-search?q=" + - encodeURIComponent(this.query || ""), + encodeURIComponent(this.query || "") + (this.fileId ? "&file_id=" + this.fileId : ""), { target: "#cv-search-modal", swap: "innerHTML", @@ -2340,6 +2436,15 @@ function importCvSearchModalData(config) { var hasKnownSeriesTarget = !!result && !!(result.cv_id || result.user_selected_cv_id || result.series_id); + if (document.querySelector("[data-import-review-workspace]")) { + return { + message: rematchPending + ? "ComicVine match applied. Pullbox is updating the file matches in the background." + : "ComicVine match updated. Ready files and remaining decisions are shown in the review lanes.", + level: rematchPending ? "info" : "success", + }; + } + if (rematchPending) { return { message: "ComicVine match applied. Pullbox is rematching the files in the background.", @@ -2519,6 +2624,12 @@ function importCvSearchModalData(config) { return; } + if (self.fileId) { + self.selecting = true; + return htmx.ajax("GET", "/import/" + self.jobId + "/files/" + self.fileId + "/assign?cv_id=" + cvId, + { target: "#cv-search-modal", swap: "innerHTML" }).finally(function () { self.selecting = false; }); + } + self.selecting = true; var activeReviewView = null; var reviewData = self.reviewPanelData(); @@ -2600,6 +2711,50 @@ function importCvSearchModalData(config) { }; } +function importReviewFileActionData(config) { + var state = importCvSearchModalData(config); + state.issueId = ""; + state.error = ""; + state.submit = async function (form) { + if (this.selecting) return; + this.selecting = true; + this.error = ""; + try { + var url = config.seriesChoice + ? "/import/" + config.jobId + "/series/" + config.seriesId + "/review-" + config.action + : "/import/" + config.jobId + "/files/" + config.fileId + "/" + config.action; + var response = await fetch(url, { + method: "POST", headers: { "X-CSRF-Token": readCsrfTokenFromBody() }, body: new FormData(form), + }); + var result = await response.json(); + if (!response.ok) throw new Error(typeof result.detail === "string" ? result.detail : (result.error && result.error.message) || "The file changed. Reopen this action and try again."); + if (!config.inline) this.close(true); + await this.refreshReview(); + var review = this.reviewPanelData(); + if (review && review.refreshReviewSummary) await review.refreshReviewSummary(); + showToast({ message: result.message, level: "success" }); + } catch (error) { + this.error = error.message || "The action could not be completed."; + } finally { this.selecting = false; } + }; + return state; +} + +function importReviewSeriesChoiceData(config) { + return importReviewFileActionData(Object.assign({}, config, { seriesChoice: true })); +} + +function importReviewFilesData(config) { + return { + close: function () { + var opener = document.getElementById("import-review-menu-trigger-" + config.seriesId); + var host = document.getElementById("cv-search-modal"); + if (host) host.innerHTML = ""; + if (opener) opener.focus({ preventScroll: true }); + }, + }; +} + function renderImportCvSearchLoadingModal(config) { var cfg = config || {}; var query = typeof cfg.query === "string" ? cfg.query : ""; @@ -5229,6 +5384,8 @@ function importProgressData(jobId, nextStep, sourceType) { duplicateCopies: 0, noMatch: 0, filesTotal: 0, + filesPresent: 0, + missingReferences: 0, conflicts: 0, }, @@ -6179,6 +6336,8 @@ function importProgressData(jobId, nextStep, sourceType) { files_duplicate: this.reviewSummary.duplicateCopies || 0, series_no_match: this.reviewSummary.noMatch || 0, files_total: this.reviewSummary.filesTotal || 0, + files_present: this.reviewSummary.filesPresent || 0, + files_missing_references: this.reviewSummary.missingReferences || 0, files_conflict: this.reviewSummary.conflicts || 0, }, stats: { @@ -6288,6 +6447,12 @@ function importProgressData(jobId, nextStep, sourceType) { data && data.scan_total_files, ); } + if (summary.files_missing_references != null) { + this.reviewSummary.missingReferences = this.numberOrZero(summary.files_missing_references); + } + this.reviewSummary.filesPresent = summary.files_present != null + ? this.numberOrZero(summary.files_present) + : Math.max(0, this.reviewSummary.filesTotal - this.reviewSummary.missingReferences); if (summary.files_conflict != null) { this.reviewSummary.conflicts = this.mergeScanSummaryMetric( summary.files_conflict, @@ -6311,6 +6476,9 @@ function importProgressData(jobId, nextStep, sourceType) { 0; this.reviewSummary.conflicts = Number(data && data.total_files_conflict) || Number(this.fileStats.conflicts) || 0; + this.reviewSummary.filesPresent = Math.max( + 0, this.reviewSummary.filesTotal - this.reviewSummary.missingReferences, + ); }, latestLogEntry: function (entries) { @@ -7344,10 +7512,17 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { Number(cfg.selectedItemCount) || (Number(cfg.matchedSelectedCount) || 0) + (Number(cfg.duplicateSelectedCount) || 0), importableItemCount: Number(cfg.importableItemCount) || 0, + selectedFilesCount: Number(cfg.selectedFilesCount) || 0, + attentionFilesCount: Number(cfg.attentionFilesCount) || 0, resolvedConflictGroupCount: Number(cfg.resolvedConflictGroupCount) || 0, conflictSeriesCount: Number(cfg.conflictSeriesCount) || 0, visibleFileConflictGroupCount: 0, confirming: false, + importGateOpen: false, + importGateTrigger: null, + reviewActionPending: false, + reviewSameComicGroups: [], + reviewCopyChoices: {}, confirmError: "", jobId: cfg.jobId, currentView: cfg.currentView || "series", @@ -7429,12 +7604,12 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { importSelectionLabel: function () { var total = this.totalSelectionCount(); - return total + " items selected for import"; + return total + " series selected to import"; }, importActionLabel: function () { - var total = this.totalSelectionCount(); - return "Import " + total + " items"; + var count = this.totalSelectionCount(); + return count ? "Import " + count + " ready series" : "Nothing ready to import"; }, toolbarSelectionLabel: function (overallTotal) { @@ -8086,6 +8261,7 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { ); var sortInput = document.querySelector("#import-step-review-shell input[name='review_sort']"); var pageInput = document.querySelector("#import-step-review-shell input[name='review_page']"); + var reasonInput = document.querySelector("#import-step-review-shell input[name='review_reason']"); if (statusInput && statusInput.value) { params.set("status", statusInput.value); @@ -8096,6 +8272,7 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { if (pageInput && pageInput.value) { params.set("page", pageInput.value); } + if (reasonInput && reasonInput.value) { params.set("reason", reasonInput.value); } var query = params.toString(); return query ? url + "?" + query : url; @@ -8105,6 +8282,112 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { return loadImportReviewShell(this.buildReviewUrl()); }, + applyReviewSummary: function (summary) { + this.selectedFilesCount = Number(summary.selected_files_total) || 0; + this.attentionFilesCount = Number(summary.review_files_open == null ? summary.needs_attention_files_total : summary.review_files_open) || 0; + this.matchedSelectedCount = Number(summary.matched_series_selected) || 0; + this.duplicateSelectedCount = Number(summary.duplicate_series_selected) || 0; + this.selectedItemCount = Number(summary.selected_items_total) || 0; + this.importableItemCount = Number(summary.importable_items_total) || 0; + this.duplicateImportableCount = Number(summary.duplicate_series_importable) || 0; + this.resolvedConflictGroupCount = Number(summary.resolved_file_conflict_groups) || 0; + this.conflictSeriesCount = Number(summary.series_conflicts_total) || 0; + this.syncSelectionSummaryUi(); + }, + + applyReviewAction: async function (url, method, payload, button) { + if (button.disabled || this.reviewActionPending) { return; } + this.reviewActionPending = true; + button.disabled = true; + try { + var response = await fetch(url, { + method: method, + headers: { "Content-Type": "application/json", "X-CSRF-Token": readCsrfTokenFromBody() }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + var error = await response.json().catch(function () { return {}; }); + throw new Error(typeof error.detail === "string" ? error.detail : "Unable to apply this decision."); + } + await this.refreshSeriesReview(); + return true; + } catch (error) { + showToast({ message: error.message || "Unable to apply this decision.", level: "error" }); + return false; + } finally { + this.reviewActionPending = false; + if (button.isConnected) { button.disabled = false; } + } + }, + + applyReviewCandidate: function (seriesId, cvId, button) { + return this.applyReviewAction("/api/v1/import/" + this.jobId + "/series/" + seriesId + "/override", "POST", { cv_id: cvId }, button); + }, + + selectReviewPage: async function (checked, checkbox) { + checkbox.disabled = true; + try { + var root = document.getElementById('import-step-review-shell'); + var controls = Array.from(root.querySelectorAll('[data-import-review-selectable], [data-import-review-duplicate-selectable]')); + var choices = controls.map(function (control) { + return { id: Number(control.getAttribute('data-import-review-selectable') || control.getAttribute('data-import-review-duplicate-selectable')), duplicate: control.hasAttribute('data-import-review-duplicate-selectable'), checked: control.checked }; + }); + for (var choice of choices) { + if (choice.checked === checked) { continue; } + if (choice.duplicate) { await this.toggleDuplicateSeriesFiles(choice.id, checked); } + else { await this.toggleSelection(choice.id, checked); } + } + await this.refreshSeriesReview(); + } finally { + if (checkbox.isConnected) { checkbox.disabled = false; } + } + }, + + resolveReviewConflict: function (groupId, fileId, button) { + return this.applyReviewAction("/api/v1/import/" + this.jobId + "/conflicts/" + groupId + "/resolve", "PUT", { chosen_file_id: fileId }, button); + }, + + skipReviewFile: function (seriesId, fileId, button) { + return this.applyReviewAction("/api/v1/import/" + this.jobId + "/series/" + seriesId + "/reconcile", "POST", { decisions: [{ imported_file_id: fileId, action: "skip" }] }, button); + }, + + keepSuggestedCopies: function (resolutions, button) { + return this.applyReviewAction("/api/v1/import/" + this.jobId + "/conflicts/resolve-bulk", "POST", { resolutions: resolutions }, button); + }, + + toggleReviewFileSelection: async function (fileId, checked, checkbox) { + var applied = await this.applyReviewAction("/api/v1/import/" + this.jobId + "/files/" + fileId + "/selection", "PUT", { include_in_import: checked }, checkbox); + if (!applied) { await this.refreshSeriesReviewQuietly(); } + }, + + openImportGate: async function (trigger) { + await this.refreshReviewSummary(); + if (!this.totalSelectionCount() || !this.hasRequiredPreferredRoot()) { return; } + this.importGateTrigger = trigger; + this.importGateOpen = true; + this.$nextTick(function () { this.$refs.importGateDialog.focus(); }.bind(this)); + }, + + closeImportGate: function () { + if (!this.importGateOpen || this.confirming) { return; } + this.importGateOpen = false; + if (this.importGateTrigger && this.importGateTrigger.isConnected) { + this.importGateTrigger.focus({ preventScroll: true }); + } + }, + + trapImportGateFocus: function (event) { + var controls = this.$refs.importGateDialog.querySelectorAll("button:not([disabled]), [href], input:not([disabled])"); + if (!controls.length) { event.preventDefault(); return; } + var first = controls[0]; + var last = controls[controls.length - 1]; + if (event.shiftKey && (document.activeElement === first || document.activeElement === this.$refs.importGateDialog)) { + event.preventDefault(); last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); first.focus(); + } + }, + refreshSeriesReviewQuietly: function () { return this.refreshSeriesReview().then(null, function () {}); }, @@ -8146,6 +8429,8 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { return; } + this.selectedFilesCount = Number(summary.selected_files_total) || 0; + this.attentionFilesCount = Number(summary.needs_attention_files_total) || 0; this.duplicateSelectedCount = Number(summary.duplicate_series_selected) || Number(summary.duplicate_files_selected) || @@ -8223,6 +8508,7 @@ function importReviewData(configOrDefaultRootId, maybeJobId) { } clearImportConflictCommitState(this.jobId); + this.importGateOpen = false; dispatchImportWizardAdvance({ step: 4, jobStatus: "importing" }); } catch (err) { this.confirmError = err && err.message ? err.message : "Failed to confirm import."; @@ -17768,6 +18054,18 @@ function orphanedRecoveryModal(config) { }; } +function importFileReaderData() { + var config = { previewOnly: true }; + return Object.assign(readerMixin(config), { + previewImportFile: function (detail) { + if (!detail || this.readerOpen) return; + config.readerManifestUrl = "/api/v1/reader/imports/" + Number(detail.jobId) + + "/files/" + Number(detail.fileId) + "/manifest"; + this.openReader({ currentTarget: detail.opener }); + }, + }); +} + function readerMixin(config) { var cfg = config || {}; var zoomSteps = [50, 67, 80, 100, 125, 150, 200, 300]; @@ -18192,7 +18490,9 @@ function readerMixin(config) { self.readerFailedPageIndex = nextIndex; self.readerErrorTitle = "Page " + (nextIndex + 1) + " could not be displayed."; self.readerErrorMessage = - "Try this page again, navigate to another page, or download the original comic."; + cfg.previewOnly + ? "This file could not be displayed. It may be damaged or no longer available. Close the reader to retry View File or choose Skip." + : "Try this page again, navigate to another page, or download the original comic."; self.showReaderControls(); resolve(false); }; @@ -21216,6 +21516,10 @@ document.addEventListener("htmx:afterSwap", function (e) { } if (e.detail.target && e.detail.target.id === "settings-content") { + // Reset before the new controls can be used, not after a delayed settle. + if (_shouldScrollSettingsContent(e)) { + _scrollSettingsContentToTop(); + } syncSettingsWorkspaceNav(document); } @@ -22156,7 +22460,6 @@ document.addEventListener("htmx:afterSettle", function (e) { } if (_shouldScrollSettingsContent(e)) { - _scrollSettingsContentToTop(); syncSettingsWorkspaceNav(document); } diff --git a/src/pullbox/ui/static/js/series-rescan.js b/src/pullbox/ui/static/js/series-rescan.js new file mode 100644 index 00000000..309049d3 --- /dev/null +++ b/src/pullbox/ui/static/js/series-rescan.js @@ -0,0 +1,103 @@ +/* Durable, non-destructive series folder reconciliation. */ +function seriesRescan(seriesId) { + return { + open: false, busy: false, loading: false, reviewing: false, error: "", + report: { job: null, counts: {}, items: [], page: 1, pages: 0 }, + timer: null, disposed: false, trigger: null, pendingPage: null, + requestedJob: new URLSearchParams(window.location.search).get("rescan"), + init: function () { + this.loadReport(1); + if (this.requestedJob) this.showReport(); + }, + destroy: function () { + this.disposed = true; + clearTimeout(this.timer); + }, + request: async function (url, options) { + var response = await fetch(url, options || { cache: "no-store" }); + var data = await response.json(); + if (!response.ok) throw new Error((data.error && data.error.message) || data.detail || "Rescan request failed."); + return data; + }, + showReport: function (trigger) { + this.trigger = trigger || document.activeElement; + this.open = true; + this.$nextTick(() => this.$refs.dialog.focus({ preventScroll: true })); + }, + closeReport: function () { + this.open = false; + if (this.trigger && this.trigger.isConnected) this.trigger.focus({ preventScroll: true }); + }, + trapFocus: function (event) { + if (!this.open) return; + var elements = Array.from(this.$refs.dialog.querySelectorAll('button:not([disabled]), a[href], [tabindex="0"]')) + .filter(function (el) { return el.getClientRects().length > 0; }); + var first = elements[0], last = elements[elements.length - 1]; + if (!first) { event.preventDefault(); return; } + if (event.shiftKey && (document.activeElement === first || document.activeElement === this.$refs.dialog)) { + event.preventDefault(); last.focus(); + } else if (!event.shiftKey && (document.activeElement === last || document.activeElement === this.$refs.dialog)) { + event.preventDefault(); first.focus(); + } + }, + start: async function () { + if (this.busy || (this.report.job && this.report.job.active)) return; + var trigger = document.activeElement; + this.busy = true; + this.error = ""; + try { + var data = await this.request("/api/v1/series/" + seriesId + "/rescan", { + method: "POST", headers: { "X-CSRF-Token": readCsrfTokenFromBody() }, + }); + this.requestedJob = data.job_id; + this.showReport(trigger); + await this.loadReport(1); + } catch (err) { + this.error = err.message; + this.showReport(trigger); + } finally { this.busy = false; } + }, + loadReport: async function (page) { + if (this.disposed) return; + if (this.loading) { this.pendingPage = page || 1; return; } + this.loading = true; + clearTimeout(this.timer); + try { + var query = new URLSearchParams({ page: page || 1 }); + if (this.requestedJob) query.set("job_id", this.requestedJob); + var data = await this.request("/api/v1/series/" + seriesId + "/rescan?" + query); + if (this.disposed) return; + var finishedNewJob = data.job && !data.job.active && + (!this.report.job || this.report.job.active || this.report.job.id !== data.job.id); + this.report = data; + this.error = ""; + if (finishedNewJob) { + this.$dispatch("series-files-rescanned"); + } + } catch (err) { this.error = err.message; } + finally { + this.loading = false; + if (!this.disposed && this.pendingPage !== null) { + var pending = this.pendingPage; + this.pendingPage = null; + this.timer = setTimeout(() => this.loadReport(pending), 0); + } else if (!this.disposed && ((this.report.job && this.report.job.active) || this.error)) { + this.timer = setTimeout(() => this.loadReport(this.report.page), 3000); + } + } + }, + reviewFile: async function (path) { + if (this.reviewing) return; + this.reviewing = true; + try { + var data = await this.request("/api/v1/import", { + method: "POST", + headers: { "Content-Type": "application/json", "X-CSRF-Token": readCsrfTokenFromBody() }, + body: JSON.stringify({ source_path: path, file_paths: [path], source_type: "filesystem", file_handling_mode: "in_place" }), + }); + window.location.assign("/import?tab=collection&resume_job_id=" + encodeURIComponent(data.id)); + } catch (err) { this.error = err.message; } + finally { this.reviewing = false; } + }, + }; +} diff --git a/src/pullbox/ui/templates/base.html b/src/pullbox/ui/templates/base.html index 316d8328..3ff34742 100644 --- a/src/pullbox/ui/templates/base.html +++ b/src/pullbox/ui/templates/base.html @@ -983,6 +983,7 @@

+ diff --git a/src/pullbox/ui/templates/components/pagination.html b/src/pullbox/ui/templates/components/pagination.html index 5ebbe155..8d19af15 100644 --- a/src/pullbox/ui/templates/components/pagination.html +++ b/src/pullbox/ui/templates/components/pagination.html @@ -5,7 +5,7 @@ (for full-page responses). Pass "" to skip hx-select entirely (for routes that return partials directly). -#} -{% macro pagination(page, total_pages, base_url, hx_target="#content", hx_swap="outerHTML transition:true", hx_select=none, page_param="page", hx_base_url=none, hx_sync=none, hx_select_oob=none, hx_push_url="true", hx_indicator=none) %} +{% macro pagination(page, total_pages, base_url, hx_target="#content", hx_swap="outerHTML transition:true", hx_select=none, page_param="page", hx_base_url=none, hx_sync=none, hx_select_oob=none, hx_push_url="true", hx_indicator=none, id_prefix="") %} {%- set sep = "&" if "?" in base_url else "?" -%} {%- set actual_hx_base_url = hx_base_url if hx_base_url is not none else base_url -%} {%- set hx_sep = "&" if "?" in actual_hx_base_url else "?" -%} @@ -41,7 +41,7 @@ {#- Previous button -#} {% if page > 1 %} + {% endif %}
- Download + >Download{% endif %}
-
Comic reader< :disabled="readerCompletionUpdating" >Mark unread -
+ {% endif %}

{% if item.status.value == 'skipped' %} Dismissed {% elif known_series_no_match %} - Issue unresolved + Needs an issue match {% elif item.status.value == 'recovery_pending' %} - Series identified + Review suggestions {% else %} - Needs match + Needs a series match {% endif %} @@ -52,13 +52,13 @@

{{ item.raw_series_name }}

{% if known_series_no_match %} + {% endfor %} + + + {% endif %} +
+ + {% for file in group.files %} + {% set metadata = file.diagnostics or {} %}{% set source = metadata.get('source_metadata') or {} %} + {% set detected = metadata.get('archive_format', source.get('archive_format', {})).get('detected') %} + {% set inspection = metadata.get('content_inspection', source.get('content_inspection', {})) %} + {% set copy_reasons = metadata.get('preferred_reasons', []) if file.is_preferred and group.recommended else metadata.get('why_not_selected', []) %} + + + {% endfor %} +
KeepFileSizeContainerComicInfoPagesWhy
{{ file.file_name }}{% if group.recommended %}{% endif %}{{ file.file_size|filesize }}{% if detected %}{{ {'cbz': 'ZIP', 'cbr': 'RAR', 'cb7': '7z', 'cbt': 'TAR'}.get(detected, detected|upper) }} (detected){% else %}{{ file.file_name.rsplit('.', 1)[-1]|upper }} (filename){% endif %}{{ 'Present' if file.has_comicinfo else 'Not found' }}{{ inspection.get('page_count', 'Unknown') }}{% if file.is_preferred and group.recommended %}Suggested copy{% endif %}{% if copy_reasons %}{{ copy_reasons|join('; ') }}{% elif not file.is_preferred %}Your choice{% endif %}
+
+
+ + {% if not group.recommended %}{% endif %} +
+ +{% endfor %} diff --git a/src/pullbox/ui/templates/partials/import_review_file_action.html b/src/pullbox/ui/templates/partials/import_review_file_action.html new file mode 100644 index 00000000..0156baa6 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_file_action.html @@ -0,0 +1,41 @@ +{% from "components/dropdown_select.html" import dropdown_select %} + diff --git a/src/pullbox/ui/templates/partials/import_review_files_inventory.html b/src/pullbox/ui/templates/partials/import_review_files_inventory.html new file mode 100644 index 00000000..0a6cca71 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_files_inventory.html @@ -0,0 +1,41 @@ +{% from "components/pagination.html" import pagination %} + diff --git a/src/pullbox/ui/templates/partials/import_review_footer_dock.html b/src/pullbox/ui/templates/partials/import_review_footer_dock.html index b4ba50f3..d55168c0 100644 --- a/src/pullbox/ui/templates/partials/import_review_footer_dock.html +++ b/src/pullbox/ui/templates/partials/import_review_footer_dock.html @@ -49,6 +49,7 @@ {% set review_pagination_params = [] %} {% if status_filter %}{% set _ = review_pagination_params.append("status=" ~ status_filter) %}{% endif %} {% if sort %}{% set _ = review_pagination_params.append("sort=" ~ sort) %}{% endif %} + {% if active_reason %}{% set _ = review_pagination_params.append("reason=" ~ active_reason) %}{% endif %} {% set review_pagination_base_url = "/import/" ~ job.id ~ "/review-partial" ~ ("?" ~ review_pagination_params|join("&") if review_pagination_params else "") %}
{{ page_dock( diff --git a/src/pullbox/ui/templates/partials/import_review_gate.html b/src/pullbox/ui/templates/partials/import_review_gate.html new file mode 100644 index 00000000..cf3e7aeb --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_gate.html @@ -0,0 +1,32 @@ + diff --git a/src/pullbox/ui/templates/partials/import_review_issue_choices.html b/src/pullbox/ui/templates/partials/import_review_issue_choices.html new file mode 100644 index 00000000..d91f6e91 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_issue_choices.html @@ -0,0 +1,27 @@ +
+
+ + + +
+
+ + {% for issue in issues %} + + + {% else %}{% endfor %} +
IssueTitleAction
#{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}{{ issue.title or 'Untitled issue' }}
+ + +
No matching issues. Try another number or title.
+
+ {% if issue_page_count > 1 %}{% endif %} +

Verified issue list for {{ series.title }}. Only this file's match changes.

+ +
diff --git a/src/pullbox/ui/templates/partials/import_review_lane_rail.html b/src/pullbox/ui/templates/partials/import_review_lane_rail.html new file mode 100644 index 00000000..eaad770a --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_lane_rail.html @@ -0,0 +1,46 @@ +
+ +
+
+
+

{{ review_lanes.get(active_review_view, 'Story Arcs' if active_review_view == 'story_arcs' else 'Review') }} · {{ lane_descriptions.get(active_review_view, 'Review the saved matches before importing.') }} {{ total }} {{ 'story arcs' if active_review_view == 'story_arcs' else 'series' }}

+ {% if active_lane and active_lane != 'ready' %} +
+ Show: + + {% for key, count in reason_counts.items() if count and key != 'ready' %} + + {% endfor %} +
+ {% endif %} + {% if active_review_view in ('confirm', 'decide', 'conflict_rows') and review_summary.file_conflict_groups %} +

Across this import: {{ review_summary.file_conflict_groups }} issue {{ 'group' if review_summary.file_conflict_groups == 1 else 'groups' }} involving {{ review_summary.files_conflict }} files in {{ review_summary.series_file_conflicts }} series.

+ {% endif %} +
+ {% if active_review_view == 'ready' %} +
+ + +
+ {% endif %} + {% if active_review_view == 'confirm' %} + {% set suggested = namespace(items=[], seen=[]) %} + {% for groups in inline_conflicts.values() %}{% for group in groups if group.preferred_file_id and group.id not in suggested.seen %} + {% set suggested.items = suggested.items + [{'conflict_group_id': group.id, 'chosen_file_id': group.preferred_file_id}] %}{% set suggested.seen = suggested.seen + [group.id] %} + {% endfor %}{% endfor %} + {% if suggested.items %}{% endif %} + {% endif %} +
diff --git a/src/pullbox/ui/templates/partials/import_review_one_page_actions.html b/src/pullbox/ui/templates/partials/import_review_one_page_actions.html new file mode 100644 index 00000000..b6c5467d --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_one_page_actions.html @@ -0,0 +1,11 @@ +{% for action in ('allow', 'skip') if one_page.get(action ~ '_token') %} +
+ + +
+ {% endfor %} diff --git a/src/pullbox/ui/templates/partials/import_review_one_page_files.html b/src/pullbox/ui/templates/partials/import_review_one_page_files.html new file mode 100644 index 00000000..91be1c3a --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_one_page_files.html @@ -0,0 +1,36 @@ +
+
+

One-page archives

+

Usually cover art or a damaged download; sometimes an intentional one-page comic. View the file, then allow it for this import or skip it.

+
+
+ {% for file in one_page.files %} + {% set block = safety_block_context_by_file_id.get(file.id, {}) %} + {% set metadata = file.diagnostics or {} %} + {% set detected = metadata.get('archive_format', {}).get('detected') %} + {% set inspection = metadata.get('content_inspection', {}) %} +
+
+

{{ safety_file_display_name_by_file_id.get(file.id, 'File') }}

+

{{ file.file_size|filesize }} · 1 page{% if inspection.get('member_count') %} · {{ inspection.member_count }} members{% endif %}{% if detected %} · {{ {'cbz': 'ZIP', 'cbr': 'RAR', 'cb7': '7z', 'cbt': 'TAR'}.get(detected, detected|upper) }}{% endif %}

+
+
+ {% if file.status.value != 'safety_blocked' %} + {{ 'Skipped' if file.status.value == 'skipped' else 'Allowed' if metadata.get('safety_exception', {}).get('allowed_once') else 'Matched' }} + {% else %} + + {% if block.get('overrideable') is sameas true %} + + {% endif %} + + {% endif %} +
+
+ {% endfor %} +
+
diff --git a/src/pullbox/ui/templates/partials/import_review_overview.html b/src/pullbox/ui/templates/partials/import_review_overview.html new file mode 100644 index 00000000..7292e62d --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_overview.html @@ -0,0 +1,45 @@ +
+
+
+

+ {{ review_summary.ready_to_import_total }} series ready to import + {% if review_summary.review_files_open %}· {{ review_summary.review_files_open }} files need you{% else %}· all decisions made{% endif %} +

+ {% set settled = review_summary.review_files_settled %} + {% set file_total = review_summary.files_present|default(review_summary.files_total) %} + {% set percent = (100 * settled / file_total)|round(1) if file_total else 100 %} +
+
+
+
+
+ Ready or handled {{ settled }} of {{ file_total }} files + Still open {{ review_summary.review_files_open }} files in {{ review_open_series }} series + {% if lane_counts.blocked %} Cannot import: {{ lane_counts.blocked }} series{% endif %} +
+ {% if review_summary.files_missing_references %}

{{ review_summary.files_missing_references }} missing file reference{{ 's' if review_summary.files_missing_references != 1 else '' }} tracked separately. Ready files can still import.

{% endif %} +
+
+
+
+ + + +
+

Ready files import while you work through the rest. Unresolved files stay in Follow-up.

+ +
+
+
+ {% for lane, kicker in [('decide', 'Start here'), ('confirm', 'Then'), ('fix_source', 'Can wait'), ('blocked', 'Will not import')] if lane != 'blocked' or lane_counts.blocked %} + + {% endfor %} +
+
diff --git a/src/pullbox/ui/templates/partials/import_review_row_menu.html b/src/pullbox/ui/templates/partials/import_review_row_menu.html new file mode 100644 index 00000000..54f4d5b4 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_row_menu.html @@ -0,0 +1,10 @@ + +
+ {% if item.status.value != 'skipped' and reason_key != 'needs_series' %} + + {% if known_series_target %}{% endif %} + {% endif %} + + {% if series_diagnostics.get('review_series_skip') %} + {% elif item.status.value not in ('skipped', 'imported', 'confirmed') and reason_key != 'needs_series' %}{% endif %} +
diff --git a/src/pullbox/ui/templates/partials/import_review_safety_categories.html b/src/pullbox/ui/templates/partials/import_review_safety_categories.html new file mode 100644 index 00000000..4371b24c --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_safety_categories.html @@ -0,0 +1,137 @@ + {% if safety_failure_summary and (active_reason in safety_reason_keys or active_review_view == 'safety_blocked') %} +
+ {% for safety_category in safety_failure_summary if not active_reason or safety_category.category == active_reason %} + {% set bulk_preview_for_category = safety_bulk_preview if safety_bulk_preview is defined and safety_bulk_preview and safety_bulk_preview.category.value == safety_category.category else none %} + {% set bulk_action_for_category = safety_bulk_action|default('allow_once') if bulk_preview_for_category else 'allow_once' %} + {% set bulk_error_for_category = safety_bulk_error if safety_bulk_error is defined and safety_bulk_error_category is defined and safety_bulk_error_category == safety_category.category else '' %} + {% set bulk_overrideable_count = safety_category.bulk_overrideable_count|default(0)|int %} +
+ {% if safety_category.category == 'single_page_comic' %} +
+

One-page archive

+ {{ safety_category.count }} {{ 'file' if safety_category.count == 1 else 'files' }} in {{ safety_category.series_count }} series +
+

Usually cover art or a damaged download; sometimes an intentional one-page comic.

+

Detected: 1 image page

+
+

Skip them all, or allow them one by one.

+ {% if not bulk_preview_for_category %} +
+
+ + + + + +
+
+ {% endif %} +
+ {% else %} +
+

{{ safety_category.label }}

+ {{ safety_category.count }} +
+

{{ safety_category.reason }}

+

{{ 'Fix the source, then recheck its files below.' if safety_category.retryable else 'Retry alone will not help.' }} {{ 'A one-time exception is available for eligible files.' if safety_category.overrideable or safety_category.bulk_overrideable else 'This check cannot be overridden.' }}

+ + {% endif %} + {% if bulk_preview_for_category %} + + {% elif safety_category.category == 'single_page_comic' %} + {% if bulk_error_for_category %} + + {% endif %} + {% elif safety_category.bulk_overrideable|default(false) %} + {% if bulk_error_for_category %} + + {% endif %} +

+ {{ bulk_overrideable_count }} of {{ safety_category.count }} file{{ '' if safety_category.count == 1 else 's' }} can be allowed once. +

+
+ + + + + +
+ {% elif bulk_error_for_category %} + + {% endif %} +
+ {% endfor %} +
+ {% endif %} diff --git a/src/pullbox/ui/templates/partials/import_review_series_checkbox.html b/src/pullbox/ui/templates/partials/import_review_series_checkbox.html new file mode 100644 index 00000000..db51a685 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_series_checkbox.html @@ -0,0 +1,5 @@ +{% if duplicate_selectable %} + 0 %}checked{% endif %} data-import-review-duplicate-selectable="{{ item.id }}" data-testid="import-review-duplicate-select" @change="toggleDuplicateSeriesFiles({{ item.id }}, $event.target.checked, $event.target)" aria-label="Import ready files for {{ item.raw_series_name }}"> +{% elif series_selectable %} + +{% endif %} diff --git a/src/pullbox/ui/templates/partials/import_review_series_choice.html b/src/pullbox/ui/templates/partials/import_review_series_choice.html new file mode 100644 index 00000000..98767d09 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_series_choice.html @@ -0,0 +1,14 @@ + diff --git a/src/pullbox/ui/templates/partials/import_review_workspace_detail.html b/src/pullbox/ui/templates/partials/import_review_workspace_detail.html new file mode 100644 index 00000000..92d9fd91 --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_workspace_detail.html @@ -0,0 +1,138 @@ +
+ {% if item.status.value == 'skipped' and series_diagnostics.get('review_series_skip') %} +
+

This series is excluded from this import. Its saved matches and source files are unchanged.

+
    + {% for group in review_file_groups_by_series_id.get(item.id, []) %}{% for file in group.rows %}
  • {{ file.file_name }}
  • {% endfor %}{% endfor %} + {% for file in safety_blocked_files_by_series_id.get(item.id, []) %}
  • {{ safety_file_display_name_by_file_id.get(file.id, 'File') }}
  • {% endfor %} +
+ +
+ {% else %} + {% if known_series_target and reason_key not in ('needs_series', 'series_conflict', 'needs_issue') and (item.cv_url or item.user_selected_cv_id or item.cv_id) %} +

ComicVine match: {{ item.cv_title or item.raw_series_name }} · {{ item.user_selected_cv_id or item.cv_id }}

+ {% endif %} + {% if reason_key in ('needs_series', 'series_conflict', 'needs_issue') %} +
+
+

{{ 'Match inputs' if reason_key in ('needs_series', 'series_conflict') else 'Match details' }}

+
+
Title
{{ item.raw_series_name }}
+
Year
{{ item.raw_year or 'Unknown' }}
+
Threshold
{{ (job.cv_match_threshold * 100)|round|int }}%
+ {% if known_series_target %}
ComicVine
{{ item.cv_title or item.raw_series_name }} · {{ item.user_selected_cv_id or item.cv_id or 'Known library series' }}
{% endif %} +
Result
{{ series_diagnostics.get('rejection_reason') or review_reasons[reason_key][0] }}
+
+
+ + {% if known_series_target %}{% endif %} +
+
+
+ {% if reason_key in ('needs_series', 'series_conflict') %} +

ComicVine candidates

+ {% set candidates = series_diagnostics.get('top_candidates', []) %} + {% if candidates %}
+ {% for candidate in candidates %} + + + + {% endfor %} +
CandidatePublisherScoreReasonAction
{{ candidate.title }}{% if candidate.year %} ({{ candidate.year }}){% endif %}{{ candidate.publisher or 'Unknown' }}{{ (candidate.score_pct|string ~ '%') if candidate.score_pct is defined else 'Unknown' }}{{ candidate.rejection_reasons|join(', ') if candidate.rejection_reasons else 'Saved candidate' }}{% set candidate_id = (candidate.get('cv_id') or candidate.get('id'))|int %}{% if candidate_id > 0 %}{% endif %}
{% else %}

No saved candidates. Search ComicVine to find the correct series.

{% endif %} + {% else %} +

Files without an issue

+ {% for group in review_file_groups_by_series_id.get(item.id, []) if group.key in ('no_match', 'skipped') %} + {% for file in group.rows %}
+
{{ file.file_name }}{{ file.file_size|filesize }}
+ {% if file.status == 'skipped' %}Skipped{% else %} +
+
{% endif %} +
{% endfor %}{% endfor %} + + {% endif %} +
+
+ {% endif %} + {% if series_diagnostics.get('kind') == 'source_layout_review' %}

Layout review: {{ series_diagnostics.get('source_layout_review_files', 1) }} {{ 'files do' if series_diagnostics.get('source_layout_review_files', 1) != 1 else 'file does' }} not fit the selected source layout and will not be matched automatically. {{ series_diagnostics.get('rejection_reason', '') }}

{% elif series_diagnostics.get('kind') == 'mylar3_path_incompatible' %}

Mylar path review: {{ series_diagnostics.get('rejection_reason', '') }} Correct the Mylar path mapping and retry this import.

{% endif %} + {% if reason_key != 'needs_series' %} + {% include "partials/import_review_copy_choices.html" %} + {% for file in safety_blocked_files_by_series_id.get(item.id, []) %} + {% set block = safety_block_context_by_file_id.get(file.id, {}) %} + {% set source_work = (file.diagnostics or {}).get('review_source_action', {}) %} +
+
+
+

{{ safety_file_display_name_by_file_id.get(file.id, 'File') }}

+

{{ block.get('reason', 'This file needs another inspection before importing.') }}

+ {% if block.get('code') %}
Inspection details

Code: {{ block.code }}

{% endif %} + {% if block.get('category') == 'source_missing' %}

You can continue with ready files. This saved Mylar reference remains available in Follow-up.

{% endif %} + {% if block.get('category') == 'outside_approved_root' %}{% endif %} +
+
+ {% if source_work.get('state') in ('pending', 'matching') %} +
Verifying source
+ {% elif file.status.value not in ('safety_blocked', 'safety_approved') %} + {{ 'Skipped' if file.status.value == 'skipped' else 'Matched' }} + {% elif file.status.value == 'safety_approved' %} + {% if row.updating %}Preparing match + {% else %}Approved · match needed{% endif %} + {% else %} + {% if block.get('category') in ('permission_unreadable', 'archive_inspection_failed', 'zero_byte', 'archive_no_pages', 'source_changed', 'source_missing') %} + + {% endif %} + {% if block.get('overrideable') is sameas true %} + + {% endif %} + + {% endif %} +
+
+ {% if source_work.get('state') == 'failed' %}

{{ source_work.get('error') }}

{% endif %} +
+ {% endfor %} + + + {% if reason_key != 'needs_issue' or row.ready_files %} + {% if reason_key == 'series_conflict' %}
Files in this folder{% endif %} + {% if item.status.value == 'duplicate' %}

Already in library. Only missing issues can be added.

{% endif %} +
+ + {% for group in review_file_groups_by_series_id.get(item.id, []) if group.key != 'conflict' %} +
+

{{ group.label }} · {{ group.count }} files

+ +
    + {% for file in group.rows %} +
  • + {% if (duplicate_selectable or item.status.value == 'matched') and file.status in ('matched', 'confirmed') %} + + {% else %} + {{ file.file_name }} + {% endif %} + {{ file.matched_issue_label or file.duplicate_reason_label or '' }} + {% if file.matched_issue_url %}Issue {{ file.matched_issue_number or 'Unknown' }} · CV {{ file.matched_issue_cv_id }}{% endif %} + {% if file.status in ('matched', 'confirmed', 'skipped') %}{{ 'Skipped' if file.status == 'skipped' else 'Matched' }}{% endif %} + {% if file.get('archive_format', {}).get('mismatch') %}Detected {{ {'cbz': 'ZIP', 'cbr': 'RAR', 'cb7': '7z', 'cbt': 'TAR'}.get(file.archive_format.detected, file.archive_format.detected|upper) }} content; .{{ file.archive_format.declared }} filename{% endif %} + {% if file.status in ('matched', 'confirmed', 'no_match') %} + + {% endif %} +
  • + {% endfor %} +
+
+ {% endfor %} +
+ {% if reason_key == 'series_conflict' %}
{% endif %} + {% endif %} + {% endif %} + {% endif %} +
diff --git a/src/pullbox/ui/templates/partials/import_review_workspace_rows.html b/src/pullbox/ui/templates/partials/import_review_workspace_rows.html new file mode 100644 index 00000000..4c122fcc --- /dev/null +++ b/src/pullbox/ui/templates/partials/import_review_workspace_rows.html @@ -0,0 +1,78 @@ +{% from "components/settings_shell.html" import ui_icon %} +{% set ready_table = active_review_view == 'ready' %} +{% macro review_expander(item, row, compact_one_page, label='', primary=false) %} + +{% endmacro %} +
+ + + {% if ready_table %}{% endif %} + + + {% if ready_table %} + {% else %}{% endif %} + + {% for item in series_items %} + {% set row = review_rows[item.id] %} + {% set reason_key = active_reason if active_reason in row.reasons else row.reasons[0] %} + {% set one_page = one_page_review.get(item.id) %} + {% set compact_one_page = one_page and reason_key == 'single_page_comic' %} + {% set series_diagnostics = item.diagnostics or {} %} + {% set known_series_target = item.cv_id or item.user_selected_cv_id or item.series_id %} + {% set series_selectable = item.status.value == 'matched' and item.files_matched > 0 %} + {% set duplicate_selectable = item.id in actionable_duplicate_ids and item.files_matched > 0 %} + {% set duplicate_selected_files = duplicate_selected_file_counts.get(item.id, 0) %} + + + {% if ready_table %}{% endif %} + + {% set available_files = [0, item.files_total - row.missing_references]|max %} + + {% if ready_table %} + + + + {% else %} + + + {% endif %} + + + + + + {% else %}{% endfor %} +
FilesComicVine matchConfidenceActionsWhat needs attentionActionDetails
{% include "partials/import_review_series_checkbox.html" %} +

{{ item.cv_title or item.raw_series_name }}

+
+ {{ item.cv_year or item.raw_year or 'Year unknown' }}{% if item.cv_publisher or item.raw_publisher %} · {{ item.cv_publisher or item.raw_publisher }}{% endif %} + {% if item.source_folder %}{{ item.source_folder.replace('\\', '/').rstrip('/').split('/')[-1] }}{% endif %} +
+
{% if ready_table %}{{ row.ready_files }} ready{% if row.decision_files %}

{{ row.decision_files }} need review

{% endif %} + {% elif available_files %}{{ row.ready_files }}/{{ available_files }}{% else %}No available files{% endif %} + {% if row.missing_references %}

{{ row.missing_references }} missing reference{{ 's' if row.missing_references != 1 else '' }}

{% endif %} + {% if item.files_already_owned %}

{{ item.files_already_owned }} already owned

{% endif %} +

{{ item.cv_title or 'Known series' }}{% if item.cv_year %} ({{ item.cv_year }}){% endif %}

{{ item.cv_publisher or '' }} {% if item.cv_id or item.user_selected_cv_id %}CV {{ item.user_selected_cv_id or item.cv_id }}{% endif %}

{% if item.cv_match_score is not none %}{{ (item.cv_match_score * 100)|round|int }}%{% else %}—{% endif %}{% include "partials/import_review_row_menu.html" %}{{ review_expander(item, row, false) }}

{{ review_reasons[reason_key][0] }}

{{ reason_descriptions.get(reason_key, 'Review the saved evidence and choose the next step.') }} {{ review_expander(item, row, compact_one_page, 'Technical detail' if row.lane in ('fix_source', 'blocked') else 'Why?') }}

+ {% if row.updating %}Updating match{% endif %}
+ {% if compact_one_page %} + {% include "partials/import_review_one_page_actions.html" %} + {% elif reason_key in ('needs_series', 'series_conflict') %} + + + {% elif reason_key == 'duplicate_copy_confirm' %} + {% set suggestions = namespace(items=[]) %}{% for group in inline_conflicts.get(item.id, []) if group.preferred_file_id %}{% set suggestions.items = suggestions.items + [{'conflict_group_id': group.id, 'chosen_file_id': group.preferred_file_id}] %}{% endfor %} + {% if suggestions.items %}{% endif %} + {{ review_expander(item, row, false, 'Change', not suggestions.items) }} + {% else %}{{ review_expander(item, row, false, row.action, true) }}{% endif %} + {% if not compact_one_page %}{% include "partials/import_review_row_menu.html" %}{% endif %} +
{{ review_expander(item, row, compact_one_page) }}
Nothing to review in this lane.
+
diff --git a/src/pullbox/ui/templates/partials/import_step_progress.html b/src/pullbox/ui/templates/partials/import_step_progress.html index 7843ad2d..b836729a 100644 --- a/src/pullbox/ui/templates/partials/import_step_progress.html +++ b/src/pullbox/ui/templates/partials/import_step_progress.html @@ -357,7 +357,8 @@

Files

-

+

+

Series

diff --git a/src/pullbox/ui/templates/partials/import_step_review.html b/src/pullbox/ui/templates/partials/import_step_review.html index 1e62db38..50427ab4 100644 --- a/src/pullbox/ui/templates/partials/import_step_review.html +++ b/src/pullbox/ui/templates/partials/import_step_review.html @@ -1,187 +1,6 @@ -{#- Import Step 3: Review Series — table of matched series for selection. - - Template context (from UI route): - - job: ImportJob instance - - series_items: list of ImportedSeries - - total: int total count - - page, page_size: pagination - - status_filter: current filter or None - - status_counts: dict of status → count - Shared controller: importReviewData() from pullbox.js. --#} {% from "components/settings_shell.html" import ui_icon, inline_alert %} {% from "components/dropdown_select.html" import dropdown_select %} - -{% set overall_series_total = review_summary.series_total if review_summary else total %} -{% set overall_importable_total = review_summary.importable_items_total if review_summary else 0 %} -{% set overall_conflict_count = review_summary.series_conflicts_total if review_summary else 0 %} -{% set active_review_view = current_view or (status_filter or 'series') %} -{% set cv_match_method_labels = { - 'exact_title_year': 'Exact match', - 'fuzzy_title': 'Close match', - 'alternate_release_candidate': 'Filename-assisted match', - 'mylar3_cv_id': 'Mylar match', - 'comicinfo_cv_id': 'ComicInfo match', - 'user_override': 'Manual match', - 'user_selected': 'Manual match', -} %} -{%- macro sort_header(field, label, extra_classes="", width="") -%} - {%- set current_field = sort.lstrip("-") -%} - {%- set current_desc = sort.startswith("-") -%} - {%- if current_field == field -%} - {%- set next_sort = field if current_desc else ("-" ~ field) -%} - {%- else -%} - {%- set next_sort = "-" ~ field -%} - {%- endif -%} - {%- set sort_qs = "sort=" ~ next_sort -%} - {%- if status_filter %}{% set sort_qs = sort_qs ~ "&status=" ~ status_filter %}{% endif -%} - - - -{%- endmacro -%} -{%- macro matched_file_target_table(rows, empty_copy="No matched file targets are available for this row yet.") -%} -
-

Matched files

-
- - - - - - - - - - {% for file_target in rows %} - - - - - - {% else %} - - - - {% endfor %} - -
FileIssueCV Issue ID
-
- {{ file_target.file_name }} - {{ file_target.file_size | filesize }} -
-
- {% if file_target.matched_issue_number %} - #{{ file_target.matched_issue_number }} - {% else %} - - {% endif %} - - {% if file_target.matched_issue_cv_id and file_target.matched_issue_url %} - {{ file_target.matched_issue_cv_id }} - {% else %} - - {% endif %} -
{{ empty_copy }}
-
-
-{%- endmacro -%} -{%- macro series_file_detail_groups(item, file_groups) -%} -
-
-

Source files

-
- {{ item.files_matched }} Importable - {{ item.files_already_owned }} Already Owned - {{ item.files_duplicate }} Extra Incoming - {{ item.files_conflict }} Conflicts - {{ item.files_no_match }} No Match -
-
- - {% for group in file_groups %} -
-
-
-

{{ group.label }}

-

{{ group.description }}

-
- {{ group.count }} -
- -
- {% for row in group.rows %} -
-
-
-
-

{{ row.file_name }}

- {{ row.file_size | filesize }} - {% if row.has_comicinfo %} - ComicInfo - {% endif %} -
- {% if row.matched_issue_label %} -

Matched issue: {{ row.matched_issue_label }}

- {% endif %} - {% if row.match_confidence %} -

Confidence: {{ row.match_confidence|replace('_', ' ')|title }}

- {% endif %} -
- {% if group.key == 'conflict' %} - Use Conflicts tab - {% elif group.key == 'already_owned' %} - Library duplicate - {% elif group.key == 'duplicate_file' %} - Excluded - {% elif group.key == 'no_match' %} - Needs decision - {% else %} - Importable - {% endif %} -
- - {% if group.key == 'duplicate_file' %} -

{{ row.duplicate_reason_label }}

- {% if row.duplicate_keep_label %} -

Kept incoming file {{ row.duplicate_keep_label }}

- {% endif %} - {% endif %} -
- {% endfor %} -
-
- {% else %} -
- No file details are available for this review row. -
- {% endfor %} -
-{%- endmacro -%} - +{% set active_review_view = current_view or 'ready' %}
{{ group.label }}

duplicateImportableCount: {{ review_summary.duplicate_series_importable if review_summary else 0 }}, selectedItemCount: {{ review_summary.selected_items_total if review_summary else 0 }}, importableItemCount: {{ review_summary.importable_items_total if review_summary else 0 }}, + selectedFilesCount: {{ review_summary.selected_files_total if review_summary else 0 }}, + attentionFilesCount: {{ review_summary.review_files_open if review_summary else 0 }}, resolvedConflictGroupCount: {{ review_summary.resolved_file_conflict_groups if review_summary else 0 }}, conflictSeriesCount: {{ review_summary.series_conflicts_total if review_summary else 0 }}, preferredRootId: {{ job.target_library_root_id|tojson }}, @@ -206,81 +27,33 @@

{{ group.label }}

x-on:import:conflicts-reset.window="handleConflictsReset($event.detail)" data-testid="import-collection-review" data-import-review-status-counts='{{ status_counts|tojson|forceescape }}' - class="space-y-6" + data-import-review-workspace + class="space-y-4" > - {% if safety_rematch_pending %} - - {% endif %} - - - -
-
- {% call inline_alert(tone="danger", icon="warning") %} - - {% endcall %} -
+
+ + + + + {% if safety_rematch_pending %} + + {% endif %} +
-
-
- - -
-
- {{ review_summary.selected_items_total if review_summary else 0 }} items selected for import - -
-
+
+ {% include "partials/import_review_overview.html" %} +
+
{% if split_series_review.requires_preferred_destination %}
Choose where future acquisit
{% endif %} - {% set selection_locked_view = active_review_view in ('no_match', 'needs_series', 'needs_issue', 'safety_blocked') %} - {% set show_selection_column = not selection_locked_view %} - {% set series_table_colspan = 11 if show_selection_column else 10 %} - {% set series_tab_sort = sort if active_review_view != 'conflicts' else 'confidence' %} - {% set conflict_tab_sort = sort if active_review_view == 'conflicts' else 'series' %} - {% set series_status_config = [ - ('matched', 'Matched', 'chip-btn-success', 'count-badge-success'), - ('confirmed', 'Confirmed', 'chip-btn-info', 'count-badge-info'), - ('imported', 'Imported', 'chip-btn-success', 'count-badge-success'), - ('duplicate', 'In Library', 'chip-btn-warning', 'count-badge-warning'), - ('failed', 'Failed', 'chip-btn-error', 'count-badge-error'), - ('skipped', 'Skipped', 'chip-btn-selected', 'count-badge-info'), - ] %} - {% set series_filter_items = [ - { - "filter_key": "series", - "label": "All", - "href": "#", - "hx_get": "/import/" ~ job.id ~ "/review-partial?sort=" ~ series_tab_sort, - "hx_target": "#import-step-review-shell", - "testid": "import-review-tab-series", - "active": active_review_view == 'series', - "count": overall_series_total, - "active_class": "chip-btn-selected", - "count_active_class": "count-badge-info", - "count_inactive_class": "count-badge-neutral", - } - ] %} - {% set needs_series_count = status_counts.get('needs_series', 0) %} - {% if needs_series_count > 0 %} - {% set _ = series_filter_items.append({ - "filter_key": "needs_series", - "label": "Needs Series Match", - "href": "#", - "hx_get": "/import/" ~ job.id ~ "/review-partial?status=needs_series&sort=" ~ series_tab_sort, - "hx_target": "#import-step-review-shell", - "testid": "import-review-series-filter-needs-series", - "active": active_review_view == 'needs_series', - "count": needs_series_count, - "active_class": "chip-btn-warning", - "count_active_class": "count-badge-warning", - "count_inactive_class": "count-badge-neutral", - }) %} - {% endif %} - {% set needs_issue_count = status_counts.get('needs_issue', 0) %} - {% if needs_issue_count > 0 %} - {% set _ = series_filter_items.append({ - "filter_key": "needs_issue", - "label": "Needs Issue Match", - "href": "#", - "hx_get": "/import/" ~ job.id ~ "/review-partial?status=needs_issue&sort=" ~ series_tab_sort, - "hx_target": "#import-step-review-shell", - "testid": "import-review-series-filter-needs-issue", - "active": active_review_view == 'needs_issue', - "count": needs_issue_count, - "active_class": "chip-btn-warning", - "count_active_class": "count-badge-warning", - "count_inactive_class": "count-badge-neutral", - }) %} - {% endif %} - {% set safety_blocked_count = status_counts.get('safety_blocked', 0) %} - {% if safety_blocked_count > 0 %} - {% set _ = series_filter_items.append({ - "filter_key": "safety_blocked", - "label": "Safety Review", - "href": "#", - "hx_get": "/import/" ~ job.id ~ "/review-partial?status=safety_blocked&sort=" ~ series_tab_sort, - "hx_target": "#import-step-review-shell", - "testid": "import-review-series-filter-safety-blocked", - "active": active_review_view == 'safety_blocked', - "count": safety_review_series_count ~ " series · " ~ safety_review_file_count ~ " file" ~ ("" if safety_review_file_count == 1 else "s"), - "active_class": "chip-btn-error", - "count_active_class": "count-badge-error", - "count_inactive_class": "count-badge-neutral", - }) %} - {% endif %} - {% set story_arc_count = review_summary.story_arcs_total if review_summary else story_arc_total|default(0) %} - {% if story_arc_count > 0 %} - {% set _ = series_filter_items.append({ - "filter_key": "story_arcs", - "label": "Story Arc follow-up", - "href": "#", - "hx_get": "/import/" ~ job.id ~ "/review-partial?status=story_arcs&page=1", - "hx_target": "#import-step-review-shell", - "testid": "import-review-tab-story-arcs", - "active": active_review_view == 'story_arcs', - "count": story_arc_count, - "active_class": "chip-btn-info", - "count_active_class": "count-badge-info", - "count_inactive_class": "count-badge-neutral", - }) %} - {% endif %} - {% for status_val, label, active_class, badge_class in series_status_config %} - {% set cnt = status_counts.get(status_val, 0) %} - {% if cnt > 0 %} - {% set _ = series_filter_items.append({ - "filter_key": status_val, - "label": label, - "href": "#", - "hx_get": "/import/" ~ job.id ~ "/review-partial?status=" ~ status_val ~ "&sort=" ~ series_tab_sort, - "hx_target": "#import-step-review-shell", - "testid": "import-review-series-filter-" ~ status_val, - "active": active_review_view == status_val, - "count": cnt, - "active_class": active_class, - "count_active_class": badge_class, - "count_inactive_class": "count-badge-neutral", - }) %} - {% endif %} - {% endfor %} - -
-
-
-
- Status -
- {{ review_summary.selected_items_total if review_summary else 0 }} of {{ overall_importable_total }} selected - {% if active_review_view in ('series', 'matched') %} -
- - -
- {% elif active_review_view == 'conflicts' %} - {% set current_visible_file_conflict_group_count = visible_file_conflict_group_count|default(file_conflict_group_count|default(0, true), true)|int %} - {% if current_visible_file_conflict_group_count > 0 %} -
- - -
- {% endif %} - {% endif %} -
-
-
-
- {% for filter_item in series_filter_items %} - {% set is_active = filter_item.active|default(false) %} - {% set series_active_class = filter_item.active_class|default('chip-btn-selected') %} - {% set series_inactive_class = filter_item.inactive_class|default('chip-btn-neutral') %} - {% set count_active_class = filter_item.count_active_class|default('count-badge-info') %} - {% set count_inactive_class = filter_item.count_inactive_class|default('count-badge-neutral') %} - {% set initial_state_class = series_active_class if is_active else series_inactive_class %} - {% set initial_count_class = count_active_class if is_active else count_inactive_class %} - - {{ filter_item.label }} - - {{ filter_item.count }} - - - {% endfor %} - {% set conflict_active = active_review_view == 'conflicts' %} - -
-
-
- - {% if active_review_view == 'safety_blocked' and safety_failure_summary %} -
- {% for safety_category in safety_failure_summary %} - {% set bulk_preview_for_category = safety_bulk_preview if safety_bulk_preview is defined and safety_bulk_preview and safety_bulk_preview.category.value == safety_category.category else none %} - {% set bulk_action_for_category = safety_bulk_action|default('allow_once') if bulk_preview_for_category else 'allow_once' %} - {% set bulk_error_for_category = safety_bulk_error if safety_bulk_error is defined and safety_bulk_error_category is defined and safety_bulk_error_category == safety_category.category else '' %} - {% set bulk_overrideable_count = safety_category.bulk_overrideable_count|default(0)|int %} -
-
-

{{ safety_category.label }}

- {{ safety_category.count }} -
-

{{ safety_category.reason }}

-
- - Why this needs review - -
-

- {{ 'Retry may help after remediation' if safety_category.retryable else 'Retry alone will not help' }} - · - {% if safety_category.overrideable %} - Trusted override available - {% elif safety_category.bulk_overrideable %} - Trusted override available for eligible files - {% else %} - Override not allowed - {% endif %} -

- {% if safety_category.examples %} -

- Examples: {{ safety_category.examples|join(', ') }} -

- {% endif %} -
-
- {% if bulk_preview_for_category %} -
-

- {{ 'Skip these files from this import' if bulk_action_for_category == 'skip' else 'Review this one-time exception' }} -

-

- {% if bulk_action_for_category == 'skip' %} - {{ bulk_preview_for_category.affected_count }} one-page archive{{ '' if bulk_preview_for_category.affected_count == 1 else 's' }} will be excluded from this import. Source files will not be changed or deleted. - {% else %} - {{ bulk_preview_for_category.affected_count }} of {{ bulk_preview_for_category.matching_count }} file{{ '' if bulk_preview_for_category.matching_count == 1 else 's' }} will be allowed once. - {% if bulk_preview_for_category.skipped_count %} - {{ bulk_preview_for_category.skipped_count }} file{{ '' if bulk_preview_for_category.skipped_count == 1 else 's' }} will stay blocked. - {% endif %} - {% endif %} -

- {% if bulk_preview_for_category.examples %} -
-

Category examples

-
    - {% for example in bulk_preview_for_category.examples %} -
  • {{ example }}
  • - {% endfor %} -
-
- {% endif %} -
- -

- {% if bulk_action_for_category == 'skip' %} - This applies only to the exact file set in this preview. You can import the source files again later. - {% else %} - This exception applies only to the exact file set in this preview. Pullbox will recheck their matches afterward. - {% endif %} -

- {% if bulk_error_for_category %} - - {% endif %} -
- - Cancel -
-
-
- {% elif safety_category.category == 'single_page_comic' %} - {% if bulk_error_for_category %} - - {% endif %} -

- If these are covers or other files you do not want imported as issues, preview and skip them together. Pullbox will leave every source file in place. -

-
- - - - -
- {% elif safety_category.bulk_overrideable|default(false) %} - {% if bulk_error_for_category %} - - {% endif %} -

- {{ bulk_overrideable_count }} of {{ safety_category.count }} file{{ '' if safety_category.count == 1 else 's' }} can be allowed once. -

-
- - - - -
- {% elif bulk_error_for_category %} - - {% endif %} -
- {% endfor %} -
- {% endif %} - - {% if active_review_view == 'story_arcs' %} - {% include "partials/import_story_arc_review_table.html" %} - {% elif active_review_view == 'conflicts' %} - {% set conflict_refresh_mode = "review" %} - {% set conflict_refresh_target = "#import-step-review-shell" %} - {% set conflict_refresh_swap = "outerHTML" %} - {% set conflict_table_hx_base_url = "/import/" ~ job.id ~ "/review-partial?status=conflicts" %} - {% set conflict_table_hx_target = "#import-step-review-shell" %} - {% set conflict_table_hx_swap = "outerHTML" %} - {% set conflict_table_hx_sync = "#import-step-review-shell:replace" %} - {% include "partials/import_conflict_cards.html" %} - {% else %} -
- - - - {% if show_selection_column %} - - {% endif %} - {{ sort_header("found_series", "Found Series") }} - {{ sort_header("year", "Year", width="w-12") }} - {{ sort_header("files", "Files", width="w-16") }} - - {{ sort_header("cv_match", "ComicVine Match") }} - {{ sort_header("cv_year", "CV Yr", extra_classes="import-review-cv-year-header", width="w-20") }} - {{ sort_header("cv_id", "CV ID", width="w-24") }} - {{ sort_header("confidence", "Confidence", width="w-24") }} - {{ sort_header("status", "Status", width="w-28") }} - - - - {% for item in series_items %} - {% set series_diagnostics = item.diagnostics or {} %} - {% set row_safety_blocked_count = series_diagnostics.get('safety_blocked_files', 0)|int %} - {% set series_conflict = item.status.value == 'no_match' and series_diagnostics.get('kind') == 'series_conflict' %} - {% set source_layout_review = item.status.value == 'no_match' and series_diagnostics.get('kind') == 'source_layout_review' %} - {% set mylar_path_review = item.status.value == 'no_match' and series_diagnostics.get('kind') == 'mylar3_path_incompatible' %} - {% set known_series_target = item.cv_id or item.user_selected_cv_id or item.series_id %} - {% set file_conflict = item.status.value == 'matched' and item.files_conflict > 0 %} - {% set file_issue_mismatch = item.status.value == 'no_match' and known_series_target and not mylar_path_review %} - {% set duplicate_issue_mismatch = item.status.value == 'duplicate' and known_series_target and item.files_no_match > 0 %} - {% set row_bucket_tokens = [] %} - {% if file_issue_mismatch or duplicate_issue_mismatch %} - {% set _ = row_bucket_tokens.append('needs_issue') %} - {% endif %} - {% if mylar_path_review %} - {% set _ = row_bucket_tokens.append('needs_path') %} - {% endif %} - {% if item.status.value == 'no_match' and not known_series_target and not series_conflict %} - {% set _ = row_bucket_tokens.append('needs_series') %} - {% endif %} - {% if row_safety_blocked_count > 0 %} - {% set _ = row_bucket_tokens.append('safety_blocked') %} - {% endif %} - {% if file_conflict or series_conflict %} - {% set _ = row_bucket_tokens.append('conflicts') %} - {% endif %} - {% set series_selectable = item.status.value == 'matched' and not file_conflict and item.files_matched > 0 %} - {% set duplicate_actionable = item.files_matched > 0 or item.files_conflict > 0 or series_diagnostics.get('actionable_duplicate_merge', false) %} - {% set duplicate_selected_files = duplicate_selected_file_counts.get(item.id, 0) if duplicate_selected_file_counts is defined else 0 %} - {% set duplicate_selectable = item.status.value == 'duplicate' and duplicate_actionable and item.files_matched > 0 %} - {% set top_candidates = series_diagnostics.get('top_candidates', []) %} - {% set pending_subitem_count = row_safety_blocked_count + item.files_no_match + item.files_conflict %} - - - {% if show_selection_column %} - - {% endif %} - - - - - {% set no_match_search_query = (item.cv_title or item.raw_series_name) if file_issue_mismatch else item.raw_series_name %} - {% set no_match_search_tip = "Change ComicVine match" if file_issue_mismatch else "Search ComicVine" %} - {% set no_match_search_aria = "Change ComicVine match" if file_issue_mismatch else "Search ComicVine" %} - - - - - - - - {% if row_safety_blocked_count > 0 %} - - - - {% elif item.status.value == 'matched' %} - - - - {% endif %} - {% if item.status.value == 'duplicate' and series_diagnostics %} - - - - {% endif %} - {% if item.status.value == 'no_match' and series_diagnostics %} - - - - {% endif %} - - {% endfor %} - {% if not series_items %} - - - - - - {% endif %} -
Action
- {% if item.status.value == 'duplicate' %} - {% if duplicate_selectable %} - 0 %}checked{% endif %} - {% if duplicate_selected_files > 0 and duplicate_selected_files < item.files_matched %}x-init="$el.indeterminate = true"{% endif %} - @change="toggleDuplicateSeriesFiles({{ item.id }}, $event.target.checked, $event.target)" - data-testid="import-review-duplicate-select" - data-import-review-duplicate-selectable="{{ item.id }}" - aria-label="Import matched files for {{ item.raw_series_name }}" - class="checkbox-pb" - data-tip="{{ duplicate_selected_files }} of {{ item.files_matched }} matched files selected for import." - data-tip-pos="top" - > - {% else %} - - {% endif %} - {% elif series_selectable %} - - {% else %} - - {% endif %} - -
{{ item.raw_series_name }}
-
- {% if item.raw_publisher %}{{ item.raw_publisher }} · {% endif %} - {% if item.source_folder and active_review_view != 'safety_blocked' %}{{ item.source_folder }}{% endif %} -
-
{{ item.raw_year or '?' }} - {% if item.files_total > 0 %} - {% if item.status.value == 'duplicate' and item.files_matched == 0 and item.files_already_owned > 0 %} - {% set files_display = item.files_matched ~ "/" ~ item.files_total %} - {% set files_tip = item.files_already_owned ~ " owned, " ~ item.files_matched ~ " importable, " ~ item.files_total ~ " total." %} - {% set files_class = "text-pb-text-sec" %} - {% elif item.status.value == 'duplicate' %} - {% set files_display = item.files_matched ~ "/" ~ item.files_total %} - {% set files_tip = item.files_matched ~ " importable, " ~ item.files_already_owned ~ " owned, " ~ item.files_no_match ~ " no match, " ~ item.files_total ~ " total." %} - {% set files_class = "text-pb-success" if item.files_matched > 0 else "text-pb-text-sec" %} - {% else %} - {% set files_display = item.files_matched ~ "/" ~ item.files_total %} - {% set files_tip = item.files_matched ~ " importable, " ~ item.files_conflict ~ " conflicts, " ~ row_safety_blocked_count ~ " blocked, " ~ item.files_total ~ " total." %} - {% set files_class = "text-pb-success" if item.files_matched == item.files_total else ("text-pb-warning" if item.files_conflict > 0 or row_safety_blocked_count > 0 else "text-pb-text-sec") %} - {% endif %} - {{ files_display }} - {% else %} - {{ item.file_count }} - {% endif %} - - {% if item.status.value == 'duplicate' %} - {% set duplicate_match_tip = "Existing library series matched by ComicVine ID." if series_diagnostics.get('duplicate_reason') == 'cv_id' else "Existing library series matched by name and year." %} -
- {{ series_diagnostics.get('existing_series_title') or item.raw_series_name }} -
- {% if item.cv_publisher or item.raw_publisher %} -
- {{ item.cv_publisher or item.raw_publisher }} -
- {% endif %} - {% elif item.cv_id %} -
{{ item.cv_title or 'CV #' ~ item.cv_id }}
- {% if item.cv_publisher %} -
{{ item.cv_publisher }}
- {% endif %} - {% elif series_conflict %} -
Conflict
- {% elif source_layout_review %} - Review required - {% else %} - No Match - {% endif %} -
{{ item.cv_year or '—' }} - {% set matched_cv_id = item.cv_id or item.user_selected_cv_id %} - {% if matched_cv_id %} - {% set matched_cv_url = item.cv_url or ("https://comicvine.gamespot.com/volume/4050-" ~ matched_cv_id ~ "/") %} - {{ matched_cv_id }} - {% else %} - - {% endif %} - - {% if item.cv_match_score is not none %} - {% set score_pct = (item.cv_match_score * 100)|int %} - {% elif item.status.value == 'duplicate' and series_diagnostics.get('duplicate_match_score') is not none %} - {% set score_pct = (series_diagnostics.get('duplicate_match_score') * 100)|int %} - {% elif item.status.value == 'duplicate' and series_diagnostics.get('duplicate_reason') in ('cv_id', 'name_year') %} - {% set score_pct = 100 %} - {% else %} - {% set score_pct = none %} - {% endif %} - {% if score_pct is not none %} - {{ score_pct }}% - {% else %} - - {% endif %} - - {% if row_safety_blocked_count > 0 %} - - Safety review - - {% elif file_conflict %} - - - Conflict - - {% elif item.cv_match_method in ('mylar3_cv_id', 'comicinfo_cv_id', 'user_override') and not source_layout_review and not mylar_path_review %} - - - CV ID - - {% elif item.status.value == 'matched' %} - - - Matched - - {% elif item.status.value == 'no_match' %} - - - {% if source_layout_review %}Layout review{% elif mylar_path_review %}Mylar path review{% elif series_conflict %}Conflict{% elif file_issue_mismatch %}Needs issue{% else %}Needs series{% endif %} - - {% elif item.status.value == 'duplicate' %} - In Library - {% else %} - {% set status_badge = { - 'confirmed': ('pill-info', 'Confirmed'), - 'imported': ('pill-success', 'Imported'), - 'failed': ('pill-error', 'Failed'), - 'skipped': ('pill-neutral', 'Skipped'), - } %} - {% set badge = status_badge.get(item.status.value, ('pill-neutral', item.status.value)) %} - {{ badge[1] }} - {% endif %} - -
- {% if row_safety_blocked_count > 0 %} - - {% elif item.status.value == 'no_match' %} - {% if file_issue_mismatch %} - - - - {% else %} - - {% endif %} - {% if series_diagnostics %} - - {% endif %} - {% elif item.status.value == 'matched' %} - - - - {% elif item.status.value == 'duplicate' %} - {% if duplicate_issue_mismatch %} - - {% endif %} - - {% if series_diagnostics %} - - {% endif %} - {% endif %} -
-
- No series found matching the current filter. -
-
- {% endif %} -
+
+
+ {% include "partials/import_review_lane_rail.html" %} +
+ {% include "partials/import_review_safety_categories.html" %} + {% if active_review_view == 'story_arcs' %} + {% include "partials/import_story_arc_review_table.html" %} + {% elif active_review_view == 'conflicts' %} + {% set conflict_refresh_mode = "review" %} + {% set conflict_refresh_target = "#import-step-review-shell" %} + {% set conflict_refresh_swap = "morph:outerHTML" %} + {% include "partials/import_conflict_cards.html" %} + {% else %} + {% include "partials/import_review_workspace_rows.html" %} + {% endif %}
+
+ {% include "partials/import_review_gate.html" %}
{% set import_review_footer_mode = active_review_view if active_review_view in ('conflicts', 'story_arcs') else "series" %} -{% if active_review_view == 'conflicts' %} - {% set conflict_footer_base_url = "/import/" ~ job.id ~ "/review-partial?status=conflicts&sort=" ~ sort %} - {% set conflict_footer_hx_target = "#import-step-review-shell" %} - {% set conflict_footer_hx_swap = "morph:outerHTML" %} - {% set conflict_footer_hx_sync = "#import-step-review-shell:replace" %} -{% endif %} diff --git a/src/pullbox/ui/templates/partials/intervention_history_results.html b/src/pullbox/ui/templates/partials/intervention_history_results.html index f4eab16f..5618f00f 100644 --- a/src/pullbox/ui/templates/partials/intervention_history_results.html +++ b/src/pullbox/ui/templates/partials/intervention_history_results.html @@ -77,7 +77,7 @@ {% if pm.issue and pm.issue.series %} - {{ pm.issue.series.title }} #{{ pm.issue.issue_number|issue_num }} + {{ pm.issue.series.title }} #{{ pm.issue.issue_number|issue_num(pm.issue.issue_number_text|default(none)) }} {% else %} No linked issue {% endif %} diff --git a/src/pullbox/ui/templates/partials/intervention_list.html b/src/pullbox/ui/templates/partials/intervention_list.html index fce7633a..823eca3b 100644 --- a/src/pullbox/ui/templates/partials/intervention_list.html +++ b/src/pullbox/ui/templates/partials/intervention_list.html @@ -65,7 +65,7 @@ {% if pm.issue and pm.issue.series %} - {{ pm.issue.series.title }} #{{ pm.issue.issue_number|issue_num }} + {{ pm.issue.series.title }} #{{ pm.issue.issue_number|issue_num(pm.issue.issue_number_text|default(none)) }} {% else %} No linked issue {% endif %} diff --git a/src/pullbox/ui/templates/partials/issue_detail_hero.html b/src/pullbox/ui/templates/partials/issue_detail_hero.html index 7a4146a8..9bda90af 100644 --- a/src/pullbox/ui/templates/partials/issue_detail_hero.html +++ b/src/pullbox/ui/templates/partials/issue_detail_hero.html @@ -17,7 +17,7 @@ {% if issue.cover_path %} #{{ issue.issue_number|issue_num }} - {{ issue_title_parts[0]|upper }}{% if issue_title_parts|length > 1 %} {{ issue_title_parts[1]|upper }} #{{ issue.issue_number|issue_num }}{% else %} #{{ issue.issue_number|issue_num }}{% endif %} + {{ issue_title_parts[0]|upper }}{% if issue_title_parts|length > 1 %} {{ issue_title_parts[1]|upper }} #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}{% else %} #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}{% endif %} {% else %} - {{ issue_title_parts[0]|upper }}{% if issue_title_parts|length > 1 %} {{ issue_title_parts[1]|upper }} #{{ issue.issue_number|issue_num }}{% else %} #{{ issue.issue_number|issue_num }}{% endif %} + {{ issue_title_parts[0]|upper }}{% if issue_title_parts|length > 1 %} {{ issue_title_parts[1]|upper }} #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}{% else %} #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}{% endif %} {% endif %} {% if issue.title %} diff --git a/src/pullbox/ui/templates/partials/issue_import_progress_modal.html b/src/pullbox/ui/templates/partials/issue_import_progress_modal.html index cd25ed6e..9016962f 100644 --- a/src/pullbox/ui/templates/partials/issue_import_progress_modal.html +++ b/src/pullbox/ui/templates/partials/issue_import_progress_modal.html @@ -21,7 +21,7 @@

Import file

- Import {{ issue.series.title }} #{{ issue.issue_number|issue_num }} + Import {{ issue.series.title }} #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}

Keep this dialog open while Pullbox prepares, transfers, and finalizes the selected file. @@ -38,7 +38,7 @@

Issue

-

#{{ issue.issue_number|issue_num }}

+

#{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}

Status

diff --git a/src/pullbox/ui/templates/partials/issue_row.html b/src/pullbox/ui/templates/partials/issue_row.html index b1101739..1e4af7d2 100644 --- a/src/pullbox/ui/templates/partials/issue_row.html +++ b/src/pullbox/ui/templates/partials/issue_row.html @@ -17,7 +17,7 @@ data-testid="series-issue-link" class="transition-colors hover:text-pb-interactive" > - #{{ issue.issue_number|issue_num }} + #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }} @@ -74,7 +74,7 @@ hx-boost="false" data-testid="series-issue-read" data-reading-interaction - aria-label="{{ reading.primary_label }} {{ series.title }} #{{ issue.issue_number|issue_num }}" + aria-label="{{ reading.primary_label }} {{ series.title }} #{{ issue.issue_number|issue_num(issue.issue_number_text|default(none)) }}" class="series-domain-issue-action-btn" data-tip="{{ reading.primary_label }}" data-tip-pos="left" @@ -90,7 +90,7 @@ aria-label="Search" @click="runIssueAutoSearch({ id: {{ issue.id }}, - label: {{ ('#' ~ issue.issue_number|issue_num)|tojson|forceescape }}, + label: {{ ('#' ~ issue.issue_number|issue_num(issue.issue_number_text|default(none)))|tojson|forceescape }}, downloadUrl: {{ ('/api/v1/issues/' ~ issue.id ~ '/download')|tojson|forceescape }} })" :class="isIssueAutoSearching({{ issue.id }}) ? 'text-pb-interactive' : ''" @@ -106,7 +106,7 @@ - + {% include "partials/series_rescan.html" %} + + + +
diff --git a/src/pullbox/ui/utilities_routes.py b/src/pullbox/ui/utilities_routes.py index 180c3ba1..4770d96d 100644 --- a/src/pullbox/ui/utilities_routes.py +++ b/src/pullbox/ui/utilities_routes.py @@ -414,7 +414,7 @@ async def load_utility_history_context( _utility_job_payload( job, can_rollback=( - job.job_type != JobType.ROLLBACK + job.job_type not in {JobType.ROLLBACK, JobType.SERIES_RESCAN} and job.state in (JobState.COMPLETED, JobState.CANCELLED) and job.id not in blocked_rollback_parent_ids ), diff --git a/src/pullbox/utilities/executors/file_converter.py b/src/pullbox/utilities/executors/file_converter.py index 58c2afb0..a40acfdb 100644 --- a/src/pullbox/utilities/executors/file_converter.py +++ b/src/pullbox/utilities/executors/file_converter.py @@ -107,26 +107,9 @@ def _detect_archive_type(source: Path) -> str: Many .cbr files are actually ZIP archives (mislabeled). This ensures the correct converter is used regardless of file extension. """ - try: - with open(source, "rb") as f: - header = f.read(8) - except OSError: - return source.suffix.lower() - - # ZIP: PK\x03\x04 - if header[:4] == b"PK\x03\x04": - return ".cbz" - # RAR: Rar!\x1a\x07 - if header[:6] == b"Rar!\x1a\x07": - return ".cbr" - # 7z: 7z\xbc\xaf\x27\x1c - if header[:6] == b"7z\xbc\xaf\x27\x1c": - return ".cb7" - # PDF: %PDF - if header[:4] == b"%PDF": - return ".pdf" - - return source.suffix.lower() + from pullbox.core.archive_format import archive_format + + return "." + archive_format(source) def _convert_sync( diff --git a/src/pullbox/utilities/executors/series_rescan.py b/src/pullbox/utilities/executors/series_rescan.py new file mode 100644 index 00000000..f96eda80 --- /dev/null +++ b/src/pullbox/utilities/executors/series_rescan.py @@ -0,0 +1,143 @@ +"""Durable background reconciliation of a single series folder.""" + +from __future__ import annotations + +import asyncio +import json +from typing import Any + +from sqlalchemy import select + +from pullbox.core.file_safety import ( + get_allowed_extensions, + get_archive_size_limit_bytes, + is_dangerous_file_blocking_enabled, +) +from pullbox.models.issue import Issue +from pullbox.models.library import LibraryFile, LibraryRoot +from pullbox.models.series import Series +from pullbox.services.series_rescan import plan_series_rescan +from pullbox.services.series_rescan_registration import apply_rescan_match +from pullbox.utilities.base_executor import ( + ApplyResult, + ExecutionMode, + ItemResult, + JobExecutor, + JobRunSummary, + ProcessedItem, +) +from pullbox.utilities.import_guards import ensure_no_active_import_file_mutation + + +class SeriesRescanExecutor(JobExecutor): + """Inspect off-loop, then apply bounded, revalidated database registrations.""" + + execution_mode = ExecutionMode.THREAD + + def validate_config(self, job_config: dict[str, Any]) -> list[str]: + series_id = job_config.get("series_id") + if type(series_id) is not int or series_id <= 0 or set(job_config) != {"series_id"}: + return ["A positive series_id is required; paths are derived from the catalog."] + return [] + + async def build_job_context(self, session: Any, job_config: dict[str, Any]) -> dict[str, Any]: + errors = self.validate_config(job_config) + if errors: + raise ValueError(errors[0]) + await ensure_no_active_import_file_mutation(session) + series = await session.get(Series, job_config["series_id"]) + if series is None or not series.path: + raise ValueError( + "This series has no configured folder. Set its folder before rescanning." + ) + roots = list( + ( + await session.scalars( + select(LibraryRoot).where( + LibraryRoot.enabled.is_(True), + LibraryRoot.allow_referenced_registrations.is_(True), + ) + ) + ).all() + ) + issues = list( + (await session.scalars(select(Issue).where(Issue.series_id == series.id))).all() + ) + files = list( + ( + await session.scalars( + select(LibraryFile).join(Issue).where(Issue.series_id == series.id) + ) + ).all() + ) + return { + "folder": series.path, + "roots": [{"id": root.id, "path": root.path} for root in roots], + "series": { + "id": series.id, + "title": series.title, + "year_start": series.year_start, + "comicvine_id": series.comicvine_id, + }, + "issues": [ + { + "id": issue.id, + "number": issue.issue_number, + "text": issue.issue_number_text, + "cv_id": issue.comicvine_id, + "type": issue.issue_type.value, + "title": issue.title, + "year": issue.release_date.year if issue.release_date else None, + } + for issue in issues + ], + "files": [ + {"id": record.id, "path": record.file_path, "issue_id": record.issue_id} + for record in files + ], + "block_dangerous": await is_dangerous_file_blocking_enabled(session), + "extensions": list(await get_allowed_extensions(session)), + "max_archive_size": await get_archive_size_limit_bytes(session), + } + + async def generate_items( + self, job_config: dict[str, Any], job_context: dict[str, Any] | None = None + ) -> list[dict[str, Any]]: + return await asyncio.to_thread(plan_series_rescan, job_context or {}) + + def process_item( + self, + item_data: dict[str, Any], + job_config: dict[str, Any], + job_context: dict[str, Any] | None = None, + ) -> ProcessedItem: + return ProcessedItem(item_id=item_data["id"], result=ItemResult.COMPLETED) + + async def apply_item_result( + self, + session: Any, + item: Any, + item_data: dict[str, Any], + processed: ProcessedItem, + job_config: dict[str, Any], + job_context: dict[str, Any] | None, + summary: JobRunSummary, + ) -> ApplyResult: + outcome, reason = await apply_rescan_match(session, job_config["series_id"], item_data) + item.after_state = json.dumps({"outcome": outcome, "reason": reason}) + return ApplyResult( + warning_increment=int(outcome == "review"), + warning_message=reason if outcome == "review" else None, + ) + + def rollback_item( + self, + item_data: dict[str, Any], + job_config: dict[str, Any], + job_context: dict[str, Any] | None = None, + ) -> ProcessedItem: + return ProcessedItem( + item_id=item_data["id"], + result=ItemResult.FAILED, + error_message="Rescans do not support rollback; source files were not changed.", + ) diff --git a/src/pullbox/utilities/job_queue.py b/src/pullbox/utilities/job_queue.py index ef9c5c68..5f8a5906 100644 --- a/src/pullbox/utilities/job_queue.py +++ b/src/pullbox/utilities/job_queue.py @@ -137,6 +137,8 @@ async def _create_rollback_job( created_by: str | None = None, ) -> UtilityJob: """Create a queued rollback child job for ``job``.""" + if job.job_type == JobType.SERIES_RESCAN: + raise ValueError("Rescans do not support rollback; source files were not changed.") existing_result = await session.execute( select(UtilityJob) .where( diff --git a/src/pullbox/utilities/job_queue_item_preparation.py b/src/pullbox/utilities/job_queue_item_preparation.py index 64bd9cec..a834ada1 100644 --- a/src/pullbox/utilities/job_queue_item_preparation.py +++ b/src/pullbox/utilities/job_queue_item_preparation.py @@ -7,6 +7,7 @@ from sqlalchemy import select +from pullbox.services.utility_operation_progress import project_utility_operation_progress from pullbox.utilities.job_queue_items import build_generated_job_items from pullbox.utilities.job_queue_state import transition_job_state from pullbox.utilities.models import ItemState, JobState, UtilityJob, UtilityJobItem @@ -36,6 +37,7 @@ async def mark_item_generation_failed( transition_job_state(job, JobState.FAILED) job.error_message = f"Item generation failed: {exc}" + await project_utility_operation_progress(session, job) await session.commit() return True diff --git a/src/pullbox/utilities/models.py b/src/pullbox/utilities/models.py index 795bfc73..c6933279 100644 --- a/src/pullbox/utilities/models.py +++ b/src/pullbox/utilities/models.py @@ -27,6 +27,7 @@ class JobType(enum.StrEnum): MASS_CONVERT_PIPELINE = "mass_convert_pipeline" MASS_RENAME = "mass_rename" DB_CHECK_CLEANUP = "db_check_cleanup" + SERIES_RESCAN = "series_rescan" EXPORT_LIBRARY = "export_library" INTEGRITY_CHECK = "integrity_check" LIBRARY_PERMISSIONS = "library_permissions" diff --git a/tests/api/test_import_api.py b/tests/api/test_import_api.py index c33fdc9b..04ad00f2 100644 --- a/tests/api/test_import_api.py +++ b/tests/api/test_import_api.py @@ -487,8 +487,8 @@ async def _seed_mixed_review_selection_job( raw_series_name=f"Matched {idx}", raw_year=2020 + idx, status=ImportSeriesStatus.MATCHED, - file_count=1, - files_total=1, + file_count=1 if idx < 2 else 2, + files_total=1 if idx < 2 else 2, files_matched=1, files_conflict=0 if idx < 2 else 1, selected_for_import=idx == 0, @@ -496,6 +496,27 @@ async def _seed_mixed_review_selection_job( session.add(imported_series) await session.flush() matched_ids.append(imported_series.id) + session.add( + ImportedFile( + import_job_id=job.id, + import_series_id=imported_series.id, + file_path=f"/tmp/mixed-review/Matched {idx} 001.cbz", + file_name=f"Matched {idx} 001.cbz", + file_format="cbz", + status=ImportedFileStatus.MATCHED, + ) + ) + if idx == 2: + session.add( + ImportedFile( + import_job_id=job.id, + import_series_id=imported_series.id, + file_path="/tmp/mixed-review/Conflicting 002.cbz", + file_name="Conflicting 002.cbz", + file_format="cbz", + status=ImportedFileStatus.CONFLICT, + ) + ) existing_series = Series( title="In Library", @@ -1828,11 +1849,12 @@ async def test_selection_state_counts_importable_items_across_review_types( assert resp.status_code == 200 data = resp.json() - assert data["matched_series_importable"] == 2 + # The third series has a ready file even though a different file conflicts. + assert data["matched_series_importable"] == 3 assert data["matched_series_selected"] == 1 assert data["duplicate_series_importable"] == 1 assert data["duplicate_series_selected"] == 1 - assert data["importable_item_count"] == 3 + assert data["importable_item_count"] == 4 assert data["selected_item_count"] == 2 assert data["selected_series_ids"] == [matched_ids[0]] assert data["selected_duplicate_series_ids"] == [duplicate_series_id] diff --git a/tests/api/test_import_reader_api.py b/tests/api/test_import_reader_api.py new file mode 100644 index 00000000..3773592a --- /dev/null +++ b/tests/api/test_import_reader_api.py @@ -0,0 +1,148 @@ +"""Read-only import previews reuse the bounded comic reader.""" + +import zipfile + +import pytest +from sqlalchemy import func, select + +from pullbox.core.library_file_ownership import build_file_identity_signature +from pullbox.models.import_job import ImportedFile, ImportedFileStatus, ImportJob +from pullbox.models.library import LibraryFile +from pullbox.models.reader import IssueReaderState +from pullbox.services.reader_content_service import ReaderContentService +from tests.api.test_reader_api import _write_cbz +from tests.ui.test_import_one_page_review import _seed_one_page_job + +pytest_plugins = ["tests.conftest_security"] + + +async def _preview_fixture(sec_db, sec_app, tmp_path): + seeded = await _seed_one_page_job(sec_db, 1) + root = tmp_path / "source" + root.mkdir() + source = root / "cover.cbz" + _write_cbz(source, page_count=1) + async with sec_db() as session: + (await session.get(ImportJob, seeded["job_id"])).source_path = str(root) + file = await session.get(ImportedFile, seeded["file_ids"][0]) + file.file_path = str(source) + file.file_name = source.name + file.source_signature = build_file_identity_signature(source) + await session.commit() + sec_app.state.reader_content_service = ReaderContentService(cache_dir=tmp_path / "cache") + url = f"/api/v1/reader/imports/{seeded['job_id']}/files/{seeded['file_ids'][0]}" + return seeded, source, url + + +async def test_import_reader_is_read_only(authenticated_client, sec_db, sec_app, tmp_path): + seeded, source, url = await _preview_fixture(sec_db, sec_app, tmp_path) + before = source.read_bytes() + response = await authenticated_client.get(url + "/manifest") + assert response.status_code == 200 + manifest = response.json() + assert manifest["page_count"] == 1 + assert "progress_url" not in manifest + assert "completion_url" not in manifest + assert "download_url" not in manifest + page = await authenticated_client.get( + manifest["page_url_template"].replace("{page_index}", "0") + ) + assert page.status_code == 200 + assert page.headers["content-type"].startswith("image/") + assert source.read_bytes() == before + async with sec_db() as session: + file = await session.get(ImportedFile, seeded["file_ids"][0]) + assert file.status == ImportedFileStatus.SAFETY_BLOCKED + assert not file.include_in_import + assert not file.diagnostics.get("safety_exception") + assert await session.scalar(select(func.count()).select_from(LibraryFile)) == 0 + assert await session.scalar(select(func.count()).select_from(IssueReaderState)) == 0 + + +@pytest.mark.parametrize( + "problem", + ["damaged", "image", "traversal", "symlink", "changed", "wrong_job", "dangerous", "missing"], +) +async def test_import_reader_reports_errors_without_approving( + authenticated_client, sec_db, sec_app, tmp_path, problem +): + seeded, source, url = await _preview_fixture(sec_db, sec_app, tmp_path) + if problem == "damaged": + source.write_bytes(b"PK\x03\x04broken") + elif problem in {"image", "traversal"}: + with zipfile.ZipFile(source, "w") as archive: + archive.writestr("../page.gif" if problem == "traversal" else "page.gif", b"broken") + elif problem == "symlink": + outside = tmp_path / "outside.cbz" + _write_cbz(outside, page_count=1) + source.unlink() + source.symlink_to(outside) + elif problem == "changed": + _write_cbz(source, page_count=2) + elif problem == "missing": + source.unlink() + elif problem == "wrong_job": + url = url.replace(f"imports/{seeded['job_id']}", "imports/999999") + async with sec_db() as session: + file = await session.get(ImportedFile, seeded["file_ids"][0]) + if problem in {"damaged", "image", "traversal", "symlink"}: + file.source_signature = build_file_identity_signature(source) + if problem == "dangerous": + file.diagnostics = {"safety_block": {"category": "dangerous_path_or_payload"}} + await session.commit() + response = await authenticated_client.get(url + "/manifest") + assert response.status_code in {404, 409, 422} + message = response.json().get("detail", response.json()) + assert "Skip" in str(message) or problem == "wrong_job" + assert str(tmp_path) not in str(message) + async with sec_db() as session: + assert ( + await session.get(ImportedFile, seeded["file_ids"][0]) + ).status == ImportedFileStatus.SAFETY_BLOCKED + + +async def test_import_reader_requires_login(unauthenticated_client): + response = await unauthenticated_client.get("/api/v1/reader/imports/1/files/1/manifest") + assert response.status_code in {401, 403} + + +async def test_import_preview_revalidates_every_page( + authenticated_client, sec_db, sec_app, tmp_path +): + _seeded, source, url = await _preview_fixture(sec_db, sec_app, tmp_path) + manifest = (await authenticated_client.get(url + "/manifest")).json() + page_url = manifest["page_url_template"].replace("{page_index}", "0") + assert (await authenticated_client.get(url + "/pages/0?revision=stale")).status_code == 409 + _write_cbz(source, page_count=2) + assert (await authenticated_client.get(page_url)).status_code == 409 + + +async def test_import_preview_supports_read_only_mylar_root( + authenticated_client, sec_db, sec_app, tmp_path +): + from pullbox.models.import_job import ImportFileHandlingMode, ImportSourceType + from pullbox.models.library import LibraryRoot + + seeded, source, url = await _preview_fixture(sec_db, sec_app, tmp_path) + async with sec_db() as session: + root = LibraryRoot( + name="Mylar", + path=str(source.parent), + enabled=True, + allow_referenced_registrations=True, + allow_managed_writes=False, + ) + session.add(root) + await session.flush() + job = await session.get(ImportJob, seeded["job_id"]) + job.source_type = ImportSourceType.MYLAR3 + job.source_path = str(tmp_path / "mylar.db") + job.file_handling_mode = ImportFileHandlingMode.IN_PLACE + file = await session.get(ImportedFile, seeded["file_ids"][0]) + file.source_signature = {**file.source_signature, "mylar_reference_root_id": root.id} + await session.commit() + assert (await authenticated_client.get(url + "/manifest")).status_code == 200 + async with sec_db() as session: + (await session.get(LibraryRoot, root.id)).enabled = False + await session.commit() + assert (await authenticated_client.get(url + "/manifest")).status_code == 409 diff --git a/tests/e2e/pages/import_page.py b/tests/e2e/pages/import_page.py index 4a3d8d10..c246af0c 100644 --- a/tests/e2e/pages/import_page.py +++ b/tests/e2e/pages/import_page.py @@ -254,11 +254,11 @@ def review_back_button(self) -> Locator: @property def review_series_tab(self) -> Locator: - return self.page.locator("[data-testid='import-review-tab-series']").first + return self.page.get_by_test_id("import-review-lane-decide") @property def review_matched_tab(self) -> Locator: - return self.page.locator("[data-testid='import-review-series-filter-matched']").first + return self.page.get_by_test_id("import-review-lane-ready") def review_sort_button(self, field: str) -> Locator: return self.page.locator(f"[data-testid='import-review-sort-{field}']").first @@ -279,23 +279,25 @@ def review_no_match_diagnostics(self) -> Locator: @property def review_matched_details_button(self) -> Locator: - return self.page.locator("[data-testid='import-review-matched-why-action']").first + return self.review_panel.locator( + "[data-import-review-series-row] > tr:first-child [data-import-review-expand-action]" + ).first @property def review_matched_diagnostics(self) -> Locator: - return self.page.locator("[data-testid='import-review-matched-diagnostics']").first + return self.page.locator("[data-import-review-detail-row]").first @property def conflicts_tab(self) -> Locator: - return self.page.locator("[data-testid='import-review-open-conflicts-filter']").first + return self.page.get_by_test_id("import-review-lane-confirm") @property def conflicts_panel(self) -> Locator: - return self.page.locator("[data-testid='import-collection-conflicts']").first + return self.page.locator("#import-review-workspace") @property def conflicts_table(self) -> Locator: - return self.page.locator("[data-testid='import-conflicts-table']").first + return self.page.get_by_test_id("import-review-workspace-table") @property def save_conflict_choices_button(self) -> Locator: diff --git a/tests/e2e/test_import_collection_page.py b/tests/e2e/test_import_collection_page.py index e6a9d032..f636ea13 100644 --- a/tests/e2e/test_import_collection_page.py +++ b/tests/e2e/test_import_collection_page.py @@ -635,6 +635,10 @@ def _goto_review_step( ) import_page.workspace_root.wait_for(state="visible", timeout=5000) import_page.review_panel.wait_for(state="visible", timeout=5000) + import_page.review_matched_tab.click() + page.wait_for_function( + "document.querySelector('[name=review_status_filter]').value === 'ready'" + ) return review_job_id def test_import_collection_renders_stable_shell_without_implicit_resume( @@ -698,14 +702,16 @@ def test_import_collection_series_and_matched_tabs_swap_review_shell( if import_page.review_matched_tab.count() == 0: pytest.skip("Matched tab is unavailable in the seeded review state.") - assert authed_page.locator("input[name='review_status_filter']").first.input_value() == "" + assert ( + authed_page.locator("input[name='review_status_filter']").first.input_value() == "ready" + ) import_page.review_matched_tab.click() import_page.wait_for_htmx(timeout=10000) authed_page.wait_for_function( """() => { const input = document.querySelector("#import-step-review-shell input[name='review_status_filter']"); - return !!input && input.value === "matched"; + return !!input && input.value === "ready"; }""", timeout=5000, ) @@ -715,7 +721,7 @@ def test_import_collection_series_and_matched_tabs_swap_review_shell( authed_page.wait_for_function( """() => { const input = document.querySelector("#import-step-review-shell input[name='review_status_filter']"); - return !!input && input.value === ""; + return !!input && input.value === "decide"; }""", timeout=5000, ) @@ -728,13 +734,11 @@ def test_import_collection_review_detail_expansion_survives_shell_refresh( import_page = ImportPage(authed_page, seeded_server) self._goto_review_step(import_page, authed_page, seeded_server) - toggle = authed_page.locator("[data-testid='import-review-matched-why-action']").first - if toggle.count() == 0: - pytest.skip("Matched detail rows are unavailable in the seeded review state.") + toggle = import_page.review_matched_details_button toggle.click() assert toggle.get_attribute("aria-expanded") == "true" - authed_page.locator("[data-testid='import-review-matched-diagnostics']").first.wait_for( + authed_page.locator("[data-import-review-detail-row]").first.wait_for( state="visible", timeout=5000, ) @@ -749,11 +753,9 @@ def test_import_collection_review_detail_expansion_survives_shell_refresh( }""" ) - refreshed_toggle = authed_page.locator( - "[data-testid='import-review-matched-why-action']" - ).first + refreshed_toggle = import_page.review_matched_details_button assert refreshed_toggle.get_attribute("aria-expanded") == "true" - authed_page.locator("[data-testid='import-review-matched-diagnostics']").first.wait_for( + authed_page.locator("[data-import-review-detail-row]").first.wait_for( state="visible", timeout=5000, ) @@ -763,6 +765,8 @@ def test_import_collection_review_series_details_expand_without_page_errors( authed_page, seeded_server: str, # type: ignore[no-untyped-def] ) -> None: + from playwright.sync_api import expect + page_errors: list[str] = [] authed_page.on("pageerror", lambda exc: page_errors.append(str(exc))) @@ -773,11 +777,13 @@ def test_import_collection_review_series_details_expand_without_page_errors( import_page.review_matched_diagnostics.wait_for(state="visible", timeout=5000) diagnostics_text = import_page.review_matched_diagnostics.text_content() or "" - assert "Matched files" in diagnostics_text - assert "CV Issue ID" in diagnostics_text + assert "Importable files" in diagnostics_text + target = import_page.review_matched_diagnostics.get_by_test_id("import-review-file-target") + expect(target).to_have_text("Issue 1 · CV 50001") + expect(target).to_have_attribute("href", "https://comicvine.gamespot.com/issue/4000-50001/") assert page_errors == [] - def test_import_collection_conflict_save_reset_round_trip_keeps_table_visible( + def test_import_collection_conflict_review_uses_shared_table_without_staged_save( self, authed_page, seeded_server: str, # type: ignore[no-untyped-def] @@ -799,25 +805,8 @@ def assert_conflicts_panel_populated() -> None: import_page.conflicts_panel.wait_for(state="visible", timeout=5000) assert_conflicts_panel_populated() - if ( - import_page.save_conflict_choices_button.count() > 0 - and import_page.save_conflict_choices_button.is_enabled() - ): - import_page.save_conflict_choices_button.click() - import_page.wait_for_htmx(timeout=10000) - import_page.conflicts_panel.wait_for(state="visible", timeout=5000) - assert_conflicts_panel_populated() - - if import_page.reset_conflict_choices_button.is_enabled(): - import_page.reset_conflict_choices_button.click() - import_page.wait_for_htmx(timeout=10000) - import_page.conflicts_panel.wait_for(state="visible", timeout=5000) - assert_conflicts_panel_populated() - if ( - import_page.conflicts_table.count() > 0 - and import_page.conflicts_table.is_visible() - ): - assert import_page.save_conflict_choices_button.is_enabled() + assert import_page.save_conflict_choices_button.count() == 0 + assert import_page.reset_conflict_choices_button.count() == 0 import_page.review_series_tab.click() import_page.wait_for_htmx(timeout=10000) @@ -841,6 +830,7 @@ def test_import_collection_review_ignores_stale_saved_selection_storage( import_page.navigate(f"/import?tab=collection&resume_job_id={review_job_id}&resume_step=3") import_page.review_panel.wait_for(state="visible", timeout=5000) + saved_summary = (import_page.review_selection_summary.text_content() or "").strip() authed_page.evaluate( """([jobId]) => { sessionStorage.setItem( @@ -854,10 +844,7 @@ def test_import_collection_review_ignores_stale_saved_selection_storage( authed_page.reload() import_page.review_panel.wait_for(state="visible", timeout=5000) - assert import_page.review_import_button.is_disabled() - assert (import_page.review_selection_summary.text_content() or "").strip() == ( - "0 items selected for import" - ) + assert (import_page.review_selection_summary.text_content() or "").strip() == saved_summary def test_import_collection_cv_search_modal_opens_without_page_errors( self, @@ -875,6 +862,7 @@ def test_import_collection_cv_search_modal_opens_without_page_errors( import_page.navigate(f"/import?tab=collection&resume_job_id={review_job_id}&resume_step=3") import_page.review_panel.wait_for(state="visible", timeout=5000) + import_page.review_matched_details_button.click() import_page.search_cv_button.click() import_page.wait_for_htmx() import_page.cv_search_modal.wait_for(state="visible", timeout=5000) @@ -901,6 +889,7 @@ def test_import_collection_cv_search_modal_shows_loading_state_on_repeat_search( import_page.navigate(f"/import?tab=collection&resume_job_id={review_job_id}&resume_step=3") import_page.review_panel.wait_for(state="visible", timeout=5000) + import_page.review_matched_details_button.click() import_page.search_cv_button.click() import_page.wait_for_htmx() import_page.cv_search_modal.wait_for(state="visible", timeout=5000) @@ -2257,6 +2246,26 @@ def test_import_collection_scan_counters_prefer_live_series_stats_over_stale_sum "conflicts": 3, } + def test_scan_summary_does_not_count_missing_references_as_files( + self, authed_page, seeded_server: str + ) -> None: + ImportPage(authed_page, seeded_server).goto(tab="collection") + state = authed_page.evaluate("""() => { + const controller = window.importProgressData(42, 2, 'mylar3', {}, 'scan'); + controller.applyJobState({ + status: 'scanning', mode: 'scan', scan_total_files: 7952, + review_summary: {files_total: 7952, files_present: 7719, + files_missing_references: 233}, + }); + const scanning = {...controller.reviewSummary}; + controller.applyJobState({status: 'review', mode: 'scan', scan_total_files: 7952, + review_summary: {files_total: 7952, files_present: 7719, + files_missing_references: 233}}); + return [scanning.filesPresent, scanning.missingReferences, + controller.reviewSummary.filesPresent, controller.reviewSummary.missingReferences]; + }""") + assert state == [7719, 233, 7719, 233] + def test_import_collection_paused_import_resumes_even_if_control_state_is_stale( self, authed_page, @@ -3091,13 +3100,13 @@ def test_import_collection_review_tables_support_shared_sorting_contract( import_page = ImportPage(authed_page, seeded_server) self._goto_review_step(import_page, authed_page, seeded_server) - import_page.review_sort_button("year").click() + authed_page.get_by_role("button", name="Series", exact=True).click() import_page.wait_for_htmx() - import_page.review_sort_button("year").click() + authed_page.get_by_role("button", name="Series", exact=True).click() import_page.wait_for_htmx() - first_series_row = import_page.review_panel.locator("table tbody tr").first - assert "Saga" in (first_series_row.text_content() or "") + assert authed_page.locator("input[name=review_sort]").input_value() == "-found_series" + assert import_page.review_panel.locator("[data-import-review-series-row]").count() > 0 def test_import_collection_source_step_keeps_selection_state_and_opens_browser( self, diff --git a/tests/e2e/test_import_history_page.py b/tests/e2e/test_import_history_page.py index 11ed7252..df490a7f 100644 --- a/tests/e2e/test_import_history_page.py +++ b/tests/e2e/test_import_history_page.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest +from playwright.sync_api import expect from tests.e2e.pages.import_page import ImportPage @@ -121,13 +122,28 @@ def test_import_history_sorting_reorders_rows_without_disturbing_shell( import_page.history_sort_button("source_path").click() import_page.wait_for_htmx() - first_row = authed_page.locator("[data-testid^='import-history-job-']").first - assert import_page.workspace_root.is_visible() assert import_page.history_panel.is_visible() assert import_page.history_results.is_visible() assert "/import?tab=history&sort=-source_path" in authed_page.url - assert "/tmp/imports/review-queue" in (first_row.text_content() or "") + sources = authed_page.locator( + "[data-testid^='import-history-job-'] td:nth-child(2) [data-tooltip-measure]" + ) + descending = sources.all_text_contents() + assert len(set(descending)) >= 2 + assert descending == sorted(descending, reverse=True) + + import_page.history_sort_button("source_path").click() + import_page.wait_for_htmx() + expect(authed_page).to_have_url( + f"{seeded_server}/import?tab=history&sort=source_path&page=1" + ) + expect(sources).not_to_have_text(descending) + ascending = sources.all_text_contents() + assert len(set(ascending)) >= 2 + assert ascending == sorted(ascending) + assert import_page.workspace_root.is_visible() + assert import_page.history_panel.is_visible() def test_import_history_log_panel_opens_without_disturbing_shell( self, diff --git a/tests/e2e/test_import_orphaned_page.py b/tests/e2e/test_import_orphaned_page.py index b0920a57..24651dba 100644 --- a/tests/e2e/test_import_orphaned_page.py +++ b/tests/e2e/test_import_orphaned_page.py @@ -74,7 +74,7 @@ def test_import_orphaned_cv_search_modal_opens_without_disturbing_shell( assert authed_page.locator("[data-testid='import-orphaned-cv-search-modal']").is_visible() assert authed_page.locator("[data-testid='import-orphaned-cv-search-input']").is_visible() - def test_import_orphaned_actions_use_icon_buttons_with_tooltips( + def test_import_orphaned_actions_use_labeled_match_buttons_with_tooltips( self, authed_page, seeded_server: str, # type: ignore[no-untyped-def] @@ -106,7 +106,7 @@ def test_import_orphaned_actions_use_icon_buttons_with_tooltips( assert action_state is not None assert action_state["searchTip"] == "Search ComicVine" assert action_state["dismissTip"] == "Dismiss" - assert action_state["searchText"] == "" + assert action_state["searchText"] == "Find series" assert action_state["dismissText"] == "" assert action_state["searchHasSvg"] is True assert action_state["dismissHasSvg"] is True diff --git a/tests/e2e/test_import_review_workspace.py b/tests/e2e/test_import_review_workspace.py new file mode 100644 index 00000000..0d2e37d6 --- /dev/null +++ b/tests/e2e/test_import_review_workspace.py @@ -0,0 +1,517 @@ +"""Browser contracts for the decision-oriented import review.""" + +from pathlib import Path + +import pytest +from jinja2 import Environment, FileSystemLoader, select_autoescape +from playwright.sync_api import expect + +from pullbox.ui.formatters import format_issue_number +from tests.e2e.pages.import_page import ImportPage +from tests.e2e.test_import_collection_page import TestImportCollectionTab as _ImportBaseline + + +@pytest.fixture(scope="module") +def copy_review_job(seeded_server): + from pullbox.database import get_session_factory + from tests.e2e.conftest import _run_async_blocking + from tests.ui.test_import_collection_shell_ui_routes import _seed_import_review_job + + return _run_async_blocking(_seed_import_review_job(get_session_factory())) + + +def test_copy_choice_can_open_file_reassignment_without_losing_review( + authed_page, seeded_server, copy_review_job, browser_name +): + job_id = copy_review_job + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + page.goto(f"{seeded_server}/import?tab=collection&resume_job_id={job_id}&resume_step=3") + page.get_by_test_id("import-review-lane-confirm").click() + expect(page.get_by_test_id("import-review-conflict-counts")).to_have_text( + "Across this import: 2 issue groups involving 4 files in 2 series." + ) + row = page.locator("[data-import-review-series-row]").filter(has_text="Review Series 7") + expander = row.locator("td:last-child > [data-import-review-expand-action]") + expander.click() + expect(row.get_by_test_id("import-review-reassign-file")).to_have_count(2) + page.evaluate("""() => { + window.copyReviewShell = document.getElementById('import-step-review-shell'); + window.copyReviewUrl = location.href; + }""") + trigger = row.get_by_test_id("import-review-reassign-file").first + trigger.click() + modal = page.get_by_test_id("import-collection-cv-search-modal") + expect(modal).to_be_visible() + expect(modal).to_contain_text("Choose the correct series for this file") + expect(modal).to_contain_text("Other files and source folders will not change.") + modal.get_by_role("button", name="Close search dialog").click() + expect(modal).not_to_be_visible() + expect(expander).to_have_attribute("aria-expanded", "true") + assert page.evaluate("""() => + window.copyReviewShell === document.getElementById('import-step-review-shell') && + window.copyReviewUrl === location.href + """) + output = Path(__file__).resolve().parents[2] / "test-results/review-workspace" + output.mkdir(parents=True, exist_ok=True) + page.add_script_tag(path="node_modules/axe-core/axe.min.js") + for theme in ("dark", "light"): + page.evaluate("theme => applyTheme(theme)", theme) + row.screenshot(path=str(output / f"{browser_name}-copy-choices-{theme}.png")) + assert ( + page.evaluate("""async () => (await axe.run('#import-step-review-shell', { + runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa'] } + })).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => ({ target: n.target, summary: n.failureSummary })) }))""") + == [] + ) + assert errors == [] + + +def test_series_file_inventory_preserves_review_and_focus(authed_page, seeded_server, browser_name): + from pullbox.database import get_session_factory + from tests.e2e.conftest import _run_async_blocking + from tests.ui.test_import_review_files_inventory import _seed_inventory + + job_id, series_id = _run_async_blocking(_seed_inventory(get_session_factory())) + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + page.goto(f"{seeded_server}/import?tab=collection&resume_job_id={job_id}&resume_step=3") + page.get_by_test_id("import-review-lane-decide").click() + page.get_by_test_id("import-review-reason-needs_series").click() + row = page.locator(f'[data-import-review-series-row="{series_id}"]') + expect(page.get_by_test_id("import-review-missing-references")).to_contain_text( + "1 missing file reference tracked separately" + ) + expect(row.locator('[aria-label="0 of 26 files matched"]')).to_have_text("0/26") + expect(row.get_by_text("1 missing reference", exact=True)).to_be_visible() + output = Path(__file__).resolve().parents[2] / "test-results/review-workspace" + output.mkdir(parents=True, exist_ok=True) + for theme in ("light", "dark"): + page.evaluate("theme => applyTheme(theme)", theme) + page.screenshot(path=str(output / f"{browser_name}-missing-references-{theme}.png")) + row.screenshot(path=str(output / f"{browser_name}-missing-reference-row-{theme}.png")) + expander = row.locator("td:last-child > [data-import-review-expand-action]") + expander.click() + expect(row.get_by_text("Files in this folder", exact=True)).to_have_count(0) + expect(row.get_by_test_id("import-review-series-file-details")).to_have_count(0) + page.evaluate("""() => { + window.inventoryShell = document.getElementById('import-step-review-shell'); + window.inventoryUrl = window.location.href; + window.inventoryScroll = document.getElementById('content')?.scrollTop || 0; + }""") + trigger = row.get_by_test_id("import-review-more-actions") + trigger.click() + menu = page.locator("[popover]:popover-open") + expect(menu.get_by_role("button")).to_have_text(["View files"]) + menu.get_by_role("button", name="View files", exact=True).click() + modal = page.get_by_test_id("import-review-files-modal") + dialog = modal.get_by_role("dialog", name="Files recorded for Unknown Series") + expect(dialog).to_be_visible() + expect(dialog).to_be_focused() + expect(dialog.locator("[data-import-review-inventory-file]")).to_have_count(25) + expect(dialog).to_contain_text("Missing reference") + page.evaluate( + "window.inventoryDialog = document.querySelector('[data-testid=import-review-files-modal]')" + ) + dialog.get_by_test_id("series-pagination-next").click() + expect(dialog.locator("[data-import-review-inventory-file]")).to_have_count(2) + assert page.evaluate( + "window.inventoryDialog === document.querySelector('[data-testid=import-review-files-modal]')" + ) + expect(dialog).to_be_focused() + close = dialog.get_by_role("button", name="Close", exact=True) + assert close.evaluate( + "el => parseFloat(getComputedStyle(el).borderTopWidth) >= 1 && el.getBoundingClientRect().height >= 28" + ) + close.focus() + page.keyboard.press("Tab") + expect(dialog.get_by_test_id("series-pagination-prev")).to_be_focused() + page.keyboard.press("Shift+Tab") + expect(close).to_be_focused() + for theme in ("light", "dark"): + page.evaluate("theme => applyTheme(theme)", theme) + page.add_script_tag(path="node_modules/axe-core/axe.min.js") + assert ( + page.evaluate("""async () => (await axe.run('[data-testid=import-review-files-modal]', { + runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa'] } + })).violations.map(v => v.id)""") + == [] + ) + close.click() + expect(modal).to_have_count(0) + expect(trigger).to_be_focused() + expect(expander).to_have_attribute("aria-expanded", "true") + assert page.evaluate("""() => window.inventoryShell === document.getElementById('import-step-review-shell') && + window.inventoryUrl === window.location.href && + window.inventoryScroll === (document.getElementById('content')?.scrollTop || 0)""") + page.set_viewport_size({"width": 390, "height": 844}) + trigger.click() + page.locator("[popover]:popover-open").get_by_role("button", name="View files").click() + expect(modal).to_be_visible() + assert dialog.evaluate("el => el.getBoundingClientRect().right <= window.innerWidth") + page.keyboard.press("Escape") + expect(modal).to_have_count(0) + expect(trigger).to_be_focused() + assert errors == [] + + +def test_one_page_review_shortcuts_and_file_decisions_preserve_the_row( + authed_page, seeded_server, browser_name +): + from pullbox.database import get_session_factory + from tests.e2e.conftest import _run_async_blocking + from tests.ui.test_import_one_page_review import _seed_one_page_job + + seeded = _run_async_blocking(_seed_one_page_job(get_session_factory())) + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + page.goto( + f"{seeded_server}/import?tab=collection&resume_job_id={seeded['job_id']}&resume_step=3" + ) + page.get_by_test_id("import-review-lane-decide").click() + page.get_by_test_id("import-review-reason-single_page_comic").click() + summary = page.get_by_test_id("import-review-safety-category-summary") + expect(summary).to_contain_text("2 files in 1 series") + expect(page.get_by_role("button", name="Select all ready", exact=True)).to_have_count(0) + expect(page.get_by_test_id("import-review-one-page-review")).to_have_count(0) + page.locator("td:last-child > [data-import-review-one-page-expand]").click() + detail = page.get_by_test_id("import-review-one-page-files") + expect(detail).to_be_visible() + expect(detail.get_by_role("button", name="Allow", exact=True)).to_have_count(2) + expect(detail.get_by_role("button", name="View File", exact=True)).to_have_count(2) + expect(page.get_by_test_id("import-review-one-page-allow")).to_contain_text("Allow All") + expect(page.get_by_test_id("import-review-more-actions")).to_have_count(0) + expect(page.get_by_test_id("import-review-one-page-more")).to_have_count(0) + expect(page.get_by_role("button", name="Change series", exact=True)).to_have_count(0) + table = page.get_by_test_id("import-review-workspace-table") + expect(table.get_by_role("columnheader")).to_have_text( + ["Series", "Files", "What needs attention", "Action", "Details"] + ) + detection = page.get_by_test_id("import-review-one-page-detection").bounding_box() + actions = page.get_by_test_id("import-review-one-page-category-actions").bounding_box() + helper = page.get_by_test_id("import-review-one-page-helper").bounding_box() + skip = page.get_by_test_id( + "import-review-safety-bulk-skip-preview-single_page_comic" + ).bounding_box() + assert detection and actions and helper and skip + assert actions["y"] > detection["y"] + detection["height"] + assert helper["y"] + helper["height"] < skip["y"] + assert abs(helper["x"] - skip["x"]) < 2 + assert abs(detection["x"] - skip["x"]) < 2 + expander = page.locator("td:last-child > [data-import-review-one-page-expand]") + expect(expander.locator("xpath=..")).not_to_contain_text("Allow All") + page.evaluate("""() => { + window.onePageShell = document.getElementById('import-step-review-shell'); + window.onePageFiles = document.querySelector('[data-testid="import-review-one-page-files"]'); + }""") + page.get_by_test_id("import-review-safety-bulk-skip-preview-single_page_comic").click() + preview = page.get_by_test_id("import-review-safety-bulk-confirmation") + expect(preview).to_be_visible() + expect(preview).to_contain_text("2 one-page archives") + assert errors == [] + assert ( + page.evaluate( + "captureImportReviewViewport(document.getElementById('import-step-review-shell')).expandedRows.length" + ) + == 1 + ) + preview.get_by_role("button", name="Keep them in review", exact=True).click() + expect(preview).not_to_be_visible() + assert errors == [] + expect(detail).to_be_visible() + output = Path(__file__).resolve().parents[2] / "test-results/review-workspace" + output.mkdir(parents=True, exist_ok=True) + for theme in ("dark", "light"): + page.evaluate("theme => applyTheme(theme)", theme) + page.add_script_tag(path="node_modules/axe-core/axe.min.js") + assert ( + page.evaluate("""async () => (await axe.run('#import-step-review-shell', { + runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa'] } + })).violations.map(v => v.id)""") + == [] + ) + page.screenshot(path=str(output / f"{browser_name}-one-page-{theme}.png"), full_page=True) + detail.get_by_role("button", name="Skip", exact=True).first.click() + expect(detail.get_by_role("button", name="Skip", exact=True)).to_have_count(1) + expect(detail.locator("[data-import-review-file-outcome]")).to_have_text("Skipped") + expect(detail).to_be_visible() + expect(page.get_by_test_id("import-review-one-page-allow")).to_contain_text("Allow All") + assert page.evaluate("""() => + window.onePageShell === document.getElementById('import-step-review-shell') && + window.onePageFiles === document.querySelector('[data-testid="import-review-one-page-files"]') + """) + page.set_viewport_size({"width": 390, "height": 844}) + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") + page.get_by_test_id("import-review-one-page-skip").get_by_role( + "button", name="Skip All" + ).click() + expect(detail).to_have_count(0) + expect(page.get_by_test_id("import-review-workspace-table")).to_contain_text( + "Nothing to review" + ) + assert errors == [] + + +def test_one_page_view_file_uses_reader_and_returns_to_review( + authed_page, seeded_server, tmp_path, browser_name +): + from pullbox.core.library_file_ownership import build_file_identity_signature + from pullbox.database import get_session_factory + from pullbox.models.import_job import ImportedFile, ImportJob + from tests.api.test_reader_api import _write_cbz + from tests.e2e.conftest import _run_async_blocking + from tests.ui.test_import_one_page_review import _seed_one_page_job + + async def seed(): + factory = get_session_factory() + seeded = await _seed_one_page_job(factory) + async with factory() as session: + (await session.get(ImportJob, seeded["job_id"])).source_path = str(tmp_path) + for index, file_id in enumerate(seeded["file_ids"][:2]): + source = tmp_path / f"Preview {index}.cbz" + if index: + source.write_bytes(b"PK\x03\x04broken archive") + else: + _write_cbz(source, page_count=1) + file = await session.get(ImportedFile, file_id) + file.file_path = str(source) + file.file_name = source.name + file.source_signature = build_file_identity_signature(source) + await session.commit() + return seeded + + seeded = _run_async_blocking(seed()) + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + page.goto( + f"{seeded_server}/import?tab=collection&resume_job_id={seeded['job_id']}&resume_step=3" + ) + page.get_by_test_id("import-review-lane-decide").click() + page.get_by_test_id("import-review-reason-single_page_comic").click() + page.locator("td:last-child > [data-import-review-one-page-expand]").click() + detail = page.get_by_test_id("import-review-one-page-files") + view = detail.get_by_role("button", name="View File", exact=True).first + page.evaluate( + "window.previewRowBefore = document.querySelector('[data-testid=import-review-one-page-files]')" + ) + view.click() + dialog = page.get_by_test_id("comic-reader-dialog") + expect(dialog).to_be_visible() + expect(page.get_by_test_id("comic-reader-page")).to_be_visible() + expect(dialog.get_by_test_id("comic-reader-active-download")).to_have_count(0) + expect(dialog.get_by_test_id("comic-reader-mark-unread")).to_have_count(0) + page.add_script_tag(path="node_modules/axe-core/axe.min.js") + assert ( + page.evaluate("""async () => (await axe.run('[data-testid=comic-reader-dialog]', { + runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa'] } + })).violations.map(v => v.id)""") + == [] + ) + page.keyboard.press("Escape") + expect(dialog).not_to_be_visible() + expect(view).to_be_focused() + expect(detail).to_be_visible() + assert page.evaluate( + "window.previewRowBefore === document.querySelector('[data-testid=import-review-one-page-files]')" + ) + detail.get_by_role("button", name="View File", exact=True).nth(1).click() + error = page.get_by_test_id("comic-reader-error") + expect(error).to_be_visible() + expect(error).to_contain_text("damaged") + expect(error).to_contain_text("Skip") + output = Path(__file__).resolve().parents[2] / "test-results/review-workspace" + output.mkdir(parents=True, exist_ok=True) + page.screenshot(path=str(output / f"{browser_name}-preview-error.png")) + error.get_by_role("button", name="Close", exact=True).click() + expect(detail).to_be_visible() + detail.get_by_role("button", name="Skip", exact=True).nth(1).click() + expect(detail.locator("[data-import-review-file-outcome]")).to_have_text("Skipped") + expect(detail.get_by_role("button", name="Allow", exact=True)).to_have_count(1) + assert errors == [] + + +def test_review_file_modal_submits_without_boosted_navigation(authed_page, seeded_server): + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + _ImportBaseline()._goto_review_step(ImportPage(page, seeded_server), page, seeded_server) + templates = Environment( + loader=FileSystemLoader("src/pullbox/ui/templates"), autoescape=select_autoescape() + ) + templates.filters["issue_num"] = format_issue_number + html = templates.get_template("partials/import_review_file_action.html").render( + job={"id": 1}, + file={"id": 1, "file_name": "Recheck source.cbz"}, + token="test-preview", + action="source", + source_action="recheck", + candidate=None, + ) + page.route( + "**/import/1/files/1/source", + lambda route: route.fulfill(json={"message": "Source verification queued."}), + ) + page.evaluate( + """html => { + window.reviewShellBeforeSubmit = document.getElementById('import-step-review-shell'); + window.reviewBoostedSubmits = 0; + document.addEventListener('htmx:beforeRequest', event => { + if (event.detail.requestConfig.boosted) window.reviewBoostedSubmits++; + }); + const host = document.getElementById('cv-search-modal'); + host.innerHTML = html; + htmx.process(host); + }""", + html, + ) + modal = page.get_by_test_id("import-review-file-action") + expect(modal).to_be_visible() + modal.get_by_role("button", name="Recheck source", exact=True).click() + expect(modal).not_to_be_visible() + expect(page.get_by_test_id("import-review-lane-ready")).to_be_visible() + assert page.evaluate("window.reviewBoostedSubmits") == 0 + assert page.evaluate( + "window.reviewShellBeforeSubmit === document.getElementById('import-step-review-shell')" + ) + assert errors == [] + + +def test_review_lanes_and_refresh_preserve_controls(authed_page, seeded_server, browser_name): + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + _ImportBaseline()._goto_review_step(ImportPage(page, seeded_server), page, seeded_server) + outcome = page.evaluate("""() => importCvSearchModalData({ jobId: 1, seriesId: 1 }) + .buildOverrideOutcome({ status: 'no_match', cv_id: 123, files_no_match: 1 }, 'ready', {})""") + assert "destinationView" not in outcome + assert "Needs Issue Match" not in outcome["message"] + page.evaluate("""() => { + window.reviewShell = document.getElementById('import-step-review-shell'); + window.reviewCancel = document.querySelector('[data-testid="import-review-cancel"]'); + window.reviewButton = document.querySelector('[data-import-review-import-button]'); + }""") + page.get_by_test_id("import-review-lane-ready").click() + expect(page.locator('input[name="review_status_filter"]')).to_have_value("ready") + page.locator("[data-import-review-selectable]").first.check() + expect(page.locator("[data-import-review-import-button]")).to_be_enabled() + action = page.locator("td:last-child > [data-import-review-expand-action]").first + action.click() + expect(action).to_have_attribute("aria-expanded", "true") + menu_trigger = page.get_by_test_id("import-review-more-actions").first + menu_trigger.click() + menu = page.locator("[popover]:popover-open") + expect(menu).to_be_visible() + menu.get_by_role("button", name="Skip series", exact=True).click() + skip_dialog = page.get_by_role("dialog", name="Skip this series?") + expect(skip_dialog).to_be_visible() + skip_dialog.get_by_role("button", name="Cancel", exact=True).click() + expect(skip_dialog).not_to_be_visible() + expect(action).to_have_attribute("aria-expanded", "true") + page.evaluate("""async () => { + await Alpine.$data(window.reviewShell).refreshSeriesReview(); + }""") + expect(action).to_have_attribute("aria-expanded", "true") + assert page.evaluate("""() => + window.reviewShell === document.getElementById('import-step-review-shell') && + window.reviewCancel === document.querySelector('[data-testid="import-review-cancel"]') && + window.reviewButton === document.querySelector('[data-import-review-import-button]') + """) + page.locator("[data-import-review-import-button]").click() + expect(page.get_by_test_id("import-review-gate")).to_be_visible() + page.get_by_role("button", name="Back to review", exact=True).click() + expect(page.get_by_test_id("import-review-gate")).not_to_be_visible() + expect(page.locator("[data-import-review-import-button]")).to_be_focused() + assert page.get_by_role("button", name="Select all ready", exact=True).evaluate( + "el => parseFloat(getComputedStyle(el).borderTopWidth) >= 1" + ) + assert action.evaluate("el => el.getBoundingClientRect().height >= 28") + assert page.evaluate("""async () => { + const shell = document.getElementById('import-step-review-shell'); + const data = Alpine.$data(shell); + const html = await (await fetch(data.buildReviewUrl())).text(); + const documentCopy = new DOMParser().parseFromString(html, 'text/html'); + documentCopy.querySelector('#import-review-state').setAttribute('data-requires-preferred-root', 'true'); + applyImportReviewWorkspace(documentCopy.body.innerHTML, shell); + const requiresRoot = data.splitSeriesRequiresPreferredRoot; + applyImportReviewWorkspace(html, shell); + return requiresRoot && !data.splitSeriesRequiresPreferredRoot; + }""") + page.add_script_tag(path="node_modules/axe-core/axe.min.js") + accessibility = page.evaluate("""async () => { + const result = await axe.run('#import-step-review-shell', { + runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa'] } + }); + return result.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })); + }""") + assert accessibility == [] + output = Path(__file__).resolve().parents[2] / "test-results/review-workspace" + output.mkdir(parents=True, exist_ok=True) + for theme in ("dark", "light"): + page.evaluate("theme => applyTheme(theme)", theme) + expect(action).to_have_attribute("aria-expanded", "true") + page.screenshot(path=str(output / f"{browser_name}-{theme}.png"), full_page=True) + page.set_viewport_size({"width": 390, "height": 844}) + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") + page.locator("[data-import-review-import-button]").click() + gate = page.get_by_test_id("import-review-gate") + expect(gate).to_be_visible() + expect(gate).to_contain_text("1 ready file from 1 selected series will import.") + expect(gate).to_contain_text("1 file still needs attention.") + expect(gate.get_by_role("button", name="Back to review", exact=True)).to_be_visible() + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") + page.screenshot(path=str(output / f"{browser_name}-mobile-gate.png"), full_page=True) + page.keyboard.press("Escape") + expect(gate).not_to_be_visible() + assert errors == [] + + +def test_inline_issue_choice_submits_only_its_file_without_navigation(authed_page, seeded_server): + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + _ImportBaseline()._goto_review_step(ImportPage(page, seeded_server), page, seeded_server) + templates = Environment( + loader=FileSystemLoader("src/pullbox/ui/templates"), autoescape=select_autoescape() + ) + templates.filters["issue_num"] = format_issue_number + html = templates.get_template("partials/import_review_issue_choices.html").render( + job={"id": 1}, + file={"id": 1, "file_name": "Needs issue.cbz"}, + series={"provider_id": "20", "title": "Test series"}, + issues=[{"provider_id": "201", "issue_number": 1, "title": "First issue"}], + token="scoped-test-token", + query="", + issue_page=1, + issue_page_count=1, + ) + submissions = [] + + def assign(route): + submissions.append(route.request.post_data) + route.fulfill(json={"message": "Issue assigned."}) + + page.route("**/import/1/files/1/assign", assign) + page.evaluate( + """html => { + window.inlineReviewShell = document.getElementById('import-step-review-shell'); + const host = document.querySelector('#import-review-lane-panel'); + host.insertAdjacentHTML('afterbegin', html); + htmx.process(host); + }""", + html, + ) + panel = page.get_by_test_id("import-review-issue-choices") + expect(panel).to_be_visible() + panel.get_by_role("button", name="Use this", exact=True).click() + expect(panel).not_to_be_visible() + assert len(submissions) == 1 + assert 'name="issue_cv_id"\r\n\r\n201' in submissions[0] + assert 'name="token"\r\n\r\nscoped-test-token' in submissions[0] + assert page.evaluate( + "window.inlineReviewShell === document.getElementById('import-step-review-shell')" + ) + assert errors == [] diff --git a/tests/e2e/test_lettered_issue_labels.py b/tests/e2e/test_lettered_issue_labels.py new file mode 100644 index 00000000..38e8897e --- /dev/null +++ b/tests/e2e/test_lettered_issue_labels.py @@ -0,0 +1,58 @@ +"""Exact issue designations remain visible when navigating the library.""" + +from pathlib import Path + +import pytest +from playwright.sync_api import expect + +pytestmark = pytest.mark.e2e + + +@pytest.fixture(scope="module") +def lettered_series(seeded_server): + from pullbox.database import get_session_factory + from pullbox.models.issue import Issue, IssueStatus + from pullbox.models.series import Series + from tests.e2e.conftest import _run_async_blocking + + async def seed(): + async with get_session_factory()() as session: + series = Series(title="Gen13 Lettered Fixture", sort_title="Gen13 Lettered Fixture") + session.add(series) + await session.flush() + issues = [ + Issue( + series_id=series.id, + issue_number=13, + issue_number_text=number, + status=IssueStatus.WANTED, + ) + for number in ["13A", "13B", "13C"] + ] + session.add_all(issues) + await session.commit() + return series.id, issues[0].id + + return _run_async_blocking(seed()) + + +def test_lettered_issues_keep_exact_labels_between_library_and_detail( + authed_page, seeded_server, lettered_series, browser_name +): + series_id, issue_id = lettered_series + page = authed_page + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + page.goto(f"{seeded_server}/series/{series_id}") + table = page.get_by_test_id("series-detail-issues-table") + for number in ["13A", "13B", "13C"]: + expect(table).to_contain_text(f"#{number}") + assert "13A" in page.locator(f"#issue-{issue_id}").get_by_test_id( + "series-issue-manual-search" + ).get_attribute("@click") + output = Path(__file__).resolve().parents[2] / "test-results/lettered-issues" + output.mkdir(parents=True, exist_ok=True) + table.screenshot(path=str(output / f"{browser_name}-series.png")) + page.goto(f"{seeded_server}/issues/{issue_id}") + expect(page.get_by_test_id("issue-detail-page")).to_contain_text("#13A") + assert errors == [] diff --git a/tests/e2e/test_series_page.py b/tests/e2e/test_series_page.py index a0f92e7d..2ff7826a 100644 --- a/tests/e2e/test_series_page.py +++ b/tests/e2e/test_series_page.py @@ -1095,23 +1095,29 @@ def test_bulk_selection_persists_across_pagination_and_can_clear( seeded_server: str, # type: ignore[no-untyped-def] ) -> None: series = SeriesListPage(authed_page, seeded_server) - series.goto("per_page=2") + series.goto("sort=title&per_page=2") - series.toggle_row_selection("Batman") + # Other tests can add series to the session-scoped library. + first_title = series.first_visible_series_title() + assert first_title + series.toggle_row_selection(first_title) assert series.selected_count_text() == "1 selected" - assert series.row_is_selected("Batman") + assert series.row_is_selected(first_title) series.click_next_page() - series.toggle_row_selection("Saga", modifiers=["ControlOrMeta"]) + second_title = series.first_visible_series_title() + assert second_title and second_title != first_title + series.toggle_row_selection(second_title, modifiers=["ControlOrMeta"]) assert series.selected_count_text() == "2 selected" + assert series.row_is_selected(second_title) series.click_prev_page() - assert series.row_is_selected("Batman") + assert series.row_is_selected(first_title) series.clear_bulk_selection() assert series.selected_count_text() == "0 selected" assert not series.select_mode_toolbar.is_visible() - assert not series.row_is_selected("Batman") + assert not series.row_is_selected(first_title) @pytest.mark.parametrize( ("view", "additive_modifier"), @@ -1229,32 +1235,47 @@ def test_select_visible_select_all_results_and_deselect_all_work_in_select_mode( seeded_server: str, # type: ignore[no-untyped-def] ) -> None: series = SeriesListPage(authed_page, seeded_server) - series.goto("per_page=2") + series.goto("sort=title&per_page=2") + total = int( + authed_page.locator("[data-series-total]").first.get_attribute("data-series-total") + ) + assert total >= 6 series.open_select_mode() + checkboxes = authed_page.locator("[data-testid='series-row-checkbox']:visible") + first_page_ids = set(checkboxes.evaluate_all("items => items.map(item => item.value)")) + assert len(first_page_ids) == 2 + + def selected_visible_ids() -> set[str]: + return set( + checkboxes.evaluate_all( + "items => items.filter(item => item.checked).map(item => item.value)" + ) + ) + series.select_visible() assert series.selected_count_text() == "2 selected" - assert series.row_is_selected("Batman") - assert series.row_is_selected("Batman Beyond") + assert selected_visible_ids() == first_page_ids series.click_next_page() + second_page_ids = set(checkboxes.evaluate_all("items => items.map(item => item.value)")) + assert len(second_page_ids) == 2 + assert first_page_ids.isdisjoint(second_page_ids) + assert selected_visible_ids() == set() assert series.select_visible_disabled() is False series.select_visible() assert series.selected_count_text() == "4 selected" - assert series.row_is_selected("Planetary") - assert series.row_is_selected("Saga") + assert selected_visible_ids() == second_page_ids series.select_all_results() - assert series.selected_count_text() == "6 selected" - assert series.row_is_selected("Planetary") - assert series.row_is_selected("Saga") + assert series.selected_count_text() == f"{total} selected" + assert selected_visible_ids() == second_page_ids series.deselect_all_visible() assert series.selected_count_text() == "0 selected" assert series.toolbar_mode() == "select" assert series.visible_row_checkbox_count() > 0 - assert not series.row_is_selected("Planetary") - assert not series.row_is_selected("Saga") + assert selected_visible_ids() == set() def test_done_clears_selection_and_restores_browse_controls( self, diff --git a/tests/e2e/test_series_rescan.py b/tests/e2e/test_series_rescan.py new file mode 100644 index 00000000..ffac513f --- /dev/null +++ b/tests/e2e/test_series_rescan.py @@ -0,0 +1,100 @@ +"""Rescan progress and saved results stay on the series page without navigation.""" + +from __future__ import annotations + +import pytest +from playwright.sync_api import expect + +pytestmark = pytest.mark.e2e + + +def test_rescan_progress_completes_without_replacing_the_page(authed_page, seeded_server, tmp_path): + page = authed_page + state = {"started": False, "complete": False} + + def report(route): + if route.request.method == "POST": + state["started"] = True + route.fulfill(status=202, json={"job_id": "rescan-test", "state": "QUEUED"}) + return + job = None + if state["started"]: + job = { + "id": "rescan-test", + "state": "COMPLETED" if state["complete"] else "RUNNING", + "active": not state["complete"], + "percent": 100 if state["complete"] else 25, + "error": None, + } + route.fulfill( + json={ + "job": job, + "counts": {"added": 1, "review": 1} if state["complete"] else {}, + "items": [ + { + "id": "file-1", + "path": "/comics/Batman/mystery.cbz", + "reason": "No unique issue matches.", + } + ] + if state["complete"] + else [], + "page": 1, + "pages": 1, + } + ) + + page.route("**/api/v1/series/1/rescan**", report) + page.goto(f"{seeded_server}/series/1") + page.evaluate( + "window.rescanPageSentinel = document.querySelector('[data-testid=series-detail-page]')" + ) + page.get_by_test_id("series-action-rescan").click() + dialog = page.get_by_role("dialog", name="Rescan folder") + expect(dialog).to_be_visible() + expect(dialog.get_by_role("progressbar")).to_have_attribute("aria-valuenow", "25") + expect(page.get_by_test_id("series-action-rescan")).to_be_disabled() + state["complete"] = True + expect(dialog.get_by_text("Rescan complete", exact=True)).to_be_visible(timeout=10000) + expect(dialog.get_by_role("progressbar")).to_have_attribute("aria-valuenow", "100") + expect(dialog.get_by_text("1 added", exact=False)).to_be_visible() + expect(dialog.get_by_role("button", name="Review in Import")).to_be_visible() + expect(page.get_by_test_id("series-action-rescan")).to_be_enabled() + assert page.evaluate( + "window.rescanPageSentinel === document.querySelector('[data-testid=series-detail-page]')" + ) + page.screenshot(path=str(tmp_path / "series-rescan-complete.png")) + dialog.get_by_role("button", name="Close", exact=True).click() + expect(dialog).not_to_be_visible() + expect(page.get_by_test_id("series-action-rescan")).to_be_focused() + page.get_by_role("button", name="Rescan results", exact=True).click() + expect(dialog).to_be_visible() + page.keyboard.press("Escape") + expect(dialog).not_to_be_visible() + requests = [] + + def start_import(route): + requests.append(route.request.post_data_json) + route.fulfill(status=201, json={"id": 17}) + + page.route("**/api/v1/import", start_import) + page.route( + "**/import?tab=collection&resume_job_id=17", + lambda route: route.fulfill(content_type="text/html", body="Import review"), + ) + page.set_viewport_size({"width": 390, "height": 844}) + page.get_by_role("button", name="Rescan results", exact=True).click() + bounds = dialog.bounding_box() + assert bounds is not None and bounds["x"] >= 0 and bounds["x"] + bounds["width"] <= 390 + expect(dialog.get_by_role("button", name="Close", exact=True)).to_be_in_viewport() + page.screenshot(path=str(tmp_path / "series-rescan-mobile.png")) + dialog.get_by_role("button", name="Review in Import").click() + expect(page).to_have_url(f"{seeded_server}/import?tab=collection&resume_job_id=17") + assert requests == [ + { + "source_path": "/comics/Batman/mystery.cbz", + "file_paths": ["/comics/Batman/mystery.cbz"], + "source_type": "filesystem", + "file_handling_mode": "in_place", + } + ] diff --git a/tests/e2e/test_settings_page.py b/tests/e2e/test_settings_page.py index f71b7890..06e40248 100644 --- a/tests/e2e/test_settings_page.py +++ b/tests/e2e/test_settings_page.py @@ -1287,6 +1287,36 @@ def test_settings_shared_dropdowns_update_without_shell_instability( assert settings.dropdown_value("settings-utilities-log-level-select") == "ERROR" assert settings.dropdown_label("settings-utilities-log-level-select") == "Error" + def test_settings_tab_settle_preserves_new_dropdown_interaction( + self, authed_page: Page, seeded_server: str + ) -> None: + settings = SettingsPage(authed_page, seeded_server) + settings.goto("ui") + authed_page.evaluate( + """() => { + htmx.config.defaultSettleDelay = 1000; + window.settingsTabSettled = false; + document.addEventListener("htmx:afterSettle", (event) => { + if (event.detail.target?.id === "settings-content") { + window.settingsTabSettled = true; + } + }); + }""" + ) + settings.switch_tab("utilities") + settings.dropdown("settings-utilities-log-level-select").locator( + "[data-dropdown-select-trigger]" + ).click() + option = authed_page.locator( + '[data-dropdown-select-panel]:visible [data-dropdown-option][data-value="ERROR"]' + ) + expect(option).to_be_visible() + authed_page.wait_for_function("() => window.settingsTabSettled === true") + + expect(option).to_be_in_viewport() + option.click() + assert settings.dropdown_value("settings-utilities-log-level-select") == "ERROR" + def test_settings_search_language_dropdown_selects_and_resets( self, authed_page, diff --git a/tests/integration/test_migrations.py b/tests/integration/test_migrations.py index e9eb41cd..fd192933 100644 --- a/tests/integration/test_migrations.py +++ b/tests/integration/test_migrations.py @@ -1873,7 +1873,8 @@ def test_library_root_management_downgrade_refuses_unrepresentable_default( def test_root_removal_protects_dependencies_without_losing_other_fk_actions(self, alembic_cfg): cfg, sync_url = alembic_cfg script = ScriptDirectory.from_config(cfg) - assert script.get_heads() == ["n5h6i7j8k901"] + assert script.get_heads() == ["o6i7j8k9l012"] + assert script.get_revision("o6i7j8k9l012").down_revision == "n5h6i7j8k901" assert script.get_revision("n5h6i7j8k901").down_revision == _IMPORT_JOB_ARCHIVE_REVISION command.upgrade(cfg, _IMPORT_JOB_ARCHIVE_REVISION) engine = create_engine(sync_url) diff --git a/tests/ui/test_import_collection_shell_ui_routes.py b/tests/ui/test_import_collection_shell_ui_routes.py index 677d88f1..857c4f38 100644 --- a/tests/ui/test_import_collection_shell_ui_routes.py +++ b/tests/ui/test_import_collection_shell_ui_routes.py @@ -836,7 +836,14 @@ async def test_import_review_uses_the_expanded_review_workspace_as_primary_surfa self, ) -> None: script = Path("src/pullbox/ui/static/js/pullbox.js").read_text() - template = Path("src/pullbox/ui/templates/partials/import_step_review.html").read_text() + template = "\n".join( + Path("src/pullbox/ui/templates/partials/" + name).read_text() + for name in ( + "import_step_review.html", + "import_review_overview.html", + "import_review_lane_rail.html", + ) + ) assert "importAllReady: async function () {" not in script assert 'data-testid="import-review-guided-summary"' not in template @@ -847,15 +854,17 @@ async def test_import_review_uses_the_expanded_review_workspace_as_primary_surfa assert 'data-testid="import-review-action-bar"' in template assert "data-import-review-selection-summary" in template assert "data-import-review-import-button" in template - assert 'data-testid="import-review-series-filters"' in template + assert 'data-testid="import-review-subtabs"' in template async def test_import_review_row_expansion_is_stable_and_action_scoped(self) -> None: script = Path("src/pullbox/ui/static/js/pullbox.js").read_text() - template = Path("src/pullbox/ui/templates/partials/import_step_review.html").read_text() + template = Path( + "src/pullbox/ui/templates/partials/import_review_workspace_rows.html" + ).read_text() assert 'id="import-review-series-row-{{ active_review_view }}-{{ item.id }}"' in template assert 'data-import-review-row-key="{{ active_review_view }}:{{ item.id }}"' in template - assert 'data-import-review-pending-subitems="{{ pending_subitem_count }}"' in template + assert 'data-import-review-pending-subitems="{{ row.attention_files }}"' in template assert "importReviewRowExpansionData" not in template assert '@click="toggle()"' not in template assert 'x-show="expanded"' not in template @@ -881,7 +890,14 @@ async def test_import_review_cancel_survives_shell_morphs_and_uses_global_dialog self, ) -> None: script = Path("src/pullbox/ui/static/js/pullbox.js").read_text() - template = Path("src/pullbox/ui/templates/partials/import_step_review.html").read_text() + template = "\n".join( + Path("src/pullbox/ui/templates/partials/" + name).read_text() + for name in ( + "import_step_review.html", + "import_review_overview.html", + "import_review_lane_rail.html", + ) + ) assert 'data-testid="import-review-cancel"' in template assert 'onclick="cancelImportReview(this)"' in template @@ -1053,7 +1069,10 @@ async def test_import_collection_renders_standardized_shell( assert "Automatic analysis could not resolve every location" not in response.text assert 'data-testid="import-managed-library-root"' in response.text assert response.text.count('data-dropdown-select-contract="v1"') >= 2 - assert "]*>", response.text, re.DOTALL) + assert len(native_selects) == 1 + assert 'class="comic-reader__fit-select"' in native_selects[0] assert 'data-testid="import-library-roots-manage"' in response.text assert 'data-testid="import-library-roots-refresh"' in response.text assert 'href="/settings?tab=media"' in response.text @@ -1217,7 +1236,7 @@ async def test_import_review_renders_detailed_statuses_without_a_disclosure_gate assert 'data-testid="import-review-needs-attention-action"' not in response.text assert 'data-testid="import-review-story-arc-follow-up-action"' not in response.text assert "Review details" not in response.text - assert 'data-testid="import-review-series-filters"' in response.text + assert 'data-testid="import-review-subtabs"' in response.text assert "data-import-review-import-button" in response.text async def test_split_series_review_requires_future_destination_without_relocation(self) -> None: @@ -1776,124 +1795,25 @@ async def test_import_review_partial_renders_series_no_match_diagnostics( series.cv_match_method = "exact_title_year" await session.commit() - response = await authenticated_client.get(f"/import/{job_id}/review-partial") + response = await authenticated_client.get(f"/import/{job_id}/review-partial?status=series") assert response.status_code == 200 - assert "Import settings" not in response.text - assert 'data-testid="import-review-library-root-display"' not in response.text - assert "Search on add" not in response.text - assert 'data-testid="import-review-sort-found_series"' in response.text - assert 'data-testid="import-review-sort-status"' in response.text - assert '' in response.text - assert 'data-testid="import-review-sort-cv_year"' in response.text - assert 'data-testid="import-review-sort-cv_id"' in response.text + assert 'data-testid="import-review-workspace-table"' in response.text assert 'href="https://comicvine.gamespot.com/review-series-1/4050-9001/"' in response.text assert 'data-testid="import-review-cv-id-link"' in response.text - assert 'class="downloads-sort-btn' in response.text - assert 'class="downloads-action-btn is-warn"' in response.text - assert 'data-tip="Search ComicVine"' in response.text - assert 'data-tip="Why no match"' in response.text - assert "return openImportCvSearchModal({ jobId: " in response.text - assert "In Library" in response.text - assert 'data-testid="import-review-series-details-action"' not in response.text - assert 'x-init="init()"' in response.text - assert "data-import-review-selection-summary" in response.text - assert "data-import-review-import-button" in response.text - assert "data-import-review-import-label" in response.text - assert "data-import-review-toolbar-selection-summary" in response.text - assert "conflictSeriesCount:" in response.text - assert "data-import-review-conflict-count" in response.text - assert 'x-text="conflictSeriesCount"' in response.text - assert ( - 'x-on:import:conflict-visibility.window="visibleFileConflictGroupCount = ' - 'Number(($event.detail && $event.detail.visibleFileConflictGroupCount) || 0)"' - in response.text - ) - assert ( - 'x-on:import:conflicts-saved.window="handleConflictsSaved($event.detail)"' - in response.text - ) - assert ( - 'x-on:import:conflicts-reset.window="handleConflictsReset($event.detail)"' - in response.text - ) - assert "In library" not in response.text assert "Test Publisher" in response.text - assert "Existing library series matched by ComicVine ID." in response.text - assert "87%" in response.text - assert "Exact match" in response.text - assert "18 issues in series" in response.text - assert "exact_title_year" not in response.text - assert "h-1 w-10 overflow-hidden" not in response.text - assert ':checked="isSelected(1)"' not in response.text - assert '@change="toggleSelection(1, $event.target.checked)"' in response.text - assert 'data-import-review-selectable="1"' in response.text - assert ">All<" in response.text - assert "Select all importable" in response.text - assert "2 items selected for import" in response.text - assert "Import 2 items" in response.text - assert "2 of 8 selected" in response.text - assert 'data-overall-total="8"' in response.text - assert "Status" in response.text - assert 'data-testid="import-review-status-header-row"' in response.text - assert 'data-testid="import-review-status-tabs-row"' in response.text - assert 'aria-label="Select Review Series 9 for import"' not in response.text - assert 'aria-label="Select Review Series 11 for import"' not in response.text - assert 'data-testid="import-review-why-action"' in response.text - assert 'data-testid="import-review-matched-why-action"' in response.text - assert 'data-testid="import-review-matched-diagnostics"' in response.text - assert ( - 'data-testid="import-review-matched-detail-grid" ' - 'class="grid items-start gap-5 sm:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]"' - ) in response.text - assert ( - 'data-testid="import-review-duplicate-detail-grid" ' - 'class="grid items-start gap-5 sm:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]"' - ) in response.text - assert ( - 'data-testid="import-review-no-match-detail-grid" ' - 'class="grid items-start gap-5 sm:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]"' - ) in response.text - file_breakdown_start = response.text.index( - 'data-testid="import-review-matched-file-breakdown"' - ) - file_breakdown_header = response.text[ - file_breakdown_start : response.text.index( - '
', - file_breakdown_start, - ) - ] - assert "btn-ghost btn-sm" not in file_breakdown_header - assert "Open series details" not in file_breakdown_header - assert 'data-testid="import-review-no-match-diagnostics"' in response.text - assert "ComicVine candidates" in response.text - assert 'data-testid="import-review-candidate-row"' in response.text + assert "ready series" in response.text + assert "Follow-up" in response.text assert "Below 70% threshold" in response.text - assert "rounded-xl border border-pb-border bg-pb-card/70" not in response.text - assert 'data-testid="import-review-tab-files"' not in response.text - assert 'data-testid="import-review-files-content"' not in response.text - assert 'data-testid="import-review-tab-series"' in response.text - assert 'class="chip-btn chip-btn-sm chip-btn-selected"' in response.text - assert "currentView === 'series'" in response.text - assert 'class="chip-btn chip-btn-sm chip-btn-neutral"' in response.text - assert "'chip-btn-warning': currentView === 'conflicts'" in response.text - assert 'id="import-step-review-shell"' in response.text - assert "reviewToken:" in response.text - assert 'hx-target="#import-step-review-shell"' in response.text - assert 'hx-swap="morph:outerHTML"' in response.text - assert "data-import-review-toolbar-selection-summary" in response.text - assert 'data-testid="import-review-save-conflict-choices"' not in response.text - assert 'data-testid="import-review-reset-conflict-choices"' not in response.text - assert '@click="saveConflictChoices()"' not in response.text - assert '@click="resetConflictChoices()"' not in response.text + assert "Candidate Series 11" in response.text + assert 'data-import-review-selectable="1"' in response.text + assert 'data-import-review-selectable="11"' not in response.text assert "Save conflict choices" not in response.text + assert "Import settings" not in response.text assert "Review your library" not in response.text - assert "Import everything Pullbox understands now." not in response.text - action_bar_index = response.text.index('data-testid="import-review-action-bar"') - status_bar_index = response.text.index('data-testid="import-review-series-filters"') - assert action_bar_index < status_bar_index - assert 'data-testid="import-review-guided-summary"' not in response.text - assert 'data-testid="import-review-details"' not in response.text + assert response.text.index('data-testid="import-review-action-bar"') < response.text.index( + 'data-testid="import-review-subtabs"' + ) async def test_import_review_partial_explains_selected_layout_review( self, @@ -2040,30 +1960,19 @@ async def test_import_review_partial_renders_matched_file_target_tables( duplicate_file.parsed_issue_number = 1.0 await session.commit() - response = await authenticated_client.get(f"/import/{job_id}/review-partial") + response = await authenticated_client.get(f"/import/{job_id}/review-partial?status=series") assert response.status_code == 200 - assert 'data-testid="import-review-matched-file-targets"' in response.text - assert "Matched files" in response.text + assert 'data-testid="import-review-file-target"' in response.text + assert "CV 81001" in response.text assert "Review Series 1 #1.cbz" in response.text assert "Review Series 9 #1.cbz" in response.text - matched_file_row_start = response.text.index("Review Series 1 #1.cbz") - matched_file_row = response.text[ - matched_file_row_start : response.text.index("", matched_file_row_start) - ] - duplicate_file_row_start = response.text.index("Review Series 9 #1.cbz") - duplicate_file_row = response.text[ - duplicate_file_row_start : response.text.index("", duplicate_file_row_start) - ] - assert 'class="pill pill-muted">1.0 KB' in matched_file_row - assert 'class="pill pill-muted">25.0 KB' in duplicate_file_row - assert 'data-testid="import-review-file-cv-issue-link"' in response.text assert 'href="https://comicvine.gamespot.com/issue/4000-81001/"' in response.text assert 'href="https://comicvine.gamespot.com/issue/4000-89001/"' in response.text - assert re.search(r">\s*#4\s*<", response.text) - assert re.search(r">\s*#1\s*<", response.text) + assert "Issue 4" in response.text + assert "Issue 1" in response.text - async def test_import_review_needs_series_dropdown_renders_file_details( + async def test_import_review_needs_series_keeps_files_in_read_only_inventory( self, authenticated_client, sec_db, @@ -2112,12 +2021,17 @@ async def test_import_review_needs_series_dropdown_renders_file_details( ) assert response.status_code == 200 - assert 'data-testid="import-review-series-file-details"' in response.text - assert 'data-testid="import-review-series-file-group-no_match"' in response.text - assert "Source files" in response.text - assert "Review Series 11 #1.cbz" in response.text - assert "No Match" in response.text - assert "Files that stayed unmatched or were blocked for manual review." in response.text + assert 'data-testid="import-review-series-file-details"' not in response.text + assert 'data-testid="import-review-series-file-group-no_match"' not in response.text + assert "Files in this folder" not in response.text + assert "Review Series 11 #1.cbz" not in response.text + inventory = await authenticated_client.get( + f"/import/{job_id}/series/{needs_series.id}/files" + ) + assert inventory.status_code == 200 + assert "Review Series 11 #1.cbz" in inventory.text + assert "/tmp/review-11" in inventory.text + assert "Change issue" not in inventory.text assert "Files tied to this unresolved series row" not in response.text assert "/tmp/review-11/file-1.cbz" not in response.text assert "Parsed From File Name" not in response.text @@ -2138,27 +2052,14 @@ async def test_import_review_series_status_views_keep_connected_table_contract( response = await authenticated_client.get(f"/import/{job_id}/review-partial{query}") assert response.status_code == 200 - status_bar = re.search( - r'data-testid="import-review-series-filters"[^>]*class="([^"]+)"', - response.text, - ) - assert status_bar is not None - assert "rounded-xl" not in status_bar.group(1) - assert "border-pb-border" not in status_bar.group(1) - assert "bg-pb-card/40" not in status_bar.group(1) - - table_shell = re.search( - r'
', - response.text, - ) - assert table_shell is not None - assert "is-clipped" in table_shell.group(1) - table_shell_index = response.text.index('class="downloads-table-wrap is-clipped"') - toolbar_index = response.text.index('class="downloads-history-toolbar"') - table_index = response.text.index('class="downloads-table min-w-[1040px] text-sm"') - assert table_shell_index < toolbar_index < table_index + assert 'data-testid="import-review-subtabs"' in response.text + assert 'class="downloads-table-wrap is-clipped"' in response.text + assert 'data-testid="import-review-workspace-table"' in response.text + assert response.text.index( + 'class="downloads-table-wrap is-clipped"' + ) < response.text.index('data-testid="import-review-workspace-table"') assert 'data-testid="import-collection-conflicts"' not in response.text - assert "Loading conflicts..." not in response.text + assert "Save conflict choices" not in response.text async def test_import_review_conflicts_filter_uses_series_count( self, @@ -2171,11 +2072,9 @@ async def test_import_review_conflicts_filter_uses_series_count( assert response.status_code == 200 conflict_filter = re.search( - r'data-testid="import-review-open-conflicts-filter"[\s\S]*?', - response.text, + r'data-testid="import-review-lane-confirm"[\s\S]*?', response.text ) assert conflict_filter is not None - assert ">Conflicts<" in conflict_filter.group(0) assert ">2" in conflict_filter.group(0) assert ">4" not in conflict_filter.group(0) @@ -2191,16 +2090,15 @@ async def test_import_review_conflicts_view_expands_file_keep_choices_by_default ) assert response.status_code == 200 - assert 'data-testid="import-review-save-conflict-choices"' in response.text - assert ':disabled="!hasVisibleFileConflictGroups()"' in response.text - assert 'x-data="{ detailsOpen: true }"' in response.text - assert 'data-testid="import-conflict-choice-row"' not in response.text - assert 'data-testid="import-series-conflict-candidate-radio"' not in response.text - assert "Choose one file to keep" in response.text + assert 'data-testid="import-review-keep-selected-copy"' in response.text + assert 'data-testid="import-review-conflict-card"' in response.text + assert "data-import-review-detail-row" in response.text + assert 'aria-expanded="false"' in response.text + assert "Duplicate copies" in response.text + assert "Keep selected copy" in response.text + assert "Save conflict choices" not in response.text + assert 'data-testid="import-review-copy-choice"' in response.text assert 'type="radio"' in response.text - detail_row_index = response.text.index('data-testid="import-conflict-detail-row"') - radio_index = response.text.index('type="radio"', detail_row_index) - assert detail_row_index < radio_index async def test_import_review_conflicts_view_hides_file_choice_toolbar_for_series_conflicts( self, @@ -2301,12 +2199,10 @@ async def test_import_review_conflicts_view_hides_file_choice_toolbar_for_series ) assert response.status_code == 200 - assert "Series match conflict" in response.text - assert 'class="downloads-action-btn"' in response.text - assert "Review ComicVine match" in response.text - assert "Conflicting candidates" in response.text - assert 'data-testid="import-review-save-conflict-choices"' not in response.text - assert 'data-testid="import-review-reset-conflict-choices"' not in response.text + assert "Choose between series matches" in response.text + assert "Review Series One" in response.text + assert "Ambiguous near match" in response.text + assert 'data-testid="import-review-search-cv-action"' in response.text assert "Save conflict choices" not in response.text assert 'type="radio"' not in response.text @@ -2327,7 +2223,8 @@ async def test_import_review_partial_uses_shared_pagination_contract( assert 'data-testid="import-review-pagination"' in response.text assert 'data-testid="page-dock-pagination"' in response.text assert ( - f'hx-get="/import/{job_id}/review-partial?sort=confidence&page=1"' in response.text + f'hx-get="/import/{job_id}/review-partial?status=confirm&sort=confidence&page=1"' + in response.text ) assert 'hx-push-url="false"' in response.text assert "per page" in response.text @@ -2399,20 +2296,13 @@ async def test_import_review_partial_renders_series_candidate_conflict( response = await authenticated_client.get(f"/import/{job_id}/review-partial") assert response.status_code == 200 - assert ">Conflict
" in response.text - assert "2 strong ComicVine candidates need review." in response.text - assert 'data-tip="Review conflict"' in response.text - assert "Conflicting ComicVine candidates" in response.text - assert "Multiple strong ComicVine candidates conflicted on the best match" in response.text - assert "return openImportCvSearchModal({ jobId: " in response.text + assert "Choose between series matches" in response.text assert "Chicken Devils" in response.text assert "Chicken Devil" in response.text - conflict_filter = re.search( - r'data-testid="import-review-open-conflicts-filter"[\s\S]*?', - response.text, - ) - assert conflict_filter is not None - assert ">3" in conflict_filter.group(0) + assert "Exact title candidate stayed within the ambiguity margin" in response.text + assert "Pluralized title outranked an exact-title near-tie" in response.text + assert 'data-testid="import-review-search-cv-action"' in response.text + assert "Save conflict choices" not in response.text async def test_import_series_details_partial_renders_grouped_diagnostics( self, @@ -2693,11 +2583,10 @@ async def test_import_review_partial_non_actionable_duplicate_hides_review_actio ) assert response.status_code == 200 - assert "No wanted or missing issues remain in the existing series" not in response.text - assert "All existing issues are already owned" in response.text - assert "replacement and upgrade paths are excluded from import review" in response.text - assert "/import/1/series/9/details-partial" not in response.text - assert 'data-testid="import-review-duplicate-unmatch-action"' in response.text + assert 'aria-label="Import ready files for Review Series 9"' not in response.text + assert 'aria-label="Import ready files for Review Series 10"' in response.text + assert "2 already owned" in response.text + assert "Only missing issues can be added" in response.text async def test_import_review_duplicate_rows_show_not_this_series_action( self, @@ -2711,8 +2600,9 @@ async def test_import_review_duplicate_rows_show_not_this_series_action( ) assert response.status_code == 200 - assert 'data-testid="import-review-duplicate-unmatch-action"' in response.text + assert 'data-testid="import-review-unmatch-action"' in response.text assert "Not this series" in response.text + assert "unmatchDuplicateSeries(" in response.text async def test_import_review_matched_rows_show_not_this_series_action( self, @@ -2803,12 +2693,11 @@ async def test_import_review_no_match_filter_hides_selection_column_and_disables ) assert response.status_code == 200 - assert '' not in response.text - assert 'aria-label="Select Review Series 11 for import"' not in response.text - assert 'aria-label="Select Review Series 12 for import"' not in response.text - assert "data-import-review-toolbar-selection-summary" in response.text - assert "Select all matched" not in response.text + assert 'data-import-review-selectable="11"' not in response.text + assert 'data-import-review-selectable="12"' not in response.text + assert "Select all ready" not in response.text assert "Deselect all" not in response.text + assert "Search ComicVine" in response.text async def test_import_review_no_match_filter_excludes_series_conflicts( self, @@ -2887,10 +2776,10 @@ async def test_import_review_no_match_filter_labels_series_found_file_mismatches ) assert response.status_code == 200 - assert "Needs issue" in response.text + assert "Some files need an issue match" in response.text assert 'data-testid="import-review-reconcile-action"' in response.text - assert 'data-tip="Change ComicVine match"' in response.text - assert "Absolute Wonder Woman 2026 Annual" in response.text + assert "Match issues" in response.text + assert "Change ComicVine match" in response.text async def test_import_review_needs_issue_match_view_uses_reconcile_action( self, @@ -2927,13 +2816,11 @@ async def test_import_review_needs_issue_match_view_uses_reconcile_action( ) assert response.status_code == 200 - assert "Review your library" not in response.text - assert "Needs Issue Match" in response.text - assert "Needs issue" in response.text assert 'data-testid="import-review-reconcile-action"' in response.text - assert 'data-tip="Reconcile files"' in response.text - assert 'data-tip="Change ComicVine match"' in response.text - assert 'data-testid="import-review-search-cv-action"' not in response.text + assert 'hx-get="/import/' in response.text + assert "/reconcile" in response.text + assert "Match issues" in response.text + assert "Not this series" in response.text async def test_import_review_needs_issue_match_includes_duplicate_rows( self, @@ -2947,11 +2834,10 @@ async def test_import_review_needs_issue_match_includes_duplicate_rows( ) assert response.status_code == 200 - assert "Needs Issue Match" in response.text assert "Review Series 9" in response.text - assert "In Library" in response.text - assert 'data-testid="import-review-reconcile-action"' in response.text - assert 'aria-label="Reconcile files for Review Series 9"' in response.text + assert "Review Series 10" in response.text + assert "Match issues" in response.text + assert "Already in library" in response.text async def test_import_review_needs_series_match_view_keeps_comicvine_search( self, @@ -2965,10 +2851,10 @@ async def test_import_review_needs_series_match_view_keeps_comicvine_search( ) assert response.status_code == 200 - assert "Review your library" not in response.text - assert "Needs Series Match" in response.text + assert "Search ComicVine" in response.text assert 'data-testid="import-review-search-cv-action"' in response.text assert 'data-testid="import-review-reconcile-action"' not in response.text + assert "Candidate Series 11" in response.text async def test_import_review_safety_blocked_view_shows_allow_and_skip_actions( self, @@ -3010,15 +2896,14 @@ async def test_import_review_safety_blocked_view_shows_allow_and_skip_actions( ) assert response.status_code == 200 - assert "Safety Review" in response.text - assert "Safety review" in response.text + assert "Large file needs approval" in response.text assert "Oversized Omnibus.cbz" in response.text assert 'data-testid="import-review-safety-category-summary"' in response.text - assert 'data-testid="import-review-safety-category-details"' in response.text + assert 'data-testid="import-review-safety-category-details"' not in response.text assert "Decompression-size limit" in response.text assert "Code: archive_decompressed_size_limit" in response.text assert "Retry alone will not help" in response.text - assert "Trusted override available" in response.text + assert "A one-time exception is available for eligible files" in response.text assert "/tmp/review-1/oversized.cbz" not in response.text assert 'data-testid="import-review-allow-safety-file"' in response.text assert 'data-testid="import-review-skip-safety-file"' in response.text @@ -3069,9 +2954,9 @@ async def test_import_review_non_overrideable_safety_block_hides_allow_action( assert response.status_code == 200 assert "Corrupt Download.cbz" in response.text assert "Archive inspection failed" in response.text - assert "Retry may help after remediation" in response.text - assert "Override not allowed" in response.text - assert "Cannot override" in response.text + assert "Fix the source, then recheck" in response.text + assert "This check cannot be overridden" in response.text + assert 'data-testid="import-review-skip-safety-file"' in response.text assert 'data-testid="import-review-allow-safety-file"' not in response.text assert "/safety/allow-once?status=safety_blocked" not in response.text assert 'data-testid="import-review-skip-safety-file"' in response.text @@ -3255,7 +3140,7 @@ async def test_import_review_rematch_poll_escapes_job_id_in_html_attribute( hostile_job_id = '1" autofocus onfocus="alert(1)' with patch( - "pullbox.ui.import_routes.has_pending_import_safety_rematch", + "pullbox.ui.import_routes.has_pending_import_review_rematch", new=AsyncMock(return_value=True), ): response = await import_review_rematch_status( # type: ignore[arg-type] @@ -3280,8 +3165,9 @@ async def test_import_review_returns_to_all_when_safety_tab_becomes_empty( ) assert response.status_code == 200 - assert 'name="review_status_filter" value=""' in response.text - assert 'data-import-review-view="series"' in response.text + assert 'name="review_status_filter" value="series"' in response.text + assert 'data-testid="import-review-workspace-table"' in response.text + assert "Preparing match" not in response.text async def test_import_review_reports_remaining_subitems_after_each_safety_skip( self, @@ -3325,7 +3211,7 @@ async def test_import_review_reports_remaining_subitems_after_each_safety_skip( series_id = series.id blocked_file_ids = [imp_file.id for imp_file in files[:2]] - response = await authenticated_client.get(f"/import/{job_id}/review-partial") + response = await authenticated_client.get(f"/import/{job_id}/review-partial?status=series") assert response.status_code == 200 assert f'id="import-review-series-row-series-{series_id}"' in response.text assert 'data-import-review-pending-subitems="2"' in response.text @@ -3434,12 +3320,10 @@ async def test_import_review_duplicate_filter_shows_deliberate_file_selection( ) assert response.status_code == 200 - assert '' in response.text assert 'data-testid="import-review-duplicate-select"' in response.text - assert 'aria-label="Import matched files for Review Series 9"' in response.text + assert 'aria-label="Import ready files for Review Series 9"' in response.text assert "toggleDuplicateSeriesFiles(9" in response.text - assert "data-import-review-toolbar-selection-summary" in response.text - assert "Select all matched" not in response.text + assert "Select all ready" not in response.text assert "Deselect all" not in response.text async def test_import_review_non_actionable_duplicate_hides_file_selection_checkbox( @@ -3481,26 +3365,24 @@ async def test_import_review_non_actionable_duplicate_hides_file_selection_check ) assert response.status_code == 200 - assert 'aria-label="Import matched files for Review Series 9"' not in response.text - assert 'aria-label="Import matched files for Review Series 10"' in response.text + assert 'aria-label="Import ready files for Review Series 9"' not in response.text + assert 'aria-label="Import ready files for Review Series 10"' in response.text - async def test_import_review_conflicted_matched_rows_are_not_bulk_selectable( + async def test_import_review_conflicted_rows_can_select_their_other_matched_files( self, authenticated_client, sec_db, ) -> None: # type: ignore[no-untyped-def] job_id = await _seed_import_review_job(sec_db) - response = await authenticated_client.get(f"/import/{job_id}/review-partial") + response = await authenticated_client.get(f"/import/{job_id}/review-partial?status=confirm") assert response.status_code == 200 - assert "Resolve file conflicts before selecting this series." in response.text - conflict_pill = r'[\s\S]*?Conflict[\s\S]*?' - assert re.search(conflict_pill, response.text) - assert '@click="selectAllImportable()"' in response.text - assert '@click="deselectAllImportable()"' in response.text - assert 'data-import-review-selectable="7"' not in response.text - assert 'aria-label="Select Review Series 7 for import"' not in response.text + assert 'data-testid="import-review-keep-selected-copy"' in response.text + assert "Choose which copy to import" in response.text + assert 'data-import-review-selectable="7"' in response.text + assert 'data-import-review-selectable="7"' in response.text + assert 'aria-label="Select Review Series 7 for import"' in response.text async def test_import_review_non_importable_status_rows_are_not_selectable( self, @@ -3517,7 +3399,7 @@ async def test_import_review_non_importable_status_rows_are_not_selectable( series.selected_for_import = False await session.commit() - response = await authenticated_client.get(f"/import/{job_id}/review-partial") + response = await authenticated_client.get(f"/import/{job_id}/review-partial?status=info") assert response.status_code == 200 assert "Review Series 6" in response.text @@ -3564,10 +3446,9 @@ async def test_import_review_duplicate_owned_only_files_show_zero_importable_wit ) assert response.status_code == 200 - assert "Owned 1/1" not in response.text - assert "0/1" in response.text - assert "1 owned, 0 importable, 1 total." in response.text - assert "100%" in response.text + assert "1 already owned" in response.text + assert 'aria-label="Import ready files for Review Series 9"' not in response.text + assert "Only missing issues can be added" in response.text async def test_import_conflicts_partial_renders_conflict_diagnostics( self, diff --git a/tests/ui/test_import_one_page_review.py b/tests/ui/test_import_one_page_review.py new file mode 100644 index 00000000..4398bd1f --- /dev/null +++ b/tests/ui/test_import_one_page_review.py @@ -0,0 +1,237 @@ +"""Compact one-page review and exact series-scoped safety decisions.""" + +import re +from unittest.mock import Mock + +import pytest + +from pullbox.models.import_job import ImportedFile, ImportedFileStatus, ImportedSeries, ImportJob +from pullbox.services.import_safety_diagnostics import ImportSafetyCategory +from tests.ui.test_import_safety_bulk_ui import ( + _csrf_header_for, + _safety_block, + _seed_bulk_safety_job, +) + +pytest_plugins = ["conftest_security"] + + +async def _seed_one_page_job(sec_db, count=2): + seeded = await _seed_bulk_safety_job(sec_db, include_dangerous=True) + async with sec_db() as session: + for file_id in seeded["file_ids"][:count]: + file = await session.get(ImportedFile, file_id) + file.file_size = 1_800_000 + file.diagnostics = { + "safety_block": _safety_block( + ImportSafetyCategory.SINGLE_PAGE_COMIC, overrideable=True + ), + "content_inspection": {"page_count": 1}, + "archive_format": {"detected": "cbz"}, + } + await session.commit() + return seeded + + +async def _review(client, seeded): + return await client.get( + f"/import/{seeded['job_id']}/review-partial?status=decide&reason=single_page_comic" + ) + + +def _token(html, action): + match = re.search( + rf'data-testid="import-review-one-page-{action}".*?name="token" value="([^"]+)"', + html, + re.DOTALL, + ) + assert match is not None + return match.group(1) + + +@pytest.mark.parametrize("count,label", [(1, "Allow All"), (2, "Allow All")]) +async def test_one_page_review_is_compact_and_has_explicit_series_actions( + authenticated_client, sec_db, count, label +): + seeded = await _seed_one_page_job(sec_db, count) + response = await _review(authenticated_client, seeded) + assert response.status_code == 200 + html = response.text + assert f"{count} {'file' if count == 1 else 'files'} in 1 series" in html + assert f"Skip all {count}" in html + assert "Review one by one" not in html + assert 'data-testid="import-review-one-page-review"' not in html + assert 'data-testid="import-review-one-page-helper"' in html + assert label in html + assert re.search(r">\s*Skip All\s*", html) + assert 'data-testid="import-review-more-actions"' not in html + assert 'data-testid="import-review-one-page-files"' in html + assert "1.7 MB" in html + assert "ZIP" in html and "1 page" in html + assert "Select all ready" not in html + assert "Why this needs review" not in html + compact = html.split('data-testid="import-review-one-page-files"', 1)[1].split("", 1)[ + 0 + ] + assert "Change series" not in compact + assert "Source files" not in compact + assert "damaged archive" not in compact + assert 'data-testid="import-review-one-page-more"' not in html + assert "Change series" not in html + assert "Source files" not in html + assert "other reasons in details" not in html + assert "What needs attention" in html + assert 'Select' not in html + assert 'Details' in html + assert "0/4" in html + assert "Cover art, a damaged download, or an intentional one-pager." in html + assert "One-page archives

" in compact + assert ">Allow" in compact + assert ">View File" in compact + assert compact.index(">View File") < compact.index(">Allow") + assert "Allow once" not in compact + assert 'data-testid="import-review-one-page-category-actions"' in html + assert 'data-testid="import-review-one-page-detection"' in html + assert "1 image page" in html + assert "of 3 members" not in html + assert "/private/import" not in html + assert _token(html, "allow") + + +@pytest.mark.parametrize("action", ["allow", "skip"]) +async def test_series_shortcut_only_changes_its_one_page_files( + authenticated_client, sec_db, monkeypatch, action +): + from pullbox.tasks import import_task + + rematch = Mock() + monkeypatch.setattr(import_task, "trigger_import_series_rematch", rematch) + seeded = await _seed_one_page_job(sec_db) + other = await _seed_one_page_job(sec_db) + html = (await _review(authenticated_client, seeded)).text + response = await authenticated_client.post( + f"/import/{seeded['job_id']}/series/{seeded['series_id']}/one-page/{action}" + "?status=decide&reason=single_page_comic", + data={"token": _token(html, action)}, + headers=_csrf_header_for(authenticated_client), + ) + assert response.status_code == 200 + async with sec_db() as session: + files = [await session.get(ImportedFile, i) for i in seeded["file_ids"]] + expected = ( + ImportedFileStatus.SAFETY_APPROVED if action == "allow" else ImportedFileStatus.SKIPPED + ) + assert [file.status for file in files[:2]] == [expected, expected] + assert all(file.status == ImportedFileStatus.SAFETY_BLOCKED for file in files[2:]) + assert not any(file.include_in_import for file in files[:2]) + for file_id in other["file_ids"]: + assert ( + await session.get(ImportedFile, file_id) + ).status == ImportedFileStatus.SAFETY_BLOCKED + parent = await session.get(ImportedSeries, seeded["series_id"]) + assert parent.selected_for_import is False + if action == "allow": + rematch.assert_called_once_with(seeded["job_id"], seeded["series_id"]) + else: + rematch.assert_not_called() + + +@pytest.mark.parametrize("change", ["file", "job", "actor", "series", "action", "added", "control"]) +async def test_series_shortcut_rejects_changed_or_wrong_scope_without_partial_approval( + authenticated_client, sec_db, change +): + from pullbox.models.import_job import ImportJobStatus + + seeded = await _seed_one_page_job(sec_db) + html = (await _review(authenticated_client, seeded)).text + token = _token(html, "skip" if change == "action" else "allow") + series_id = seeded["series_id"] + async with sec_db() as session: + if change == "file": + file = await session.get(ImportedFile, seeded["file_ids"][1]) + file.diagnostics = { + "safety_block": _safety_block( + ImportSafetyCategory.DANGEROUS_PATH_OR_PAYLOAD, overrideable=True + ) + } + elif change == "job": + (await session.get(ImportJob, seeded["job_id"])).status = ImportJobStatus.COMPLETED + elif change == "actor": + from itsdangerous import URLSafeTimedSerializer + + from pullbox.core.config_resolver import get_application_secret + + signer = URLSafeTimedSerializer(get_application_secret(), salt="import-review-file-v1") + scope = signer.loads(token) + scope["actor"] += 1 + token = signer.dumps(scope) + elif change == "series": + parent = ImportedSeries(import_job_id=seeded["job_id"], raw_series_name="Other") + session.add(parent) + await session.flush() + series_id = parent.id + elif change == "added": + session.add( + ImportedFile( + import_job_id=seeded["job_id"], + import_series_id=series_id, + file_name="New cover.cbz", + file_path="/private/import/New cover.cbz", + file_size=1000, + file_format="cbz", + status=ImportedFileStatus.SAFETY_BLOCKED, + diagnostics={ + "safety_block": _safety_block( + ImportSafetyCategory.SINGLE_PAGE_COMIC, overrideable=True + ) + }, + ) + ) + elif change == "control": + from pullbox.models.import_job import ImportControlRequest + + ( + await session.get(ImportJob, seeded["job_id"]) + ).control_request = ImportControlRequest.CANCEL + await session.commit() + response = await authenticated_client.post( + f"/import/{seeded['job_id']}/series/{series_id}/one-page/allow", + data={"token": token}, + headers=_csrf_header_for(authenticated_client), + ) + assert response.status_code == 409 + async with sec_db() as session: + for file_id in seeded["file_ids"]: + assert ( + await session.get(ImportedFile, file_id) + ).status == ImportedFileStatus.SAFETY_BLOCKED + + +async def test_one_page_shortcut_cannot_approve_an_ineligible_file(authenticated_client, sec_db): + seeded = await _seed_one_page_job(sec_db) + async with sec_db() as session: + file = await session.get(ImportedFile, seeded["file_ids"][1]) + file.diagnostics = { + "safety_block": _safety_block( + ImportSafetyCategory.SINGLE_PAGE_COMIC, overrideable=False + ) + } + await session.commit() + html = (await _review(authenticated_client, seeded)).text + assert 'data-testid="import-review-one-page-allow"' not in html + assert 'data-testid="import-review-one-page-skip"' in html + + +async def test_one_page_shortcut_requires_csrf(authenticated_client, sec_db): + seeded = await _seed_one_page_job(sec_db) + html = (await _review(authenticated_client, seeded)).text + response = await authenticated_client.post( + f"/import/{seeded['job_id']}/series/{seeded['series_id']}/one-page/allow", + data={"token": _token(html, "allow")}, + ) + assert response.status_code == 403 + async with sec_db() as session: + for file_id in seeded["file_ids"]: + assert ( + await session.get(ImportedFile, file_id) + ).status == ImportedFileStatus.SAFETY_BLOCKED diff --git a/tests/ui/test_import_review_file_details.py b/tests/ui/test_import_review_file_details.py new file mode 100644 index 00000000..cb09cb4b --- /dev/null +++ b/tests/ui/test_import_review_file_details.py @@ -0,0 +1,32 @@ +"""Inline review details describe current outcomes, including rechecked sources.""" + +from pullbox.models.import_job import ImportedFile, ImportedFileStatus +from pullbox.ui.import_review_tables import _build_import_review_file_detail_row + + +def test_rechecked_file_displays_format_from_source_evidence(): + file = ImportedFile( + file_name="Batman.cbr", + status=ImportedFileStatus.MATCHED, + diagnostics={ + "source_metadata": { + "archive_format": {"declared": "cbr", "detected": "cbz", "mismatch": True}, + "content_inspection": {"page_count": 24}, + } + }, + ) + row = _build_import_review_file_detail_row(file, {}) + assert row["archive_format"] == {"declared": "cbr", "detected": "cbz", "mismatch": True} + assert row["page_count"] == 24 + + +def test_ready_file_is_not_labeled_as_an_excluded_duplicate(): + file = ImportedFile(file_name="Batman.cbz", status=ImportedFileStatus.MATCHED) + row = _build_import_review_file_detail_row(file, {}) + assert row["duplicate_reason_label"] is None + + +def test_actual_duplicate_keeps_its_explanation(): + file = ImportedFile(file_name="Batman.cbz", status=ImportedFileStatus.DUPLICATE_FILE) + row = _build_import_review_file_detail_row(file, {}) + assert "Excluded because" in row["duplicate_reason_label"] diff --git a/tests/ui/test_import_review_file_routes.py b/tests/ui/test_import_review_file_routes.py new file mode 100644 index 00000000..1e1d1929 --- /dev/null +++ b/tests/ui/test_import_review_file_routes.py @@ -0,0 +1,137 @@ +"""Review file previews are read-only, scoped, and CSRF protected.""" + +import re +from html import unescape +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +from pullbox.api.middleware import SESSION_COOKIE_NAME +from pullbox.models.import_job import ImportedFile, ImportedFileStatus +from pullbox.providers.base import IssueSummary +from pullbox.services.auth_service import AuthService +from tests.ui.test_import_collection_shell_ui_routes import _seed_import_review_job + +pytest_plugins = ["conftest_security"] + + +def csrf(client): + return { + "x-csrf-token": AuthService.get_csrf_token_from_session( + client.cookies.get(SESSION_COOKIE_NAME) + ) + or "" + } + + +def token(html): + return unescape(re.search(r'name="token" value="([^"]+)"', html).group(1)) + + +async def test_assignment_preview_and_apply_are_file_scoped( + authenticated_client, sec_db, monkeypatch +): + from pullbox.ui import import_review_file_routes as routes + + metadata = AsyncMock() + metadata.get_series_metadata.return_value = SimpleNamespace( + provider_id="20", + title="Second", + year_start=2020, + publisher="Test", + issue_count=1, + comicvine_url=None, + ) + metadata.get_issue_summaries_for_series.return_value = [ + IssueSummary("201", 1, "Second", "2020-01-01", None, "issue") + ] + monkeypatch.setattr(routes, "build_metadata_service", AsyncMock(return_value=metadata)) + job_id = await _seed_import_review_job(sec_db) + response = await authenticated_client.get(f"/import/{job_id}/files/1/assign?cv_id=20") + assert response.status_code == 200 + assert "dropdown-select-contract" in response.text + assert "Only this file" in response.text + async with sec_db() as session: + assert (await session.get(ImportedFile, 1)).import_series_id == 1 + data = {"token": token(response.text), "cv_id": 20, "issue_cv_id": 201} + rejected = await authenticated_client.post( + f"/import/{job_id}/files/2/assign", data=data, headers=csrf(authenticated_client) + ) + assert rejected.status_code == 422 + response = await authenticated_client.post( + f"/import/{job_id}/files/1/assign", data=data, headers=csrf(authenticated_client) + ) + assert response.status_code == 200, response.text + async with sec_db() as session: + assert (await session.get(ImportedFile, 1)).import_series_id != 1 + assert (await session.get(ImportedFile, 2)).import_series_id == 1 + + +async def test_source_preview_does_not_scan_and_post_only_queues( + authenticated_client, sec_db, monkeypatch +): + from pullbox.services import import_review_source_actions as actions + from pullbox.tasks import import_task + + inspect = Mock(side_effect=AssertionError("Preview and enqueue must not scan")) + monkeypatch.setattr(actions, "inspect_review_source", inspect) + trigger = Mock() + monkeypatch.setattr(import_task, "trigger_import_review_source_action", trigger) + job_id = await _seed_import_review_job(sec_db) + async with sec_db() as session: + file = await session.get(ImportedFile, 1) + file.status = ImportedFileStatus.SAFETY_BLOCKED + file.diagnostics = { + "safety_block": {"category": "archive_inspection_failed", "overrideable": False} + } + await session.commit() + preview = await authenticated_client.get(f"/import/{job_id}/files/1/source") + assert preview.status_code == 200 + assert "This does not grant a safety exception" in preview.text + data = {"token": token(preview.text), "source_action": "recheck"} + rejected = await authenticated_client.post(f"/import/{job_id}/files/1/source", data=data) + assert rejected.status_code == 403 + result = await authenticated_client.post( + f"/import/{job_id}/files/1/source", data=data, headers=csrf(authenticated_client) + ) + assert result.status_code == 200, result.text + trigger.assert_called_once_with(job_id, 1) + inspect.assert_not_called() + async with sec_db() as session: + file = await session.get(ImportedFile, 1) + assert file.status is ImportedFileStatus.SAFETY_BLOCKED + assert file.diagnostics["review_source_action"]["state"] == "pending" + + +async def test_inline_issue_choices_are_bounded_and_use_the_same_scoped_assignment( + authenticated_client, sec_db, monkeypatch +): + from pullbox.ui import import_review_file_routes as routes + + metadata = AsyncMock() + metadata.get_series_metadata.return_value = SimpleNamespace( + provider_id="20", title="Second", year_start=2020 + ) + metadata.get_issue_summaries_for_series.return_value = [ + IssueSummary(str(200 + i), i, f"Issue {i}", "2020-01-01", None, "issue") + for i in range(1, 32) + ] + monkeypatch.setattr(routes, "build_metadata_service", AsyncMock(return_value=metadata)) + job_id = await _seed_import_review_job(sec_db) + url = f"/import/{job_id}/files/1/assign?cv_id=20&inline=true" + response = await authenticated_client.get(url) + assert response.status_code == 200 + assert 'data-testid="import-review-issue-choices"' in response.text + assert response.text.count('data-testid="import-review-use-issue"') == 25 + assert 'role="dialog"' not in response.text + assert "issue_page=2" in response.text + second = await authenticated_client.get(url + "&issue_page=2") + assert second.text.count('data-testid="import-review-use-issue"') == 6 + filtered = await authenticated_client.get(url + "&q=Issue%2031") + assert filtered.text.count('data-testid="import-review-use-issue"') == 1 + assert 'value="231"' in filtered.text + rejected = await authenticated_client.post( + f"/import/{job_id}/files/2/assign", + data={"token": token(filtered.text), "cv_id": 20, "issue_cv_id": 231}, + headers=csrf(authenticated_client), + ) + assert rejected.status_code == 422 diff --git a/tests/ui/test_import_review_files_inventory.py b/tests/ui/test_import_review_files_inventory.py new file mode 100644 index 00000000..fda45758 --- /dev/null +++ b/tests/ui/test_import_review_files_inventory.py @@ -0,0 +1,175 @@ +"""The optional file inventory supports series identification without decisions.""" + +import re +import sys +from pathlib import Path + +import pytest +from sqlalchemy import select + +from pullbox.models.import_job import ( + ImportedFile, + ImportedFileStatus, + ImportedSeries, + ImportJob, + ImportJobStatus, + ImportSeriesStatus, + ImportSourceType, +) + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +pytest_plugins = ["conftest_security"] + + +async def _seed_inventory(sec_db, count=27): + async with sec_db() as session: + job = ImportJob( + source_path="/library", + source_type=ImportSourceType.MYLAR3, + status=ImportJobStatus.REVIEW, + ) + session.add(job) + await session.flush() + series = ImportedSeries( + import_job_id=job.id, + raw_series_name="Unknown Series", + raw_year=2024, + status=ImportSeriesStatus.NO_MATCH, + source_folder="/library/Unknown Series", + files_total=count, + files_no_match=count, + file_count=count, + diagnostics={"kind": "series_no_match", "top_candidates": []}, + ) + session.add(series) + await session.flush() + for index in range(count): + session.add( + ImportedFile( + import_job_id=job.id, + import_series_id=series.id, + file_name=f"Unknown Series {index:03}.cbz", + file_path=f"/library/Unknown Series/Unknown Series {index:03}.cbz", + file_size=1024 * (index + 1), + file_format="cbz", + status=ImportedFileStatus.SAFETY_BLOCKED + if index == 0 + else ImportedFileStatus.NO_MATCH, + diagnostics={ + "safety_block": { + "category": "source_missing", + "code": "source_missing", + "reason": "Recorded file not found", + } + } + if index == 0 + else {}, + ) + ) + await session.commit() + return job.id, series.id + + +async def test_series_match_details_and_menu_stay_series_focused(authenticated_client, sec_db): + job_id, series_id = await _seed_inventory(sec_db) + response = await authenticated_client.get( + f"/import/{job_id}/review-partial?status=decide&reason=needs_series" + ) + assert response.status_code == 200 + row = response.text.split(f'data-import-review-series-row="{series_id}"', 1)[1].split( + "", 1 + )[0] + assert "Files in this folder" not in row + assert 'data-testid="import-review-series-file-details"' not in row + assert "ComicVine candidates" in row + menu = re.search(r"
", row, re.S).group() + assert re.findall(r"]*>(.*?)", menu, re.S) == ["View files"] + assert f'hx-get="/import/{job_id}/series/{series_id}/files"' in menu + assert "toggleImportReviewRow" not in menu + assert re.search( + r'data-testid="import-review-primary-action"[^>]*>Search ComicVine', row + ) + assert "Skip series" in row + assert 'data-testid="import-review-recheck-source"' not in row + + +async def test_inventory_is_paginated_read_only_and_labels_missing_references( + authenticated_client, sec_db +): + job_id, series_id = await _seed_inventory(sec_db) + async with sec_db() as session: + before = [ + (f.id, f.status, f.file_path, f.diagnostics) + for f in (await session.scalars(select(ImportedFile).order_by(ImportedFile.id))).all() + ] + url = f"/import/{job_id}/series/{series_id}/files" + response = await authenticated_client.get(url) + assert response.status_code == 200 + html = response.text + assert 'role="dialog" aria-modal="true"' in html + assert "/library/Unknown Series" in html + assert "Recorded during the scan" in html + assert "Missing reference" in html + assert "1.0 KB" in html + assert html.count('data-import-review-inventory-file="') == 25 + assert "Unknown Series 025.cbz" not in html + assert not re.search(r"hx-(post|put)=|<(input|select|form)\b", html) + assert 'id="pagination-' not in html + assert re.search(r'data-page-url="[^"]*files_page=2"[^>]*hx-push-url="false"', html) + response = await authenticated_client.get(url + "?files_page=2") + assert response.status_code == 200 + page_two = response.text + assert page_two.count('data-import-review-inventory-file="') == 2 + assert "Unknown Series 025.cbz" in page_two + assert "Unknown Series 000.cbz" not in page_two + async with sec_db() as session: + after = [ + (f.id, f.status, f.file_path, f.diagnostics) + for f in (await session.scalars(select(ImportedFile).order_by(ImportedFile.id))).all() + ] + assert after == before + assert (await session.get(ImportJob, job_id)).status == ImportJobStatus.REVIEW + + +async def test_inventory_rejects_series_from_another_job(authenticated_client, sec_db): + job_id, series_id = await _seed_inventory(sec_db) + other_job, other_series = await _seed_inventory(sec_db) + for job, series in ((job_id, other_series), (other_job, series_id), (job_id, 999999)): + assert ( + await authenticated_client.get(f"/import/{job}/series/{series}/files") + ).status_code == 404 + + +async def test_inventory_requires_authentication(unauthenticated_client, sec_db): + job_id, series_id = await _seed_inventory(sec_db) + response = await unauthenticated_client.get( + f"/import/{job_id}/series/{series_id}/files", follow_redirects=False + ) + assert response.status_code in (302, 303, 401, 403) + + +@pytest.mark.parametrize("count", [0, 1, 27]) +async def test_inventory_bounds_pages_and_handles_empty_groups(authenticated_client, sec_db, count): + job_id, series_id = await _seed_inventory(sec_db, count=count) + url = f"/import/{job_id}/series/{series_id}/files" + assert (await authenticated_client.get(url + "?files_page=0")).status_code == 422 + response = await authenticated_client.get(url + "?files_page=99999") + assert response.status_code == 200 + html = response.text + assert html.count('data-import-review-inventory-file="') == (2 if count == 27 else count) + if not count: + assert "No files were recorded for this series." in html + + +async def test_inventory_escapes_names_and_shows_other_source_folders(authenticated_client, sec_db): + job_id, series_id = await _seed_inventory(sec_db, count=1) + async with sec_db() as session: + file = await session.scalar(select(ImportedFile)) + file.file_name = ".cbz" + file.file_path = "C:\\Mixed Folder\\comic.cbz" + await session.commit() + response = await authenticated_client.get(f"/import/{job_id}/series/{series_id}/files") + assert response.status_code == 200 + assert "