diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..99ed5992 --- /dev/null +++ b/404.html @@ -0,0 +1,2148 @@ + + + + + + + + + + + + + + + + + + + + + Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..bea36fb1 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +datarecce.io \ No newline at end of file diff --git a/assets/images/Logo_Recce_white-fs8.png b/assets/images/Logo_Recce_white-fs8.png new file mode 100644 index 00000000..be041cd7 Binary files /dev/null and b/assets/images/Logo_Recce_white-fs8.png differ diff --git a/assets/images/case-studies/rio/RIOPREFEITURA-vertical-branco-425x600.png b/assets/images/case-studies/rio/RIOPREFEITURA-vertical-branco-425x600.png new file mode 100644 index 00000000..571123fd Binary files /dev/null and b/assets/images/case-studies/rio/RIOPREFEITURA-vertical-branco-425x600.png differ diff --git a/assets/images/case-studies/rio/preview-card.png b/assets/images/case-studies/rio/preview-card.png new file mode 100644 index 00000000..25c16e38 Binary files /dev/null and b/assets/images/case-studies/rio/preview-card.png differ diff --git a/assets/images/case-studies/rio/rio-lineage.png b/assets/images/case-studies/rio/rio-lineage.png new file mode 100644 index 00000000..750fee75 Binary files /dev/null and b/assets/images/case-studies/rio/rio-lineage.png differ diff --git a/assets/images/case-studies/rio/rio-pr-comment.png b/assets/images/case-studies/rio/rio-pr-comment.png new file mode 100644 index 00000000..689ccb0e Binary files /dev/null and b/assets/images/case-studies/rio/rio-pr-comment.png differ diff --git a/assets/images/dbt-cloud/dev-artifacts.png b/assets/images/dbt-cloud/dev-artifacts.png new file mode 100644 index 00000000..98631ea1 Binary files /dev/null and b/assets/images/dbt-cloud/dev-artifacts.png differ diff --git a/assets/images/dbt-cloud/login-dbt-cloud.png b/assets/images/dbt-cloud/login-dbt-cloud.png new file mode 100644 index 00000000..84f31e6b Binary files /dev/null and b/assets/images/dbt-cloud/login-dbt-cloud.png differ diff --git a/assets/images/dbt-cloud/prod-artifacts.png b/assets/images/dbt-cloud/prod-artifacts.png new file mode 100644 index 00000000..8fdb3192 Binary files /dev/null and b/assets/images/dbt-cloud/prod-artifacts.png differ diff --git a/assets/images/dbt-cloud/select-run-job.png b/assets/images/dbt-cloud/select-run-job.png new file mode 100644 index 00000000..ae3fca93 Binary files /dev/null and b/assets/images/dbt-cloud/select-run-job.png differ diff --git a/assets/images/demo/clv-customers-model-fs8.png b/assets/images/demo/clv-customers-model-fs8.png new file mode 100644 index 00000000..73581228 Binary files /dev/null and b/assets/images/demo/clv-customers-model-fs8.png differ diff --git a/assets/images/demo/clv-profile-diff-fs8.png b/assets/images/demo/clv-profile-diff-fs8.png new file mode 100644 index 00000000..b6ef5f80 Binary files /dev/null and b/assets/images/demo/clv-profile-diff-fs8.png differ diff --git a/assets/images/demo/clv-query-diff-fs8.png b/assets/images/demo/clv-query-diff-fs8.png new file mode 100644 index 00000000..32516563 Binary files /dev/null and b/assets/images/demo/clv-query-diff-fs8.png differ diff --git a/assets/images/demo/clv-value-diff-fs8.png b/assets/images/demo/clv-value-diff-fs8.png new file mode 100644 index 00000000..2b510322 Binary files /dev/null and b/assets/images/demo/clv-value-diff-fs8.png differ diff --git a/assets/images/dotted-bg.png b/assets/images/dotted-bg.png new file mode 100644 index 00000000..023a42d0 Binary files /dev/null and b/assets/images/dotted-bg.png differ diff --git a/assets/images/env-prep/prep-env-clone-source.png b/assets/images/env-prep/prep-env-clone-source.png new file mode 100644 index 00000000..259196b8 Binary files /dev/null and b/assets/images/env-prep/prep-env-clone-source.png differ diff --git a/assets/images/env-prep/prep-env-github-pr-outdated.png b/assets/images/env-prep/prep-env-github-pr-outdated.png new file mode 100644 index 00000000..81d936d6 Binary files /dev/null and b/assets/images/env-prep/prep-env-github-pr-outdated.png differ diff --git a/assets/images/env-prep/prep-env-limit-data-range.png b/assets/images/env-prep/prep-env-limit-data-range.png new file mode 100644 index 00000000..a00783f4 Binary files /dev/null and b/assets/images/env-prep/prep-env-limit-data-range.png differ diff --git a/assets/images/env-prep/prep-env-pr-outdated.png b/assets/images/env-prep/prep-env-pr-outdated.png new file mode 100644 index 00000000..cb219852 Binary files /dev/null and b/assets/images/env-prep/prep-env-pr-outdated.png differ diff --git a/assets/images/events/DataRecce-DataCoves_Fireside-chat_20240827_small.jpg b/assets/images/events/DataRecce-DataCoves_Fireside-chat_20240827_small.jpg new file mode 100644 index 00000000..a3fb424d Binary files /dev/null and b/assets/images/events/DataRecce-DataCoves_Fireside-chat_20240827_small.jpg differ diff --git a/assets/images/events/the-data-renegade_happy-hour.jpg b/assets/images/events/the-data-renegade_happy-hour.jpg new file mode 100644 index 00000000..d807edd0 Binary files /dev/null and b/assets/images/events/the-data-renegade_happy-hour.jpg differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 00000000..2116baa4 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/images/features/actions-menu.png b/assets/images/features/actions-menu.png new file mode 100644 index 00000000..8c3e71b5 Binary files /dev/null and b/assets/images/features/actions-menu.png differ diff --git a/assets/images/features/checklist.png b/assets/images/features/checklist.png new file mode 100644 index 00000000..aebef820 Binary files /dev/null and b/assets/images/features/checklist.png differ diff --git a/assets/images/features/clipboard-to-github.gif b/assets/images/features/clipboard-to-github.gif new file mode 100644 index 00000000..02849a68 Binary files /dev/null and b/assets/images/features/clipboard-to-github.gif differ diff --git a/assets/images/features/code-diff.png b/assets/images/features/code-diff.png new file mode 100644 index 00000000..93411090 Binary files /dev/null and b/assets/images/features/code-diff.png differ diff --git a/assets/images/features/histogram-diff.gif b/assets/images/features/histogram-diff.gif new file mode 100644 index 00000000..00a4a6fc Binary files /dev/null and b/assets/images/features/histogram-diff.gif differ diff --git a/assets/images/features/histogram-diff.png b/assets/images/features/histogram-diff.png new file mode 100644 index 00000000..a13674d4 Binary files /dev/null and b/assets/images/features/histogram-diff.png differ diff --git a/assets/images/features/lineage-diff.gif b/assets/images/features/lineage-diff.gif new file mode 100644 index 00000000..2368213e Binary files /dev/null and b/assets/images/features/lineage-diff.gif differ diff --git a/assets/images/features/lineage-diff.png b/assets/images/features/lineage-diff.png new file mode 100644 index 00000000..fcc5b4f2 Binary files /dev/null and b/assets/images/features/lineage-diff.png differ diff --git a/assets/images/features/model-schema-change-detected.png b/assets/images/features/model-schema-change-detected.png new file mode 100644 index 00000000..cb98dcc6 Binary files /dev/null and b/assets/images/features/model-schema-change-detected.png differ diff --git a/assets/images/features/multi-node-row-count-diff.gif b/assets/images/features/multi-node-row-count-diff.gif new file mode 100644 index 00000000..bc567ab3 Binary files /dev/null and b/assets/images/features/multi-node-row-count-diff.gif differ diff --git a/assets/images/features/multi-node-selection.gif b/assets/images/features/multi-node-selection.gif new file mode 100644 index 00000000..a87a99de Binary files /dev/null and b/assets/images/features/multi-node-selection.gif differ diff --git a/assets/images/features/multi-node-value-diff.gif b/assets/images/features/multi-node-value-diff.gif new file mode 100644 index 00000000..92d4d145 Binary files /dev/null and b/assets/images/features/multi-node-value-diff.gif differ diff --git a/assets/images/features/node-details-panel.gif b/assets/images/features/node-details-panel.gif new file mode 100644 index 00000000..08af10c0 Binary files /dev/null and b/assets/images/features/node-details-panel.gif differ diff --git a/assets/images/features/node-selection.png b/assets/images/features/node-selection.png new file mode 100644 index 00000000..e9cd048a Binary files /dev/null and b/assets/images/features/node-selection.png differ diff --git a/assets/images/features/node.png b/assets/images/features/node.png new file mode 100644 index 00000000..c9387014 Binary files /dev/null and b/assets/images/features/node.png differ diff --git a/assets/images/features/preset-checks-prep.png b/assets/images/features/preset-checks-prep.png new file mode 100644 index 00000000..cc9d9470 Binary files /dev/null and b/assets/images/features/preset-checks-prep.png differ diff --git a/assets/images/features/preset-checks-template.png b/assets/images/features/preset-checks-template.png new file mode 100644 index 00000000..f59a1e0b Binary files /dev/null and b/assets/images/features/preset-checks-template.png differ diff --git a/assets/images/features/preset-checks.png b/assets/images/features/preset-checks.png new file mode 100644 index 00000000..a7558d97 Binary files /dev/null and b/assets/images/features/preset-checks.png differ diff --git a/assets/images/features/profile-diff.png b/assets/images/features/profile-diff.png new file mode 100644 index 00000000..75ed3519 Binary files /dev/null and b/assets/images/features/profile-diff.png differ diff --git a/assets/images/features/query-diff.gif b/assets/images/features/query-diff.gif new file mode 100644 index 00000000..fa9a82d7 Binary files /dev/null and b/assets/images/features/query-diff.gif differ diff --git a/assets/images/features/query-diff.png b/assets/images/features/query-diff.png new file mode 100644 index 00000000..9a73c192 Binary files /dev/null and b/assets/images/features/query-diff.png differ diff --git a/assets/images/features/row-count-diff-multiple.gif b/assets/images/features/row-count-diff-multiple.gif new file mode 100644 index 00000000..6625211f Binary files /dev/null and b/assets/images/features/row-count-diff-multiple.gif differ diff --git a/assets/images/features/row-count-diff-selector.gif b/assets/images/features/row-count-diff-selector.gif new file mode 100644 index 00000000..99e01bd3 Binary files /dev/null and b/assets/images/features/row-count-diff-selector.gif differ diff --git a/assets/images/features/row-count-diff-single.gif b/assets/images/features/row-count-diff-single.gif new file mode 100644 index 00000000..c1997934 Binary files /dev/null and b/assets/images/features/row-count-diff-single.gif differ diff --git a/assets/images/features/row-count-diff.png b/assets/images/features/row-count-diff.png new file mode 100644 index 00000000..42a0d826 Binary files /dev/null and b/assets/images/features/row-count-diff.png differ diff --git a/assets/images/features/schema-diff-node-selection.png b/assets/images/features/schema-diff-node-selection.png new file mode 100644 index 00000000..0b68f81b Binary files /dev/null and b/assets/images/features/schema-diff-node-selection.png differ diff --git a/assets/images/features/schema-diff.gif b/assets/images/features/schema-diff.gif new file mode 100644 index 00000000..8cb97200 Binary files /dev/null and b/assets/images/features/schema-diff.gif differ diff --git a/assets/images/features/schema-diff.png b/assets/images/features/schema-diff.png new file mode 100644 index 00000000..22a52771 Binary files /dev/null and b/assets/images/features/schema-diff.png differ diff --git a/assets/images/features/select-node-children.gif b/assets/images/features/select-node-children.gif new file mode 100644 index 00000000..04ee4d32 Binary files /dev/null and b/assets/images/features/select-node-children.gif differ diff --git a/assets/images/features/state-file-dev.png b/assets/images/features/state-file-dev.png new file mode 100644 index 00000000..092799a1 Binary files /dev/null and b/assets/images/features/state-file-dev.png differ diff --git a/assets/images/features/state-file-pr.png b/assets/images/features/state-file-pr.png new file mode 100644 index 00000000..0e069a96 Binary files /dev/null and b/assets/images/features/state-file-pr.png differ diff --git a/assets/images/features/top-k-diff.gif b/assets/images/features/top-k-diff.gif new file mode 100644 index 00000000..a21caab8 Binary files /dev/null and b/assets/images/features/top-k-diff.gif differ diff --git a/assets/images/features/top-k-diff.png b/assets/images/features/top-k-diff.png new file mode 100644 index 00000000..48b3fc8a Binary files /dev/null and b/assets/images/features/top-k-diff.png differ diff --git a/assets/images/features/value-diff-detail.gif b/assets/images/features/value-diff-detail.gif new file mode 100644 index 00000000..faf0fffe Binary files /dev/null and b/assets/images/features/value-diff-detail.gif differ diff --git a/assets/images/features/value-diff-multiple.gif b/assets/images/features/value-diff-multiple.gif new file mode 100644 index 00000000..1e5fff60 Binary files /dev/null and b/assets/images/features/value-diff-multiple.gif differ diff --git a/assets/images/features/value-diff.png b/assets/images/features/value-diff.png new file mode 100644 index 00000000..2e3d354a Binary files /dev/null and b/assets/images/features/value-diff.png differ diff --git a/assets/images/jaffle-shop/jaffle_shop_check.png b/assets/images/jaffle-shop/jaffle_shop_check.png new file mode 100644 index 00000000..dbd1fc67 Binary files /dev/null and b/assets/images/jaffle-shop/jaffle_shop_check.png differ diff --git a/assets/images/jaffle-shop/jaffle_shop_lineage.png b/assets/images/jaffle-shop/jaffle_shop_lineage.png new file mode 100644 index 00000000..301b21d2 Binary files /dev/null and b/assets/images/jaffle-shop/jaffle_shop_lineage.png differ diff --git a/assets/images/jaffle-shop/jaffle_shop_query.png b/assets/images/jaffle-shop/jaffle_shop_query.png new file mode 100644 index 00000000..1c2e4c31 Binary files /dev/null and b/assets/images/jaffle-shop/jaffle_shop_query.png differ diff --git a/assets/images/landing/cloud/approve-checks.png b/assets/images/landing/cloud/approve-checks.png new file mode 100644 index 00000000..48c8f756 Binary files /dev/null and b/assets/images/landing/cloud/approve-checks.png differ diff --git a/assets/images/landing/cloud/cloud-review.png b/assets/images/landing/cloud/cloud-review.png new file mode 100644 index 00000000..bc72c8b8 Binary files /dev/null and b/assets/images/landing/cloud/cloud-review.png differ diff --git a/assets/images/landing/cloud/continue.png b/assets/images/landing/cloud/continue.png new file mode 100644 index 00000000..ab74f5f1 Binary files /dev/null and b/assets/images/landing/cloud/continue.png differ diff --git a/assets/images/landing/cloud/data-team.png b/assets/images/landing/cloud/data-team.png new file mode 100644 index 00000000..7f630839 Binary files /dev/null and b/assets/images/landing/cloud/data-team.png differ diff --git a/assets/images/landing/cloud/review-together.png b/assets/images/landing/cloud/review-together.png new file mode 100644 index 00000000..85c5d104 Binary files /dev/null and b/assets/images/landing/cloud/review-together.png differ diff --git a/assets/images/landing/home/cross-env.png b/assets/images/landing/home/cross-env.png new file mode 100644 index 00000000..e09fae3c Binary files /dev/null and b/assets/images/landing/home/cross-env.png differ diff --git a/assets/images/landing/home/diffs.png b/assets/images/landing/home/diffs.png new file mode 100644 index 00000000..3fad8c88 Binary files /dev/null and b/assets/images/landing/home/diffs.png differ diff --git a/assets/images/landing/home/featured_image.png b/assets/images/landing/home/featured_image.png new file mode 100644 index 00000000..84bb57e0 Binary files /dev/null and b/assets/images/landing/home/featured_image.png differ diff --git a/assets/images/landing/home/recce-pr-comment.png b/assets/images/landing/home/recce-pr-comment.png new file mode 100644 index 00000000..63466de4 Binary files /dev/null and b/assets/images/landing/home/recce-pr-comment.png differ diff --git a/assets/images/onboarding/material.svg b/assets/images/onboarding/material.svg new file mode 100644 index 00000000..c84f1f5d --- /dev/null +++ b/assets/images/onboarding/material.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/pr/ci-cd.png b/assets/images/pr/ci-cd.png new file mode 100644 index 00000000..38b4c1d4 Binary files /dev/null and b/assets/images/pr/ci-cd.png differ diff --git a/assets/images/pr/copy-markdown-pr-comment.png b/assets/images/pr/copy-markdown-pr-comment.png new file mode 100644 index 00000000..48808982 Binary files /dev/null and b/assets/images/pr/copy-markdown-pr-comment.png differ diff --git a/assets/images/pr/copy-markdown.png b/assets/images/pr/copy-markdown.png new file mode 100644 index 00000000..ab16fd11 Binary files /dev/null and b/assets/images/pr/copy-markdown.png differ diff --git a/assets/images/pr/lineage-dbt.png b/assets/images/pr/lineage-dbt.png new file mode 100644 index 00000000..c59b7c31 Binary files /dev/null and b/assets/images/pr/lineage-dbt.png differ diff --git a/assets/images/pr/lineage-diff.png b/assets/images/pr/lineage-diff.png new file mode 100644 index 00000000..f9568220 Binary files /dev/null and b/assets/images/pr/lineage-diff.png differ diff --git a/assets/images/pr/value-diff.png b/assets/images/pr/value-diff.png new file mode 100644 index 00000000..bb09f003 Binary files /dev/null and b/assets/images/pr/value-diff.png differ diff --git a/assets/images/readme/recce-overview-screenshot.png b/assets/images/readme/recce-overview-screenshot.png new file mode 100644 index 00000000..1c27f33d Binary files /dev/null and b/assets/images/readme/recce-overview-screenshot.png differ diff --git a/assets/images/recce-cloud/app-install-authorize.png b/assets/images/recce-cloud/app-install-authorize.png new file mode 100644 index 00000000..b2ee61ff Binary files /dev/null and b/assets/images/recce-cloud/app-install-authorize.png differ diff --git a/assets/images/recce-cloud/app-install.png b/assets/images/recce-cloud/app-install.png new file mode 100644 index 00000000..dd80f010 Binary files /dev/null and b/assets/images/recce-cloud/app-install.png differ diff --git a/assets/images/recce-cloud/branch-merged-delete-codespace.png b/assets/images/recce-cloud/branch-merged-delete-codespace.png new file mode 100644 index 00000000..d204edfa Binary files /dev/null and b/assets/images/recce-cloud/branch-merged-delete-codespace.png differ diff --git a/assets/images/recce-cloud/check-codespace-in-github.png b/assets/images/recce-cloud/check-codespace-in-github.png new file mode 100644 index 00000000..618489cb Binary files /dev/null and b/assets/images/recce-cloud/check-codespace-in-github.png differ diff --git a/assets/images/recce-cloud/checks.png b/assets/images/recce-cloud/checks.png new file mode 100644 index 00000000..8d17ae4e Binary files /dev/null and b/assets/images/recce-cloud/checks.png differ diff --git a/assets/images/recce-cloud/codespace-troubleshoot-1.png b/assets/images/recce-cloud/codespace-troubleshoot-1.png new file mode 100644 index 00000000..a27b318a Binary files /dev/null and b/assets/images/recce-cloud/codespace-troubleshoot-1.png differ diff --git a/assets/images/recce-cloud/codespace-troubleshoot-2.png b/assets/images/recce-cloud/codespace-troubleshoot-2.png new file mode 100644 index 00000000..ab5eea07 Binary files /dev/null and b/assets/images/recce-cloud/codespace-troubleshoot-2.png differ diff --git a/assets/images/recce-cloud/codespaces-queued.png b/assets/images/recce-cloud/codespaces-queued.png new file mode 100644 index 00000000..0cfa011f Binary files /dev/null and b/assets/images/recce-cloud/codespaces-queued.png differ diff --git a/assets/images/recce-cloud/create-in-codespace.png b/assets/images/recce-cloud/create-in-codespace.png new file mode 100644 index 00000000..9422caa9 Binary files /dev/null and b/assets/images/recce-cloud/create-in-codespace.png differ diff --git a/assets/images/recce-cloud/dbt-artifacts.png b/assets/images/recce-cloud/dbt-artifacts.png new file mode 100644 index 00000000..7f14016f Binary files /dev/null and b/assets/images/recce-cloud/dbt-artifacts.png differ diff --git a/assets/images/recce-cloud/dbt-cloud-api-trigger.png b/assets/images/recce-cloud/dbt-cloud-api-trigger.png new file mode 100644 index 00000000..afa0626b Binary files /dev/null and b/assets/images/recce-cloud/dbt-cloud-api-trigger.png differ diff --git a/assets/images/recce-cloud/dbt-cloud-deploy-generate-docs.png b/assets/images/recce-cloud/dbt-cloud-deploy-generate-docs.png new file mode 100644 index 00000000..2e61abf5 Binary files /dev/null and b/assets/images/recce-cloud/dbt-cloud-deploy-generate-docs.png differ diff --git a/assets/images/recce-cloud/delete-codespace-in-github.png b/assets/images/recce-cloud/delete-codespace-in-github.png new file mode 100644 index 00000000..668bafe2 Binary files /dev/null and b/assets/images/recce-cloud/delete-codespace-in-github.png differ diff --git a/assets/images/recce-cloud/github-token.png b/assets/images/recce-cloud/github-token.png new file mode 100644 index 00000000..5697fc3a Binary files /dev/null and b/assets/images/recce-cloud/github-token.png differ diff --git a/assets/images/recce-cloud/ngrok-expose.png b/assets/images/recce-cloud/ngrok-expose.png new file mode 100644 index 00000000..223cdf93 Binary files /dev/null and b/assets/images/recce-cloud/ngrok-expose.png differ diff --git a/assets/images/recce-cloud/open-codespace-in-browser.png b/assets/images/recce-cloud/open-codespace-in-browser.png new file mode 100644 index 00000000..d1299f16 Binary files /dev/null and b/assets/images/recce-cloud/open-codespace-in-browser.png differ diff --git a/assets/images/recce-cloud/pr-checks-all-approved.png b/assets/images/recce-cloud/pr-checks-all-approved.png new file mode 100644 index 00000000..9342a2db Binary files /dev/null and b/assets/images/recce-cloud/pr-checks-all-approved.png differ diff --git a/assets/images/recce-cloud/pr-checks-wo-approved.png b/assets/images/recce-cloud/pr-checks-wo-approved.png new file mode 100644 index 00000000..fb07dbf0 Binary files /dev/null and b/assets/images/recce-cloud/pr-checks-wo-approved.png differ diff --git a/assets/images/recce-cloud/query-diff.png b/assets/images/recce-cloud/query-diff.png new file mode 100644 index 00000000..f423855f Binary files /dev/null and b/assets/images/recce-cloud/query-diff.png differ diff --git a/assets/images/recce-cloud/recce-active.png b/assets/images/recce-cloud/recce-active.png new file mode 100644 index 00000000..da2c4ac8 Binary files /dev/null and b/assets/images/recce-cloud/recce-active.png differ diff --git a/assets/images/recce-cloud/recce-cloud-home.png b/assets/images/recce-cloud/recce-cloud-home.png new file mode 100644 index 00000000..84bd6e8c Binary files /dev/null and b/assets/images/recce-cloud/recce-cloud-home.png differ diff --git a/assets/images/recce-cloud/recce-cloud-open-pr.png b/assets/images/recce-cloud/recce-cloud-open-pr.png new file mode 100644 index 00000000..22cb74be Binary files /dev/null and b/assets/images/recce-cloud/recce-cloud-open-pr.png differ diff --git a/assets/images/recce-cloud/repo-list.png b/assets/images/recce-cloud/repo-list.png new file mode 100644 index 00000000..e965cd6f Binary files /dev/null and b/assets/images/recce-cloud/repo-list.png differ diff --git a/assets/images/recce-cloud/self-hosted-architecture.png b/assets/images/recce-cloud/self-hosted-architecture.png new file mode 100644 index 00000000..7a7718a6 Binary files /dev/null and b/assets/images/recce-cloud/self-hosted-architecture.png differ diff --git a/assets/images/recce-cloud/self-hosted-instance-lifecycle.png b/assets/images/recce-cloud/self-hosted-instance-lifecycle.png new file mode 100644 index 00000000..cf026f78 Binary files /dev/null and b/assets/images/recce-cloud/self-hosted-instance-lifecycle.png differ diff --git a/assets/images/recce-cloud/set-prebuild-specific-regions.png b/assets/images/recce-cloud/set-prebuild-specific-regions.png new file mode 100644 index 00000000..25e0782e Binary files /dev/null and b/assets/images/recce-cloud/set-prebuild-specific-regions.png differ diff --git a/assets/images/recce-cloud/setup-architecture.png b/assets/images/recce-cloud/setup-architecture.png new file mode 100644 index 00000000..1832a54e Binary files /dev/null and b/assets/images/recce-cloud/setup-architecture.png differ diff --git a/assets/images/recce-cloud/setup-codespaces-pr.png b/assets/images/recce-cloud/setup-codespaces-pr.png new file mode 100644 index 00000000..8394dbe3 Binary files /dev/null and b/assets/images/recce-cloud/setup-codespaces-pr.png differ diff --git a/assets/images/recce-cloud/setup-run-base.png b/assets/images/recce-cloud/setup-run-base.png new file mode 100644 index 00000000..42e5055b Binary files /dev/null and b/assets/images/recce-cloud/setup-run-base.png differ diff --git a/assets/images/recce-cloud/setup-run-pr.png b/assets/images/recce-cloud/setup-run-pr.png new file mode 100644 index 00000000..c1e80d06 Binary files /dev/null and b/assets/images/recce-cloud/setup-run-pr.png differ diff --git a/assets/images/recce-cloud/sign-in-authorize.png b/assets/images/recce-cloud/sign-in-authorize.png new file mode 100644 index 00000000..56571864 Binary files /dev/null and b/assets/images/recce-cloud/sign-in-authorize.png differ diff --git a/assets/images/recce-cloud/sign-in.png b/assets/images/recce-cloud/sign-in.png new file mode 100644 index 00000000..a3c8f324 Binary files /dev/null and b/assets/images/recce-cloud/sign-in.png differ diff --git a/assets/images/recce-cloud/sse-c.png b/assets/images/recce-cloud/sse-c.png new file mode 100644 index 00000000..46663700 Binary files /dev/null and b/assets/images/recce-cloud/sse-c.png differ diff --git a/assets/images/recce-cloud/tailscale-dashboard-fs8.png b/assets/images/recce-cloud/tailscale-dashboard-fs8.png new file mode 100644 index 00000000..6cbc1d36 Binary files /dev/null and b/assets/images/recce-cloud/tailscale-dashboard-fs8.png differ diff --git a/assets/images/recce-cloud/tailscale-expose.png b/assets/images/recce-cloud/tailscale-expose.png new file mode 100644 index 00000000..a2659957 Binary files /dev/null and b/assets/images/recce-cloud/tailscale-expose.png differ diff --git a/assets/images/recce-logo-orange.png b/assets/images/recce-logo-orange.png new file mode 100644 index 00000000..2f80e996 Binary files /dev/null and b/assets/images/recce-logo-orange.png differ diff --git a/assets/images/recce-logo-stacked.png b/assets/images/recce-logo-stacked.png new file mode 100644 index 00000000..59fe94e3 Binary files /dev/null and b/assets/images/recce-logo-stacked.png differ diff --git a/assets/images/recce-logo-white.png b/assets/images/recce-logo-white.png new file mode 100644 index 00000000..ad04c76c Binary files /dev/null and b/assets/images/recce-logo-white.png differ diff --git a/assets/images/recce-og-image.png b/assets/images/recce-og-image.png new file mode 100644 index 00000000..e987927e Binary files /dev/null and b/assets/images/recce-og-image.png differ diff --git a/assets/images/recce-orange-bg_100.png b/assets/images/recce-orange-bg_100.png new file mode 100644 index 00000000..2323e8a3 Binary files /dev/null and b/assets/images/recce-orange-bg_100.png differ diff --git a/assets/images/recce-orange-bg_140.png b/assets/images/recce-orange-bg_140.png new file mode 100644 index 00000000..c5d3d030 Binary files /dev/null and b/assets/images/recce-orange-bg_140.png differ diff --git a/assets/images/recce-orange-bg_200.png b/assets/images/recce-orange-bg_200.png new file mode 100644 index 00000000..390b5982 Binary files /dev/null and b/assets/images/recce-orange-bg_200.png differ diff --git a/assets/images/recce-ui-elements.png b/assets/images/recce-ui-elements.png new file mode 100644 index 00000000..1c27f33d Binary files /dev/null and b/assets/images/recce-ui-elements.png differ diff --git a/assets/images/social/blog/archive/2024.png b/assets/images/social/blog/archive/2024.png new file mode 100644 index 00000000..647bc59b Binary files /dev/null and b/assets/images/social/blog/archive/2024.png differ diff --git a/assets/images/social/blog/category/announcements.png b/assets/images/social/blog/category/announcements.png new file mode 100644 index 00000000..de2827da Binary files /dev/null and b/assets/images/social/blog/category/announcements.png differ diff --git a/assets/images/social/blog/category/best-practices.png b/assets/images/social/blog/category/best-practices.png new file mode 100644 index 00000000..8d1f5da7 Binary files /dev/null and b/assets/images/social/blog/category/best-practices.png differ diff --git a/assets/images/social/blog/category/concepts.png b/assets/images/social/blog/category/concepts.png new file mode 100644 index 00000000..dbb5fb25 Binary files /dev/null and b/assets/images/social/blog/category/concepts.png differ diff --git a/assets/images/social/blog/category/data-exploration.png b/assets/images/social/blog/category/data-exploration.png new file mode 100644 index 00000000..70c3437c Binary files /dev/null and b/assets/images/social/blog/category/data-exploration.png differ diff --git a/assets/images/social/blog/category/data-validation.png b/assets/images/social/blog/category/data-validation.png new file mode 100644 index 00000000..5b189985 Binary files /dev/null and b/assets/images/social/blog/category/data-validation.png differ diff --git a/assets/images/social/blog/category/dbt.png b/assets/images/social/blog/category/dbt.png new file mode 100644 index 00000000..ab011dcf Binary files /dev/null and b/assets/images/social/blog/category/dbt.png differ diff --git a/assets/images/social/blog/category/features.png b/assets/images/social/blog/category/features.png new file mode 100644 index 00000000..eb962087 Binary files /dev/null and b/assets/images/social/blog/category/features.png differ diff --git a/assets/images/social/blog/category/impact-assessment.png b/assets/images/social/blog/category/impact-assessment.png new file mode 100644 index 00000000..66d3a5a9 Binary files /dev/null and b/assets/images/social/blog/category/impact-assessment.png differ diff --git a/assets/images/social/blog/category/pr-review.png b/assets/images/social/blog/category/pr-review.png new file mode 100644 index 00000000..5b1d01db Binary files /dev/null and b/assets/images/social/blog/category/pr-review.png differ diff --git a/assets/images/social/blog/category/pull-request.png b/assets/images/social/blog/category/pull-request.png new file mode 100644 index 00000000..9bd008b3 Binary files /dev/null and b/assets/images/social/blog/category/pull-request.png differ diff --git a/assets/images/social/blog/category/self-serve-review.png b/assets/images/social/blog/category/self-serve-review.png new file mode 100644 index 00000000..7d88c253 Binary files /dev/null and b/assets/images/social/blog/category/self-serve-review.png differ diff --git a/assets/images/social/blog/category/usage.png b/assets/images/social/blog/category/usage.png new file mode 100644 index 00000000..9702b2df Binary files /dev/null and b/assets/images/social/blog/category/usage.png differ diff --git a/assets/images/social/blog/category/webinar.png b/assets/images/social/blog/category/webinar.png new file mode 100644 index 00000000..a4685cfa Binary files /dev/null and b/assets/images/social/blog/category/webinar.png differ diff --git a/assets/images/social/blog/index.png b/assets/images/social/blog/index.png new file mode 100644 index 00000000..26cf8058 Binary files /dev/null and b/assets/images/social/blog/index.png differ diff --git a/assets/images/social/blog/posts/2024-03-04_recce-introduction.png b/assets/images/social/blog/posts/2024-03-04_recce-introduction.png new file mode 100644 index 00000000..e67cc86f Binary files /dev/null and b/assets/images/social/blog/posts/2024-03-04_recce-introduction.png differ diff --git a/assets/images/social/blog/posts/2024-03-11_recce-hands-on.png b/assets/images/social/blog/posts/2024-03-11_recce-hands-on.png new file mode 100644 index 00000000..7705d738 Binary files /dev/null and b/assets/images/social/blog/posts/2024-03-11_recce-hands-on.png differ diff --git a/assets/images/social/blog/posts/2024-03-20_histogram-overlay-top-k-dbt.png b/assets/images/social/blog/posts/2024-03-20_histogram-overlay-top-k-dbt.png new file mode 100644 index 00000000..641cf8ce Binary files /dev/null and b/assets/images/social/blog/posts/2024-03-20_histogram-overlay-top-k-dbt.png differ diff --git a/assets/images/social/blog/posts/2024-06-20_check-critical-models.png b/assets/images/social/blog/posts/2024-06-20_check-critical-models.png new file mode 100644 index 00000000..321cc965 Binary files /dev/null and b/assets/images/social/blog/posts/2024-06-20_check-critical-models.png differ diff --git a/assets/images/social/blog/posts/2024-08-27_firesidechat.png b/assets/images/social/blog/posts/2024-08-27_firesidechat.png new file mode 100644 index 00000000..fed2f91b Binary files /dev/null and b/assets/images/social/blog/posts/2024-08-27_firesidechat.png differ diff --git a/assets/images/social/blog/posts/2024-09-16_support-self-serve-data-pr-review.png b/assets/images/social/blog/posts/2024-09-16_support-self-serve-data-pr-review.png new file mode 100644 index 00000000..e4f9af05 Binary files /dev/null and b/assets/images/social/blog/posts/2024-09-16_support-self-serve-data-pr-review.png differ diff --git a/assets/images/social/blog/posts/2024-09-19_coalesce2024.png b/assets/images/social/blog/posts/2024-09-19_coalesce2024.png new file mode 100644 index 00000000..4df207a5 Binary files /dev/null and b/assets/images/social/blog/posts/2024-09-19_coalesce2024.png differ diff --git a/assets/images/social/blog/posts/2024-09-25_dbt-data-pr-comment-template.png b/assets/images/social/blog/posts/2024-09-25_dbt-data-pr-comment-template.png new file mode 100644 index 00000000..01779b58 Binary files /dev/null and b/assets/images/social/blog/posts/2024-09-25_dbt-data-pr-comment-template.png differ diff --git a/assets/images/social/blog/posts/2024-10-09_explore-data-impact-with-focus.png b/assets/images/social/blog/posts/2024-10-09_explore-data-impact-with-focus.png new file mode 100644 index 00000000..6b3e0e4a Binary files /dev/null and b/assets/images/social/blog/posts/2024-10-09_explore-data-impact-with-focus.png differ diff --git a/assets/images/social/blog/tags.png b/assets/images/social/blog/tags.png new file mode 100644 index 00000000..1dc64984 Binary files /dev/null and b/assets/images/social/blog/tags.png differ diff --git a/assets/images/social/case-studies.png b/assets/images/social/case-studies.png new file mode 100644 index 00000000..7951dbed Binary files /dev/null and b/assets/images/social/case-studies.png differ diff --git a/assets/images/social/case-studies/rio-department-of-health.png b/assets/images/social/case-studies/rio-department-of-health.png new file mode 100644 index 00000000..903ea2d8 Binary files /dev/null and b/assets/images/social/case-studies/rio-department-of-health.png differ diff --git a/assets/images/social/cloud.png b/assets/images/social/cloud.png new file mode 100644 index 00000000..67c9389f Binary files /dev/null and b/assets/images/social/cloud.png differ diff --git a/assets/images/social/docs/architecture/overview.png b/assets/images/social/docs/architecture/overview.png new file mode 100644 index 00000000..e54c18ed Binary files /dev/null and b/assets/images/social/docs/architecture/overview.png differ diff --git a/assets/images/social/docs/demo.png b/assets/images/social/docs/demo.png new file mode 100644 index 00000000..fbf20372 Binary files /dev/null and b/assets/images/social/docs/demo.png differ diff --git a/assets/images/social/docs/features/checklist.png b/assets/images/social/docs/features/checklist.png new file mode 100644 index 00000000..bb979a5b Binary files /dev/null and b/assets/images/social/docs/features/checklist.png differ diff --git a/assets/images/social/docs/features/lineage.png b/assets/images/social/docs/features/lineage.png new file mode 100644 index 00000000..82b03e62 Binary files /dev/null and b/assets/images/social/docs/features/lineage.png differ diff --git a/assets/images/social/docs/features/node-selection.png b/assets/images/social/docs/features/node-selection.png new file mode 100644 index 00000000..7a581a02 Binary files /dev/null and b/assets/images/social/docs/features/node-selection.png differ diff --git a/assets/images/social/docs/features/preset-checks.png b/assets/images/social/docs/features/preset-checks.png new file mode 100644 index 00000000..e4b54cbb Binary files /dev/null and b/assets/images/social/docs/features/preset-checks.png differ diff --git a/assets/images/social/docs/features/query.png b/assets/images/social/docs/features/query.png new file mode 100644 index 00000000..12d5d3f7 Binary files /dev/null and b/assets/images/social/docs/features/query.png differ diff --git a/assets/images/social/docs/features/recce-run.png b/assets/images/social/docs/features/recce-run.png new file mode 100644 index 00000000..40e3c651 Binary files /dev/null and b/assets/images/social/docs/features/recce-run.png differ diff --git a/assets/images/social/docs/features/recce-summary-example.png b/assets/images/social/docs/features/recce-summary-example.png new file mode 100644 index 00000000..2f56cef0 Binary files /dev/null and b/assets/images/social/docs/features/recce-summary-example.png differ diff --git a/assets/images/social/docs/features/recce-summary.png b/assets/images/social/docs/features/recce-summary.png new file mode 100644 index 00000000..8b556453 Binary files /dev/null and b/assets/images/social/docs/features/recce-summary.png differ diff --git a/assets/images/social/docs/features/state-file.png b/assets/images/social/docs/features/state-file.png new file mode 100644 index 00000000..fd27d623 Binary files /dev/null and b/assets/images/social/docs/features/state-file.png differ diff --git a/assets/images/social/docs/get-started-jaffle-shop.png b/assets/images/social/docs/get-started-jaffle-shop.png new file mode 100644 index 00000000..01366556 Binary files /dev/null and b/assets/images/social/docs/get-started-jaffle-shop.png differ diff --git a/assets/images/social/docs/get-started.png b/assets/images/social/docs/get-started.png new file mode 100644 index 00000000..a1494337 Binary files /dev/null and b/assets/images/social/docs/get-started.png differ diff --git a/assets/images/social/docs/guides/best-practices-prep-env.png b/assets/images/social/docs/guides/best-practices-prep-env.png new file mode 100644 index 00000000..1b86fadc Binary files /dev/null and b/assets/images/social/docs/guides/best-practices-prep-env.png differ diff --git a/assets/images/social/docs/guides/scenario-ci.png b/assets/images/social/docs/guides/scenario-ci.png new file mode 100644 index 00000000..6345be31 Binary files /dev/null and b/assets/images/social/docs/guides/scenario-ci.png differ diff --git a/assets/images/social/docs/guides/scenario-dev.png b/assets/images/social/docs/guides/scenario-dev.png new file mode 100644 index 00000000..23266575 Binary files /dev/null and b/assets/images/social/docs/guides/scenario-dev.png differ diff --git a/assets/images/social/docs/guides/scenario-pr-review.png b/assets/images/social/docs/guides/scenario-pr-review.png new file mode 100644 index 00000000..5b1d01db Binary files /dev/null and b/assets/images/social/docs/guides/scenario-pr-review.png differ diff --git a/assets/images/social/docs/index.png b/assets/images/social/docs/index.png new file mode 100644 index 00000000..e54c18ed Binary files /dev/null and b/assets/images/social/docs/index.png differ diff --git a/assets/images/social/docs/installation.png b/assets/images/social/docs/installation.png new file mode 100644 index 00000000..cdfde759 Binary files /dev/null and b/assets/images/social/docs/installation.png differ diff --git a/assets/images/social/docs/recce-cloud/architecture/index.png b/assets/images/social/docs/recce-cloud/architecture/index.png new file mode 100644 index 00000000..e54c18ed Binary files /dev/null and b/assets/images/social/docs/recce-cloud/architecture/index.png differ diff --git a/assets/images/social/docs/recce-cloud/architecture/security.png b/assets/images/social/docs/recce-cloud/architecture/security.png new file mode 100644 index 00000000..8ce34b44 Binary files /dev/null and b/assets/images/social/docs/recce-cloud/architecture/security.png differ diff --git a/assets/images/social/docs/recce-cloud/architecture/self-hosted.png b/assets/images/social/docs/recce-cloud/architecture/self-hosted.png new file mode 100644 index 00000000..0fb33943 Binary files /dev/null and b/assets/images/social/docs/recce-cloud/architecture/self-hosted.png differ diff --git a/assets/images/social/docs/recce-cloud/expose-recce-instance-visibility.png b/assets/images/social/docs/recce-cloud/expose-recce-instance-visibility.png new file mode 100644 index 00000000..1e0ae112 Binary files /dev/null and b/assets/images/social/docs/recce-cloud/expose-recce-instance-visibility.png differ diff --git a/assets/images/social/docs/recce-cloud/getting-started-recce-cloud.png b/assets/images/social/docs/recce-cloud/getting-started-recce-cloud.png new file mode 100644 index 00000000..b8cb9bdc Binary files /dev/null and b/assets/images/social/docs/recce-cloud/getting-started-recce-cloud.png differ diff --git a/assets/images/social/docs/recce-cloud/index.png b/assets/images/social/docs/recce-cloud/index.png new file mode 100644 index 00000000..e54c18ed Binary files /dev/null and b/assets/images/social/docs/recce-cloud/index.png differ diff --git a/assets/images/social/docs/recce-cloud/setup-gh-actions.png b/assets/images/social/docs/recce-cloud/setup-gh-actions.png new file mode 100644 index 00000000..55da3dd8 Binary files /dev/null and b/assets/images/social/docs/recce-cloud/setup-gh-actions.png differ diff --git a/assets/images/social/docs/recce-cloud/setup-gh-codespaces.png b/assets/images/social/docs/recce-cloud/setup-gh-codespaces.png new file mode 100644 index 00000000..8a61563d Binary files /dev/null and b/assets/images/social/docs/recce-cloud/setup-gh-codespaces.png differ diff --git a/assets/images/social/docs/reference/configuration.png b/assets/images/social/docs/reference/configuration.png new file mode 100644 index 00000000..e55341a1 Binary files /dev/null and b/assets/images/social/docs/reference/configuration.png differ diff --git a/assets/images/social/docs/start-with-dbt-cloud.png b/assets/images/social/docs/start-with-dbt-cloud.png new file mode 100644 index 00000000..5cfbce1c Binary files /dev/null and b/assets/images/social/docs/start-with-dbt-cloud.png differ diff --git a/assets/images/social/docs/start-with-sqlmesh.png b/assets/images/social/docs/start-with-sqlmesh.png new file mode 100644 index 00000000..ea855001 Binary files /dev/null and b/assets/images/social/docs/start-with-sqlmesh.png differ diff --git a/assets/images/social/firesidechat-watch.png b/assets/images/social/firesidechat-watch.png new file mode 100644 index 00000000..b0ff5573 Binary files /dev/null and b/assets/images/social/firesidechat-watch.png differ diff --git a/assets/images/social/firesidechat.png b/assets/images/social/firesidechat.png new file mode 100644 index 00000000..49c88e0b Binary files /dev/null and b/assets/images/social/firesidechat.png differ diff --git a/assets/images/social/index.png b/assets/images/social/index.png new file mode 100644 index 00000000..e7c571b1 Binary files /dev/null and b/assets/images/social/index.png differ diff --git a/assets/images/sqlmesh/sqlmesh-lineage.png b/assets/images/sqlmesh/sqlmesh-lineage.png new file mode 100644 index 00000000..43fe5a85 Binary files /dev/null and b/assets/images/sqlmesh/sqlmesh-lineage.png differ diff --git a/assets/images/sqlmesh/sqlmesh-query-diff.png b/assets/images/sqlmesh/sqlmesh-query-diff.png new file mode 100644 index 00000000..cbbe80bb Binary files /dev/null and b/assets/images/sqlmesh/sqlmesh-query-diff.png differ diff --git a/assets/images/temp.png b/assets/images/temp.png new file mode 100644 index 00000000..fc894ea6 Binary files /dev/null and b/assets/images/temp.png differ diff --git a/assets/images/testimonials/testimonial-1.jpg b/assets/images/testimonials/testimonial-1.jpg new file mode 100644 index 00000000..2da61e01 Binary files /dev/null and b/assets/images/testimonials/testimonial-1.jpg differ diff --git a/assets/images/testimonials/testimonial-2.jpg b/assets/images/testimonials/testimonial-2.jpg new file mode 100644 index 00000000..3fc008fa Binary files /dev/null and b/assets/images/testimonials/testimonial-2.jpg differ diff --git a/assets/images/testimonials/testimonial-3.jpg b/assets/images/testimonials/testimonial-3.jpg new file mode 100644 index 00000000..f61e4a30 Binary files /dev/null and b/assets/images/testimonials/testimonial-3.jpg differ diff --git a/assets/images/testimonials/testimonial-4.jpg b/assets/images/testimonials/testimonial-4.jpg new file mode 100644 index 00000000..3fe70e4e Binary files /dev/null and b/assets/images/testimonials/testimonial-4.jpg differ diff --git a/assets/images/testimonials/testimonial-5.jpg b/assets/images/testimonials/testimonial-5.jpg new file mode 100644 index 00000000..31a5e103 Binary files /dev/null and b/assets/images/testimonials/testimonial-5.jpg differ diff --git a/assets/javascripts/bundle.83f73b43.min.js b/assets/javascripts/bundle.83f73b43.min.js new file mode 100644 index 00000000..43d8b70f --- /dev/null +++ b/assets/javascripts/bundle.83f73b43.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Wi=Object.create;var gr=Object.defineProperty;var Di=Object.getOwnPropertyDescriptor;var Vi=Object.getOwnPropertyNames,Vt=Object.getOwnPropertySymbols,Ni=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,ao=Object.prototype.propertyIsEnumerable;var io=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&io(e,r,t[r]);if(Vt)for(var r of Vt(t))ao.call(t,r)&&io(e,r,t[r]);return e};var so=(e,t)=>{var r={};for(var o in e)yr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Vt)for(var o of Vt(e))t.indexOf(o)<0&&ao.call(e,o)&&(r[o]=e[o]);return r};var xr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var zi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Vi(t))!yr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=Di(t,n))||o.enumerable});return e};var Mt=(e,t,r)=>(r=e!=null?Wi(Ni(e)):{},zi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var co=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var lo=xr((Er,po)=>{(function(e,t){typeof Er=="object"&&typeof po!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function p(k){var ft=k.type,qe=k.tagName;return!!(qe==="INPUT"&&a[ft]&&!k.readOnly||qe==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function c(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(k){o=!1}function d(k){s(k.target)&&(o||p(k.target))&&c(k.target)}function y(k){s(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function L(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function te(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,te())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",L,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",y,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var qr=xr((hy,On)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var $a=/["'&<>]/;On.exports=Pa;function Pa(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof It=="object"&&typeof Yr=="object"?Yr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof It=="object"?It.ClipboardJS=r():t.ClipboardJS=r()})(It,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ui}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(A){return!1}}var d=function(A){var M=f()(A);return u("cut"),M},y=d;function L(V){var A=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[A?"right":"left"]="-9999px";var F=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(F,"px"),M.setAttribute("readonly",""),M.value=V,M}var X=function(A,M){var F=L(A);M.container.appendChild(F);var D=f()(F);return u("copy"),F.remove(),D},te=function(A){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},F="";return typeof A=="string"?F=X(A,M):A instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(A==null?void 0:A.type)?F=X(A.value,M):(F=f()(A),u("copy")),F},J=te;function k(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(M){return typeof M}:k=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},k(V)}var ft=function(){var A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=A.action,F=M===void 0?"copy":M,D=A.container,Y=A.target,$e=A.text;if(F!=="copy"&&F!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&k(Y)==="object"&&Y.nodeType===1){if(F==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(F==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if($e)return J($e,{container:D});if(Y)return F==="cut"?y(Y):J(Y,{container:D})},qe=ft;function Fe(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(M){return typeof M}:Fe=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Fe(V)}function ki(V,A){if(!(V instanceof A))throw new TypeError("Cannot call a class as a function")}function no(V,A){for(var M=0;M0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Fe(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function($e){return Y.onClick($e)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,$e=this.action(Y)||"copy",Dt=qe({action:$e,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Dt?"success":"error",{action:$e,text:Dt,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return y(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,$e=!!document.queryCommandSupported;return Y.forEach(function(Dt){$e=$e&&!!document.queryCommandSupported(Dt)}),$e}}]),M}(s()),Ui=Fi},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,d,y){var L=c.apply(this,arguments);return l.addEventListener(u,L,y),{destroy:function(){l.removeEventListener(u,L,y)}}}function p(l,f,u,d,y){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(L){return s(L,f,u,d,y)}))}function c(l,f,u,d){return function(y){y.delegateTarget=a(y.target,f),y.delegateTarget&&d.call(l,y)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,d,y){if(!u&&!d&&!y)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(y))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,d,y);if(a.nodeList(u))return l(u,d,y);if(a.string(u))return f(u,d,y);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,d,y){return u.addEventListener(d,y),{destroy:function(){u.removeEventListener(d,y)}}}function l(u,d,y){return Array.prototype.forEach.call(u,function(L){L.addEventListener(d,y)}),{destroy:function(){Array.prototype.forEach.call(u,function(L){L.removeEventListener(d,y)})}}}function f(u,d,y){return s(document.body,u,d,y)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||p(d,L)})},y&&(n[d]=y(n[d])))}function p(d,y){try{c(o[d](y))}catch(L){u(i[0][3],L)}}function c(d){d.value instanceof nt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){p("next",d)}function f(d){p("throw",d)}function u(d,y){d(y),i.shift(),i.length&&p(i[0][0],i[0][1])}}function uo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof he=="function"?he(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function H(e){return typeof e=="function"}function ut(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ut(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ue=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=he(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(L){t={error:L}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(H(l))try{l()}catch(L){i=L instanceof zt?L.errors:[L]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=he(f),d=u.next();!d.done;d=u.next()){var y=d.value;try{ho(y)}catch(L){i=i!=null?i:[],L instanceof zt?i=q(q([],N(i)),N(L.errors)):i.push(L)}}}catch(L){o={error:L}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ho(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Ue.EMPTY;function qt(e){return e instanceof Ue||e&&"closed"in e&&H(e.remove)&&H(e.add)&&H(e.unsubscribe)}function ho(e){H(e)?e():e.unsubscribe()}var Pe={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var dt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Ue(function(){o.currentObservers=null,Qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(j);var To=function(e){oe(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){oe(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var At={now:function(){return(At.delegate||Date).now()},delegate:void 0};var Ct=function(e){oe(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=At);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(gt);var Lo=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(yt);var kr=new Lo(Oo);var Mo=function(e){oe(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=vt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(vt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(gt);var _o=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(yt);var me=new _o(Mo);var S=new j(function(e){return e.complete()});function Yt(e){return e&&H(e.schedule)}function Hr(e){return e[e.length-1]}function Xe(e){return H(Hr(e))?e.pop():void 0}function ke(e){return Yt(Hr(e))?e.pop():void 0}function Bt(e,t){return typeof Hr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return H(e==null?void 0:e.then)}function Jt(e){return H(e[bt])}function Xt(e){return Symbol.asyncIterator&&H(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Zi();function tr(e){return H(e==null?void 0:e[er])}function rr(e){return fo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return H(e==null?void 0:e.getReader)}function U(e){if(e instanceof j)return e;if(e!=null){if(Jt(e))return ea(e);if(xt(e))return ta(e);if(Gt(e))return ra(e);if(Xt(e))return Ao(e);if(tr(e))return oa(e);if(or(e))return na(e)}throw Zt(e)}function ea(e){return new j(function(t){var r=e[bt]();if(H(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ta(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?De(t):Qo(function(){return new ir}))}}function jr(e){return e<=0?function(){return S}:E(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,d=0,y=!1,L=!1,X=function(){f==null||f.unsubscribe(),f=void 0},te=function(){X(),l=u=void 0,y=L=!1},J=function(){var k=l;te(),k==null||k.unsubscribe()};return E(function(k,ft){d++,!L&&!y&&X();var qe=u=u!=null?u:r();ft.add(function(){d--,d===0&&!L&&!y&&(f=Ur(J,p))}),qe.subscribe(ft),!l&&d>0&&(l=new at({next:function(Fe){return qe.next(Fe)},error:function(Fe){L=!0,X(),f=Ur(te,n,Fe),qe.error(Fe)},complete:function(){y=!0,X(),f=Ur(te,a),qe.complete()}}),U(k).subscribe(l))})(c)}}function Ur(e,t){for(var r=[],o=2;oe.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function R(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Ie(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var wa=O(h(document.body,"focusin"),h(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Ie()||document.body),G(1));function et(e){return wa.pipe(m(t=>e.contains(t)),K())}function $t(e,t){return C(()=>O(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Le(+!r*t)):le,Q(e.matches(":hover"))))}function Jo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Jo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Jo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Tt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),O(h(t,"load"),h(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),Te(1))))}var Xo=new g,Ta=C(()=>typeof ResizeObserver=="undefined"?Tt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Xo.next(t)))),v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ta.pipe(w(r=>r.observe(t)),v(r=>Xo.pipe(b(o=>o.target===t),_(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function St(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Zo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ve(e){return{x:e.offsetLeft,y:e.offsetTop}}function en(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function tn(e){return O(h(window,"load"),h(window,"resize")).pipe(Me(0,me),m(()=>Ve(e)),Q(Ve(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ne(e){return O(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe(Me(0,me),m(()=>pr(e)),Q(pr(e)))}var rn=new g,Sa=C(()=>I(new IntersectionObserver(e=>{for(let t of e)rn.next(t)},{threshold:0}))).pipe(v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function tt(e){return Sa.pipe(w(t=>t.observe(e)),v(t=>rn.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function on(e,t=16){return Ne(e).pipe(m(({y:r})=>{let o=ce(e),n=St(e);return r>=n.height-o.height-t}),K())}var lr={drawer:R("[data-md-toggle=drawer]"),search:R("[data-md-toggle=search]")};function nn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function ze(e){let t=lr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function Oa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function La(){return O(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function an(){let e=h(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:nn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Ie();if(typeof o!="undefined")return!Oa(o,r)}return!0}),pe());return La().pipe(v(t=>t?S:e))}function ye(){return new URL(location.href)}function lt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function sn(){return new g}function cn(){return location.hash.slice(1)}function pn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ma(e){return O(h(window,"hashchange"),e).pipe(m(cn),Q(cn()),b(t=>t.length>0),G(1))}function ln(e){return Ma(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function Pt(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function mn(){let e=matchMedia("print");return O(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():S))}function zr(e,t){return new j(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function je(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function fn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function un(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function dn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function hn(){return O(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(dn),Q(dn()))}function bn(){return{width:innerWidth,height:innerHeight}}function vn(){return h(window,"resize",{passive:!0}).pipe(m(bn),Q(bn()))}function gn(){return z([hn(),vn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(ee("size")),n=z([o,r]).pipe(m(()=>Ve(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function _a(e){return h(e,"message",t=>t.data)}function Aa(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function yn(e,t=new Worker(e)){let r=_a(t),o=Aa(t),n=new g;n.subscribe(o);let i=o.pipe(Z(),ie(!0));return n.pipe(Z(),Re(r.pipe(W(i))),pe())}var Ca=R("#__config"),Ot=JSON.parse(Ca.textContent);Ot.base=`${new URL(Ot.base,ye())}`;function xe(){return Ot}function B(e){return Ot.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?Ot.translations[e].replace("#",t.toString()):Ot.translations[e]}function Se(e,t=document){return R(`[data-md-component=${e}]`,t)}function ae(e,t=document){return P(`[data-md-component=${e}]`,t)}function ka(e){let t=R(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>R(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function xn(e){if(!B("announce.dismiss")||!e.childElementCount)return S;if(!e.hidden){let t=R(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),ka(e).pipe(w(r=>t.next(r)),_(()=>t.complete()),m(r=>$({ref:e},r)))})}function Ha(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function En(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Ha(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))}function Rt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Tn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Sn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ln=Mt(qr());function Qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,(0,Ln.default)(c))," "],[]).slice(0,-1),i=xe(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=xe();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Mn(e){let t=e[0].score,r=[...e],o=xe(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreQr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>Qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function _n(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Kr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function An(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ra(e){var o;let t=xe(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Cn(e,t){var o;let r=xe();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ra)))}var Ia=0;function ja(e){let t=z([et(e),$t(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Zo(e)).pipe(ne(Ne),pt(1),He(t),m(()=>en(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function Fa(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ia++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(Z(),ie(!1)).subscribe(a);let s=a.pipe(Ht(c=>Le(+!c*250,kr)),K(),v(c=>c?r:S),w(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>$t(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),re(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),d=u.width/2;if(l.role==="tooltip")return{x:d,y:8+u.height};if(u.y>=f.height/2){let{height:y}=ce(l);return{x:d,y:-16-y}}else return{x:d,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),re(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(R(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),ve(me),re(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ja(e).pipe(w(c=>i.next(c)),_(()=>i.complete()),m(c=>$({ref:e},c)))})}function mt(e,{viewport$:t},r=document.body){return Fa(e,{content$:new j(o=>{let n=e.title,i=wn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Ua(e,t){let r=C(()=>z([tn(e),Ne(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function kn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(W(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),O(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Me(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),h(n,"mousedown").pipe(W(a),re(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Ie())==null||c.blur()}}),r.pipe(W(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Ua(e,t).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function Wa(e){return e.tagName==="CODE"?P(".c, .c1, .cm",e):[e]}function Da(e){let t=[];for(let r of Wa(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function Hn(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Da(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,Tn(p,i)),s.replaceWith(a.get(p)))}return a.size===0?S:C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=[];for(let[l,f]of a)c.push([R(".md-typeset",f),R(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?Hn(f,u):Hn(u,f)}),O(...[...a].map(([,l])=>kn(l,t,{target$:r}))).pipe(_(()=>s.complete()),pe())})}function $n(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return $n(t)}}function Pn(e,t){return C(()=>{let r=$n(e);return typeof r!="undefined"?fr(r,e,t):S})}var Rn=Mt(Br());var Va=0;function In(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return In(t)}}function Na(e){return ge(e).pipe(m(({width:t})=>({scrollable:St(e).width>t})),ee("scrollable"))}function jn(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(jr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Rn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Va++}`;let l=Sn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(mt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=In(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(W(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:S)))}}return P(":scope > span[id]",e).length&&e.classList.add("md-code__content"),Na(e).pipe(w(c=>n.next(c)),_(()=>n.complete()),m(c=>$({ref:e},c)),Re(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function za(e,{target$:t,print$:r}){let o=!0;return O(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),w(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Fn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),za(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}var Un=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Gr,Qa=0;function Ka(){return typeof mermaid=="undefined"||mermaid instanceof Element?Tt("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):I(void 0)}function Wn(e){return e.classList.remove("mermaid"),Gr||(Gr=Ka().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Un,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Gr.subscribe(()=>co(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Qa++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Gr.pipe(m(()=>({ref:e})))}var Dn=x("table");function Vn(e){return e.replaceWith(Dn),Dn.replaceWith(An(e)),I({ref:e})}function Ya(e){let t=e.find(r=>r.checked)||e[0];return O(...e.map(r=>h(r,"change").pipe(m(()=>R(`label[for="${r.id}"]`))))).pipe(Q(R(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Nn(e,{viewport$:t,target$:r}){let o=R(".tabbed-labels",e),n=P(":scope > input",e),i=Kr("prev");e.append(i);let a=Kr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(Z(),ie(!0));z([s,ge(e),tt(e)]).pipe(W(p),Me(1,me)).subscribe({next([{active:c},l]){let f=Ve(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=pr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ne(o),ge(o)]).pipe(W(p)).subscribe(([c,l])=>{let f=St(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),O(h(i,"click").pipe(m(()=>-1)),h(a,"click").pipe(m(()=>1))).pipe(W(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(W(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=R(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(p),b(f=>!(f.metaKey||f.ctrlKey)),w(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),re(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let y of P("[data-tabs]"))for(let L of P(":scope > input",y)){let X=R(`label[for="${L.id}"]`);if(X!==c&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),L.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),s.pipe(W(p)).subscribe(()=>{for(let c of P("audio, video",e))c.pause()}),Ya(n).pipe(w(c=>s.next(c)),_(()=>s.complete()),m(c=>$({ref:e},c)))}).pipe(Ke(se))}function zn(e,{viewport$:t,target$:r,print$:o}){return O(...P(".annotate:not(.highlight)",e).map(n=>Pn(n,{target$:r,print$:o})),...P("pre:not(.mermaid) > code",e).map(n=>jn(n,{target$:r,print$:o})),...P("pre.mermaid",e).map(n=>Wn(n)),...P("table:not([class])",e).map(n=>Vn(n)),...P("details",e).map(n=>Fn(n,{target$:r,print$:o})),...P("[data-tabs]",e).map(n=>Nn(n,{viewport$:t,target$:r})),...P("[title]",e).filter(()=>B("content.tooltips")).map(n=>mt(n,{viewport$:t})))}function Ba(e,{alert$:t}){return t.pipe(v(r=>O(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function qn(e,t){let r=R(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ba(e,t).pipe(w(n=>o.next(n)),_(()=>o.complete()),m(n=>$({ref:e},n)))})}var Ga=0;function Ja(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?Ne(o):I({x:0,y:0}),i=O(et(t),$t(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ve(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Qn(e){let t=e.title;if(!t.length)return S;let r=`__tooltip_${Ga++}`,o=Rt(r,"inline"),n=R(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),O(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Me(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Ja(o,e).pipe(w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))}).pipe(Ke(se))}function Xa({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Be(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=ze("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Kn(e,t){return C(()=>z([ge(e),Xa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function Yn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(Z(),ie(!0));o.pipe(ee("active"),He(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue(P("[title]",e)).pipe(b(()=>B("content.tooltips")),ne(a=>Qn(a)));return r.subscribe(o),t.pipe(W(n),m(a=>$({ref:e},a)),Re(i.pipe(W(n))))})}function Za(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),ee("active"))}function Bn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?S:Za(o,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))})}function Gn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ee("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function es(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(ne(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Jn(e){let t=P("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Pt("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),re(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(ve(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),es(t).pipe(W(n.pipe(Ce(1))),ct(),w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))})}function Xn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(w(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Jr=Mt(Br());function ts(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Zn({alert$:e}){Jr.default.isSupported()&&new j(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ts(R(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function ei(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function rs(e,t){let r=new Map;for(let o of P("url",e)){let n=R("loc",o),i=[ei(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(ei(new URL(s),t))}}return r}function ur(e){return un(new URL("sitemap.xml",e)).pipe(m(t=>rs(t,new URL(e))),de(()=>I(new Map)))}function os(e,t){if(!(e.target instanceof Element))return S;let r=e.target.closest("a");if(r===null)return S;if(r.target||e.metaKey||e.ctrlKey)return S;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):S}function ti(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function ri(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function ns(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=ti(document);for(let[o,n]of ti(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return We(P("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),S}),Z(),ie(document))}function oi({location$:e,viewport$:t,progress$:r}){let o=xe();if(location.protocol==="file:")return S;let n=ur(o.base);I(document).subscribe(ri);let i=h(document.body,"click").pipe(He(n),v(([p,c])=>os(p,c)),pe()),a=h(window,"popstate").pipe(m(ye),pe());i.pipe(re(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),O(i,a).subscribe(e);let s=e.pipe(ee("pathname"),v(p=>fn(p,{progress$:r}).pipe(de(()=>(lt(p,!0),S)))),v(ri),v(ns),pe());return O(s.pipe(re(e,(p,c)=>c)),s.pipe(v(()=>e),ee("pathname"),v(()=>e),ee("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),w(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",pn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(ee("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ni=Mt(qr());function ii(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ni.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function jt(e){return e.type===1}function dr(e){return e.type===3}function ai(e,t){let r=yn(e);return O(I(location.protocol!=="file:"),ze("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function si(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=Xr(n))==null?void 0:l.pathname;if(i===void 0)return;let a=ss(o.pathname,i);if(a===void 0)return;let s=ps(t.keys());if(!t.has(s))return;let p=Xr(a,s);if(!p||!t.has(p.href))return;let c=Xr(a,r);if(c)return c.hash=o.hash,c.search=o.search,c}function Xr(e,t){try{return new URL(e,t)}catch(r){return}}function ss(e,t){if(e.startsWith(t))return e.slice(t.length)}function cs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oS)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>h(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),re(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?S:(i.preventDefault(),I(new URL(p)))}}return S}),v(i=>ur(i).pipe(m(a=>{var s;return(s=si({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(n=>lt(n,!0)),z([r,o]).subscribe(([n,i])=>{R(".md-header__topic").appendChild(Cn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ls(e,{worker$:t}){let{searchParams:r}=ye();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),ze("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=ye();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=O(t.pipe(Ae(jt)),h(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function pi(e,{worker$:t}){let r=new g,o=r.pipe(Z(),ie(!0));z([t.pipe(Ae(jt)),r],(i,a)=>a).pipe(ee("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ee("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=R("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ls(e,{worker$:t}).pipe(w(i=>r.next(i)),_(()=>r.complete()),m(i=>$({ref:e},i)),G(1))}function li(e,{worker$:t,query$:r}){let o=new g,n=on(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=R(":scope > :first-child",e),s=R(":scope > :last-child",e);ze("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(re(r),Wr(t.pipe(Ae(jt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(w(()=>s.innerHTML=""),v(({items:l})=>O(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Be(4),Vr(n),v(([f])=>f)))),m(Mn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(ne(l=>{let f=fe("details",l);return typeof f=="undefined"?S:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(w(l=>o.next(l)),_(()=>o.complete()),m(l=>$({ref:e},l)))}function ms(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ye();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function mi(e,t){let r=new g,o=r.pipe(Z(),ie(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),ms(e,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))}function fi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=O(h(n,"keydown"),h(n,"focus")).pipe(ve(se),m(()=>n.value),K());return o.pipe(He(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(w(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ui(e,{index$:t,keyboard$:r}){let o=xe();try{let n=ai(o.search,t),i=Se("search-query",e),a=Se("search-result",e);h(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Ie();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of P(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...P(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Ie()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=pi(i,{worker$:n});return O(s,li(a,{worker$:n,query$:s})).pipe(Re(...ae("search-share",e).map(p=>mi(p,{query$:s})),...ae("search-suggest",e).map(p=>fi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ye}}function di(e,{index$:t,location$:r}){return z([t,r.pipe(Q(ye()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>ii(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function fs(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Zr(e,o){var n=o,{header$:t}=n,r=so(n,["header$"]);let i=R(".md-sidebar__scrollwrap",e),{y:a}=Ve(i);return C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=s.pipe(Me(0,me));return c.pipe(re(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of P(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2})}}}),ue(P("label[tabindex]",e)).pipe(ne(l=>h(l,"click").pipe(ve(se),m(()=>l),W(p)))).subscribe(l=>{let f=R(`[id="${l.htmlFor}"]`);R(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),fs(e,r).pipe(w(l=>s.next(l)),_(()=>s.complete()),m(l=>$({ref:e},l)))})}function hi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return st(je(`${r}/releases/latest`).pipe(de(()=>S),m(o=>({version:o.tag_name})),De({})),je(r).pipe(de(()=>S),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return je(r).pipe(m(o=>({repositories:o.public_repos})),De({}))}}function bi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return st(je(`${r}/releases/permalink/latest`).pipe(de(()=>S),m(({tag_name:o})=>({version:o})),De({})),je(r).pipe(de(()=>S),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}function vi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return hi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return bi(r,o)}return S}var us;function ds(e){return us||(us=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return S}return vi(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(de(()=>S),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function gi(e){let t=R(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(_n(o)),t.classList.add("md-source__repository--active")}),ds(e).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function hs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ee("hidden"))}function yi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):hs(e,t)).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){let o=new Map,n=P(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(ee("height"),m(({height:s})=>{let p=Se("main"),c=R(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(ee("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),He(i),v(([p,c])=>t.pipe(Fr(([l,f],{offset:{y:u},size:d})=>{let y=u+d.height>=Math.floor(s.height);for(;f.length;){let[,L]=f[0];if(L-c=u&&!y)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Be(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(Z(),ie(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=O(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),He(o.pipe(ve(se))),re(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(W(a),ee("offset"),_e(250),Ce(1),W(n.pipe(Ce(1))),ct({delay:250}),re(i)).subscribe(([,{prev:s}])=>{let p=ye(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),bs(e,{viewport$:t,header$:r}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function vs(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Be(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),W(o.pipe(Ce(1))),ie(!0),ct({delay:250}),m(a=>({hidden:a})))}function Ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(a),ee("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),h(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),vs(e,{viewport$:t,main$:o,target$:n}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))}function wi({document$:e,viewport$:t}){e.pipe(v(()=>P(".md-ellipsis")),ne(r=>tt(r).pipe(W(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?mt(n,{viewport$:t}).pipe(W(e.pipe(Ce(1))),_(()=>n.removeAttribute("title"))):S})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>P(".md-status")),ne(r=>mt(r,{viewport$:t}))).subscribe()}function Ti({document$:e,tablet$:t}){e.pipe(v(()=>P(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),ne(r=>h(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),re(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function gs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Si({document$:e}){e.pipe(v(()=>P("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),b(gs),ne(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Oi({viewport$:e,tablet$:t}){z([ze("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),re(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ys(){return location.protocol==="file:"?Tt(`${new URL("search/search_index.js",eo.base)}`).pipe(m(()=>__index),G(1)):je(new URL("search/search_index.json",eo.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Go(),Ut=sn(),Lt=ln(Ut),to=an(),Oe=gn(),hr=Pt("(min-width: 960px)"),Mi=Pt("(min-width: 1220px)"),_i=mn(),eo=xe(),Ai=document.forms.namedItem("search")?ys():Ye,ro=new g;Zn({alert$:ro});var oo=new g;B("navigation.instant")&&oi({location$:Ut,viewport$:Oe,progress$:oo}).subscribe(ot);var Li;((Li=eo.version)==null?void 0:Li.provider)==="mike"&&ci({document$:ot});O(Ut,Lt).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});to.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&<(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&<(r);break;case"Enter":let o=Ie();o instanceof HTMLLabelElement&&o.click()}});wi({viewport$:Oe,document$:ot});Ti({document$:ot,tablet$:hr});Si({document$:ot});Oi({viewport$:Oe,tablet$:hr});var rt=Kn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Gn(e,{viewport$:Oe,header$:rt})),G(1)),xs=O(...ae("consent").map(e=>En(e,{target$:Lt})),...ae("dialog").map(e=>qn(e,{alert$:ro})),...ae("palette").map(e=>Jn(e)),...ae("progress").map(e=>Xn(e,{progress$:oo})),...ae("search").map(e=>ui(e,{index$:Ai,keyboard$:to})),...ae("source").map(e=>gi(e))),Es=C(()=>O(...ae("announce").map(e=>xn(e)),...ae("content").map(e=>zn(e,{viewport$:Oe,target$:Lt,print$:_i})),...ae("content").map(e=>B("search.highlight")?di(e,{index$:Ai,location$:Ut}):S),...ae("header").map(e=>Yn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("header-title").map(e=>Bn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Mi,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>yi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>xi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})),...ae("top").map(e=>Ei(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})))),Ci=ot.pipe(v(()=>Es),Re(xs),G(1));Ci.subscribe();window.document$=ot;window.location$=Ut;window.target$=Lt;window.keyboard$=to;window.viewport$=Oe;window.tablet$=hr;window.screen$=Mi;window.print$=_i;window.alert$=ro;window.progress$=oo;window.component$=Ci;})(); +//# sourceMappingURL=bundle.83f73b43.min.js.map + diff --git a/assets/javascripts/bundle.83f73b43.min.js.map b/assets/javascripts/bundle.83f73b43.min.js.map new file mode 100644 index 00000000..fe920b7d --- /dev/null +++ b/assets/javascripts/bundle.83f73b43.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an + +
+

Staying in context is especially important when you’re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

+ + + + + + + +
+
+ + +
+
+

The Ultimate PR Comment Template Boilerplate for dbt data projects

+

If you’re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

+

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

+
+ Example of a PR comment with comprehensive data validation checks +
Example of a PR comment with comprehensive data validation checks
+
+

The benefits of using a PR comment template

+

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

+ + + + +
+
+ +
+
+ + +
+
+

Meet Recce at Coalesce 2024 and The Data Renegade Happy Hour

+

The Recce team will be joining Coalesce 2024 in Las Vegas! Meet our founder, CL Kao, and product manager, Karen Hsieh, who has also been hosting the Taipei dbt meetups. As a company focused on helping data teams prevent bad merges and improve data quality, we believe Coalesce is the perfect venue to connect with fellow data professionals, share insights, and gain fresh perspectives.

+
+ Firesidechat banner +
We are attending Coalesce 2024
+
+

At Recce, our mission is to transform the data PR review process, ensuring that data pipelines not only run smoothly but also deliver accurate, validated results. We believe that data should be correct, collaborative, and continuously improved. Coalesce 2024 offers an ideal platform for these crucial conversations, gathering experts across the field to discuss the future of data management. Whether it’s gaining new insights into best practices or forging valuable partnerships, Coalesce is where we aim to make an impact.

+ + + + +
+
+ +
+
+ + +
+
+

The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review

+

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the ‘T’, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

+

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

+
+ Move Fast and DON'T Break Prod +
Move Fast and DON'T Break Prod
+
+ + + + +
+
+ +
+
+ + +
+
+

From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity

+

Top priorities for data-driven organizations are data productivity, cost reduction, and error prevention. The four strategies to improve DataOps are:

+
    +
  1. start with small, manageable improvements,
  2. +
  3. follow a clear blueprint,
  4. +
  5. conduct regular data reviews, and
  6. +
  7. gradually introduce best practices across the team.
  8. +
+

In a recent fireside chat, CL Kao, founder of Recce, and Noel Gomez, co-founder of Datacoves, shared their combined experience of over two decades in the data and software industry. They discussed practical strategies to tackle these challenges, the evolution from DevOps to DataOps, and the need for companies to focus on data quality to avoid costly mistakes.

+
+ Firesidechat banner +
Data Productivity - Beyonig DevOps & dbt
+
+ + + + +
+
+ +
+
+ + +
+
+

Identify and Automate Data Checks on Critical dbt Models

+

Do you know which are the critical models in your data project?

+

I’m sure the answer is yes. Even if you don’t rank models, you can definitely point to which models you should tread carefully around.

+

Do you check these critical models for data impact with every pull request?

+

Maybe some, but it’s probably on a more ad-hoc basis. If they really are critical models, you need to be aware of unintended impact. The last thing you want to do is mistakenly change historical metrics, or lose data.

+
+ Every dbt project has critical models +
Impacted Lineage DAG from Recce showing modified and impacted models on the California Integrated Travel Project dbt project
+
+

Identifying critical models

+

Knowing the critical models in your project comes from your domain knowledge. You know these models have:

+ + + + +
+
+ +
+
+ + +
+
+

Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt

+

Data profiling stats are a really efficient way to get an understanding of the distribution of data in a dbt model. You can immediately see skewed data and spot data outliers, something which is difficult to do when checking data at the row level. Here's how Recce can help you make the most of these high-level data stats:

+

Visualize data change with histogram and top-k charts

+

Profiling stats become even more useful when applied to data change validation. Let’s say you’ve updated a data model in dbt and changed the calculation logic for a column — how can you get an overview of how the data was changed or impacted? This is where checking the top-k values, or the histogram, of before-and-after you made the changes, comes in handy — But there’s one major issue...

+
+ The best way to visualize data change in a histogram chart +
The best way to visualize data change in a histogram chart
+
+

Something’s not right

+

If you generate a histogram graph from prod data, then do the same for your dev branch, you’ve got two distinct graphs. The axes don’t match, and it’s difficult to compare:

+ + + + +
+
+ +
+
+ + +
+
+

Hands-On Data Impact Analysis for dbt Data Projects with Recce

+

dbt data projects aren’t getting any smaller and, with the increasing complexity of DAGs, properly validating your data modeling changes has become a difficult task. The adoption of best practices such as data project pull request templates, and other ‘pull request guard rails’ has increased merge times and prolonged the QA process for pull requests.

+
+ Validate data modeling changes in dbt projects by comparing two environments with Recce +
Validate data modeling changes in dbt projects by comparing two environments with Recce
+
+

The difficulty comes from your responsibility to check not only the model SQL code, but also the data, which is a product of your code. Even when code looks right, silent errors and hard to notice bugs can make their way into the data. A proper pull request review is not complete with data validation.

+ + + + +
+
+ +
+
+ + +
+
+

Next-Level Data Validation Toolkit for dbt Data Projects — Introducing Recce

+
+ Build the ultimate PR comment to validate your data modeling changes +
Recce: Data Validation Toolkit for dbt
+
+

Validating data modeling changes and reviewing pull requests for dbt projects can be a challenging task. The difficulty of performing a proper ‘code review’ for data projects, due to both the code and data needing review, means the data validation stage is often omitted, poorly implemented, or drastically slows down time-to-merge for your time sensitive data updates.

+

How can you maintain data best practices, but speed up the validation and review process?

+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/assets/images/check-critical-models/Recce-CI_PR-comment.png b/blog/assets/images/check-critical-models/Recce-CI_PR-comment.png new file mode 100644 index 00000000..5007f55d Binary files /dev/null and b/blog/assets/images/check-critical-models/Recce-CI_PR-comment.png differ diff --git a/blog/assets/images/check-critical-models/Recce-ci-summary.png b/blog/assets/images/check-critical-models/Recce-ci-summary.png new file mode 100644 index 00000000..ef91533e Binary files /dev/null and b/blog/assets/images/check-critical-models/Recce-ci-summary.png differ diff --git a/blog/assets/images/check-critical-models/critical-model.png b/blog/assets/images/check-critical-models/critical-model.png new file mode 100644 index 00000000..3d8eb40f Binary files /dev/null and b/blog/assets/images/check-critical-models/critical-model.png differ diff --git a/blog/assets/images/check-critical-models/recce-ci-yml.png b/blog/assets/images/check-critical-models/recce-ci-yml.png new file mode 100644 index 00000000..80a18c3b Binary files /dev/null and b/blog/assets/images/check-critical-models/recce-ci-yml.png differ diff --git a/blog/assets/images/check-critical-models/recce-yml.png b/blog/assets/images/check-critical-models/recce-yml.png new file mode 100644 index 00000000..fd8b81e5 Binary files /dev/null and b/blog/assets/images/check-critical-models/recce-yml.png differ diff --git a/blog/assets/images/check-critical-models/schema-row-count-diff.png b/blog/assets/images/check-critical-models/schema-row-count-diff.png new file mode 100644 index 00000000..5d4fca61 Binary files /dev/null and b/blog/assets/images/check-critical-models/schema-row-count-diff.png differ diff --git a/blog/assets/images/coalesce-2024/coalesce-2024-attending.png b/blog/assets/images/coalesce-2024/coalesce-2024-attending.png new file mode 100644 index 00000000..76b6d85e Binary files /dev/null and b/blog/assets/images/coalesce-2024/coalesce-2024-attending.png differ diff --git a/blog/assets/images/coalesce-2024/coalesce-2024-event-preview.png b/blog/assets/images/coalesce-2024/coalesce-2024-event-preview.png new file mode 100644 index 00000000..ad556315 Binary files /dev/null and b/blog/assets/images/coalesce-2024/coalesce-2024-event-preview.png differ diff --git a/blog/assets/images/coalesce-2024/the-data-renegade_happy-hour.jpg b/blog/assets/images/coalesce-2024/the-data-renegade_happy-hour.jpg new file mode 100644 index 00000000..d807edd0 Binary files /dev/null and b/blog/assets/images/coalesce-2024/the-data-renegade_happy-hour.jpg differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/all-signal-no-noise-medium.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/all-signal-no-noise-medium.png new file mode 100644 index 00000000..d380a863 Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/all-signal-no-noise-medium.png differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/checklist.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/checklist.png new file mode 100644 index 00000000..aebef820 Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/checklist.png differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/lineage-diff.gif b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/lineage-diff.gif new file mode 100644 index 00000000..69661efa Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/lineage-diff.gif differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/model-code-diff.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/model-code-diff.png new file mode 100644 index 00000000..ac886db2 Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/model-code-diff.png differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/pr-validations.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/pr-validations.png new file mode 100644 index 00000000..cb2ec29c Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/pr-validations.png differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/profile-diff.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/profile-diff.png new file mode 100644 index 00000000..5f141b28 Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/profile-diff.png differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/query-diff.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/query-diff.png new file mode 100644 index 00000000..286fd23c Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/query-diff.png differ diff --git a/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/schema-diff.png b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/schema-diff.png new file mode 100644 index 00000000..c50b43a5 Binary files /dev/null and b/blog/assets/images/data-validaton-toolkit-for-dbt-data-projects/schema-diff.png differ diff --git a/blog/assets/images/dbt-data-pr-comment-template/Prefeitura-do-Rio-de-Janeiro_Recce.png b/blog/assets/images/dbt-data-pr-comment-template/Prefeitura-do-Rio-de-Janeiro_Recce.png new file mode 100644 index 00000000..d5b4603b Binary files /dev/null and b/blog/assets/images/dbt-data-pr-comment-template/Prefeitura-do-Rio-de-Janeiro_Recce.png differ diff --git a/blog/assets/images/dbt-data-pr-comment-template/cal-itp.jpeg b/blog/assets/images/dbt-data-pr-comment-template/cal-itp.jpeg new file mode 100644 index 00000000..a5649a60 Binary files /dev/null and b/blog/assets/images/dbt-data-pr-comment-template/cal-itp.jpeg differ diff --git a/blog/assets/images/dbt-data-pr-comment-template/dbt-pr-comment-template-data-recce.png b/blog/assets/images/dbt-data-pr-comment-template/dbt-pr-comment-template-data-recce.png new file mode 100644 index 00000000..703589c6 Binary files /dev/null and b/blog/assets/images/dbt-data-pr-comment-template/dbt-pr-comment-template-data-recce.png differ diff --git a/blog/assets/images/dbt-data-pr-comment-template/perfect-pr-comment.jpeg b/blog/assets/images/dbt-data-pr-comment-template/perfect-pr-comment.jpeg new file mode 100644 index 00000000..c772f7b6 Binary files /dev/null and b/blog/assets/images/dbt-data-pr-comment-template/perfect-pr-comment.jpeg differ diff --git a/blog/assets/images/dbt-data-pr-comment-template/pr-comment.png b/blog/assets/images/dbt-data-pr-comment-template/pr-comment.png new file mode 100644 index 00000000..f0da1cfe Binary files /dev/null and b/blog/assets/images/dbt-data-pr-comment-template/pr-comment.png differ diff --git a/blog/assets/images/explore-data-impact-with-focus/explore-data-impact.png b/blog/assets/images/explore-data-impact-with-focus/explore-data-impact.png new file mode 100644 index 00000000..bc4873ec Binary files /dev/null and b/blog/assets/images/explore-data-impact-with-focus/explore-data-impact.png differ diff --git a/blog/assets/images/firesidechat-20240827/bad-data-cost.png b/blog/assets/images/firesidechat-20240827/bad-data-cost.png new file mode 100644 index 00000000..7d6608b0 Binary files /dev/null and b/blog/assets/images/firesidechat-20240827/bad-data-cost.png differ diff --git a/blog/assets/images/firesidechat-20240827/firesidechat_banner.jpg b/blog/assets/images/firesidechat-20240827/firesidechat_banner.jpg new file mode 100644 index 00000000..f2c0b044 Binary files /dev/null and b/blog/assets/images/firesidechat-20240827/firesidechat_banner.jpg differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/checklist.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/checklist.png new file mode 100644 index 00000000..3d08245a Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/checklist.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/code-change.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/code-change.png new file mode 100644 index 00000000..44b0df54 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/code-change.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/customer-segments-top-k.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/customer-segments-top-k.png new file mode 100644 index 00000000..0f0670a5 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/customer-segments-top-k.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/customer-segments.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/customer-segments.png new file mode 100644 index 00000000..7b666914 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/customer-segments.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/impact-assessment-recce.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/impact-assessment-recce.png new file mode 100644 index 00000000..ffc4b22d Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/impact-assessment-recce.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/impact-overview.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/impact-overview.png new file mode 100644 index 00000000..b2a1eaa6 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/impact-overview.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/profile-diff.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/profile-diff.png new file mode 100644 index 00000000..ef5c54f6 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/profile-diff.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/query-diff-results.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/query-diff-results.png new file mode 100644 index 00000000..6b98bbdf Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/query-diff-results.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/value-diff-1.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/value-diff-1.png new file mode 100644 index 00000000..981ea957 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/value-diff-1.png differ diff --git a/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/value-diff-2.png b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/value-diff-2.png new file mode 100644 index 00000000..f32cf6e8 Binary files /dev/null and b/blog/assets/images/hands-on-data-impact-analysis-dbt-data-projects-recce/value-diff-2.png differ diff --git a/blog/assets/images/histogram-overlay-top-k-dbt/histogram-overlay-chart-data-recce-dbt.png b/blog/assets/images/histogram-overlay-top-k-dbt/histogram-overlay-chart-data-recce-dbt.png new file mode 100644 index 00000000..0e1e4060 Binary files /dev/null and b/blog/assets/images/histogram-overlay-top-k-dbt/histogram-overlay-chart-data-recce-dbt.png differ diff --git a/blog/assets/images/histogram-overlay-top-k-dbt/histogram-overlay.png b/blog/assets/images/histogram-overlay-top-k-dbt/histogram-overlay.png new file mode 100644 index 00000000..c600f30a Binary files /dev/null and b/blog/assets/images/histogram-overlay-top-k-dbt/histogram-overlay.png differ diff --git a/blog/assets/images/histogram-overlay-top-k-dbt/histogram-side-by-side.png b/blog/assets/images/histogram-overlay-top-k-dbt/histogram-side-by-side.png new file mode 100644 index 00000000..f3888fb7 Binary files /dev/null and b/blog/assets/images/histogram-overlay-top-k-dbt/histogram-side-by-side.png differ diff --git a/blog/assets/images/histogram-overlay-top-k-dbt/recce-histogram.gif b/blog/assets/images/histogram-overlay-top-k-dbt/recce-histogram.gif new file mode 100644 index 00000000..464ed598 Binary files /dev/null and b/blog/assets/images/histogram-overlay-top-k-dbt/recce-histogram.gif differ diff --git a/blog/assets/images/histogram-overlay-top-k-dbt/top-k-diff.png b/blog/assets/images/histogram-overlay-top-k-dbt/top-k-diff.png new file mode 100644 index 00000000..e408b005 Binary files /dev/null and b/blog/assets/images/histogram-overlay-top-k-dbt/top-k-diff.png differ diff --git a/blog/assets/images/histogram-overlay-top-k-dbt/top-k.png b/blog/assets/images/histogram-overlay-top-k-dbt/top-k.png new file mode 100644 index 00000000..9b3a0755 Binary files /dev/null and b/blog/assets/images/histogram-overlay-top-k-dbt/top-k.png differ diff --git a/blog/assets/images/self-serve-review/self-service-review-data.png b/blog/assets/images/self-serve-review/self-service-review-data.png new file mode 100644 index 00000000..e4eb7b01 Binary files /dev/null and b/blog/assets/images/self-serve-review/self-service-review-data.png differ diff --git a/blog/assets/images/support-self-serve-data-20240916/data-chaos-in-4-acts.jpg b/blog/assets/images/support-self-serve-data-20240916/data-chaos-in-4-acts.jpg new file mode 100644 index 00000000..d5faf968 Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/data-chaos-in-4-acts.jpg differ diff --git a/blog/assets/images/support-self-serve-data-20240916/data-incidents.png b/blog/assets/images/support-self-serve-data-20240916/data-incidents.png new file mode 100644 index 00000000..20fdb87d Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/data-incidents.png differ diff --git a/blog/assets/images/support-self-serve-data-20240916/data-profile.jpg b/blog/assets/images/support-self-serve-data-20240916/data-profile.jpg new file mode 100644 index 00000000..f24adcbb Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/data-profile.jpg differ diff --git a/blog/assets/images/support-self-serve-data-20240916/finger-pointing.jpg b/blog/assets/images/support-self-serve-data-20240916/finger-pointing.jpg new file mode 100644 index 00000000..4afae177 Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/finger-pointing.jpg differ diff --git a/blog/assets/images/support-self-serve-data-20240916/move-fast.png b/blog/assets/images/support-self-serve-data-20240916/move-fast.png new file mode 100644 index 00000000..e7376e25 Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/move-fast.png differ diff --git a/blog/assets/images/support-self-serve-data-20240916/new-challenges.png b/blog/assets/images/support-self-serve-data-20240916/new-challenges.png new file mode 100644 index 00000000..4d5fd9a9 Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/new-challenges.png differ diff --git a/blog/assets/images/support-self-serve-data-20240916/pdf-download-recce-deck.png b/blog/assets/images/support-self-serve-data-20240916/pdf-download-recce-deck.png new file mode 100644 index 00000000..a3e93cbc Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/pdf-download-recce-deck.png differ diff --git a/blog/assets/images/support-self-serve-data-20240916/show-me-whats-different.png b/blog/assets/images/support-self-serve-data-20240916/show-me-whats-different.png new file mode 100644 index 00000000..7366b59e Binary files /dev/null and b/blog/assets/images/support-self-serve-data-20240916/show-me-whats-different.png differ diff --git a/blog/category/announcements/index.html b/blog/category/announcements/index.html new file mode 100644 index 00000000..9c1ac437 --- /dev/null +++ b/blog/category/announcements/index.html @@ -0,0 +1,2377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Announcements - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Announcements

+
+ +
+
+ + +
+
+

Meet Recce at Coalesce 2024 and The Data Renegade Happy Hour

+

The Recce team will be joining Coalesce 2024 in Las Vegas! Meet our founder, CL Kao, and product manager, Karen Hsieh, who has also been hosting the Taipei dbt meetups. As a company focused on helping data teams prevent bad merges and improve data quality, we believe Coalesce is the perfect venue to connect with fellow data professionals, share insights, and gain fresh perspectives.

+
+ Firesidechat banner +
We are attending Coalesce 2024
+
+

At Recce, our mission is to transform the data PR review process, ensuring that data pipelines not only run smoothly but also deliver accurate, validated results. We believe that data should be correct, collaborative, and continuously improved. Coalesce 2024 offers an ideal platform for these crucial conversations, gathering experts across the field to discuss the future of data management. Whether it’s gaining new insights into best practices or forging valuable partnerships, Coalesce is where we aim to make an impact.

+ + + + +
+
+ +
+
+ + +
+
+

From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity

+

Top priorities for data-driven organizations are data productivity, cost reduction, and error prevention. The four strategies to improve DataOps are:

+
    +
  1. start with small, manageable improvements,
  2. +
  3. follow a clear blueprint,
  4. +
  5. conduct regular data reviews, and
  6. +
  7. gradually introduce best practices across the team.
  8. +
+

In a recent fireside chat, CL Kao, founder of Recce, and Noel Gomez, co-founder of Datacoves, shared their combined experience of over two decades in the data and software industry. They discussed practical strategies to tackle these challenges, the evolution from DevOps to DataOps, and the need for companies to focus on data quality to avoid costly mistakes.

+
+ Firesidechat banner +
Data Productivity - Beyonig DevOps & dbt
+
+ + + + +
+
+ +
+
+ + +
+
+

Next-Level Data Validation Toolkit for dbt Data Projects — Introducing Recce

+
+ Build the ultimate PR comment to validate your data modeling changes +
Recce: Data Validation Toolkit for dbt
+
+

Validating data modeling changes and reviewing pull requests for dbt projects can be a challenging task. The difficulty of performing a proper ‘code review’ for data projects, due to both the code and data needing review, means the data validation stage is often omitted, poorly implemented, or drastically slows down time-to-merge for your time sensitive data updates.

+

How can you maintain data best practices, but speed up the validation and review process?

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/best-practices/index.html b/blog/category/best-practices/index.html new file mode 100644 index 00000000..e66e16b5 --- /dev/null +++ b/blog/category/best-practices/index.html @@ -0,0 +1,2285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Best Practices - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Best Practices

+
+ +
+
+ + +
+
+

The Ultimate PR Comment Template Boilerplate for dbt data projects

+

If you’re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

+

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

+
+ Example of a PR comment with comprehensive data validation checks +
Example of a PR comment with comprehensive data validation checks
+
+

The benefits of using a PR comment template

+

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/concepts/index.html b/blog/category/concepts/index.html new file mode 100644 index 00000000..39d0fbf0 --- /dev/null +++ b/blog/category/concepts/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Concepts - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Concepts

+
+ +
+
+ + +
+
+

The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review

+

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the ‘T’, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

+

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

+
+ Move Fast and DON'T Break Prod +
Move Fast and DON'T Break Prod
+
+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/data-exploration/index.html b/blog/category/data-exploration/index.html new file mode 100644 index 00000000..db97b4d9 --- /dev/null +++ b/blog/category/data-exploration/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Data Exploration - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Data Exploration

+
+ +
+
+ + +
+
+

Explore data impact and focus on tracking data validations with Recce's new interface

+

There’s nothing worse than being distracted while you’re in the middle of working on a complex task or debugging an issue. All the things that you were juggling in your mind come crashing down, and you have to pick up the pieces and start again.

+

Recce's updated interface lets you stay on track while assessing and exploring data impact in your dbt project when making dbt data model changes, and performing dbt PR review.

+
+ +

Track and record data impact assessment results

+

Staying in context is especially important when you’re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/data-validation/index.html b/blog/category/data-validation/index.html new file mode 100644 index 00000000..fe236a2b --- /dev/null +++ b/blog/category/data-validation/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Data Validation - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Data Validation

+
+ +
+
+ + +
+
+

Explore data impact and focus on tracking data validations with Recce's new interface

+

There’s nothing worse than being distracted while you’re in the middle of working on a complex task or debugging an issue. All the things that you were juggling in your mind come crashing down, and you have to pick up the pieces and start again.

+

Recce's updated interface lets you stay on track while assessing and exploring data impact in your dbt project when making dbt data model changes, and performing dbt PR review.

+
+ +

Track and record data impact assessment results

+

Staying in context is especially important when you’re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/dbt/index.html b/blog/category/dbt/index.html new file mode 100644 index 00000000..d8b1787f --- /dev/null +++ b/blog/category/dbt/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + dbt - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

dbt

+
+ +
+
+ + +
+
+

The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review

+

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the ‘T’, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

+

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

+
+ Move Fast and DON'T Break Prod +
Move Fast and DON'T Break Prod
+
+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/features/index.html b/blog/category/features/index.html new file mode 100644 index 00000000..aa5145dd --- /dev/null +++ b/blog/category/features/index.html @@ -0,0 +1,2374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Features - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Features

+
+ +
+
+ + +
+
+

Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt

+

Data profiling stats are a really efficient way to get an understanding of the distribution of data in a dbt model. You can immediately see skewed data and spot data outliers, something which is difficult to do when checking data at the row level. Here's how Recce can help you make the most of these high-level data stats:

+

Visualize data change with histogram and top-k charts

+

Profiling stats become even more useful when applied to data change validation. Let’s say you’ve updated a data model in dbt and changed the calculation logic for a column — how can you get an overview of how the data was changed or impacted? This is where checking the top-k values, or the histogram, of before-and-after you made the changes, comes in handy — But there’s one major issue...

+
+ The best way to visualize data change in a histogram chart +
The best way to visualize data change in a histogram chart
+
+

Something’s not right

+

If you generate a histogram graph from prod data, then do the same for your dev branch, you’ve got two distinct graphs. The axes don’t match, and it’s difficult to compare:

+ + + + +
+
+ +
+
+ + +
+
+

Hands-On Data Impact Analysis for dbt Data Projects with Recce

+

dbt data projects aren’t getting any smaller and, with the increasing complexity of DAGs, properly validating your data modeling changes has become a difficult task. The adoption of best practices such as data project pull request templates, and other ‘pull request guard rails’ has increased merge times and prolonged the QA process for pull requests.

+
+ Validate data modeling changes in dbt projects by comparing two environments with Recce +
Validate data modeling changes in dbt projects by comparing two environments with Recce
+
+

The difficulty comes from your responsibility to check not only the model SQL code, but also the data, which is a product of your code. Even when code looks right, silent errors and hard to notice bugs can make their way into the data. A proper pull request review is not complete with data validation.

+ + + + +
+
+ +
+
+ + +
+
+

Next-Level Data Validation Toolkit for dbt Data Projects — Introducing Recce

+
+ Build the ultimate PR comment to validate your data modeling changes +
Recce: Data Validation Toolkit for dbt
+
+

Validating data modeling changes and reviewing pull requests for dbt projects can be a challenging task. The difficulty of performing a proper ‘code review’ for data projects, due to both the code and data needing review, means the data validation stage is often omitted, poorly implemented, or drastically slows down time-to-merge for your time sensitive data updates.

+

How can you maintain data best practices, but speed up the validation and review process?

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/impact-assessment/index.html b/blog/category/impact-assessment/index.html new file mode 100644 index 00000000..a3d396f2 --- /dev/null +++ b/blog/category/impact-assessment/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Impact assessment - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Impact assessment

+
+ +
+
+ + +
+
+

Explore data impact and focus on tracking data validations with Recce's new interface

+

There’s nothing worse than being distracted while you’re in the middle of working on a complex task or debugging an issue. All the things that you were juggling in your mind come crashing down, and you have to pick up the pieces and start again.

+

Recce's updated interface lets you stay on track while assessing and exploring data impact in your dbt project when making dbt data model changes, and performing dbt PR review.

+
+ +

Track and record data impact assessment results

+

Staying in context is especially important when you’re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/pr-review/index.html b/blog/category/pr-review/index.html new file mode 100644 index 00000000..14bf964d --- /dev/null +++ b/blog/category/pr-review/index.html @@ -0,0 +1,2285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + PR Review - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

PR Review

+
+ +
+
+ + +
+
+

The Ultimate PR Comment Template Boilerplate for dbt data projects

+

If you’re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

+

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

+
+ Example of a PR comment with comprehensive data validation checks +
Example of a PR comment with comprehensive data validation checks
+
+

The benefits of using a PR comment template

+

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/pull-request/index.html b/blog/category/pull-request/index.html new file mode 100644 index 00000000..d33fb73c --- /dev/null +++ b/blog/category/pull-request/index.html @@ -0,0 +1,2285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Pull Request - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Pull Request

+
+ +
+
+ + +
+
+

The Ultimate PR Comment Template Boilerplate for dbt data projects

+

If you’re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

+

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

+
+ Example of a PR comment with comprehensive data validation checks +
Example of a PR comment with comprehensive data validation checks
+
+

The benefits of using a PR comment template

+

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/self-serve-review/index.html b/blog/category/self-serve-review/index.html new file mode 100644 index 00000000..5bf679ad --- /dev/null +++ b/blog/category/self-serve-review/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Self-Serve Review - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Self-Serve Review

+
+ +
+
+ + +
+
+

The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review

+

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the ‘T’, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

+

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

+
+ Move Fast and DON'T Break Prod +
Move Fast and DON'T Break Prod
+
+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/usage/index.html b/blog/category/usage/index.html new file mode 100644 index 00000000..4dd5ff8b --- /dev/null +++ b/blog/category/usage/index.html @@ -0,0 +1,2333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Usage - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Usage

+
+ +
+
+ + +
+
+

Identify and Automate Data Checks on Critical dbt Models

+

Do you know which are the critical models in your data project?

+

I’m sure the answer is yes. Even if you don’t rank models, you can definitely point to which models you should tread carefully around.

+

Do you check these critical models for data impact with every pull request?

+

Maybe some, but it’s probably on a more ad-hoc basis. If they really are critical models, you need to be aware of unintended impact. The last thing you want to do is mistakenly change historical metrics, or lose data.

+
+ Every dbt project has critical models +
Impacted Lineage DAG from Recce showing modified and impacted models on the California Integrated Travel Project dbt project
+
+

Identifying critical models

+

Knowing the critical models in your project comes from your domain knowledge. You know these models have:

+ + + + +
+
+ +
+
+ + +
+
+

Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt

+

Data profiling stats are a really efficient way to get an understanding of the distribution of data in a dbt model. You can immediately see skewed data and spot data outliers, something which is difficult to do when checking data at the row level. Here's how Recce can help you make the most of these high-level data stats:

+

Visualize data change with histogram and top-k charts

+

Profiling stats become even more useful when applied to data change validation. Let’s say you’ve updated a data model in dbt and changed the calculation logic for a column — how can you get an overview of how the data was changed or impacted? This is where checking the top-k values, or the histogram, of before-and-after you made the changes, comes in handy — But there’s one major issue...

+
+ The best way to visualize data change in a histogram chart +
The best way to visualize data change in a histogram chart
+
+

Something’s not right

+

If you generate a histogram graph from prod data, then do the same for your dev branch, you’ve got two distinct graphs. The axes don’t match, and it’s difficult to compare:

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/category/webinar/index.html b/blog/category/webinar/index.html new file mode 100644 index 00000000..ebed7baf --- /dev/null +++ b/blog/category/webinar/index.html @@ -0,0 +1,2288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Webinar - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Webinar

+
+ +
+
+ + +
+
+

From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity

+

Top priorities for data-driven organizations are data productivity, cost reduction, and error prevention. The four strategies to improve DataOps are:

+
    +
  1. start with small, manageable improvements,
  2. +
  3. follow a clear blueprint,
  4. +
  5. conduct regular data reviews, and
  6. +
  7. gradually introduce best practices across the team.
  8. +
+

In a recent fireside chat, CL Kao, founder of Recce, and Noel Gomez, co-founder of Datacoves, shared their combined experience of over two decades in the data and software industry. They discussed practical strategies to tackle these challenges, the evolution from DevOps to DataOps, and the need for companies to focus on data quality to avoid costly mistakes.

+
+ Firesidechat banner +
Data Productivity - Beyonig DevOps & dbt
+
+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/check-critical-models/index.html b/blog/check-critical-models/index.html new file mode 100644 index 00000000..d8288c65 --- /dev/null +++ b/blog/check-critical-models/index.html @@ -0,0 +1,2592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Identify and Automate Data Checks on Critical dbt Models - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + +

Identify and Automate Data Checks on Critical dbt Models

+

Do you know which are the critical models in your data project?

+

I’m sure the answer is yes. Even if you don’t rank models, you can definitely point to which models you should tread carefully around.

+

Do you check these critical models for data impact with every pull request?

+

Maybe some, but it’s probably on a more ad-hoc basis. If they really are critical models, you need to be aware of unintended impact. The last thing you want to do is mistakenly change historical metrics, or lose data.

+
+ Every dbt project has critical models +
Impacted Lineage DAG from Recce showing modified and impacted models on the California Integrated Travel Project dbt project
+
+

Identifying critical models

+

Knowing the critical models in your project comes from your domain knowledge. You know these models have:

+ + +
    +
  • a particular significance to business,
  • +
  • a ton of downstream models,
  • +
  • or, from experience, you’ll get a call about that data if something goes wrong
  • +
+

If you check these models in an ad-hoc way, it might be time to apply a more formal ranking system to models, which will also help the triage process if something does go wrong.

+

When to check critical models

+

Checking that these critical models didn’t change is really important, and you need to do this before merging your updated models into prod. You might have a data observability tool in place to monitor prod data but, if you merge a breaking change, it’s already too late. The downstream damage is already done.

+

You can check the data in two ways before merging:

+
    +
  • During development: This is when you’re making SQL changes, and editing models and metrics, checking the data and proactively and looking for data impact.
  • +
  • Automatically in CI: These checks run automatically in continuous integration (CI). You still need to review the checks, but they are run automatically for each pull request (PR) on your data project.
  • +
+

This mix of manual and automated checks gives you a good chance at comprehensive coverage, but you still need the domain knowledge to identify those critical models.

+

A plan for checking critical models

+

Checking the data in critical models involves comparing the data generated from your development branch with production (or staging data). The process looks like:

+
    +
  • Identify critical models — Models which are bottlenecks or are important to business
  • +
  • Decide which checks to run on these models — Data should not change in these models so you should run structural and data profiling checks
  • +
  • Automate the checks — Run the checks in CI with each PR
  • +
  • Review check results — As part of PR review check if there is impact, and if it’s is intended or not
  • +
+

Automating Critical Model Checks

+

Critical models checks should run with each PR, here’s how you can do that with the preset-checks feature in Recce, by committing a checklist to your data project repo.

+

Let’s say you want to have the following structural checks run with each PR for your customers model:

+
    +
  • Row count — You shouldn’t lose any data.
  • +
  • Schema — The schema should not change unexpectedly.
  • +
+

These are fundamental checks that all critical models should have, regardless of the type of PR.

+
+ Schema and Row Count Diff in Recce +
Schema and row count checks performed in Data Recce
+
+

You can run these checks in the Recce UI, and any of these checks can also be automated in CI. Here’s how:

+

1. Generate your check file (recce.yml) and commit to your project

+

To add the row count and schema checks to your dbt CI job you just need to commit a Recce checklist (recce.yml) to your dbt project. From the Recce UI you would:

+
    +
  1. Add the check to your checklist.
  2. +
  3. Copy the preset check template to your recce.yml
  4. +
+
+ Recce.yml in the dbt project root +
Recce.yml in the dbt project root
+
+

Then commit the recce.yml in the root of your dbt data project. Each branch will get a copy of these checks, and Recce will run the checks each time a PR is opened.

+

2. Run Recce in CI

+

When Recce runs in CI, the checks in recce.yml will be automatically run. Recce also provides a command to generate a PR summary with your results, which can also be automatically added to the PR comments for reviewers to check

+
+ Run Recce in CI +
Run Recce in CI
+
+

3. Review the results

+

The Recce summary, posted to your PR comments, shows you the following things:

+
    +
  • A diagram of the impacted lineage — this is just the part of the lineage that has modifications, or is downstream of modified models
  • +
  • A list of checks that detected a data mismatch
  • +
+
+ Review Recce CI Summary +
Review Recce CI Summary
+
+

If there was a difference in the row count, or the schema changed, on the customers model, then those checks will be listed here.

+

The idea behind the Recce Summary is ‘all signal, no noise’, which means you only want actionable or useful information — You only want to know when there’s a difference, if everything is the same there’s no point telling you that, it’s just noise that would crowd out the signal.

+

4. (optional) Data impact exploration

+

If one of the checks indicated a data-mismatch, or you’ve otherwise seen something that you want to inspect, you can download the Recce state file and run Recce in review mode to see the results of the checks.

+
+ Download the Recce state file and execute Recce in Review Mode +
Download the Recce state file and execute Recce in Review Mode
+
+

To perform live checks on the data, you need only add a dbt_project.yml and profiles.yml, there’s no need to checkout the whole dbt project. Then you can perform live checks to compare dev with prod/staging.

+

Conclusion

+

Every data project will have those critical models. It’s fine to check them ad-hoc while you are working on the data project and validating the data generated from your changes. But it’s best practice to identify and automate checks on these critical models.

+
    +
  • Identify your critical models
  • +
  • Curate a checklist for things you know should not change in these models (project specific)
  • +
  • Run this checklist in your PRs with Recce
  • +
  • Review impact if necessary
  • +
+

Get Recce

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/data-validaton-toolkit-for-dbt-data-projects/index.html b/blog/data-validaton-toolkit-for-dbt-data-projects/index.html new file mode 100644 index 00000000..152e2140 --- /dev/null +++ b/blog/data-validaton-toolkit-for-dbt-data-projects/index.html @@ -0,0 +1,2589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Data Validation Toolkit for dbt Data Projects - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + +

Next-Level Data Validation Toolkit for dbt Data Projects — Introducing Recce

+
+ Build the ultimate PR comment to validate your data modeling changes +
Recce: Data Validation Toolkit for dbt
+
+

Validating data modeling changes and reviewing pull requests for dbt projects can be a challenging task. The difficulty of performing a proper ‘code review’ for data projects, due to both the code and data needing review, means the data validation stage is often omitted, poorly implemented, or drastically slows down time-to-merge for your time sensitive data updates.

+

How can you maintain data best practices, but speed up the validation and review process?

+ + +

Recce — The data validation toolkit for dbt projects

+

Recce (short for reconnaissance) is a data modeling validation toolkit with a focus on environment diffing. Take two dbt environments, such as dev and prod, and compare them using the suite of diff tools in Recce.

+

Your Diffing Toolkit

+

With Recce you’re able to validate your data modeling changes against a known-good baseline. The only real way to verify modeling changes is to check it against historical/production data.

+

Lineage DAG Diff

+

Start from the zone of impact of your changes, and see which models have been modified, added, and removed. The dbt docs lineage DAG only shows you the current state of the DAG, Recce shows you how the DAG differs from before you made any changes.

+
+ Lineage DAG Diff +
Lineage DAG Diff in Recce
+
+

Data Profile Diff & Value Diff

+

Perform holistic checks by diffing the data profile stats for your development branch, then check the percentage of values matching for each column in a model.

+
+ Data Profile Diff +
Data Profile in Recce
+
+

Query Diff

+

If something needs further investigation, drill down and query the data. One query will run on both environments, and you’ll be able to see the difference on a row-by-row basis. Enable change-only view to see just what’s changed.

+
+ SQL Query Diff +
SQL Query Diff in Recce
+
+

Schema and Row Count

+

In addition to the above diffs, you can also check the schema and row count, just to be sure you didn’t lose any data, or an important column.

+
+ Schema and Row Count Diff +
Schema and Row Count in Recce
+
+

Create your checklist

+

As you create validations, add them to your checklist with notes about what you found, and re-re-run checks if the data changes. When you’re ready, export the checks to your PR comment.

+
+ PR Checklist +
Data Project PR Checklist in Recce
+
+

Create your all signal, no noise PR comment

+

When you’re ready to share your validations as proof-of-correctness for your work, you can export checks into your PR comment template. You can copy your notes, and export a screenshot of the check as it appears in Recce. By curating the validations for your PR comment, you can create an ‘all-signal, no noise’ comment with the validations that are relevant to the context of your changes.

+
+ PR Validations +
Data Modeling Validations in PR Comment
+
+

The reviewer will be able to see the query and results of your data spot-checks and have the comprehensive information required to request further investigation, or sign-off on your changes.

+

Why Recce?

+

As mentioned above, whether you’re the pull request author, or reviewer, you’ve got the difficult task of understanding what is going on and trying to verify if the intentions of the PR were realized without screwing up production data. Here’s some common issues we’ve heard about working on large, or business critical, dbt data projects:

+
    +
  • QA for pull requests takes too long — stakeholders want to merge new data features faster.
  • +
  • dbt build worked, but the data was actually wrong.
  • +
  • I’m sick of downtime from silent errors making it into prod.
  • +
  • CI takes too long and current data quality tools are costly to run.
  • +
  • I just want to see a summary of what changed for modified models.
  • +
+

If any of these pain points ring true, Recce can help with the code review on your data project. +Open-source and available now

+

Recce OSS is available on GitHub now. Follow the instructions in our Getting Started guide to start using Recce to validate your data modeling changes.

+ +

Try Recce Online

+

If you want to try Recce out without having to install, check out the demo instance below.

+

Demo

+

The demo PR makes a simple change to the dbt’s Jaffle Shop project and changes how customer_lifetime_value (CLV) is calculated (fixes it to only calculated completed orders).

+
+ Model Code Diff +
Can you validate this code change using Recce?
+
+

The expectation from this change is that CLV will be reduced overall, and that this will also impact the customer segments downstream model, With that in mind, see if you can determine if the if the PR has any issues by checking the data in Recce:

+ +

Hint: Run a Profile Diff, then a Query Diff, on the customers model, then check for downstream impact.

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/dbt-data-pr-comment-template/index.html b/blog/dbt-data-pr-comment-template/index.html new file mode 100644 index 00000000..9f3b9566 --- /dev/null +++ b/blog/dbt-data-pr-comment-template/index.html @@ -0,0 +1,2754 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The Ultimate PR Comment Template Boilerplate for dbt data projects - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + +

The Ultimate PR Comment Template Boilerplate for dbt data projects

+

If you’re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

+

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

+
+ Example of a PR comment with comprehensive data validation checks +
Example of a PR comment with comprehensive data validation checks
+
+

The benefits of using a PR comment template

+

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

+ + +
    +
  1. As the PR author, the structure of the template helps you to define and describe your work related to this PR, which helps to you to perform data validation and understand data impact.
  2. +
  3. As the PR reviewer, the sections help you to review the PR by following the logical steps that the author took by seeing exactly what work as done, and how it was checked. Making data quality and data integrity checks integral to the PR review process.
  4. +
  5. For future you, it provides a historical record of what was done and why, which is invaluable as part of data best practices.
  6. +
+

Check the end of the article for some examples from the data projects of Prefeitura do Rio de Janeiro and the California Integrated Travel Project (Cal-ITP), who both make deft use or PR comment templates to improve their pull request process.

+

Where can you get a dbt PR comment template from?

+

dbt provides an official pull request template that you can paste into your comment when you open a PR. It’s a great template, and an essential foundation to dbt best practices but, due to the complicated nature of reviewing dbt data project pull requests, there's still room for improvement.

+

The official dbt PR comment template

+

The official dbt template includes the following sections:

+
    +
  • Description & motivation
  • +
  • To-do before merge
  • +
  • Screenshots
  • +
  • Validation of models
  • +
  • Changes to existing models
  • +
  • Checklist (of small things that are sometimes overlooked)
  • +
+

At Recce, we’re all about validating your work - providing the proof-of-correctness that shows why your work is complete and the PR can be merged. So, we feel this PR comment template can be improved to make it even better suited to this job.

+

Let’s add a few new sections, and update/re-define some of the existing sections to make them clearer.

+
+ What sections should the perfect PR comment contain? +
What sections should the perfect PR comment contain?
+
+

Making a better PR comment template for dbt data projects

+

To improve the dbt PR comment template and make your data quality check processes even better, we can start with adding some new sections that will clarify the purpose of the PR and make it easier to review.

+

Type of change

+

Classifying the type of change helps to frame the work. It guides the PR author toward the type of validations they should run based on the category of work, and the reviewer will have an initial idea how to properly review the work

+

A sensible set of change types to start with could be as follows:

+
    +
  • [ ] New model
  • +
  • [ ] Bugfix
  • +
  • [ ] Refactoring
  • +
  • [ ] Breaking change
  • +
  • [ ] Documentation
  • +
  • [ ] (Other project-specific item)
  • +
+ +

Discussion surrounding a piece of work can happen anywhere, and likely starts with a ticket, chat, or Github issue etc. Provide links to any related discussion that can help clarify your work. This is essential to validating data quality with context. (dbt covers this in the ‘description’ section, but I prefer to have this in its own section.)

+

Impact considerations

+

In this section, include validation on how downstream models have/have not been impacted and what considerations are required. This is crucial for understanding data impact and helps ensure that critical models or exposures remain unaffected. As with validation of models, use screenshots and queries to illustrate the impact.

+

Updated Sections

+

Some of the sections could also be better defined, and potential use clarified:

+

Screenshots → Lineage DAG/Diff

+

You can use screenshots anywhere in the PR comment, so let’s change this section to be ‘Lineage DAG’ and use it for specifically showing the parts of the DAG that have been impacted by this PR. This is where you’d share the Lineage Diff screenshot from Recce that shows only the modified+ part of the DAG after comparing it to before you made any changes.

+

Validation of models

+

This is how you prove that your work is correct and complete. Arguably the most important section of the PR comment as it’s how the reviewer will know that you’ve conducted a thorough data validation.

+

The dbt template mentions sharing dbt test results in this section, but depending on the size of the your project, the test results might crowd out important, PR-specific, data validations. So we’ve moved dbt test results to their own section below.

+

This section should consist of custom queries, profiling stats, data quality checks, data comparisons, and diffs, examples of data impact, anything that helps to validate your work and serve as proof-of-correctness. Use the Recce data validation toolkit to check your work, and include any of the relevant check results, such as

+
    +
  • Ad-hoc queries — Spot-check queries to confirm the results are as expected
  • +
  • Profiling stats — Do you see the expected impact in the overall profile of the data
  • +
  • Value diff stats — Compare the percentage of matched rows between dev and prod
  • +
  • Profile diff — A statistical comparison of dev and prod
  • +
  • Schema diff — If the schema has changed and if it’s intended
  • +
+

Take screenshots of the checks that you ran and post them as validation of models.

+
+

Pro Tip

+

Export the Recce State File and attach it to PR comment. The Recce State File contains your checks and the PR reviewer will be able to load the checks in Review Mode to view the results.

+
+

dbt test results

+

As mentioned above, depending on the size of your project, you may consider moving dbt test results to their own section. This enables your validation of models section to focus on the checks that help to validate your PR-specific work.

+

The updated PR comment template for dbt data projects

+

Here’s the updated PR comment template that you can use to adapt for your needs. As mentioned, this is adapted directly from the official dbt example, with the changes that were detailed above applied. By aligning with dbt best practices, and augmenting with Recce data checks, this template enhances data integrity and supports a more streamlined PR review process.

+ + +

What a PR Template looks like in-action

+

To see what this PR comment looks like in action check out this demo PR comment that shows how using this template together with Recce data validation checks, can create the ultimate PR comment. The data quality check results listed help ensure data impact is accurately assessed.

+

If every PR comment had the data validation check results listed, you’d be able to sign-off a PR and merge in no time.

+
+ A demo showing a PR comment template in use with Recce data validations +
A demo showing a PR comment template in use with Recce data validations
+
+

Who’s using PR comment templates like this?

+

For teams who collaborate on data changes and hold data integrity in high regard, the use of a PR comment templates can streamline the PR review process. As is demonstrated in these real-world examples.

+

Municipal government of Rio de Janeiro

+

A great example of a PR template in action is by data team for the municipal government of Rio de Janeiro (Prefeitura do Rio de Janeiro). The team there uses a PR template and Recce checks for their validation of models, ensuring data quality and data integrity throughout the data review process:

+
+ Prefeitura do Rio de Janeiro uses Recce in their pr comment +
Prefeitura do Rio de Janeiro uses Recce in their pr comment
+
+

The California Integrated Travel Project (Cal-ITP)

+

Cal-ITP is a textbook example of dbt best practices with their extensively detailed PR comments, and supplemental data impact assessments.

+
    +
  • Previous and resolved issues are linked.
  • +
  • The work is clearly described with reasoning.
  • +
  • The reviewer is tagged with questions about changes.
  • +
  • Post-merge actions are defined, adhering to dataops best practices.
  • +
+
+ Cal-ITP is a textbook example of data due diligence +
Cal-ITP is a textbook example of data due diligence
+
+

Conclusion

+

Using a PR comment template for dbt projects is an excellent way to streamline the pull request process, ensuring that both authors and reviewers are aligned on what changes have been made and how those changes have been validated.

+

The updated PR comment template not only helps to define the work contained in the PR, but also helps the PR author to consider and anticipates the types of data impact that may occur. Using Recce’s data validation checks, such as spot-check queries, profile diffs, and schema comparisons, the validation process is standardized, which helps speed up the PR review and time-to-merge.

+

Teams like Cal-ITP and the municipal government of Rio de Janeiro have adopted PR comment templates and use them as part of their PR process to improve reviewability and ensure data integrity.

+

For your dbt projects, use this structured approach to PR comments to achieve faster, more thorough reviews, and ensure that any changes you introduce follow dbt best practices and are well-documented and correctly validated before merging.

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/explore-data-impact-with-focus/index.html b/blog/explore-data-impact-with-focus/index.html new file mode 100644 index 00000000..ca13c5b8 --- /dev/null +++ b/blog/explore-data-impact-with-focus/index.html @@ -0,0 +1,2563 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Explore data impact and focus on tracking data validations with Recce's new interface - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + +

Explore data impact and focus on tracking data validations with Recce's new interface

+

There’s nothing worse than being distracted while you’re in the middle of working on a complex task or debugging an issue. All the things that you were juggling in your mind come crashing down, and you have to pick up the pieces and start again.

+

Recce's updated interface lets you stay on track while assessing and exploring data impact in your dbt project when making dbt data model changes, and performing dbt PR review.

+
+ +

Track and record data impact assessment results

+

Staying in context is especially important when you’re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

+ + +
    +
  • Keep track of the data models that you are checking.
  • +
  • Keep track of the type of data validations that you are running.
  • +
  • Record the results of your data validations and impact checks.
  • +
  • Record the meaning of the results.
  • +
+

It’s important to record and track these things as you validate your work during data modeling, and then also after opening a PR on your dbt data project.

+

How to explore data impact without being distracted

+

This is why, in the latest version of Recce, you can stay in context while adding data validation checks and continue from where you left off after adding a check.

+
    +
  1. Explore changes in your dbt data project by running data validations.
  2. +
  3. Add a data validation check to your checklist.
  4. +
  5. Continue where you left off.
  6. +
+
+ Explore data impact and focus on tracking data validations with Recce's new interface +
Explore data impact and focus on tracking data validations with Recce's updated interface
+
+

Data comparison results show potential impact

+

When exploring data impact with Recce, the Lineage Diff is the main interface. The default view is modified+ and so represents the potential impact radius of your changes.

+

Lineage Diff is where you’ll focus your data impact assessment efforts, so it’s essential that it stays visible during your exploration. This means you can stay focused on your job of validating the correctness of your work, without having to leave your train-of-thought to view results on another page.

+

Perform multiple checks without losing context

+

Now, when you run a diff, the results are shown in the the panel below. There’s no need to leave the lineage diff interface; and you can see the lineage, schema, and diff results, all displayed together.

+

If you do add the results of a diff to your checklist, when you return to the Lineage view, everything is exactly as you left it, so you can continue your work.

+

Model and row-level diffs

+

You can also run model-level diffs such as data profiling, or column-level diffs like histogram and top-k, right from the main interface. The results for each diff will be shown right below the lineage.

+

The new interface means you can find and explore data impact without losing your flow state. Then, when you’ve got the check results you need, add them to your checklist.

+

Save and share data checks with your team for PR review

+

To save a diff to your checklist, click the ‘Add to Checklist’ button and you’ll be taken to the checklist to add your check annotation. Then go back to the Lineage tab to continue where you left off.

+

When you’ve finished your data validation work, you can export the checklist as a Recce File and share with your colleagues, or add checks to your PR comment. If you’re a Recce Cloud user, your checks and approval status will be automatically synced.

+

Join us to make data productive

+

For early access to Recce Cloud, and a hand in making Recce better meet your needs, book a meeting for a demo and chat. We're looking forward to hearing from you!

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/from-devops-to-dataops-effective-data-productivity/index.html b/blog/from-devops-to-dataops-effective-data-productivity/index.html new file mode 100644 index 00000000..bec214dc --- /dev/null +++ b/blog/from-devops-to-dataops-effective-data-productivity/index.html @@ -0,0 +1,2553 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + +

From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity

+

Top priorities for data-driven organizations are data productivity, cost reduction, and error prevention. The four strategies to improve DataOps are:

+
    +
  1. start with small, manageable improvements,
  2. +
  3. follow a clear blueprint,
  4. +
  5. conduct regular data reviews, and
  6. +
  7. gradually introduce best practices across the team.
  8. +
+

In a recent fireside chat, CL Kao, founder of Recce, and Noel Gomez, co-founder of Datacoves, shared their combined experience of over two decades in the data and software industry. They discussed practical strategies to tackle these challenges, the evolution from DevOps to DataOps, and the need for companies to focus on data quality to avoid costly mistakes.

+
+ Firesidechat banner +
Data Productivity - Beyonig DevOps & dbt
+
+ + +

Noel Gomez began his journey at Amgen, where he worked extensively on data governance and digital transformation. He learned that data quality is essential to preventing errors. With his software development background, he realized that many software practices weren’t applied to analytics—and saw the opportunity to merge these worlds.

+

CL Kao worked on version control systems predating Git. He was involved more heavily in data when he helped start Taiwan’s civic tech community, focusing on making public data understandable.

+

Despite their different paths, both CL and Noel agree that many software practices can be adapted to data management, but there are critical differences. What are the similarities, and what sets DataOps apart?

+

Similarities Between DevOps and DataOps

+

DevOps and DataOps stem from a shared philosophy:

+
    +
  1. DevOps and DataOps both emphasize agility, adapting quickly to changing requirements while maintaining efficiency, rooted in lean manufacturing and agile software development.
  2. +
  3. Cross-functional collaboration is essential in both practices. By reducing handoffs between teams, they ensure workflows are smooth. This helps keep pipelines running efficiently and deliver faster insights.
  4. +
  5. A focus on quality is key in both DevOps and DataOps. DevOps ensures clean, bug-free code and DataOps ensures accurate, reliable data for trustworthy decision-making insights.
  6. +
  7. Infrastructure as code is a foundational principle in both practices. It enables teams to manage systems through version-controlled code to ensure consistency across environments and reduce risks from manual configuration.
  8. +
  9. Version control in DevOps and DataOps allows teams to track changes, review updates, and roll back to previous versions to prevent errors in production.
  10. +
  11. Testing and automation are crucial in both practices. They help identify issues early and ensure reliability, with DevOps focusing on code quality and DataOps on data accuracy throughout the pipeline.
  12. +
+

DevOps and DataOps share these principles, but applying them to data introduces unique challenges. While DevOps follows the well-established "preview, decide, deploy" cycle, DataOps often lacks this maturity. As CL and Noel pointed out, in many data environments, teams "deploy first, then decide"—leading to reactive fixes instead of proactive prevention.

+

Why is DataOps so different from DevOps, despite having the same underlying principles?

+

Key Differences Between DevOps and DataOps

+

DevOps focuses on smooth software deployment without downtime, while DataOps ensures the data driving business decisions is accurate and trustworthy, while revisiting how we extract insights.

+

In DataOps, it’s not just about system uptime; the real challenge is preventing incorrect or incomplete data from leading to flawed decisions or costly errors. This introduces a new layer of complexity, as ensuring data accuracy can be more difficult than ensuring software stability.

+

A key distinction between DevOps and DataOps is the confidence teams have before releasing changes. In DevOps, you can deploy code changes confidently because everything has been tested in a controlled environment before production. In DataOps, even with the right tools, validating data is trickier because real-world data constantly changes and behaves unpredictably. It’s not just about running tests; it’s about verifying that the insights or automation from the data still make sense after transformations and processes.

+

1. Different Goals

+
    +
  • +

    DevOps: Focus on Reliable Software Delivery
    +The primary goal of DevOps is to ensure software functions properly and consistently for users. Teams strive to deploy small, frequent changes confidently, without disruptions or compromising user experience. A significant part of this is validating the application performs as expected—whether it's a website or backend system—without crashes or performance degradation.
    Even if DevOps achieves perfect functionality delivery, the data generated or consumed by the software may not be accurate. Noel pointed out, “Their website isn’t going down... but it’s still not delivering the data for analytics and decision-making.” This highlights a key limitation of DevOps: ensuring software stability doesn’t guarantee data correctness.

    +
  • +
  • +

    DataOps: Focus on Data Accuracy and Validity
    +DataOps focuses on validating data. The goal is to ensure systems are running and to confirm the data being used or processed is accurate and reliable. This means checking if the data makes sense—whether the joins, transformations, and calculations are correct and result in actionable insights. If data is incorrect or missing, even the best-maintained software won’t provide value, and businesses could make poor decisions based on faulty insights.

    +
  • +
+

2. The Validation Process

+
    +
  • +

    DevOps: Validating Applications
    +In DevOps, validation focuses on application functionality. Automated tests ensure the application runs correctly, features behave as expected, and the infrastructure remains stable. The primary concern is application stability while introducing changes—ensuring services are consistently available for users without unexpected crashes or interruptions.

    +
  • +
  • +

    DataOps: Validating Data
    +DataOps validation focuses on data correctness. Teams must ensure data transformations, aggregations, and insights are accurate before use in decision-making or customer-facing products. This involves automated tests and in-depth data checks to ensure changes in one part of the pipeline don’t affect other areas. For example, an innocent change could lead to missing or duplicated data, affecting downstream dashboards and reports. This makes DataOps more complex and crucial, as faulty data can lead to poor business decisions or financial losses, worsening with more automated systems.

    +
  • +
+

Why DataOps is More Complex

+

Ensuring data correctness introduces more complexity than ensuring software stability due to unique data management challenges, making DataOps a more intricate process.

+
    +
  1. +

    High Costs of Bad Data
    +Poor data quality can lead to significant financial losses, inefficiencies, and missed opportunities. Minor data errors can propagate through decision-making processes, leading to flawed insights and costly consequences. This makes the stakes in DataOps much higher than traditional DevOps.

    +
  2. +
  3. +

    Data-Centric Software Stacks
    +As software systems become more data-driven, especially with AI and automated decision-making, managing data quality becomes complex. Without a coherent strategy, errors in data pipelines or transformations can lead to inaccurate decisions and a greater need for robust data management processes.

    +
  4. +
  5. +

    Disconnected Teams and Legacy Processes
    +A common issue in DataOps is the disconnect between teams responsible for producing data and those using it, which leads to misunderstandings and inconsistencies. Many organizations still use outdated, manual processes with little awareness of modern data management practices. This absence of governance, code review, and version control makes it challenging to implement best practices and increases the risk of errors.

    +
  6. +
  7. +

    Underrated Data Pipelines
    +Data pipelines are the backbone of decision-making processes, yet they often don't get the respect they deserve. While they may not be as flashy as AI models or analytics tools, everything depends on the quality and reliability of the data flowing through them. Inconsistent or poorly managed pipelines can lead to data issues, undermining the decision-making framework.

    +
  8. +
+

As the software stack becomes more data-centric and data plays a larger role in decision-making, especially in AI trends, this complexity increases. Without a structured approach to managing data, companies risk relying on flawed or inaccurate data, leading to costly mistakes.

+
+ Firesidechat banner +
bad data cost 15%~25% of Revenue
+
+

Practical DataOps Strategies

+

As the discussion dove into the root causes of DataOps challenges, CL and Noel shared four practical strategies from their work with various companies. Attendees were eager to ask: How do you encourage teams using legacy systems and entrenched processes to adopt best practices? How do you recommend data professionals secure buy-in for implementing modern solutions?

+

The four strategies to improve DataOps are: start with small, manageable improvements, follow a clear blueprint, conduct regular data reviews, and gradually introduce best practices across the team. Each step helps build a strong foundation for effective DataOps implementation.

+

If you're interested in their strategies and answers to these questions, as well as detailed advice on implementing DataOps in your organization,

+

Sign up using the link below to access the full video recording: https://datarecce.io/firesidechat/

+

Closing Thoughts: A Brief Look at Datacoves and Recce

+

The fireside chat ended with something that stood out—the passion of these founders. After understanding the challenges teams face with data, CL and Noel focused on different key points, leading them to build two distinct but complementary products.

+

Datacoves, co-founded by Noel Gomez, focuses on accelerating DataOps workflows by integrating open-source tools like dbt. It's designed to be flexible, scalable, and provides teams a clear path to improve data quality without rigid systems. As Noel put it, "It's really about explaining to people and teaching them what's possible out there because we're very passionate about this kind of stuff."

+

Recce, founded by CL Kao, ensures data correctness across the pipeline. It simplifies validating data changes by curating proof of correctness, helping teams maintain confidence in their data as things get more complex. CL's vision is clear: "We are very passionate about the future of data-driven software and its development workflow."

+

What data problems resonate with you? Let's connect and chat!

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/hands-on-data-impact-analysis-recce/index.html b/blog/hands-on-data-impact-analysis-recce/index.html new file mode 100644 index 00000000..3291ad1f --- /dev/null +++ b/blog/hands-on-data-impact-analysis-recce/index.html @@ -0,0 +1,2663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hands-On Data Impact Analysis for dbt Data Projects with Recce - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + +

Hands-On Data Impact Analysis for dbt Data Projects with Recce

+

dbt data projects aren’t getting any smaller and, with the increasing complexity of DAGs, properly validating your data modeling changes has become a difficult task. The adoption of best practices such as data project pull request templates, and other ‘pull request guard rails’ has increased merge times and prolonged the QA process for pull requests.

+
+ Validate data modeling changes in dbt projects by comparing two environments with Recce +
Validate data modeling changes in dbt projects by comparing two environments with Recce
+
+

The difficulty comes from your responsibility to check not only the model SQL code, but also the data, which is a product of your code. Even when code looks right, silent errors and hard to notice bugs can make their way into the data. A proper pull request review is not complete with data validation.

+ + +

What is data validation and why you need to do it

+

Validation is the process of checking that your work is correct and achieves your intention. Common forms of validation are data change impact analysis and regression testing.

+

With impact analysis checks you want to verify that any impact observed is desirable, such as a change in business logic for which you would expect to see data change. Regression testing, on the other hand, is about confirming that data change did not take place. This is useful for cases in which any change to modeling code should not impact the data.

+

For both of these cases comparing the resultant data from your dev branch with a known-good baseline is the key - Show me what’s different, or help me validate that nothing changed. This is where Recce can help.

+

Data Validation Toolkit: Recce

+

Recce is a toolkit especially designed to validate data modeling changes in dbt projects by comparing two dbt environments, such as your dev branch and production data. Recce can help you validate your changes during development, and then use those validations in your PR comment for better PR review - the ultimate purpose of modeling validation.

+

Use Case: Data Impact Analysis in dbt’s Jaffle Shop

+

Jaffle Shop is dbt’s standard intro project for dbt, so hopefully everyone is familiar with it. For this use-case demo, I’ll show how to go from a modeling code change in Jaffle Shop and validate that change using the check suite diffing of tools in Recce.

+

Follow along with the drill down steps detailed below by using the online Recce demo, and the actual PR:

+ +

The code change

+

The code change for this example is simple. It’s a one-liner in the Customers model that modifies the calculation used for customer_lifetime_value to only include completed orders.

+
+ Business logic change to the customers model +
Business logic change to the customers model
+
+

The expected impact

+

This is a change to business logic (maybe more a bug fix), so we would expect to see data impact. As the engineer working on the PR, you’ll already have an expectation of the type of impact you should see.

+

Previously, the calculation for custom_lifetime_value included all orders, regardless of status, meaning returned, and return_pending were erroneously included. After adjusting the calculation we would expect to see:

+
    +
  • An overall reduction in the customer_lifetime_value figures in this table.
  • +
  • Impact to downstream tables that use this figure
  • +
+

We don’t know the scale of the impact, yet. Let’s first validate our expectations and then check the impact.

+

Data drill-down process

+

For a small change such as this, you might go straight to some smaller data spot-checks, but in larger projects, or with more complex updates, it’s better to follow a process of ‘drilling down’ into the data from high level checks, and then narrowing the focus to spot-checks. This will give you the full overview of impact to model and then give you the clues about what to check next.

+

Impact Overview

+

Lineage DAG Diff shows how the DAG has changed based on your branch code and shows added, removed, and modified models.

+
+ Lineage Diff in Recce with Row Count and Schema Diff +
Lineage Diff in Recce with Row Count and Schema Diff
+
+

In the Lineage Diff for this example, you can see that only the customers model has been modified.

+

Clicking the model pulls up the schema and you can check the row count. If there were schema changes they would be shown here (added, removed, and renamed columns). We don’t see any indication of change to the schema, the row count shows no change, and the only modified model is the customers model, so you have validated that there is no undesired change to the high level structure of the project.

+

Note: Lineage Diff differs (ha!) from the regular dbt lineage DAG because it shows the state of the DAG compared to another state. Whereas the dbt docs lineage DAG shows only the current state.

+

High-level data impact assessment

+

The next stage of validation is high-level assessment. There are a number of checks that you can employ for this, currently Profile Diff, Value-Diff, and Top-K Diff. Each providing a different statistical comparison to your baseline data.

+

Profile Diff

+

With the Customers model still selected, click the Advanced Diffs button and click Profile Diff.

+
+ Data Profile Diff in Recce +
Data Profile Diff in Recce
+
+

The Profile Diff shows lots of stats for each column but, for this modeling change, check the min, max, and avg. You can see that the average value for the customer_lifetime_value column has dropped, which confirms our expectation, because a lot of orders have been excluded from the calculation now.

+

Value Diff

+

A Value Diff is also a good way to get a high level overview of change to a model. With the Customers model selected in the Lineage, click Value Diff from the Advanced Diffs. Select customer_id as the primary key, and check the All Columns checkbox, then click the Execute button.

+
+ Choose the primary key and which columns to diff +
Choose the primary key and which columns to diff
+
+

The Value Diff shows the percentage match for each column:

+
+ Value Diff results in Recce +
Value Diff results in Recce
+
+

All columns are 100% match, except the customer_lifetime_value column, which now shows only 1.19% match (2 rows) - That’s quite a lot of impact, you better spot-check some of the data! ;-)

+

Fine-grained data spot-checks

+

Query Diff enables you to run a query against both environments, and diff the difference. It’s the best way to spot-check data and see what’s different. You can use any macros that are installed in your dbt project, and in a lot of ways this is like creating an ephemeral model especially for the purpose of data validation. In the simplest form, we can just select a subset of rows and see where the change is.

+

With the Customers model still selected in the Lineage Diff, click the Query button at the bottom right and then adjust the query to select a subset of data:

+
select * from {{ ref("customers") }} where customer_id < 100;
+
+

Note: Avoid using LIMIT in Query Diff because LIMIT does not return the same rows each time, which is required form a useful query diff.

+

Click Run Diff to query both dbt environments.

+

When the results are returned click on the key icon in the customer_id column to confirm the primary key. Any columns with a mismatched value will be displayed in red.

+
+ Query Diff results in Recce +
Query Diff results in Recce
+
+

Spot-checking the customer_lifetime_value column you can see that the current value (the dev branch) are all decreased. This is correct as the calculation we adjusted should only result in a decrease in total order value.

+

Also, from the Value Diff we ran earlier, we know that only the customer_lifetime_value column should have changed, and that’s confirmed from this Query Diff.

+

Checking downstream impact

+

This code change updates a key model, and the customer_lifetime_value column is used downstream in the customer_segments table. With the impact being so great (~99% of customer_lifetime_value has changed) we should check how this affects downstream models.

+
+ The customer_segments model uses customer_lifetime_value +
The customer_segments model uses customer_lifetime_value
+
+

Running a Top-K Diff on the value_segment column of customer_segments shows how the distribution has changed with a notable reduction in customers categorized as ‘High value’.

+
+ Top-K Diff on customer_segments.value_segment +
Top-K Diff on customer_segments.value_segment
+
+

Given the change in distribution, if this was a real world situation, you might choose to notify downstream stakeholders.

+

The Checklist

+

The process above demonstrates how you would validate your work during development but, as mentioned, the ultimate purpose of data validation is to provide proof-of-correctness of your work for PR review.

+

At each stage in the validation process Recce allows you to add a validation check to the checklist, along with your notes. Just click the Add to Checklist or Add Check button when you see it, and the current validation check will be added to your checklist.

+
+ Validation checklist in Recce +
Validation checklist in Recce
+
+

In the checklist you can add a note for your validation to explain your findings and provide context. Then, when you’re ready to create a PR, you can copy the checklist items into your PR to help the reviewer understand and sign-off on your work.

+

Conclusion

+

In the above steps, you performed a data impact analysis and successfully validated the code changes by inspecting the data at varying grains of detail.

+

Check back for more articles in the future that look at other use cases such as validating a refactoring job, and also how to make the most of Recce validations in your PR comment.

+

Get started with Recce

+

Recce OSS is available on GitHub now. Follow the instructions in our Getting Started guide to start using Recce to validate your data modeling changes in your project.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/histogram-overlay-top-k-dbt/index.html b/blog/histogram-overlay-top-k-dbt/index.html new file mode 100644 index 00000000..b252abea --- /dev/null +++ b/blog/histogram-overlay-top-k-dbt/index.html @@ -0,0 +1,2506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + +

Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt

+

Data profiling stats are a really efficient way to get an understanding of the distribution of data in a dbt model. You can immediately see skewed data and spot data outliers, something which is difficult to do when checking data at the row level. Here's how Recce can help you make the most of these high-level data stats:

+

Visualize data change with histogram and top-k charts

+

Profiling stats become even more useful when applied to data change validation. Let’s say you’ve updated a data model in dbt and changed the calculation logic for a column — how can you get an overview of how the data was changed or impacted? This is where checking the top-k values, or the histogram, of before-and-after you made the changes, comes in handy — But there’s one major issue...

+
+ The best way to visualize data change in a histogram chart +
The best way to visualize data change in a histogram chart
+
+

Something’s not right

+

If you generate a histogram graph from prod data, then do the same for your dev branch, you’ve got two distinct graphs. The axes don’t match, and it’s difficult to compare:

+ + +
+ Histogram side-by-side +
Comparing to distinct histogram charts is impossible
+
+

You might be able to spot some differences, such as at the top end of the graph, but the overall impact is mostly hidden.

+

The same is true for top-k. Cross-referencing categories might be doable when there are only a handful, but it’s still not a meaningful way to visualize the differences:

+
+ Top-K comparison +
Cross-referencing Top-K will become difficult with more categories
+
+

There’s a real possibility that you’ll miss some edge cases when you can’t compare precisely and accurately. That means silent errors, or even pipeline-breaking errors, will make it into prod. Errors that you won’t find out about until the client or stakeholder calls to asks what’s up with the data.

+

How to meaningfully compare histograms and top-k

+

The best way to diff profile stats like histogram and top-k stats, is to plot them on a single chart, overlaid on top of each other using shared axes.

+

Here’s the same histogram charts as the image above, but with the histograms plotted on a single chart:

+
+ Histogram Overlay Chart in Recce +
An overlaid histogram makes the charts actually useful!
+
+

In this overlay histogram chart, you can quickly see the distribution change in a meaningful way. At each value range the scale of the data change is clear. The same is true for top-k. When the values are compared directly next to each other the impact is immediately understandable.

+
+ Top-K Diff side-by-side +
Side-by-side Top-K values for direct comparison
+
+

Doing this manually isn’t straight forward. You’d need to code it in a Jupyter notebook with python and would take a lot of configuration, especially given that for dbt data projects you’ll be pulling data from two different schemas if you’re diffing dev and prod. Checking this kind of data profile diff for each PR in your project would require an unreasonable amount of work.

+

Easily diff histogram, top-k and other profiling stats in Recce

+

You can get histogram and top-k diffs, especially designed for dbt data projects, as part of the suite of data modeling validation tools in Recce.

+

Recce compares your development and production datasets (or any two dbt envs) and enables a visual representation of data change through multiple diffing tools and query comparisons.

+
+ Histogram overlay chart for your dbt PR Review with Recce +
Get an overlaid histogram diff for your dbt PR Review with Recce
+
+

Improved visibility of data change for dbt

+

As a data or analytics engineer, the improved visibility of data change makes validating your work quicker, easier, and more accurate.

+

As a PR reviewer you can do your job more efficiently and confidently sign off on data changes knowing that an edge case won’t come back to bite you.

+

Recce is open-source and available now, so you can start properly validating your dev branch right away.

+

Get Recce

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 00000000..63109867 --- /dev/null +++ b/blog/index.html @@ -0,0 +1,2657 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Articles - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

The Recce Blog

+

Here you can find articles about dbt best practices and how to get the most out of Recce. For more articles, don't forget to also check out our Medium publication.

+
+
+ +
+
+ + +
+
+

Explore data impact and focus on tracking data validations with Recce's new interface

+

There’s nothing worse than being distracted while you’re in the middle of working on a complex task or debugging an issue. All the things that you were juggling in your mind come crashing down, and you have to pick up the pieces and start again.

+

Recce's updated interface lets you stay on track while assessing and exploring data impact in your dbt project when making dbt data model changes, and performing dbt PR review.

+
+ +

Track and record data impact assessment results

+

Staying in context is especially important when you’re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

+ + + + +
+
+ +
+
+ + +
+
+

The Ultimate PR Comment Template Boilerplate for dbt data projects

+

If you’re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

+

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

+
+ Example of a PR comment with comprehensive data validation checks +
Example of a PR comment with comprehensive data validation checks
+
+

The benefits of using a PR comment template

+

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

+ + + + +
+
+ +
+
+ + +
+
+

Meet Recce at Coalesce 2024 and The Data Renegade Happy Hour

+

The Recce team will be joining Coalesce 2024 in Las Vegas! Meet our founder, CL Kao, and product manager, Karen Hsieh, who has also been hosting the Taipei dbt meetups. As a company focused on helping data teams prevent bad merges and improve data quality, we believe Coalesce is the perfect venue to connect with fellow data professionals, share insights, and gain fresh perspectives.

+
+ Firesidechat banner +
We are attending Coalesce 2024
+
+

At Recce, our mission is to transform the data PR review process, ensuring that data pipelines not only run smoothly but also deliver accurate, validated results. We believe that data should be correct, collaborative, and continuously improved. Coalesce 2024 offers an ideal platform for these crucial conversations, gathering experts across the field to discuss the future of data management. Whether it’s gaining new insights into best practices or forging valuable partnerships, Coalesce is where we aim to make an impact.

+ + + + +
+
+ +
+
+ + +
+
+

The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review

+

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the ‘T’, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

+

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

+
+ Move Fast and DON'T Break Prod +
Move Fast and DON'T Break Prod
+
+ + + + +
+
+ +
+
+ + +
+
+

From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity

+

Top priorities for data-driven organizations are data productivity, cost reduction, and error prevention. The four strategies to improve DataOps are:

+
    +
  1. start with small, manageable improvements,
  2. +
  3. follow a clear blueprint,
  4. +
  5. conduct regular data reviews, and
  6. +
  7. gradually introduce best practices across the team.
  8. +
+

In a recent fireside chat, CL Kao, founder of Recce, and Noel Gomez, co-founder of Datacoves, shared their combined experience of over two decades in the data and software industry. They discussed practical strategies to tackle these challenges, the evolution from DevOps to DataOps, and the need for companies to focus on data quality to avoid costly mistakes.

+
+ Firesidechat banner +
Data Productivity - Beyonig DevOps & dbt
+
+ + + + +
+
+ +
+
+ + +
+
+

Identify and Automate Data Checks on Critical dbt Models

+

Do you know which are the critical models in your data project?

+

I’m sure the answer is yes. Even if you don’t rank models, you can definitely point to which models you should tread carefully around.

+

Do you check these critical models for data impact with every pull request?

+

Maybe some, but it’s probably on a more ad-hoc basis. If they really are critical models, you need to be aware of unintended impact. The last thing you want to do is mistakenly change historical metrics, or lose data.

+
+ Every dbt project has critical models +
Impacted Lineage DAG from Recce showing modified and impacted models on the California Integrated Travel Project dbt project
+
+

Identifying critical models

+

Knowing the critical models in your project comes from your domain knowledge. You know these models have:

+ + + + +
+
+ +
+
+ + +
+
+

Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt

+

Data profiling stats are a really efficient way to get an understanding of the distribution of data in a dbt model. You can immediately see skewed data and spot data outliers, something which is difficult to do when checking data at the row level. Here's how Recce can help you make the most of these high-level data stats:

+

Visualize data change with histogram and top-k charts

+

Profiling stats become even more useful when applied to data change validation. Let’s say you’ve updated a data model in dbt and changed the calculation logic for a column — how can you get an overview of how the data was changed or impacted? This is where checking the top-k values, or the histogram, of before-and-after you made the changes, comes in handy — But there’s one major issue...

+
+ The best way to visualize data change in a histogram chart +
The best way to visualize data change in a histogram chart
+
+

Something’s not right

+

If you generate a histogram graph from prod data, then do the same for your dev branch, you’ve got two distinct graphs. The axes don’t match, and it’s difficult to compare:

+ + + + +
+
+ +
+
+ + +
+
+

Hands-On Data Impact Analysis for dbt Data Projects with Recce

+

dbt data projects aren’t getting any smaller and, with the increasing complexity of DAGs, properly validating your data modeling changes has become a difficult task. The adoption of best practices such as data project pull request templates, and other ‘pull request guard rails’ has increased merge times and prolonged the QA process for pull requests.

+
+ Validate data modeling changes in dbt projects by comparing two environments with Recce +
Validate data modeling changes in dbt projects by comparing two environments with Recce
+
+

The difficulty comes from your responsibility to check not only the model SQL code, but also the data, which is a product of your code. Even when code looks right, silent errors and hard to notice bugs can make their way into the data. A proper pull request review is not complete with data validation.

+ + + + +
+
+ +
+
+ + +
+
+

Next-Level Data Validation Toolkit for dbt Data Projects — Introducing Recce

+
+ Build the ultimate PR comment to validate your data modeling changes +
Recce: Data Validation Toolkit for dbt
+
+

Validating data modeling changes and reviewing pull requests for dbt projects can be a challenging task. The difficulty of performing a proper ‘code review’ for data projects, due to both the code and data needing review, means the data validation stage is often omitted, poorly implemented, or drastically slows down time-to-merge for your time sensitive data updates.

+

How can you maintain data best practices, but speed up the validation and review process?

+ + + + +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/index.html b/blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/index.html new file mode 100644 index 00000000..d508d2ed --- /dev/null +++ b/blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/index.html @@ -0,0 +1,2453 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Meet Recce at Coalesce 2024 and The Data Renegade Happy Hour - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + +

Meet Recce at Coalesce 2024 and The Data Renegade Happy Hour

+

The Recce team will be joining Coalesce 2024 in Las Vegas! Meet our founder, CL Kao, and product manager, Karen Hsieh, who has also been hosting the Taipei dbt meetups. As a company focused on helping data teams prevent bad merges and improve data quality, we believe Coalesce is the perfect venue to connect with fellow data professionals, share insights, and gain fresh perspectives.

+
+ Firesidechat banner +
We are attending Coalesce 2024
+
+

At Recce, our mission is to transform the data PR review process, ensuring that data pipelines not only run smoothly but also deliver accurate, validated results. We believe that data should be correct, collaborative, and continuously improved. Coalesce 2024 offers an ideal platform for these crucial conversations, gathering experts across the field to discuss the future of data management. Whether it’s gaining new insights into best practices or forging valuable partnerships, Coalesce is where we aim to make an impact.

+ + +

Coalesce provides invaluable insights

+

This journey didn’t start overnight. The idea for Recce was born from deep conversations with data practitioners at Coalesce 2023 in San Diego. At the time, CL Kao was working on PipeRider, a data impact assessment tool for dbt data projects.

+

It became clear from the conversations that the need for a more comprehensive solution—something beyond data comparisons and observability—was critical. From those learnings, CL pivoted PipeRider into what is now Recce, focused on improving the entire data PR review process. Coalesce 2023 provided invaluable insights, and this year, we’re returning with an even stronger vision.

+

The Analytics Development Lifecycle (ADLC) white paper sets the stage for the discussions at Coalesce, and we’re eager to dive in. Trends like DataOps, automation, collaboration and data quality resonates deeply with Recce’s goals. While modern data practices draw significant inspiration from software engineering, data has its own unique challenges. One such challenge is that simply applying software methods directly to data isn’t always effective. Data review, for example, requires more than just SQL code reviews; it demands a thorough examination of the actual data.

+

The Data Renegade Happy Hour: Where Data Power Meets Happy Hour

+

It’s not just all work! Coalesce also gives us the chance to let loose and connect in a more relaxed setting. That’s why we’re teaming up with some of the brightest minds in data to co-host a special event:

+

Join the team from Recce and Tobiko, Cube, Paradime.io, Datacoves, and Steep on October 8th at the Data Renegade Happy Hour.

+
+ Firesidechat banner +
Data Renegade Happy Hour
+
+

It’s set to be a fun-filled evening where you can connect with industry pros over witty banter, enjoy data-themed cocktails, and be amazed by world-class magic from international champion John George. +Don’t miss the perfect blend of insights and entertainment. + RSVP Soon, space is limited!

+

RSVP Today!

+

Chat with us

+

We are looking forward to engaging with the data community on some of the most pressing challenges facing teams today, including how to prevent bad merges, validate results, and perform robust impact analysis. Every data practitioner who’s ever created or reviewed a PR knows the frustration of spot checks and incomplete data analysis.

+

We’re also curious to understand what’s holding teams back from addressing these issues. Why do 74% of business stakeholders report revenue loss due to poor data quality, yet many data teams still struggle to prioritize improvements? Everyone talks about the need for high-quality data, but not everyone is taking action. We want to hear how different industries, company sizes, and data teams approach these challenges and discuss how Recce can play a role in solving them—or even understand why Recce might not be the right fit in some cases.

+

Stay tuned for more details about where to find us during the event! We can’t wait to meet you all, exchange ideas, and explore how we can transform the way data is managed—stickers and limited swags are waiting for you, too.

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/self-serve-analytics-pr-review/index.html b/blog/self-serve-analytics-pr-review/index.html new file mode 100644 index 00000000..2e743d6e --- /dev/null +++ b/blog/self-serve-analytics-pr-review/index.html @@ -0,0 +1,3006 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Support Self-Serve Data with Comprehensive PR Review - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + +

The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review

+

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the ‘T’, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

+

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

+
+ Move Fast and DON'T Break Prod +
Move Fast and DON'T Break Prod
+
+ + +

dbt transformations are stored as a series of SQL models that transform the data through raw, staging, intermediate, and then mart stages, in which the data is ready for production use. Keeping all of these transformations in the same place, as the SSoT for how your data is generated, ensures that data validation and data quality checks can be systematically applied at every stage. This also facilitates PR review to maintain data integrity.

+
    +
  • Version control - the dbt project can be managed in a version control system such as git, essential for dbt CI workflows and PR review.
  • +
  • Modular SQL - dbt encourages a modular design to data pipelines. This better facilitates reviewing data models and transformed data, and enabling the re-use and referencing of data models.
  • +
  • Reproducible pipelines - the dbt project contains everything required to run the pipeline, which means anyone can checkout the project from version control and build the data. This is a fundamental part of data best practices and dataops.
  • +
+

If you’ve come from a software engineering (SE) background, then these benefits may be familiar to you. Version control, modularity, and reproducibility are the tenets of DevOps, and have benefited software development for years. Through dbt, you’re able to adapt them as part of a new practice of DataOps, and the pull request (PR) is at the center of the process.

+

Data Incidents still happen

+

Given the benefits that dbt brings, you’d be forgiven for thinking that the data problem was solved - That data was always accurate, and bad data was never merged into production. Unfortunately, due to the relative infancy of DataOps, that’s still not the case. Data incidents still happen.

+

As CL Kao, founder of Data Recce put it in a recent fireside chat with Noel Gomez of Datacoves:

+
+

“The construct that turned devops from a best practice into a viable, productionized, practice, is the pull request… preview, decide, deploy. (However) In data, we deploy, and then decide if we want to fix it later
- CL Kao, Data Productivity - Beyond DevOps and dbt +

+
+

The “deploy and then fix it later” quote probably rings true for a lot of data teams that are working under tight deadlines, with many simultaneous PRs, and find it difficult to perform due diligence on every PR before merging.

+

Look ma, no guardrails!

+

The version-controlled nature of dbt makes code review straightforward, but data review remains a challenge. It’s vital to build guardrails around the PR review process to ensure that data quality is upheld and the data impact of changes is thoroughly assessed.

+

How much does a data incident cost?

+

Merging bad data, whether that be from accidentally changing historic data, to introducing silent errors into production data, can be very costly. Data impact assessment prevents such incidents through rigorous data validation, data quality checks, and structured PR review - Avoiding substantial financial losses caused by bad data.

+
+ The Cost of Data Incidents +
The Cost of Data Incidents
+
+

Mikkel Dengsoe of Sync recently calculated that if the cost of a data incident ranges from $1,000 to $100,000 per-incident, that for a large data team that could result in costs of “between $500,000 and $4,000,000 per year”.

+

As Mikkel puts it, the value of data for business critical applications is high:

+
+

“A few hours of incorrect data for an eCommerce ad bidding machine learning model can easily cost $100,000.” Mikkel Dengsoe, The cost of data incidents +

+
+

Why dbt data PRs are hard to review

+

Firstly, data change is inherently hard to review. The pull request review process from software practices is not suited to reviewing data. PR review is all about reviewing code, not data. When you open a pull request on a data project, you can easily see how the SQL code and transformation logic has changed, but the data is still a black box, and makes data impact assessment and data validation a unique challenge.

+
+ New Challenges to Data Integrity +
New Challenges to Data Integrity
+
+

Secondly, dbt has introduced ‘self-serve data’., where more users within an organization can access and manipulate data. This shift means that dataops practices much evolve to ensure that data quality and data integrity is maintained as the number of data users grows.

+
+ + + +
+ + +
+

Download the Deck

+ +
+
+ +
+ +
+ + +
+
+
+
+
+ +
+ +

What is self-serve data?

+

The core principle of self-serve data is to enable wider access to data without the need for specialized knowledge, or the help of data specialists. As atlan says, users should be able to “access, analyze, and manipulate the data” by themselves:

+
+

Self-serve data refers to tools… that enable …users to access, analyze, and manipulate data without requiring the direct intervention of data specialists +— atlan +

+
+

dbt takes it a step further, in that users should not require specialized knowledge to create new data products:

+
+

A self-serve data platform… supports creating new data products without the need for custom tooling or specialized knowledge. +— dbt +

+
+

All you need to know is SQL, and you can start using dbt and access and manipulate the data to your needs.

+

The actual implementation of self-serve data will differ from organization to organization, but at the very least you would expect that analysts will be shifting some analytics out of BI and directly into dbt. In other cases, non-technical (in engineering terms) teams, such as finance or marketing, may also be directly accessing the data project.

+

Data, meet integrity

+

An initial worry from data engineering teams is that self-serve data may introduce data integrity issues, and undermine the work of data engineers who are traditionally tasked with maintaining the stability and integrity of data and data pipelines. Indeed, to successfully adopt self-serve analytics, a shift in internal culture is required.

+
+ Self-serve data doesn’t have to mean self-serve chaos +
Self-serve data doesn’t have to mean self-serve chaos
+
+

Rather than gatekeeping access to data, data engineering teams should actively support the self-serve nature of data. The pull request process is central to maintaining data integrity. It’s the stage in which data quality is upheld, and the intention of the PR author defined and validated.

+

How to support a self-serve data culture

+

With the PR review being central to ensuring data integrity, the team must validate both the code and the data changes to understand the data impact. Following dbt best practices for data validation and data review is critical to building trust in self-serve data environments.

+

If the author of the PR records the steps that they took to validate the data generated from the models in the PR, this enables:

+
    +
  1. The PR author is able to review their own work, ensuring data quality.
  2. +
  3. The data team is able to review the work of others (peer review), enhancing DataOps processes.
  4. +
  5. The data validation checks can be shared with others for approval ensuring data integrity by enabling business-context review.
  6. +
+

To enable this PR review workflow for data, the PR process for data projects needs to be augmented with suitable tools, enabling 'self-serve review' for self-serve data.

+
+ Data impact assessment is more important than ever +
Data impact assessment is more important than ever
+
+

Communication overhead

+

As more people modify the data project, more PR review is required, bringing with it communication overhead. The team needs to communicate their adjustments and modifications to the data pipeline that require review, and the data team needs to be able to support this review.

+

To enable better comunication between data teams and stakeholders, we must first understand what the review process for data actually looks like, and understand how to review data.

+

How to perform data impact assessment for PR review

+

Data impact is difficult to assess. You’re faced with the problem of having to interpret if the numbers you’re looking at are correct. How can you know that with confidence, ensuring data quality?

+

1. Show me the change

+

The best way to perform data impact assessment is to see exactly how the data has changed.

+

Benn Stancil, of Mode Analytics, takes a humorous look at this in a recent blog post. Benn provides two methods. The first, a complicated, convoluted process following the data through contract alerts, and log files. The second, simply:

+
+

“Check if historical values of the metric are the same today as they were yesterday.” +
- Benn Stancil, All I want to know is what’s different +

+
+

It’s that simple. You want to be able to easily compare data from before-and-after making data model changes. Doing this will, and confirming that metrics or key figures didn’t change will give you the confidence to merge a PR knowing that unintended impact did not occur, therefore maintaining data integrity.

+
+ Just show me what's different +
Just show me what's different
+
+

2. Help me validate change

+

Confirming that data didn’t change is good for refactoring work and detecting unintended impact, but there are also instances when you would expect to see data change - You want to confirm that the data is correct, and that the SQL (data model logic change) has the intended data impact.

+

Comparing data, and the structure of the data pipeline, before-and-after making changes will enable you to see data impact clearly. You want to be able to easily see newly added, removed, and modified data models, and perform data validation checks to ensure accuracy. By doing this, you can assess data quality and look into the models to see how that data has changed, ensuring data integrity throughout the PR review process.

+

What’s your intention and expectation

+

To understand if data impact is correct, you need to already have an expectation of what to look for. This expectation is based on your intention, or, what you are trying to achieve. This is why the pull request template is such a fundamental tool, it helps the PR author clarify these things.

+
    +
  • Intention - What you trying to do, such as fix a bug, add a new column, refactor a group of models etc.
  • +
  • Expectation - Given you intention, what is the expected data impact once the work is completed? For example, no rows should change on models X and Y, or the average value of Z column should remain constant.
  • +
+

Once these are defined, the process of validating data impact becomes a lot easier. The main problem is then the mechanism to perform the data validation checks to prove your expectation, and the groundwork that is needed in your data project to enable that.

+

Clarity through comparison

+

Verifying your work before pushing to production is the only way to be absolutely confident that there are no breaking changes. In order to do this, you need to have a duplication of your production environment, or a method to reproduce, or replicate, a subset of production, so that you have a known-correct baseline to check your work against.

+

Noel Gomez, of Datacoves, describes the importance of having reproducible environments:

+
+

“We want to make sure that we have reproducible environments… this allows you to experiment. …you’re working in a different environment, you’re not going to break something. (then) there should be a review before (it goes to production). The goal is ‘Does this data make sense?’”
- Noel Gomez, Data Productivity - Beyond DevOps and dbt +

+
+

Reproducible environments in dbt

+

Following dbt best practices for how to replicate data environments include:

+
    +
  • Per-developer schemas - this allows the person modifying the pipeline to check their work by building models into their own schema. Enabling the developer (data engineer, analyst etc) to perform data quality checks before opening a PR.
  • +
  • Per-PR schemas - Each time a PR is opened, the new project code should build the project into a PR-specific schema. This enables automated, dbt CI-time (continuous integration) checks to run (more on that below), helping with data validation and ensuring the PR review process is efficient.
  • +
  • Staging schema (subset of prod) - As part of CD (continuous delivery) build a subset of production data into a separate schema. This acts as the known-good baseline and can be used to check developer and PR schemas against. The benefit is that you don’t need to replicate the whole of production, which can be time consuming and expensive.
  • +
+

As Noel mentions in the quote above, having the ability to replicate environments empowers developers—those modifying the data pipeline and validating changes—to confidently assess data quality and data impact without compromising production data. A strategy that streamlines the development process and also adheres to data integrity principles within a self-serve data culture.

+

Data validation with Recce

+

Once you have a suitable system in place to manage your database environments during development and deployment, you’re then able to perform comparisons between these environments. This is where Recce can help enhance your data validation process and improve your dbt PR review.

+

Recce provides a suite of tools that are specially designed to assess data impact by comparing the data in two dbt environments.

+
    +
  • You’re clear what your intention is
  • +
  • You’re clear what your expectation is
  • +
  • Now, prove it
  • +
+

Recce helps you to prove that your data model changes have the intended impact through its data impact check framework. Recce data checks, and enable you to:

+
    +
  • compare your data
  • +
  • record your findings
  • +
  • share those findings
  • +
+

Recce Checks

+

Recce checks are individual data validations that show the results of a specific type of comparison between your data environments. These checks help to prove that your intention was successful, and your expectation has been realized in the data. You use these check results for:

+
    +
  • validating your work, as you work
  • +
  • sharing as part of discussion between colleagues or stakeholders
  • +
  • sharing as proof-of-correctness of your work
  • +
  • allowing others to approve the results for data validation
  • +
+

There are various check types that range from high level profiling, right down to row level data inspection. Each type of check provides an insight into how data differs, or not, compared to the previous state. By recording the results of your checks, you can demonstrate that proper impact assessment has been carried out and share the results with others for data QA.

+

Structural Checks

+

Structural checks provide an overview of structural impact to the data project.

+
    +
  • Lineage Diff - Shows how the lineage has changed in the PR and the dependencies between models.
  • +
  • Row Count Diff - Shows the row count change for models that gives a high level indication that the amount of data is the same.
  • +
  • Schema Diff - Shows if any models have added, removed, or renamed columns, providing insight into data structure modifications.
  • +
+

Statistical Checks

+

Statistical checks provide a more detailed look at the data which helps to determine the type of data impact that may have occurred.

+
    +
  • Profile Diff - Shows the comparison of statistical profiles, such as mean, median, standard deviation, and distinct count, between the current and previous data.
  • +
  • Value Diff - Shows the percentage matched rate for each column between the current and previous data.
  • +
  • Top-K Diff - Shows the most frequent values in categorical fields between current and previous data.
  • +
  • Histogram Diff (distribution) - Shows an overlay comparison of data distribution between current and previous data.
  • +
+
+ Data Propfile Diff +
Detect statistical anomalies with Data Profile Diff
+
+

Row-Level Checks

+

Row-level checks enable you to do data spot-checks on specific rows.

+
    +
  • Query Diff - Run any query and see how the results of that query differ between current and previous data.
  • +
+

Checks can be added to the Recce Checklist as part of your validation work, or they can be added automatically (see below).

+

Recce checklist

+

Organized into a checklist, Recce checks help to collectively validate a PR, ensuring data quality and data integrity throughout the development process. Each check contains:

+
    +
  • Check results - The results of a specific check that helps to validate the success of the PR.
  • +
  • Author’s annotation - A description that explains what is being shown and why the check results are vital for understanding the data quality and data integrity of the PR.
  • +
+

The checklist helps to guide the reviewer through the process that was used to validate the PR. When accompanied by a detailed PR comment, will enable the reviewer to understand the author's intention and expectation, allowing for informed sign-off on the PR or requests for further data checks.

+

Automate checks for critical models (preset checks)

+

In addition to checks that fall within the scope of the PR, there should also be global checks that run regardless of the work being done for the PR. These global checks are called Preset Checks in Recce, are designed to ensure complete data quality coverage of your project, and target models that have:

+
    +
  • Significance importance to the business,
  • +
  • Numerous downstream dependencies,
  • +
  • Historical issues that may prompt inquiries about data integrity if problems arise (in other words, you'll get a call about that data if something goes wrong!)
  • +
+

Read more about the importance of identifying and checking critical dbt models as part of data validation strategry for dbt and maintaining data quality in dbt PR reviews.

+

Share validation check results

+

Once you’ve curated a checklist of data validation checks, it’s time to share those checks in your PR comment. With Recce you can do this in two ways:

+
    +
  1. Share individual checks for focused data impact discussions
  2. +
  3. Share the Recce File, which contains the complete checklist and allows other team members to continue review or help validate impact.
  4. +
  5. Use Recce Cloud for to sync checks across Recce instances and block PR merging until all data impact has been assessed
  6. +
+

Sharing individual checks

+

Sharing individual checks can be useful for smaller PRs, or when you want to have discussion around the results. For more complex PRs, with many checks, the Recce File is the more powerful choice for collaborating on data validation.

+

Sharing the Recce File

+

Exporting the Recce File offers two key advantages:

+
    +
  1. The full checklist is made available for review, enhancing transparency in your data validation processes.
  2. +
  3. Reviewers can interact with data checks and perform and save additional checks if necessary.
  4. +
+

The PR author can export the Recce File, then attach it to the PR, or save it in your organization’s shared storage platform. Another colleague can then run Recce in ‘review mode’ to start the PR review, facilitating thorough data validation and promoting teamwork in reviewing data PRs.

+

Recce Cloud

+

For those using Recce Cloud, the process is a lot smoother, with checks synced between Recce Instances, and the ability to launch a Recce Instance and review a PR within minutes from the Recce Cloud interface.

+

With Recce Cloud, there’s no need to export and share the Recce File, or deal with manually exporting checks.

+

Recce Cloud integrates with your GitHub PR and can block merging until all checks have been reviewed.

+
+ + + +
+ + +
+

Download the Deck

+ +
+
+ +
+ +
+ + +
+
+
+
+
+ +
+ +

Conclusion

+

dbt brings a lot of benefits to data projects by incorporating essential software engineering practices into the data analytics workflow. By embracing and adopting organizations can support a self-serve data culture that empowers teams to access and leverage data effectively, while also maintaining data integrity.

+

For discussion on how to properly adopt devops best practices for datatops, check out the Data Productivity - Beyond DevOps and dbt Fireside Chat between the founders of Recce and Datacoves.

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+

Get Recce updates in your inbox

+

Interested in data best practices and Recce usage tips?

+ +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/tags/index.html b/blog/tags/index.html new file mode 100644 index 00000000..b0511033 --- /dev/null +++ b/blog/tags/index.html @@ -0,0 +1,2668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Tags - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Tags

+

Following is a list of tags for Recce Blog content:

+

Best Practices

+ +

Data Integrity

+ +

Data Quality

+ +

Data Review

+ +

Data Validaton

+ +

DataOps

+ +

Self-Serve Analytics

+ +

Self-Serve Data

+ +

data exploration

+ +

data impact

+ +

data validaton

+ +

dbt CI

+ +

dbt PR Review

+ +

dbt data model validation

+ +

dbt models

+ +

impact assessment

+ +

root cause analysis

+ + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/case-studies/index.html b/case-studies/index.html new file mode 100644 index 00000000..71de323a --- /dev/null +++ b/case-studies/index.html @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + Recce Case Studies - Read about how real data teams are using Recce + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+ See what our users are saying about Recce + +

+
+
+
+
+ +
+
+
+ + + +

Recce

+
+ + +
+
+ + + + +
+ +
+ +
+
+
+
Case Study
+

Rio de Janeiro Department of Health

+

Prefeitura do Rio de Janeiro uses Recce to ensure data correctness of the health records for 7 million people

+

Read how the department's data team took their PR merge time from over a day to just 1 hour.

+ + Read More + +
+ +
+ +
+
+
+ +
+ + + + +
+ +
+ +
+

Book a demo

+

Find out more about how Recce can improve your data PR review process

+
+ + +
+ + + +

+ + Book us with Cal.com + +

+ + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/case-studies/rio-department-of-health/index.html b/case-studies/rio-department-of-health/index.html new file mode 100644 index 00000000..458fd64e --- /dev/null +++ b/case-studies/rio-department-of-health/index.html @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + Recce Case Study - Rio de Janeiro Department of Health + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+ See what our users are saying about Recce + +

+
+
+
+
+ +
+
+
+ + + +

Recce

+
+ + +
+
+ + + + +
+ +
+ +
+
+
+
Case Study
+

Rio de Janeiro Department of Health

+

Prefeitura do Rio de Janeiro uses Recce to ensure data correctness of the health records for 7 million people

+

Recce enabled a formalized and streamlined approach to developing with dbt, providing complete visibility into data impact from modeling changes and eliminating the risk of merging bad data into production.

+
+ +
+ +
+
+
+ +
+ +
+

The Stats

+
+
+
+

Pull Requests Now Merge in 1 Hour

+

Decreased dbt pull-request review-time from 1 day to an hour or less

+
+
+

Complete Visibility into Data Impact

+

PR authors can now see and understand the data impact from dbt modeling changes

+
+
+

Enhanced Stakeholder Communication

+

The single-source-of-truth for discussion with stakeholders and PR reviewers

+
+
+
+
+ +
+ +
+
+
+ +

The challenges of maintaining the health records for 7 million people

+ +

The data team at the Prefeitura do Rio de Janeiro (City of Rio de Janeiro) health department uses Recce, and the workflows that Recce encourages, to substantially improve the pull request process on their large-scale electronic health record (EHR) integration data project.

+ +

The health department's data team, consisting of six data scientists and engineers, was tasked with integrating EHR data from various sources covering around 7 million users and 100 million events.

+ +
+ +
+ +

+ In my job I have to understand data change, and Recce was a game changer. It made our PR review much quicker - From a day to less than an hour to merge. +

+
+ +
+
+ +
+

Thiago Trabach

+
Head of Data Science & Engineering
Prefeitura do Rio de Janeiro
+
+
+
+ +
+ +

A better dbt pull request review process was needed

+ +

Daily work on the project involved defining and maintaining a growing dbt project with increasing complexity. This resulted in frequent PRs requiring thorough review. Thiago, the head of data and engineering, was responsible for reviewing the majority of PRs and needed a solution to better optimize the PR review process to ensure data quality and consistency.

+ +

Before implementing Recce, the PR review process was unstructured and time-consuming. The manual approach sometimes resulted in errors making their way into production.

+ +

The solution to data integrity

+ +

Thiago and the team adopted Recce, which helped facilitate a more structured PR review process. This allowed the data team to demonstrate the impact of code changes on data, and better equipped Thiago to review and sign-off the changes.

+ +

The new PR review process that Recce enabled the data team to:

+ +
    +
  • Understand data impact
    By using Recce to check their work during development, the data team ensures that code changes are having the expected impact.
  • +
  • Provide proof-of-correctness
    By using Recce to generate visualizations, lineage diff diagrams, data profiles, and more, the team has concrete evidence of the data changes resulting from their code modifications.
  • +
  • PR review validation
    Thiago reviews the evidence presented in the PR, focusing on the key metrics and visualizations generated by Recce, to ensure that the changes align with the developer's stated intentions and do not introduce unexpected issues.
  • +
+ +
+ +
+ Lineage Diff helps with model refactoring work by showing the removal of a model and a new source for modified model +
+ +
+ +

Lineage Diff helps with model refactoring work by showing the removal of a model and a new source for modified model

+ +

The result after implementing Recce in the data team

+ +

After implementing Recce into their PR review process, the Health Department of Rio de Janeiro saw significant improvements in the following areas:

+ +
    +
  • Reduced PR review time
    The time required to review a pull request decreased dramatically from a day to under an hour.
  • +
  • Increased data accuracy
    he structured process and visual evidence provided by Recce helped to identify and rectify data inconsistencies before they reached production, minimizing the risk of errors.
  • +
  • Improved communication
    Recce checks provided a single source of truth on data impact that facilitated discussion around the changes.
  • +
+ + +

Examples of where Recce was used

+ +

The health department data teams used Recce in refactoring work which involved changing data sources and other jobs including addressing existing data quality issues and modifying data models.

+ +

Recce specifically helped to:

+ +
    +
  • Uncover situations in which duplicate records existed.
  • +
  • Demonstrate the improved accuracy of patient records by removing duplicate rows.
  • +
  • Comparing data profiling stats after SQL logic changes, or updates to source tables, to understand how these changes may impact downstream data.
  • +
  • Proving that records previously containing null values have been fixed.
  • +
  • Used Lineage Diff to ensure that scope of impact was as expected.
  • + + +
+ +
+ + Actual screenshots of query diff and row count diff results from the Prefeitura do Rio de Janeiro data project + +
+ +
+ +

These represent just a handful of the real world situations in which Recce helped to validate the impact of dbt data model code changes on data; and enabled the team to merge PRs with the confidence there would not be unexpected data impact.

+ +

Conclusion

+ +

The intuitive nature of Recce, and ease-of-implementation into existing data workflows, made it a perfect fit for the team at Prefeitura do Rio de Janeiro. Recce not only provided the tools to perform data validation, but also encouraged a formalized PR review process that spans from development-time data modeling, right through to the final PR review.

+ +

The suite of tools offered by Recce provides unparalleled insight into data impact, making it an essential part of any data or analytics engineer’s toolkit. Join other teams like the Prefeitura do Rio de Janeiro and stop merging to prod in the dark - merge with confidence after performing a Data Recce.

+ +

Bonus

+ +

The team created a script to help analysts easily generate the necessary dbt artifacts and start a Recce Instance with one command. Check out the script, recce.sh (or recce.ps1 for Windows users), in their data project repo.

+ + +
+ +
+
+
+ + +
+ +
+ +
+

Book a demo

+

Find out more about how Recce can improve your data PR review process

+
+ + +
+ + + +

+ + Book us with Cal.com + +

+ + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/cloud/index.html b/cloud/index.html new file mode 100644 index 00000000..d1b36d84 --- /dev/null +++ b/cloud/index.html @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + Recce Cloud - Support dbt Self Serve Data with Self Serve Review + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+ See what our users are saying about Recce + +

+
+
+
+
+ +
+
+
+ + + +

Recce

+
+ + +
+
+ + + + + +
+
+
+ +
+

Recce Cloud

+

Collaborative data review
for dbt projects

+ +

Bring data teams and stakeholders together to define 'data correctness' with collaborative checks and an interactive PR review process +

+ + Early Access +
+ + +
+
+
+ +
+ +
+
+

Build Better Data.

+
+ + +
+ +
+
+

Build data relationships
through team collaboration

+

Work together on checking and validating data

+
+ +
+

Build data trust
in your data and your team

+

Visualizing and interpreting change with stakeholders builds trust

+
+ +
+

Build data foundations
for data analytics

+

Confidence in data results in a solid foundation for downstream usage

+
+
+ +
+
+
+ +
+ + +
+ +
+

Sign up for Early Access

+
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+
+
+
+ + + +
+ +
+
+ +
+ + +
+ +
+ +
+
+
+ +
+
+ +
+
+

Review Together

+

Collaborate with your team on data validation checks and determine the definition of correctness together. Data engineers, analysts, and business stakeholders can share, work on, and interpret data checks together

+
+
+
+ + +
+
+
+ +
+
+ +
+
+

Continue where you left off

+

Data modeling takes time, especially validating your work. Save data validation checks for multiple PRs and seamlessly continue where you left off later

+
+
+
+ + +
+
+
+ +
+
+ +
+
+

Seamless Review in the Cloud

+

When it's time to review, the PR reviewer can seamlessly take over and review any data checks in the Cloud. Be alerted to any data mismatches and drill down into the data without needing to install anything locally

+
+
+
+ + +
+
+
+ +
+
+ +
+
+

Approve checks to merge

+

Recce’s GitHub app will block merging if there are unapproved checks. Your last gatekeeper to providing a solid foundation of reliable data

+
+
+
+ + +
+
+ + +
+ +
+

Book a demo

+

Find out more about how Recce can improve your data PR review process

+
+ + +
+ + + +

+ + Book us with Cal.com + +

+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/architecture/overview/index.html b/docs/architecture/overview/index.html new file mode 100644 index 00000000..a274dff0 --- /dev/null +++ b/docs/architecture/overview/index.html @@ -0,0 +1,2399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Overview - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Overview

+ +

The core concepts in Recce are Tasks, Runs, and Checks.

+

Tasks

+

A task represents the implementation of specific functionality. For example, tasks such as QueryDiffTask and ValueDiffTask demonstrate different types of operational processes.

+
def submit_run(task_type, params) -> Run:
+    task = find_task(task_type)
+    result, error = task(params)
+
+    return Run(
+        type=task_type,
+        params=params,
+        result=result,
+        error=error,
+    )
+
+

A task is responsible for:

+
    +
  1. Implementing the logic of the task, primarily involving accepting parameters and ultimately generating a result.
  2. +
  3. Implementing cancellation.
  4. +
  5. Implementing multi-threading.
  6. +
  7. Reporting progress, including progress percentage and messages.
  8. +
+

Runs

+

A run documents a single execution of a Task, capturing details such as the type of execution, parameters used, and the final outcomes. There are two methods to execute a run:

+
    +
  1. Directly specify the type along with the corresponding parameters. +
    curl 'http://localhost:8000/api/runs' \
    +-H "Content-Type: application/json" \
    +-d @- <<EOF
    +{
    +    "type": "query_diff",
    +    "params": {
    +        "sql_template": "select * from {{ ref('customers') }}"
    +    }
    +}
    +EOF
    +
  2. +
  3. Execute via a specific check. +
    curl 'http://localhost:8000/api/checks/e2a44206-8568-44e9-8d36-da8856df477d/run' \
    +-H 'Content-Type: application/json' \
    +--data-raw '{}'
    +
  4. +
+

In the Recce instance, each run is recorded, so in theory, it is possible to access the run history. However, the current UI does not offer this feature. You can see the complete run history from the exported recce state.

+

Checks

+

A Check is an item recorded in the user’s Checklist. A Check can be generated through a run or automatically created from preset checks when the Recce is initiated.

+

A single Check can execute multiple runs; therefore, in theory, it is possible to obtain the run history of a Check, although the current UI does not yet implement this feature.

+

Additionally, a Check records information in view_options, which is used to determine how the run results are displayed.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/demo/index.html b/docs/demo/index.html new file mode 100644 index 00000000..f0a0681f --- /dev/null +++ b/docs/demo/index.html @@ -0,0 +1,2420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Online Demo - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Recce Online Demo

+

Try Recce without installing using this online demo.

+

Recce Online Demo

+

The demo showcases a pull request that fixes the customer_lifetime_value calculation in dbt's Jaffle Shop project to only included completed orders.

+
+

customers.sql +

+
Jaffle Shop customers.sql
+
+

Examples

+

Some example validation checks you might create include:

+

Value Diff

+

Run a Value Diff to check the percentage match of the customer_lifetime_value column between production and the development branch.

+
+

CLV Value Diff +

+
Value Diff - Customers Model
+
+

Profile Diff

+

Check the Profile Diff of the customers table to see how the customer_lifetime_value has been impacted.

+
+

CLV Profile Diff +

+
Profile Diff - Customers Model
+
+

Query Diff

+

Run a Query Diff to compare the the actual values in prod and dev.

+
select customer_id, customer_lifetime_value from {{ ref("customers") }} where customer_id < 50;
+
+
+

CLV Query Diff +

+
Query Diff - Customers Model
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/checklist/index.html b/docs/features/checklist/index.html new file mode 100644 index 00000000..0ea887e7 --- /dev/null +++ b/docs/features/checklist/index.html @@ -0,0 +1,2270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Checklist - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Checklist

+

Save your validation checks to the Recce checklist with a description of your findings.

+

These checks can later be added to your pull request comment as proof-of-correctness for your modeling changes.

+
+

Recce Checklist +

+
Checklist
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/lineage/index.html b/docs/features/lineage/index.html new file mode 100644 index 00000000..88150be4 --- /dev/null +++ b/docs/features/lineage/index.html @@ -0,0 +1,3098 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Lineage - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Lineage

+ +

The Lineage Diff is the main interface to Recce and allows you to quickly see the potential area of impact from your dbt data modeling changes.

+

Lineage Diff

+

It's from the Lineage Diff that you will determine which models to investigate further; and also perform the various data validation checks that will serve as proof-of-correctness of your work.

+
+

Recce Lineage Diff +

+
Lineage Diff
+
+

Node Summary

+

+

Models are color-coded to indicate their status:

+
    +
  • Added models are green.
  • +
  • Removed models are red.
  • +
  • Modified models are orange.
  • +
+

The two icons at the bottom right of each node indicate if a row count or schema change has been detected. Grayed out icons indicate no change.

+
+

Model with Schema Change detected +

+
Model with Schema Change detected
+
+

Note: A row count changed icon is only shown if there is row count diff executed on this node.

+
+

Open node details panel +

+
Open the node details panel
+
+

Click a model to open the node details panel and perform other data validation checks.

+

Filter Nodes

+

In the top control bar, you can change the rule to filter the nodes:

+
    +
  1. Mode:
      +
    • Changed Models: Modified nodes and their downstream + 1st degree of their parents.
    • +
    • All: Show all nodes.
    • +
    +
  2. +
  3. Package: Filter by dbt package names.
  4. +
  5. Select: Select nodes by node selection.
  6. +
  7. Exclude: Exclude nodes by node selection.
  8. +
+

Select Nodes

+

Click a node to select it, or click the Select nodes button at the top-right corner to select multiple nodes for further operations. For detail, see the Multi Nodes Selections section

+

Row Count Diff

+

A row count diff can be performed on nodes selected using the select and exclude options:

+

+

After selecting nodes, run the row count diff by:

+
    +
  1. Clicking the 3 dots (...) button at the top-right corner.
  2. +
  3. Clicking Row Count Diff by Selector.
  4. +
+

Node Details

+

The node details panel shows information about a node, such as node type, schema and row count changes, and allows you to perform diffs on the node using the options accessed via the Explore Change button.

+

Schema Diff

+

Schema Diff shows added, removed, and renamed columns. Click a model in the Lineage Diff to open the node details and view the Schema Diff.

+
+

Note

+

Schema Diff requires catalog.json in both environments.

+
+
+

Recce Schema Diff +

+
Schema Diff
+
+
+

Recce Schema Diff +

+
Schema Diff showing renamed column
+
+

Row Count Diff

+

Row Count Diff shows the difference in row count between the base and current environments.

+
    +
  1. Click the model in the Lineage DAG.
  2. +
  3. Click the Explore Change button in the node details panel.
  4. +
  5. Click Row Count Diff.
  6. +
+
+

Recce Row Count Diff - Single model +

+
Row Count Diff - Single model
+
+

Code Diff

+

Code Diff shows the model code that has changed for a particular model.

+

Code diff

+
    +
  1. Click the model in the Lineage DAG.
  2. +
  3. Click the Explore Change button in the node details panel.
  4. +
  5. Click Code Diff.
  6. +
+

Value Diff

+

Value Diff shows the matched count and percentage for each column in the table. It uses the primary key(s) to uniquely identify the records between the model in both environments.

+

The primary key is automatically inferred by the first column with the unique test. If no primary key is detected at least one column is required to be specified as the primary key.

+
+

Recce Value Diff +

+
Value Diff
+
+
    +
  • Added: Newly added PKs.
  • +
  • Removed: Removed PKs.
  • +
  • Matched: For a column, the count of matched value of common PKs.
  • +
  • Matched %: For a column, the ratio of matched over common PKs.
  • +
+
+

Note

+

Value Diff uses the compare_column_values from audit-helper. To use Value Diff, ensure that audit-helper is installed in your project.

+
packages:
+  - package: dbt-labs/audit_helper
+    version: <version>
+
+
+

View mismatched values at the row level by clicking the show mismatched values option on a column name:

+

+

Profile Diff

+

Profile Diff compares the basic statistic (e.g. count, distinct count, min, max, average) for each column in models between two environments.

+
    +
  1. Select the model from the Lineage DAG.
  2. +
  3. Click the Expore Change button.
  4. +
  5. Click Profile Diff.
  6. +
+
+

Recce Profile Diff +

+
Profile Diff
+
+

Please refer to the dbt-profiler documentation for the definitions of profiling stats.

+
+

Note

+

Profile diff uses the get_profile from dbt-profiler. To use Profile Diff, ensure that dbt-profiler is installed in your project.

+
packages:
+  - package: data-mie/dbt_profiler
+    version: <version>
+
+
+

Histogram Diff

+

Histogram Diff compares the distribution of a numeric column in an overlay histogram chart.

+
+

Recce Histogram Diff +

+
Histogram Diff
+
+

A Histogram Diff can be generated in two ways.

+

Via the Explore Change button menu:

+
    +
  1. Select the model from the Lineage DAG.
  2. +
  3. Click the Explore Change button.
  4. +
  5. Click Histogram Diff.
  6. +
  7. Select a column to diff.
  8. +
  9. Click Execute.
  10. +
+

Via the column options menu:

+
    +
  1. Select the model from the Lineage DAG.
  2. +
  3. Hover over the column in the Node Details panel.
  4. +
  5. Click the vertical 3 dots ...
  6. +
  7. Click Histogram Diff.
  8. +
+
+

Generate a Recce Histogram Diff +

+
Generate a Recce Histogram Diff from the column options
+
+

Top-K Diff

+

Top-K Diff compares the distribution of a categorical column. The top 10 elements are shown by default, which can be expanded to the top 50 elements.

+
+

Recce Top-K Diff +

+
Recce Top-K Diff
+
+

A Top-K Diff can be generated in two ways.

+

Via the Explore Change button menu:

+
    +
  1. Select the model from the Lineage DAG.
  2. +
  3. Click the Explore Change button.
  4. +
  5. Click Top-K Diff.
  6. +
  7. Select a column to diff.
  8. +
  9. Click Execute.
  10. +
+

Via the column options menu:

+
    +
  1. Select the model from the Lineage DAG.
  2. +
  3. Hover over the column in the Node Details panel.
  4. +
  5. Click the vertical 3 dots ...
  6. +
  7. Click Top-K Diff.
  8. +
+
+

Generate a Recce Top-K Diff +

+
Generate a Recce Top-K Diff
+
+

Multi-Node Selection

+

Multiple nodes can be selected in the Lineage DAG. This enables actions to be performed on multiple nodes at the same time such as Row Count Diff, or Value Diff.

+

Select Nodes Individually

+

To select multiple nodes individually, click the check box on the nodes you wish to select.

+
+

Select multiple nodes individually +

+
Select multiple nodes individually
+
+

Select Parent or Child nodes

+

To select a node and all of its parents or children:

+
    +
  1. Click the checkbox on the node
  2. +
  3. Right click the node
  4. +
  5. Click to select either parent or child nodes
  6. +
+
+

Select a node and its parents or children +

+
Select a node and its parents or children
+
+

Perform actions on multiple nodes

+

After selecting the desired nodes, use the Actions menu at the top right of the screen to perform diffs or add checks.

+
+

Perform actions on multiple nodes +

+
Perform actions on multiple nodes
+
+

Example - Row Count Diff

+

An example of selecting multiple nodes to perform a multi-node row count diff:

+
+

Perform a Row Count Diff on multiple nodes +

+
Perform a Row Count Diff on multiple nodes
+
+

Example - Value Diff

+

An example of selecting multiple nodes to perform a multi-node Value Diff:

+
+

Perform a Value Diff on multiple nodes +

+
Perform a Value Diff on multiple nodes
+
+

Screenshot

+

In the diff result, we can find a Copy to Clipboard button. it's a handy feature to copy the result image to clipboard and paste in your PR comment.

+
+

Copy a diff screenshot to the clipboard - Multiple models +

+
Copy a diff result screenshot to the clipboard and paste to GitHub
+
+
+

Note

+

FireFox does not support to copy image to clipboard. Recce show a modal instead. You can download the image to local or right-click on the image to copy the image.

+
+

Add to Checklist

+

In the lineage page, we can run different type of check. However, for these reason we would like to add to checklist

+
    +
  1. Keep the check and I can rerun this after my code change
  2. +
  3. Add my result and interpretation for review purpose
  4. +
+

Lineage Diff

+

Lineage diff by selector

+
    +
  1. Select nodes by Select and Exclude on the top control.
  2. +
  3. Click ... at the top-right corner
  4. +
  5. Click the Lineage diff
  6. +
+

Lineage diff by multi nodes selection

+
    +
  1. Click Select nodes button at the top-right corner
  2. +
  3. Select nodes
  4. +
  5. Click the Add lineage diff check button
  6. +
+

Schema Diff

+

Schema diff by node selector

+
    +
  1. Select nodes by Select and Exclude on the top control.
  2. +
  3. Click ... at the top-right corner
  4. +
  5. Click the Schema diff button
  6. +
+

Schema diff by multi nodes selection

+
    +
  1. Click Select nodes button at the top-right corner
  2. +
  3. Select nodes
  4. +
  5. Click the Add schema check button
  6. +
+

Schema diff for single node

+
    +
  1. Select a node, then the node detail would show.
  2. +
  3. Click Add check button on the node detail pane.
  4. +
  5. Click Schema check
  6. +
+

Row Count Diff

+

Row count diff by node selector

+
    +
  1. Select nodes by Select and Exclude on the top control.
  2. +
  3. Click ... at the top-right corner
  4. +
  5. Click the Row Count Diff by Selctor, then it will run the row count diff
  6. +
  7. Click the Add to checklist in the result page.
  8. +
+

Row count diff by multi nodes selection

+
    +
  1. Click Select nodes button
  2. +
  3. Select nodes
  4. +
  5. Click Row count diff, then it will run the row count diff
  6. +
  7. Select a node, then the run result would show.
  8. +
  9. Click Add to checklist
  10. +
+

Other Diffs

+
    +
  1. Execute the diff
  2. +
  3. Click Add to checklist
  4. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/node-selection/index.html b/docs/features/node-selection/index.html new file mode 100644 index 00000000..cefd857c --- /dev/null +++ b/docs/features/node-selection/index.html @@ -0,0 +1,2430 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Node Selection - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Node Selection

+ +

Recce supports dbt node selection in the lineage diff. This enables you to target specific resources with data checks by selecting or excluding nodes.

+

Supported syntax and methods

+

Since Recce uses dbt's built-in node selector, it supports most of the selecting methods. Here are some examples:

+
    +
  • Select a node: my_model
  • +
  • select by tag: tag:nightly
  • +
  • Select by wildcard: customer*
  • +
  • Select by graph operators: my_model+, +my_model, +my_model, 1+my_model+
  • +
  • Select by union: model1 model2
  • +
  • Select by intersection: stg_invoices+,stg_accounts+
  • +
  • Select by state: state:modified, state:modified+
  • +
+

Use state method

+

In dbt, you need to specify the --state option in the CLI. In Recce, we use the base environment as the state, allowing you to use the selector on the fly.

+

Removed nodes

+

Another difference is that in dbt, you cannot select removed nodes. However, in Recce, you can select removed nodes and also find them using the graph operator. This is a notable distinction from dbt's node selection capabilities.

+

Supported Diff

+

In addition to lineage diff, other types of diff also support node selection. You can find these features in the ... button at the top right corner. Currently supported diffs include:

+
    +
  • Lineage diff
  • +
  • Row count diff
  • +
  • Schema diff
  • +
+

+

Limitation

+ + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/preset-checks/index.html b/docs/features/preset-checks/index.html new file mode 100644 index 00000000..331d6284 --- /dev/null +++ b/docs/features/preset-checks/index.html @@ -0,0 +1,2446 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Preset Checks - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Preset Checks

+ +

In a dbt project, there may be some checks that need to be conducted for every PR. For example, this could be an SQL query, or checking whether an important model has had a schema change.

+

Preset checks can be the fixed checks that are generated every time a new Recce instance is initiated.

+

Configure the Preset Check

+

To configure the preset checks, add the settings to the recce config file.

+
    +
  1. Add a check to your checklist + alt text
  2. +
  3. Open the menu for the check and select Get Preset Check Template.
  4. +
  5. +

    Copy the yaml config from the dialog + alt text

    +
  6. +
  7. +

    Paste the config into the recce.yml file located at the root of the project:

    +
    # recce.yml
    +checks:
    +  - name: Query diff of customers
    +    description: |
    +      This is the demo preset check.
    +
    +      Please run the query and paste the screenshot to the PR comment.
    +    type: query_diff
    +    params:
    +      sql_template: select * from {{ ref("customers") }}
    +    view_options:
    +      primary_keys:
    +        - customer_id
    +
    +
  8. +
+

Create the Preset Checks

+

Recce Server

+
    +
  1. When a new Recce instance is launched, all preset checks are automatically set up, but these checks are not executed at this time. + alt text
  2. +
  3. When the Run Query button is pressed, the check will be executed.
  4. +
+

Recce Run

+
    +
  1. Running recce run executes all preset checks. The default output file is recce_state.json. +
    $ recce run
    +───────────────────────────────── DBT Artifacts ─────────────────────────────────
    +Base:
    +    Manifest: 2024-04-10 08:54:41.546402+00:00
    +    Catalog:  2024-04-10 08:54:42.251611+00:00
    +Current:
    +    Manifest: 2024-04-22 03:24:11.262489+00:00
    +    Catalog:  2024-04-10 06:15:13.813125+00:00
    +───────────────────────────────── Preset checks ─────────────────────────────────
    +                            Recce Preset Checks
    +──────────────────────────────────────────────────────────────────────────────
    +Status      Name                 Type         Execution Time   Failed Reason
    +──────────────────────────────────────────────────────────────────────────────
    +[Success]   Query of customers   Query Diff   0.10 seconds     N/A
    +──────────────────────────────────────────────────────────────────────────────
    +The state file is stored at [recce_state.json]
    +
  2. +
  3. You can view the check results by launching the recce server. +
    recce server recce_state.json
    +
  4. +
  5. You can show the summary of the state by the recce summary command +
    recce summary recce_state.json
    +
  6. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/query/index.html b/docs/features/query/index.html new file mode 100644 index 00000000..fc92726b --- /dev/null +++ b/docs/features/query/index.html @@ -0,0 +1,2433 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Query - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Query

+ +

Query page provides an AdHoc query interface to run arbitrary query or diff the query result between two environments. If you're a dbt user, you can use any dbt macros that are installed in your project.

+

Execute Query

+
select * from {{ ref("mymodel") }}
+
+

Actions

+
    +
  • Run: performs query in the current environment.
  • +
  • Run Diff: performs the same query in both environments and diffs the results.
  • +
+

Form

+
    +
  • Primary key: select the primary key(s) used to compare the query results.
  • +
+
+

Note

+

If the primary key(s) is specified, the query will occur in the warehouse; otherwise, the query will happen across two environments and the comparison will take place on the client side.

+
+

Recce Query Diff

+
+

Tip

+

In Mac, you can use ⌘ Enter to run a query or use ⌘ ⇧ Enter to run a query diff.

+
+

Query Result

+

Recce Query Diff

+
    +
  • Primary Key: When comparison occurs on the client side, we can select the primary key by clicking the key icon. The primary key columns are used to be identified as the same record for both sides. If no primary key is specified, the records is compared by the row's index.
  • +
  • Pinned Column: The pinned column would show first in the column list.
  • +
  • Changed Only: By selecting the Changed only, we can show only the changed rows and columns. The pinned columns are always shown even they are not changed.
  • +
+

Shortcut to query a model

+
    +
  1. In the lineage page, select a model
  2. +
  3. Click the Query button
  4. +
  5. Then the query page is shown and filled with the query for this model
  6. +
+

Add to Checklist

+

Click the + button in the result pane, then you can add the query result to the checklist.

+

How Query Diff Works

+

In the current version, Recce provides two ways to compare the query result between two environments.

+

Query diff occurs in the client side:

+

Without providing primary key(s) upfront, AdDoc query compare in the client side. That is, Recce fetches the first 2,000 rows and compare in the client side. The advantage is it has more flexibility to query sql for no PK, especially when column structures differ or no clear primary key exists. +However, the limitation is that we cannot find the mismatched rows in a big query result.

+

Query diff occurs in the warehouse:

+

With primary key(s) given, it can perform a query diff in the warehouse. It only displays changed, added, or removed rows. Therefore, if only one record is different among a million, that specific record will be visible. Hence, it also reduces the amount of data transferred.

+

Another similar feature is Value Diff. Value diff is based on a chosen model, so you don’t need to write SQL to operate it, though it naturally offers less flexibility. Additionally, value diff can show a summary or actual diff records, whereas query diff only shows the actual diff records.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/recce-run/index.html b/docs/features/recce-run/index.html new file mode 100644 index 00000000..d9234b23 --- /dev/null +++ b/docs/features/recce-run/index.html @@ -0,0 +1,2268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Run - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Run

+ +

Recce provides a Web UI for interactively exploring and comparing two environments in depth. However, in the CI workflow, using the command line interface is more appropriate.

+

To execute Recce from the command line:

+
recce run
+
+

or to specify an output file:

+
recce run -o recce_pr123.json
+
+

This command will:

+
    +
  1. Collect the dbt artifacts for both the base and current environments.
  2. +
  3. Run the preset checks: Preset checks are checks that have been configured specifically for this dbt project.
  4. +
  5. Output the result. The default output path is recce_state.json.
  6. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/recce-summary-example/index.html b/docs/features/recce-summary-example/index.html new file mode 100644 index 00000000..dc47841b --- /dev/null +++ b/docs/features/recce-summary-example/index.html @@ -0,0 +1,2312 @@ + + + + + + + + + + + + + + + + + + + + + + + Recce Summary - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Recce Summary

+

Lineage Graph

+
graph LR
+model.jaffle_shop.customers["customers
+
+[What's Changed]
+Code, Schema, Value Diff"]
+style model.jaffle_shop.customers stroke:#ffa502
+model.jaffle_shop.customers---->model.jaffle_shop.customer_segments
+model.jaffle_shop.customers---->model.jaffle_shop.customer_order_pattern
+model.jaffle_shop.customer_segments["customer_segments"]
+model.jaffle_shop.customer_order_pattern["customer_order_pattern"]
+
+

Checks Summary

+ + + + + + + + + + + + + +
Total ChecksData Mismatch Detected
53
+

Checks of Data Mismatch Detected

+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRelated Models
Model schema of customersSchema Diffcustomers
Value diff of customersValue Diffcustomers
Query diff of customers avg lifetime valueQuery DiffN/A
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/recce-summary/index.html b/docs/features/recce-summary/index.html new file mode 100644 index 00000000..1831b949 --- /dev/null +++ b/docs/features/recce-summary/index.html @@ -0,0 +1,2652 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Summary - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Summary

+ +

Recce Summary command is used to generate a summary based on the input state file. In the previous section, the Run command was used to generate a state file based on the two environments. It provides a way to integrate Recce into your CI/CD pipeline. The Summary command is used to generate a summary based on the output of Run command. You can also integrate the Summary command into your CI/CD pipeline to generate a summary based on the state file generated by the Run command. Therefor, the generated summary can be posted to your repository hosting platform, such as GitHub, GitLab, or Bitbucket.

+

Usage

+
recce summary <Path-of-recce-state-file>
+
+

Example

+
recce summary recce-state.json
+
+

Output

+

The output of the summary command will be markdown format. The markdown output will contain the following sections:

+
    +
  • Lineage Graph - A graph that shows the lineage of the models that are impacted by the modified models.
  • +
  • Checks Summary - A summary of the checks that are detected mismatch between base and current environments.
  • +
+

Example Output

+
# Recce Summary
+
+## Lineage Graph
+
+```mermaid
+graph LR
+model.jaffle_shop.customers["customers
+
+[What's Changed]
+Code, Schema, Value Diff"]
+style model.jaffle_shop.customers stroke:#ffa502
+model.jaffle_shop.customers---->model.jaffle_shop.customer_segments
+model.jaffle_shop.customers---->model.jaffle_shop.customer_order_pattern
+model.jaffle_shop.customer_segments["customer_segments"]
+model.jaffle_shop.customer_order_pattern["customer_order_pattern"]
+
+```
+
+## Checks Summary
+
+| Total Checks | Data Mismatch Detected |
+| ------------ | ---------------------- |
+| 5            | 3                      |
+
+### Checks of Data Mismatch Detected
+
+| Name                                       | Type        | Related Models |
+| ------------------------------------------ | ----------- | -------------- |
+| Model schema of customers                  | Schema Diff | customers      |
+| Value diff of customers                    | Value Diff  | customers      |
+| Query diff of customers avg lifetime value | Query Diff  | N/A            |
+
+

The rendered output will look like this. Example Output

+

Content of the Summary

+

Lineage Graph

+

The lineage graph shows the lineage of the models that are impacted by the modified models. The graph is generated using the mermaid library. The graph is a directed graph that shows the relationship between the models. The graph is generated based on the modified models and their's children models. +If the model is modified or impacted by the modified model, it will be highlighted in the [What's Changed] section.

+

Example of Lineage Graph

+
graph LR
+model.jaffle_shop.customers["customers
+
+[What's Changed]
+Code, Schema, Value Diff"]
+style model.jaffle_shop.customers stroke:#ffa502
+model.jaffle_shop.customers---->model.jaffle_shop.customer_segments
+model.jaffle_shop.customers---->model.jaffle_shop.customer_order_pattern
+model.jaffle_shop.customer_segments["customer_segments"]
+model.jaffle_shop.customer_order_pattern["customer_order_pattern"]
+

Checks Summary

+

Shows the total number of checks and the number of data mismatched checks. The table will contain the following columns:

+
    +
  • Total Checks - The total number of checks.
  • +
  • Data Mismatch Detected - The number of checks which data mismatched between base and current environments.
  • +
+

Checks of Data Mismatch Detected

+

Shows the checks that are detected data mismatch between base and current environments. +If the check is detected data mismatch, we will suggest the PR reviewer should take a look at the check and investigate the data mismatch is expected or not. +The table will contain the following columns:

+
    +
  • Name - The name of the check.
  • +
  • Type - The type of the check.
  • +
  • Related Models - The models that are related to the check. If a check is not related to any models, it will be N/A.
  • +
+

If all the check between base and current environments are matched, the Checks of Data Mismatch Detected section will not be shown.

+

Example of Data Mismatch Detected

+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRelated Models
Model schema of customersSchema Diffcustomers
Value diff of customersValue Diffcustomers
Query diff of customers avg lifetime valueQuery DiffN/A
+

How to Integrate with CI/CD Pipeline

+

The generated summary can be posted to any kinds of repository hosting platform, such as GitHub, GitLab, or Bitbucket. +Please refer to the Recce CI integration with GitHub Action section for more information on how to integrate Recce with your CI/CD pipeline.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/features/state-file/index.html b/docs/features/state-file/index.html new file mode 100644 index 00000000..83bc1bf8 --- /dev/null +++ b/docs/features/state-file/index.html @@ -0,0 +1,2581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Recce State File - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Recce State File

+ +

Introduction

+

The state file is the serialized state of a recce instance. There are two scenarios where we need to export the state file:

+
    +
  • PR Review: We can include the state file in the PR Review Comment. If the reviewer wants to further connect to the PR environment, they can use this file to see the final results and perform deeper audits.
  • +
  • Development: During development, we often need to save the state and even switch between different branches for development.
  • +
+

PR Review

+

In the PR review process, the state file can serve as a medium of communication between the submitter and the reviewer.

+

State File For PR Review

+

Create a State File for Review

+
    +
  • +

    Export from Web UI: To export the current Recce state, you can use the Export button located in the top right corner of the Web UI to export the state to a file.

    +
  • +
  • +

    Output of the Recce Run: + A more mature dbt project may have a CI/CD process in place where dbt transformations are run, and the results are placed in a PR-specific environment. In such cases, you can integrate recce run in your automation workflow, making it convenient for reviewers to audit the results to determine whether the merge can proceed.

    +
  • +
+

What's in the State File

+
    +
  • Checks: These are the data from the checks that have been added to the checklist on the Checks page.
  • +
  • Runs: Each execution in Recce represents a run, analogous to a query in the warehouse. However, typically, a single run may submit series of queries in the warehouse and get the final run result.
  • +
  • Environments' Artifacts: The base and current environments' manifest.json and catalog.json.
  • +
  • Runtime Information: Such as git branch information, PR information in the CI runner.
  • +
+

Review the State File

+

To review a PR, you can download the corresponding state file and open the file with additional --review option.

+
recce server --review recce_state.json
+
+

In this mode, the Recce instance won't use the dbt artifacts inside target and target-base. Instead, it will use the artifacts from the Recce state file.

+

Development

+

State File For Development

+

When running the Recce server, you can specify an additional file argument

+
recce server recce_issue_1.json
+
+

If this file exists, Recce will use it as the initial state. If it doesn't exist, Recce will create a new one. When the server is terminated, the final state will be written into this file.

+

Recce Cloud

+
+

Note

+

Currently, Recce Cloud is still under development. If you are interested, please sign up for a Recce Cloud invite or contact us in the dbt slack #tool-recce channel

+
+

Although a state file can store the state, it is not very suitable for recording the latest review status of a PR. Especially since a PR may include the submitter, reviewer, and the automated processes in the CI workflow. The purpose of Recce Cloud is to solve the PR review status management issue.

+

Prerequisites

+

Before uploading the state file to Recce Cloud, you need to define a password to encrypt the state file. The password is used to encrypt the state file before uploading it to Recce Cloud. It will also be used to decrypt the state file when you download it from Recce Cloud. The password is not stored in Recce Cloud, so you need to keep it safe.

+

PR Review Workflow

+

This is a most common workflow: the submitter pushes commits to the GitHub PR, and the reviewer is responsible for reviewing/auditing the PR. This includes checking code changes, ensuring requirements are met, and assessing whether there is any impact on existing models.

+

As a submitter

+
    +
  1. Push changes to the remote
  2. +
  3. Create PR for review
  4. +
  5. Run dbt to prepare the PR review environment +
    dbt run
    +
  6. +
  7. Launch recce server for this PR branch +
    recce server --cloud --password <password-to-encrypt-state-file>
    +
  8. +
  9. Add recce checks for review
  10. +
  11. Leave description and screenshots in the PR comments
  12. +
+

As a reviewer

+
    +
  1. Checkout the PR branch
  2. +
  3. Launch recce server for this PR branch in the review mode +
    recce server --review --cloud --password <password-to-encrypt-state-file>
    +
  4. +
  5. If all checks are good, mark all checks as Approved. Otherwise, leave comment in the PR comment or the recce check description.
  6. +
+

PR Review Workflow with CI

+

For more mature projects, we introduce CI automation to standardize the process and reduce human-caused variability or errors. +In the workflow, we will do the following two things in the CI:

+

Execute dbt to create a PR environment. +Execute recce to update dbt artifacts, rerun check runs, and update the PR status to Recce Cloud.

+

As a submitter

+
    +
  1. Push changes to the remote
  2. +
  3. Create PR for review
  4. +
+

In the CI workflow of the PR push event

+
    +
  1. The github action workflow is triggered by the push event
  2. +
  3. Checkout the PR branch
  4. +
  5. Fetch the dbt artifacts for the base environment
  6. +
  7. Run dbt for the PR environment +
    dbt run
    +
  8. +
  9. Run recce for the current PR and upload state to the recce cloud. +
    recce run --cloud --password <password-to-encrypt-state-file>
    +
  10. +
+

As a submitter and reviewer, collaborate the state in the review mode recce server

+
    +
  1. Checkout the PR branch
  2. +
  3. Launch recce server for this PR branch in the review mode +
    recce server --review --cloud --password <password-to-encrypt-state-file>
    +
  4. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/get-started-jaffle-shop/index.html b/docs/get-started-jaffle-shop/index.html new file mode 100644 index 00000000..f3a79d03 --- /dev/null +++ b/docs/get-started-jaffle-shop/index.html @@ -0,0 +1,2320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 Minute Tutorial - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

5 Minute Tutorial

+ +

Jaffle Shop is an example project officially provided by dbt-labs. This document uses jaffle_shop_duckdb to enable you to start using recce locally from scratch within five minutes.

+
    +
  1. Clone the “Jaffle Shop” dbt data project +
    git clone git@github.com:dbt-labs/jaffle_shop_duckdb.git
    +cd jaffle_shop_duckdb
    +
  2. +
  3. Prepare virtual env +
    python -m venv venv
    +source venv/bin/activate
    +
  4. +
  5. Installation +
    pip install -r requirements.txt
    +pip install recce
    +
  6. +
  7. Provide additional environment to compare. Edit ./profiles.yml to add one more target. +
    jaffle_shop:
    +  target: dev
    +  outputs:
    +  dev:
    +    type: duckdb
    +    path: 'jaffle_shop.duckdb'
    +    threads: 24
    ++ prod:
    ++   type: duckdb
    ++   path: 'jaffle_shop.duckdb'
    ++   schema: prod
    ++   threads: 24
    +
  8. +
  9. Prepare production environment +
    dbt seed --target prod
    +dbt run --target prod
    +dbt docs generate --target prod --target-path ./target-base
    +
  10. +
  11. Prepare development environment. First, edit an existing model ./models/staging/stg_payments.sql. +
    ...
    +
    +renamed as (
    +         payment_method,
    +
    +-        -- `amount` is currently stored in cents, so we convert it to dollars
    +-        amount / 100 as amount
    ++        amount
    +
    +         from source
    +)
    +
    + run on development environment. +
    dbt seed
    +dbt run
    +dbt docs generate
    +
  12. +
  13. Run the recce server +
    recce server
    +
    + Open the link http://0.0.0.0:8000, you can see the lineage diff +
  14. +
  15. Switch to the Query tab, run this query +
    select * from {{ ref("orders") }} order by 1
    +
    + Click the Run Diff or press Cmd + Shift + Enter + Click on the 🔑 icon next to the order_id column to compare records that are uniquely identified by their order_id. +
  16. +
  17. Click the + to add the query result to checklist +
  18. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/get-started/index.html b/docs/get-started/index.html new file mode 100644 index 00000000..c4ce9589 --- /dev/null +++ b/docs/get-started/index.html @@ -0,0 +1,2550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Getting Started - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Getting Started

+

Prerequisites

+

Recce requires that your dbt project has two environments to compare. For example, one for production and another for development.

+

Prepare two targets with separate schemas in your dbt profile. Your profiles.yml might look something like this:

+
jaffle_shop:
+  target: dev
+  outputs:
+    dev:
+      type: duckdb
+      path: jaffle_shop.duckdb
+      schema: dev
+    prod:
+      type: duckdb
+      path: jaffle_shop.duckdb
+      schema: main
+
+

Install Recce

+

Install Recce using pip: +

pip install -U recce
+

+

Use Recce in your dbt project

+

The following instructions give an overview of the process of using Recce in your dbt project. For a hands-on tutorial, please check the Jaffle Shop Tutorial.

+

Navigate to your dbt project.

+
cd your-dbt-project/
+
+

Prepare dbt artifacts

+

Recce expects two sets of dbt artifacts to be present:

+
    +
  • target-base/ - dbt artifacts for to be used as the base for the comparison e.g. production
  • +
  • target/ - dbt artifacts for your development branch
  • +
+

Generate artifacts for the base environment

+

Checkout the main branch of your project and generate the required artifacts into target-base. You can skip dbt build if this environment already exists.

+
git checkout main
+
+dbt run --target prod
+dbt docs generate --target prod --target-path target-base/
+
+

Generate artifacts for the target environment

+
git checkout feature/my-awesome-feature
+
+dbt run
+dbt docs generate
+
+

Start the Recce server

+

Start the Recce server with the follow command:

+
recce server
+
+

Recce use dbt artifacts, which is generated when every invocation. You can find these files in the target/ folder.

+ + + + + + + + + + + + + + + + + +
artifactsdbt command
manifest.jsondbt docs generate, dbt run, ..
catalog.jsondbt docs generate
+
+

Tip

+

The regeneration of the catalog.json file is not required after every dbt run. it is only required to regenerate this file when models or columns are added or updated.

+
+

First Time Guide for Recce instance

+

After you start the Recce server, you can see the Recce instance, the Web UI of the active Recce server.

+

Here are the 3 steps to use Recce: (see the image below)

+
    +
  1. Click the model you want to check
  2. +
  3. Click “Explore Change”
  4. +
  5. Click “Add to Checklist” +first time guide of Recce instance
  6. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/guides/best-practices-prep-env/index.html b/docs/guides/best-practices-prep-env/index.html new file mode 100644 index 00000000..5621655c --- /dev/null +++ b/docs/guides/best-practices-prep-env/index.html @@ -0,0 +1,2690 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Best Practices for Preparing Environments - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Best Practices for Preparing Environments

+ +

Recce is designed to compare two environments in your data project. To use it effectively, it is crucial to prepare environments through CI.

+

However, there are many challenges in preparing environments.

+
    +
  1. Your source data might be continuously updating.
  2. +
  3. Your transformations might be time-consuming.
  4. +
  5. The base branch may have other PRs merged at any time.
  6. +
  7. The generated environment will leave data in the warehouse, which also needs to be properly managed.
  8. +
+

This article will not focus on how to use Recce but rather on how to effectively prepare environments for Recce use.

+

Best Practices

+

Use schema to manage your environments

+

In dbt, you can leverage profiles and targets to specify the credentials for your database connections. By using profiles and dynamically setting the schema, you can direct the transformation results to different schemas, effectively creating separate environments. Here's how you can achieve this using env_var or arg to dynamically change the schema:

+
    +
  1. +

    Define Your Profile and Target: +Your profiles.yml should have different targets that you can switch between. Here’s an example of a profiles.yml file:

    +
    my_profile:
    +  target: dev
    +  outputs:
    +    dev:
    +      type: postgres
    +      host: localhost
    +      user: db_user
    +      password: db_pass
    +      port: 5432
    +      dbname: my_db
    +      schema: "{{ env_var('DBT_SCHEMA') }}"
    +    prod:
    +      type: postgres
    +      host: prod_host
    +      user: prod_user
    +      password: prod_pass
    +      port: 5432
    +      dbname: prod_db
    +      schema: public
    +
    +
  2. +
  3. +

    Run dbt with the Specified Schema: +Now, when you run dbt commands, the schema setting will dynamically use the value of the DBT_SCHEMA environment variable.

    +
    DBT_SCHEMA=pr_env dbt run
    +
    +
  4. +
  5. +

    Using args in dbt: +You can also pass arguments directly in your dbt commands to dynamically set variables. For example:

    +
    dbt run --vars '{"schema_name": "pr_123"}'
    +
    +

    And modify your profiles.yml to use this variable:

    +
    my_profile:
    +  target: dev
    +  outputs:
    +    dev:
    +      type: postgres
    +      host: localhost
    +      user: db_user
    +      password: db_pass
    +      port: 5432
    +      dbname: my_db
    +      schema: "{{ var('schema_name') }}"
    +
    +
  6. +
+

This approach allows you to dynamically create different environments by changing the schema on-the-fly. This is particularly useful for creating isolated environments for different PRs or testing scenarios, ensuring that your transformations are scoped to the correct schema and avoiding conflicts between different environments.

+

Prepare single base environment for all PRs to compare

+

Using the production environment as the base environment is a straightforward choice. However, to make Recce more efficient, using the staging environment might be more suitable.

+

This staging environment can have the following characteristics:

+
    +
  1. Ensure that the transformed results reflect the latest commit of the base branch.
  2. +
  3. Use the same source data as the PR environment.
  4. +
  5. Use the same transformation logic as the PR environment.
  6. +
+

The basic principle is that the staging environment's configuration should be as close as possible to the PR environments, except for using a different git commit.

+

Prepare per-PR environment

+

A moderately sized data project may have multiple branches in development simultaneously. To avoid interference, it is recommended that each PR have its own isolated PR environment. The schema name can be pr_<number>

+

Reduce the update frequency of the source data used by both the base and PR environments

+

Some data projects may have source data that updates every hour or even every second. This can result in different transformation outcomes due to varying source data at different times, leading to Recce comparison results lacking discernibility.

+

Currently, some data warehouses support zero-copy clone (snowflake, bigquery, databricks), which allows us to freeze the source data at a specific point in time. Considering updating the source data weekly can significantly reduce the variability in environments caused by source data changes.

+

alt text

+

Limit the data range used in transformations

+

Most data is temporal. When preparing the base and PR environments, we can use only the data from the last month. This can greatly reduce the data volume while still verifying correctness.

+

If zero-copy clone for the source data is not supported and the source data continues to update, you can consider excluding the current week's data. This approach can ensure that transformations yield consistent results regardless of when they are executed.

+

For example, you can design the transformation to only use data from the last month, up to Sunday at 00:00. This approach combines the benefits of shorter execution times and reduced data volatility.

+

alt text

+
SELECT
+    *
+FROM
+    {{ source('your_source_name', 'orders') }}
+{% if target.name != 'prod' %}
+WHERE
+    order_date >= DATEADD(month, -1, CURRENT_DATE)
+    AND order_date < DATE_TRUNC('week', CURRENT_DATE)
+{% endif %}
+
+

Ensure that the base environment is always up-to-date

+

There are two scenarios that may cause the base environment to be out of date:

+
    +
  1. New Source Data Changes: If you update your data weekly, ensure that your base environment is updated at least once a week as well.
  2. +
  3. New PRs Merged into base branch: You can trigger a base environment update on merge events to ensure it remains current.
  4. +
+

Ensure the PR branch is in sync with the base branch

+

If the PR is executed after the base branch has been updated, the comparison with the base environment will mix the changes from the PR with the changes from other PRs merged into the base branch. This results in comparison outcomes that do not accurately reflect the impact of the current PR.

+

alt text

+

GitHub can automatically detect whether a PR is in sync. You need to enable this feature in the repository settings. Once enabled, you will see whether the PR is up-to-date directly on the PR page.

+

alt text

+

You can also to check if the PR is up-to-date in the CI workflow before preparing the PR environment. Here is an example in github action +

- name: Check if PR is up-to-date
+  if: github.event_name == 'pull_request'
+  run: |
+    git fetch origin main
+    UPSTREAM=${GITHUB_BASE_REF:-'main'}
+    HEAD=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
+    if [ "$(git rev-list --left-only --count ${HEAD}...origin/${UPSTREAM})" -eq 0 ]; then
+      echo "Branch is up-to-date"
+    else
+      echo "Branch is not up-to-date"
+      exit 1
+    fi
+

+

Consider how to obtain your artifacts for environments

+

Recce relies on the base and current environment artifacts to find the corresponding tables in the data warehouse for comparison. So, the question is how to obtain the artifacts of the environments to be compared.

+

Here are a few methods you can choose:

+
    +
  1. In CI, upload the generated artifact to the cloud storage (e.g., AWS S3).
  2. +
  3. For dbt Cloud users, you can download artifacts for the latest run of a given job.
  4. +
  5. For GitHub Actions users, you can use the GitHub CLI (gh) to download artifacts for the latest run of a given workflow.
  6. +
+

If the methods mentioned above are too complex, a stateless approach is to directly check out the base branch and run dbt docs generate to generate the artifacts.

+

Cleaning up PR environments on PR closed

+

As the number of PRs in a project increases, automatically generated environments also grow. To manage this, you can create a workflow that listens for PR close events and performs cleanup actions. Additionally, you can schedule periodic cleanups to remove outdated environments, such as those not used for a week.

+

In dbt, you can use the dbt run-operation command to clear a specific schema corresponding to an environment. This can be especially useful for environments named in a pattern such as pr_{env}.

+

Here’s how you can define a macro to clear an environment schema:

+
{% macro clear_schema(schema_name) %}
+{% set drop_schema_command = "DROP SCHEMA IF EXISTS " ~ schema_name ~ " CASCADE;" %}
+{% do run_query(drop_schema_command) %}
+{% endmacro %}
+
+

Run the macro

+
dbt run-operation clear_schema --args "{'schema_name': 'pr_123'}"
+
+

Example

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EnvironmentsSchema NameWhen to run# of environmentsData range
ProductionpublicDaily1All
StagingstagingDaily + On Merge11 month, excluding this week
PRpr_<number>On Push# of opened PR1 month, excluding this week
+
    +
  • Automate environment generation using GitHub Actions.
  • +
  • PR Environment will only be generated automatically when the PR is up-to-date.
  • +
  • Artifacts will be stored under the workflow’s artifacts.
  • +
  • PR environments are removed on PR closed.
  • +
  • Use staging environment as the base environment for Recce.
  • +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/guides/scenario-ci/index.html b/docs/guides/scenario-ci/index.html new file mode 100644 index 00000000..346d4ce8 --- /dev/null +++ b/docs/guides/scenario-ci/index.html @@ -0,0 +1,2599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Continuous Integration (CI) - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + +

Recce CI integration with GitHub Action

+

Recce provides the recce run command for CI/CD pipeline. You can integrate Recce with GitHub Actions (or other CI tools) to compare the data models between two environments when a new pull-request is created. The below image describes the basic architecture.

+

ci/cd architecture

+

The following guide demonstrates how to configure Recce in GitHub Actions.

+

Prerequisites

+

Before integrating Recce with GitHub Actions, you will need to configure the following items:

+
    +
  • +

    Set up two environments in your data warehouse. For example, one for base and another for pull request.

    +
  • +
  • +

    Provide the credentials profile for both environments in your profiles.yml so that Recce can access your data warehouse. You can put the credentials in a profiles.yml file, or use environment variables.

    +
  • +
  • +

    Set up the data warehouse credentials in your GitHub repository secrets.

    +
  • +
+

Set up Recce with GitHub Actions

+

We suggest setting up two GitHub Actions workflows in your GitHub repository. One for the base environment and another for the PR environment.

+
    +
  • +

    Base environment workflow: Triggered on every merge to the main branch. This ensures that base artifacts are readily available for use when a PR is opened.

    +
  • +
  • +

    PR environment workflow: Triggered on every push to the pull-request branch. This workflow will compare base models with the current PR environment.

    +
  • +
+

Base Workflow (Main Branch)

+

This workflow will perform the following actions:

+
    +
  1. Run dbt on the base environment.
  2. +
  3. Upload the generated DBT artifacts to github workflow artifacts for later use.
  4. +
+
name: Recce CI Base Branch
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - main
+
+concurrency:
+  group: recce-ci-base
+  cancel-in-progress: true
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.10.x"
+
+      - name: Install dependencies
+        run: |
+          pip install -r requirements.txt
+
+      - name: Run DBT
+        run: |
+          dbt deps
+          dbt seed --target ${{ env.DBT_BASE_TARGET }}
+          dbt run --target ${{ env.DBT_BASE_TARGET }}
+          dbt docs generate --target ${{ env.DBT_BASE_TARGET }}
+        env:
+          DBT_BASE_TARGET: "prod"
+
+      - name: Upload DBT Artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: target
+          path: target/
+
+
+

Note

+

Please place the above file in .github/workflows/dbt_base.yml. This workflow path will also be used in the next PR workflow. If you place it in a different location, please remember to make the corresponding changes in the next step.

+
+

PR Workflow (Pull Request Branch)

+

This workflow will perform the following actions:

+
    +
  1. Run dbt on the PR environment.
  2. +
  3. Download previously generated base artifacts from base workflow.
  4. +
  5. Use Recce to compare the PR environment with the downloaded base artifacts.
  6. +
  7. Use Recce to generate the summary of the current changes and post it as a comment on the pull request. Please refer to the Recce Summary for more information.
  8. +
+
name: Recce CI PR Branch
+
+on:
+  pull_request:
+    branches: [main]
+
+jobs:
+  check-pull-request:
+    name: Check pull request by Recce CI
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Merge Base Branch into PR
+        uses: DataRecce/PR-Update@v1
+        with:
+          baseBranch: ${{ github.event.pull_request.base.ref }}
+          autoMerge: false
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.10.x"
+      - name: Install dependencies
+        run: |
+          pip install -r requirements.txt
+          pip install recce
+      - name: Prepare dbt Base environment
+        run: |
+          gh repo set-default ${{ github.repository }}
+          base_branch=${{ github.base_ref }}
+          run_id=$(gh run list --workflow ${WORKFLOW_BASE} --branch ${base_branch} --status success --limit 1 --json databaseId --jq '.[0].databaseId')
+          echo "Download artifacts from run $run_id"
+          gh run download ${run_id} -n target -D target-base
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          WORKFLOW_BASE: ".github/workflows/dbt_base.yml"
+      - name: Prepare dbt Current environment
+        run: |
+          git checkout ${{ github.event.pull_request.head.sha }}
+          dbt deps
+          dbt seed --target ${{ env.DBT_CURRENT_TARGET}}
+          dbt run --target ${{ env.DBT_CURRENT_TARGET}}
+          dbt docs generate --target ${{ env.DBT_CURRENT_TARGET}}
+        env:
+          DBT_CURRENT_TARGET: "dev"
+
+      - name: Run Recce CI
+        run: |
+          recce run --github-pull-request-url ${{ github.event.pull_request.html_url }}
+
+      - name: Upload DBT Artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: target
+          path: target/
+
+      - name: Upload Recce State File
+        uses: actions/upload-artifact@v4
+        id: recce-artifact-uploader
+        with:
+          name: recce-state-file
+          path: recce_state.json
+
+      - name: Prepare Recce Summary
+        id: recce-summary
+        run: |
+          recce summary recce_state.json > recce_summary.md
+          cat recce_summary.md >> $GITHUB_STEP_SUMMARY
+          echo '${{ env.NEXT_STEP_MESSAGE }}' >> recce_summary.md
+
+          # Handle the case when the recce summary is too long to be displayed in the GitHub PR comment
+          if [[ `wc -c recce_summary.md | awk '{print $1}'` -ge '65535' ]]; then
+            echo '# Recce Summary
+          The recce summary is too long to be displayed in the GitHub PR comment.
+          Please check the summary detail in the [Job Summary](${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}) page.
+          ${{ env.NEXT_STEP_MESSAGE }}' > recce_summary.md
+          fi
+
+        env:
+          NEXT_STEP_MESSAGE: |
+            ## Next Steps
+            If you want to check more detail information about the recce result, please download the [artifact](${{ steps.recce-artifact-uploader.outputs.artifact-url }}) file and open it by [Recce](https://pypi.org/project/recce/) CLI.
+
+            ### How to check the recce result
+            ```bash
+            # Unzip the downloaded artifact file
+            tar -xf recce-state-file.zip
+
+            # Launch the recce server based on the state file
+            recce server --review recce_state.json
+
+            # Open the recce server http://localhost:8000 by your browser
+            ```
+
+      - name: Comment on pull request
+        uses: thollander/actions-comment-pull-request@v2
+        with:
+          filePath: recce_summary.md
+          comment_tag: recce
+
+

Review the Recce State File

+

Review the downloaded Recce state file with the following command:

+
recce server --review recce_state.json
+
+

In the Recce server --review mode, you can review the comparison results of the data models between the base and current environments. It will contain the row counts of modified data models, and the results of any Recce Preset Checks.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/guides/scenario-dev/index.html b/docs/guides/scenario-dev/index.html new file mode 100644 index 00000000..7931c653 --- /dev/null +++ b/docs/guides/scenario-dev/index.html @@ -0,0 +1,2519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Development - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Development

+ +

In developing a project with dbt, there are numerous methods available to help you query warehouse data for validation. Recce, in particular, allows for further comparison with production or a specific baseline environment.

+

Prepare the environments

+

In order to enable Recce to compare the base and current environment, you need to prepare artifacts for both environments.

+

For base environment, put the dbt artifacts in your target-base/ path. You can have the following options

+
    +
  1. Download the artifacts from remote storage: If you use dbt cloud, you can download the latest artifacts in your production environment. For non dbt cloud case, you can upload the latest artifacts to cloud storage (e.g. s3), and write a scripts to download artifacts.
  2. +
  3. Generate the artifacts for the production environment: +
    dbt docs generate --target prod --target-path target-base/
    +
  4. +
+

For current developing environment, for most of the dbt command, it would generate the manifest.json. If you want to update the schema information, you have to run the dbt docs generate to generate the catalog.json.

+

Recce also watch the the target/ and target-base/ folders. If there is artifact file changed, the recce web ui would reload to the latest version.

+

Development Cycle

+

The common development cycle is

+
    +
  1. Write the code, validate the change, commit your code
  2. +
  3. Push the commits to remote
  4. +
  5. Review the impacts of your changes
  6. +
+

Here, I assume your pull request hasn't been marked as "ready for review" yet, and you're still in the process of development, verifying correctness on your own. In this scenario, Recce can assist you in conducting this validation.

+

Check the Lineage

+

DBT provides a method to identify modified models using dbt ls -s state:modified+, but this is obviously not usable within dbt docs. While you can determine how many models are affected using this command, you can't visualize these results.

+

In Recce, you can conduct an initial assessment of your impact scope by Lineage diff, which may help you identify potential unintended impacts.

+

Validate the Models' Metadata

+

With lineage diff, you can start from the modified models to confirm your impact. An inexpensive method is to examine the impact scope of the affected models' metadata.

+

Firstly, you can start by examining Schema diff to see if any changes are detected in each model's schema. Sometimes, a change from Integer to Text, or from Decimal to Numeric, may have subtle impacts on your downstream models.

+

Additionally, whether the models with schema changes have only added columns is worth noting, as this might not significantly affect your downstream processes. However, if columns are removed, it's essential to pay special attention to ensure it's the expected outcome.

+

Next, you can examine the Row count diff for the affected models. Typically, row counts are stored in the warehouse's metadata, meaning you can obtain row count information without much cost. This allows you to quickly determine if row counts are the same or if there are significant changes. Common issues may arise from an erroneous join resulting in unexpected data volumes and erroneous outcomes. Row count diff provides a fast method to identify similar errors.

+

Observing the summary of each node can help you quickly review schema and row count changes. By using the lineage diff graph, conducting basic checks on schema and row count, you can already gain a basic level of confidence in the changes made during your development process.

+

Node summary

+

Validate the Column's Summary

+

Apparently, model metadata alone is insufficient. Sometimes, we need to assess the magnitude of impact that the changes currently in development have on the critical Marts models.

+

Recce provides 4 powerful diff tools to compare the data level changes.

+
    +
  1. Value Diff: You can use value diff to observe the matched percentage for each column.
  2. +
  3. Profile Diff: You can use profile diff to compare basic statistical values for each column, such as count, distinct count, min, max, and average.
  4. +
  5. Histogram Diff: You can use histogram diff to examine the distribution changes of numeric columns.
  6. +
  7. Top-K Diff: You can use top-k diff to analyze distribution changes of categorical columns.
  8. +
+

It's important to note that these queries may take longer to execute and require reading larger amounts of data. Please choose the appropriate method based on the data volume of each model.

+

Validate by Adhoc Query

+

If you want to choose the most flexible method, Query diff is the way to go. You can compare individual records, perform complex operations like where, group by, order by. Or even query multiple models with joins.

+

AdHoc queries also support the use of dbt macros, providing the highest level of flexibility for validation. However, the downside is that you'll need to write the queries yourself.

+

Check Driven Development

+

Test Driven Development (TDD) is a common development pattern where you write tests first, then begin development, validating until the tests pass.

+

When developing in dbt, of course, you can implement the TDD process through dbt tests. However, writing tests is not the only method. First, tests require very precise validation logic, and second, sometimes we don't want to impact the original data definitions. In such cases, what we want to verify is that the data doesn't change too much, rather than a specific logic.

+

For example, if we want to make slight adjustments to the definition of "revenue". In the concept of TDD, we would consider what the input data is and what the output should be. But more often, what we want to verify is just ensuring that the changes in revenue for each month are within an expected range.

+

In recce, we can a simple query for your validation code

+
SELECT
+    date_trunc('month', order_date) AS month,
+    SUM(amount) AS revenue
+FROM
+    orders
+GROUP BY
+    month
+ORDER BY
+    month desc
+
+

Next, you can add this check to your checklist. After modifying your code each time, rerun this check until it meets your requirements.

+

Store your data

+

During development, we may inevitably switch branches. If you want to preserve the current state, you can use the Export the state file for later loading.

+

Another technique is to specify the file directly when opening it. +

recce server recce_issue_123.json
+

+

When you close the Recce instance, it saves your current session to this file. The next time you open it using the same file, it will restore to the previous state.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/guides/scenario-pr-review/index.html b/docs/guides/scenario-pr-review/index.html new file mode 100644 index 00000000..10c585b0 --- /dev/null +++ b/docs/guides/scenario-pr-review/index.html @@ -0,0 +1,2422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + PR Review - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

PR Review

+ +

When you've finished developing your feature, we should turn the pull request (PR) into ready for review state. At this point, the PR submitter needs to provide the necessary information for the PR reviewer. Some projects may offer a PR template. In the dbt official blog, this article provides an excellent example.

+

Recce at this stage aims to assist the submitter in gathering more information to ensure that the reviewer can merge the pull request (PR) with greater confidence.

+

Screenshots

+

Lineage

+

Firstly, the Lineage DAG is crucial information within a dbt project as it helps us understand the dependencies between models. In dbt docs, it provides lineage diagrams. Usually, we can paste this diagram into the PR comment to help the reviewer understand the latest lineage status.

+

lineage in dbt

+

However, during PR reviews, we may be more interested in understanding what changes have been made and presenting them through the Lineage DAG. At this point, you can utilize recce to capture a screenshot of the Lineage diff and embed it within your PR comment.

+

lineage diff in recce

+

Checks

+

Another core feature of Recce is its various checks, which allow us to compare key models with the base environment. The typical workflow is as follows:

+
    +
  1. Generate the various diffs you need.
  2. +
  3. Identify the query and its result that you want to present to the reviewer.
  4. +
  5. Add it to the checklist.
  6. +
  7. Click the Copy to Clipboard button and paste it in the corresponding position within the PR comment.
  8. +
  9. Write a description of your check, including explanations and intentions.
  10. +
  11. Click the Copy markdown button + Copy markdown
  12. +
  13. paste it in the corresponding position within the PR comment. + GitHub Comment
  14. +
+

Share the Recce File

+

If you want the reviewer to access your environment, you can also attach the Recce state file to the PR comment.

+

As a Submitter

+
    +
  1. Export the recce state file
  2. +
  3. Attach the state file into the PR comment
  4. +
+

As a Reviewer

+
    +
  1. Download the state file
  2. +
  3. In your dbt project folder, run this command +
    recce server --review <recce state file>
    +
  4. +
+

By adding the --review option, the Recce server will use the DBT artifacts from the state file to connect to both the base and the pull request (PR) environments.

+
+

Note

+

Although the artifacts are from the Recce state, you still need to provide the profiles.yml and dbt_project.yml files so that Recce knows which credentials to use to connect to the data warehouse.

+
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..10679c09 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,2616 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Overview - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

What is Recce?

+

Recce is a data validation toolkit designed to enhance the pull request (PR) review process for dbt projects. Recce provides enhanced visibility into the data impact from dbt modeling changes by comparing the data in dev and prod environments. Using Recce for data impact assessment before merging a PR ensures that production data remains stable and accurate.

+

Key Features

+

Manual and Automated Data Checks

+

Recce checks help you to assess data impact and explore data change both manually and automatically.

+
    +
  • Manual checks - Create a Recce Checklist of data checks that help to validate your data modeling work during development, including data profile comparisons, structural comparisons, and row-level data checks.
  • +
  • Automated checks - Integrate Recce Checks into your CI process and post a data impact summary automatically to your PR thread when opening a PR.
  • +
+

Collaboration and Replication

+

Share Recce checks with your team for stakeholder and PR review. Checks results can be either shared individually, or your full Recce environment can be exported and replicated with one command.

+

Why Recce

+

dbt has brought software engineering best practices to data projects, but “bad merges” still happen, allowing erroneous data and silent errors to make their way into prod data.

+

Understand data impact

+

Recce provides data and analytics engineers with a toolkit to explore data impact caused by dbt data modeling changes. The varying levels of Recce checks enable holistic or fine grained impact assessment so you can drill down to find the root cause of data change.

+

Improved confidence merging

+

The improved visibility into data impact gives PR reviewers the confidence to sign-off PRs knowing that prod data will not change unexpectedly.

+
+ +

How Recce Works

+

Recce compares dbt environments using the dbt artifacts from both dev and prod environments.

+
    +
  1. +

    Generate artifacts for the prod environment:

    +
    # Build prod and generate dbt docs into ./target-base
    +dbt seed --target prod
    +dbt run --target prod
    +dbt docs generate --target prod --target-path ./target-base
    +
    +
  2. +
  3. +

    Switch to your dev branch and generate dev artifacts:

    +
    # Switch to your dev branch
    +git switch my-awesome-branch
    +
    +# build your dev environment
    +dbt seed
    +dbt run
    +dbt docs generate
    +
    +
  4. +
  5. +

    Start your Recce Instance:

    +
    recce server
    +
    +
  6. +
+

Open your the Recce web UI to start exploring and understanding data impact, and validating your work.

+

What you get

+

Interactive impact assessment environment

+

recce server launches a web UI with an interactive impact assessment environment. Use the tools in Recce to explore the impact to your data models from your branch changes.

+

Focused data impact exploration

+

The main interface to Recce is the lineage DAG, which shows modified nodes and potentially impacted downstream nodes. You can quickly see if critical nodes are within the impact radius and focus your data validation efforts.

+
+

Recce Lineage Diff +

+
+
+

Getting Started

+

Try the 5-minute tutorial that uses dbt’s Jaffle Shop project, or take the online demo for a test run, which includes an actual PR and related Recce Instance.

+

What does Recce mean?

+

Recce (/ˈrɛki/), pronounced 'reh-kee', is short for 'reconnaissance'. We chose this name as it's the perfect fit for a tool you'll use to perform a 'data reconnaissance' to discover and assess the impact of data modeling changes. Add a Data Recce to your pull request workflow and stop pushing breaking changes to production!

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/installation/index.html b/docs/installation/index.html new file mode 100644 index 00000000..79d259fe --- /dev/null +++ b/docs/installation/index.html @@ -0,0 +1,2274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Installation - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Installation

+

Install Recce in your dbt project with pip:

+
pip install recce
+
+

To take full advantage of all the features of Recce, ensure that dbt_profiler and audit-helper are installed via the packages.yml file in your dbt project .

+
packages:
+  - package: dbt-labs/audit_helper
+    version: <version>
+  - package: data-mie/dbt_profiler
+    version: <version>
+
+

For full instructions on using Recce, check the Getting Started guide.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/architecture/index.html b/docs/recce-cloud/architecture/index.html new file mode 100644 index 00000000..4a5c3ea9 --- /dev/null +++ b/docs/recce-cloud/architecture/index.html @@ -0,0 +1,2218 @@ + + + + + + + + + + + + + + + + + + + + + + + Overview - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Recce Cloud Architecture

+

In this section, we will describe the architecture of Recce Cloud.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/architecture/security/index.html b/docs/recce-cloud/architecture/security/index.html new file mode 100644 index 00000000..19df60c7 --- /dev/null +++ b/docs/recce-cloud/architecture/security/index.html @@ -0,0 +1,2338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Security - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Security

+

Recce Cloud is designed to be secure by default. We take security seriously and consider the data privacy from the beginning of the design phase. In this document, we will describe how we handle your data on Recce Cloud.

+

State File in Recce Cloud

+

When users execute recce run or recce server with option --cloud, Recce will upload the state file into the Recce Cloud. In the meanwhile, Recce will also request users provide the encryption password by option --password or -p. Recce will use the password to encrypt the state file when uploading and decrypt the state file when downloading. The password will not be stored in Recce Cloud, so users need to keep it safe.

+

How Recce Encrypts the State File

+

Once users choose to upload the state file to Recce Cloud, Recce will store the state file in the cloud storage. Recce Cloud will use AWS S3 to store the state file and enable the server-side encryption with customer-provided keys (SSE-C). Recce Cloud will use the encryption password provided by users to encrypt the state file before uploading it to the S3 bucket. The encryption algorithm is AES-256. The state file will be decrypted with the same password when downloading it from the S3 bucket.

+

Flow of encrypt/decrypt state file

+

Based on the above flow chart, the state file will be encrypted and decrypted on the AWS S3 side. Recce Cloud server will only in charge of generating the pre-signed URL for uploading and downloading the state file. Which means Recce Cloud server will not have access to the state file content and the password key.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/architecture/self-hosted/index.html b/docs/recce-cloud/architecture/self-hosted/index.html new file mode 100644 index 00000000..085b31bf --- /dev/null +++ b/docs/recce-cloud/architecture/self-hosted/index.html @@ -0,0 +1,2382 @@ + + + + + + + + + + + + + + + + + + + + + + + Self Hosted Recce Instance - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +
+

Note

+

The Self-Hosted Recce Instance is currently under development.

+
+

Self-Hosted Recce Instance

+

In a collaborative dbt project, we often want each in-progress PR to have a corresponding Recce server for review, which we call a Recce Instance. There are three solutions to create a Recce Instance.

+
    +
  • +

    GitHub Codespaces: Run the Recce Instance in the user's GitHub Codespace. Due to design limitations of codespaces, each codespace must be opened under a specific user, so each user needs to launch their own Recce Instance. This method also requires every user to have a GitHub account, which might not be suitable for some teams.

    +
  • +
  • +

    Self-Hosted Recce Instance: Run the Recce Instance on your own cloud infrastructure (AWS, GCP, Azure, or on-premise machines). Since it is self-hosted, it offers greater flexibility compared to GitHub Codespaces, and because the server is hosted on your own infrastructure, it can provide better privacy and security. However, this comes at the cost of additional management overhead.

    +
  • +
  • +

    Recce-Managed Recce Instance: We would host the Recce Instance on Recce's own cloud infrastructure, directly connecting to your warehouse. The advantage is minimal setup cost, but there are privacy and security considerations. Currently, we do not offer this method.

    +
  • +
+

In this document, we will explain the design and architecture of the Self-hosted Recce Instance solution.

+

Components

+

alt text

+
    +
  1. Recce Cloud: Provides an API to manage instances through an user interface.
  2. +
  3. Recce Cloud Agent: A service running on your own cloud infrastructure. It communicates with Recce Cloud, creates and manages Recce Instances. The agent requires configuration with the Recce Cloud API token and the necessary data warehouse credentials for the Recce Instances.
  4. +
  5. Recce Instance: An instance running Recce that requires a publicly accessible URL.
  6. +
+

Recce Cloud Agent Type

+
    +
  • +

    Docker: The Docker agent runs instances within Docker containers, making it suitable for a single VM serving multiple Recce Instances through different ports. However, if too many instances are run concurrently, it may lead to insufficient VM resources.

    +
  • +
  • +

    GCP Cloud Run: This approach runs the Recce Instances on GCP Cloud Run. Since GCP Cloud Run can scale the number of instances based on demand, it avoids the resource limitations faced by a single VM with Docker agents.

    +
  • +
+

Recce Instance Lifecycle

+

alt text

+
    +
  1. The user creates a Recce Instance for a PR.
  2. +
  3. The agent receives the request and provisions the Recce Instance.
  4. +
  5. Once the instance is ready, the URL endpoint becomes accessible.
  6. +
  7. The user can stop or start an instance as needed.
  8. +
  9. The user can delete a Recce Instance, and the agent will handle the deletion of the underlying container.
  10. +
+

Access Control

+

Recce Instances allow you to connect directly to the database through the interface, making access control a key consideration. Here are several options:

+
    +
  • Virtual Private Network: If your Recce Instance is running inside a VPC and is not accessible externally, this is the most secure and reliable approach.
  • +
  • Basic Authentication: The Recce Instance is publicly accessible but protected via HTTP Basic Authentication. Accessing the Recce Instance requires providing a user/password.
  • +
  • Tunnel-based Authentication: The Recce Instance runs within a VPC but can be exposed for external access using services such as ngrok or Tailscale. These services provide access control features to secure the instance.
  • +
  • Recce Cloud Authentication: Utilize Recce Cloud's login state to access the Recce Instance. The Recce Instance acts as an OIDC app within Recce Cloud, authenticating users through the OIDC protocol.
  • +
+

FAQ

+

Q: Does Recce Cloud make requests to the Agent's endpoint?

+

No. All actions are initiated by the agent making requests to the Recce Cloud API.

+

Q: Do I need to store my credentials in Recce Cloud?

+

No. All credentials are configured within the agent, which uses these settings to start a Recce Instance. This ensures you do not encounter any credential leakage issues.

+

Q: Does Recce Agent manage the entire organization or a single repository?

+

It manages the entire organization. However, support for single repository configurations may be available in the future.

+

Q: Do I need to define what container image the Recce Instance uses? Can I customize my own container image?

+

We provide an official Recce container that will check out your branch and install the necessary dependencies. You can also customize your own container image to speed up startup times or provide more flexible initialization processes.

+

Q: How do I run a Recce Cloud Agent? Where should my Cloud Run Agent be hosted?

+

The Recce Cloud Agent is a Python application. The recommended execution method varies depending on the agent type. The Docker agent runs within a Docker container, while the GCP Cloud Run agent runs within a Cloud Run container.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/expose-recce-instance-visibility/index.html b/docs/recce-cloud/expose-recce-instance-visibility/index.html new file mode 100644 index 00000000..7d4f83a2 --- /dev/null +++ b/docs/recce-cloud/expose-recce-instance-visibility/index.html @@ -0,0 +1,2412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Share Recce Instance Access - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Share Recce Instance Access

+ +
+

Note

+

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

+
+

As a Recce Cloud user, you can launch a Recce Instance in Cloud Mode or use GitHub Codespaces. However, both of these methods require a GitHub Access Token, which restricts the usage of Recce to those with GitHub accounts.

+

For situations in which you would like to share your Recce Instance with non-GitHub users, such as stakeholders or other teams, we currently recommend the use of one of the following third-party utilities:

+ +

These services provide an endpoint for your Recce Instance, with optional authentication, that will enable other users to access Recce.

+

These approaches serve as a workaround to expose a Recce instance to non-GitHub users. We are currently working on official support for enabling this feature without the need for third-party tools.

+

Provide external access to a Recce Instance

+
+

Note

+

Using these tools requires registering additional accounts, and you may need to subscribe to a paid plan to accommodate your usage and data transfer volume. For details, please refer to the Pricing Plans of ngrok and tailscale.

+
+

ngrok

+

After installing the Ngrok client, you can create an endpoint for the Recce Instance that will allow other users to participate in the dbt PR review process, without any additional tools or setup.

+
    +
  1. Setup the ngrok agent
  2. +
+

ngrok supports multiple platforms, including macOS, Linux, and Windows. Please refer to the official installation guide for details.

+
    +
  1. +

    Connect your ngrok agent to your ngrok account. +

    ngrok config add-authtoken <TOKEN>
    +

    +
  2. +
  3. +

    Put the Recce Instance online +

    ngrok http <recce-instance-port>
    +

    +

    ngrok forwarding to Recce

    +
  4. +
  5. +

    Secure access with authentication

    +

    ngrok provides a range of authentication options, from basic methods to integration with multiple OAuth providers. +

    # basic auth
    +ngrok http <recce-instance-port> --basic-auth 'username:password'
    +
    +# OAuth
    +ngrok http <recce-instance-port> --oauth=google --oauth-allow-email=user@example.com
    +

    +

    For the full usage of settings and options, please refer to the ngrok http docs for details.

    +
  6. +
+

Tailscale

+

Tailscale enables you to create your own private network (called a 'Tailnet') and invite members to join it. Once set up, you can easily expose your Recce instance, making it accessible to all devices within the Tailnet.

+
    +
  1. +

    Setup tailscale

    +

    To create a Tailnet, first create an account, then download Tailscale and follow the official guide to continue set-up. Once configured, you can then invite other members to join.

    +

    Manage Tailscale devices

    +

    It also supports integration with GitHub Codespaces.

    +
  2. +
  3. +

    Connect your device to your account +

    tailscale up --authkey <AUTH_KEY>
    +

    +
  4. +
  5. +

    Put the Recce Instance online

    +

    The devices within your Tailnet can access the Recce Instance now.

    +
    tailscale serve <recce-instance-port>
    +
    +

    alt text

    +

    If you need a more fine-grained access control policy, please refer to the Tailscale docs.

    +
  6. +
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/getting-started-recce-cloud/index.html b/docs/recce-cloud/getting-started-recce-cloud/index.html new file mode 100644 index 00000000..30567afe --- /dev/null +++ b/docs/recce-cloud/getting-started-recce-cloud/index.html @@ -0,0 +1,2742 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Demo Tutorial - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Demo Tutorial

+ +

Estimated Time: 20 minutes

+
+

Note

+

Recce Cloud is currently in private alpha and scheduled for general availability soon. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

+
+

The following guide uses the official Jaffle Shop DuckDB project from dbt-labs, and provides everything you need to get started with Recce Cloud. By the end of the guide you'll be able to create and sync Recce checks with a GitHub PR via Recce Cloud.

+

To see what you'll get, check out the first section from the following Loom:

+
+ +

Clone Jaffle Shop to your own private repository

+
    +
  1. Create a private repository in your GitHub account.
  2. +
  3. Clone the Jaffle Shop DuckDB dbt data project: +
    git clone git@github.com:dbt-labs/jaffle_shop_duckdb.git
    +cd jaffle_shop_duckdb
    +
  4. +
  5. Change the remote url to the repository you just created: +
    git remote set-url origin git@github.com:<owner>/<repo>.git
    +
  6. +
  7. Push to your newly created repository: +
    git push
    +
  8. +
+

Authorize Recce Cloud to access the repository

+

Recce Cloud needs access to your data project's repository in order to sync your checks status to the pull request thread.

+
    +
  1. Visit Recce Cloud. If it is your first time logging in, click the Continue with Github button to authorize the Recce Cloud integration to access your GitHub account. + alt text + alt text
  2. +
  3. Click the Install button to install the Recce Cloud GitHub app to your personal or organization account. + alt text
  4. +
  5. On the app installation page, authorize Recce Cloud to access the repository you created in the previous section. + alt text
  6. +
  7. Authorized repositories will then be shown in your Recce Cloud account. + alt text
  8. +
+

Configure the Jaffle Shop DuckDB data project

+

Set up the Jaffle Shop project and install Recce.

+
    +
  1. Create a new Python virtual env: +
    python -m venv venv
    +source venv/bin/activate
    +
  2. +
  3. Install the requirements and Recce: +
    pip install -r requirements.txt
    +pip install recce
    +
  4. +
  5. Add a production environment to the project by editing ./profiles.yml and adding the following target: +
    jaffle_shop:
    +  target: dev
    +  outputs:
    +    dev:
    +      type: duckdb
    +      path: 'jaffle_shop.duckdb'
    +      threads: 24
    ++   prod:
    ++     type: duckdb
    ++     path: 'jaffle_shop.duckdb'
    ++     schema: prod
    ++     threads: 24
    +
  6. +
  7. Add the following packages required by Recce for some features (highly recommended). Create a ./packages.yml file in the root of your project with the following packages: +
    packages:
    +- package: dbt-labs/audit_helper
    +  version: 0.12.0
    +- package: data-mie/dbt_profiler
    +  version: 0.8.2
    +
    + Install the packages: +
    dbt deps
    +
  8. +
+

Prepare the base environment

+

Recce requires to two environments to compare. The base represents your point of reference (the known-good base), and target represents your PR/development branch.

+
    +
  1. Prepare production (base) environment. (Note the custom --target-path): +
    dbt seed --target prod
    +dbt run --target prod
    +dbt docs generate --target prod --target-path ./target-base
    +
  2. +
  3. Add the target-base/ folder to the .gitignore file: +
     target/
    ++target-base/
    + dbt_packages/
    + dbt_modules/
    + logs/
    +
  4. +
  5. Remove the existing GitHub action workflows: +
    rm -rf .github/
    +
  6. +
  7. Push the changes to remote: +
    git add .
    +git commit -m 'Configure project and prep for Recce'
    +git push 
    +
  8. +
+
+

Important

+

By default, Recce expects the dbt artifacts for the base environment to be located in a folder named target-base.

+
+

The base environment preparation is now complete. The data in the prod schema, and artifacts in the target-base folder, represent stable (production) data.

+

As a PR author, you'll be working on data models, making changes to the project, and validating your work for correctness.

+

Prepare the review state for the PR

+

In this section, you'll make a new branch, update a data model, and create a pull request.

+
    +
  1. +

    Checkout a branch: +

    git checkout -b feature/recce-getting-started
    +

    +
  2. +
  3. +

    Edit the staging model located in ./models/staging/stg_payments.sql as follows: +

    ...
    +
    +renamed as (
    +         payment_method,
    +
    +-        -- `amount` is currently stored in cents, so we convert it to dollars
    +-        amount / 100 as amount
    ++        amount
    +
    +         from source
    +)
    +

    +
  4. +
  5. +

    Run dbt on the development environment (the default target): +

    dbt seed
    +dbt run
    +dbt docs generate
    +

    +
  6. +
  7. +

    Commit the change: +

    git add models/staging/stg_payments.sql
    +git commit -m 'Update the model'
    +git push -u origin feature/recce-getting-started 
    +

    +
  8. +
  9. +

    Create a pull request for this branch in your GitHub repository.

    +
  10. +
+
+

Important

+

Don't forget to create a branch for the commit above, before continuing with this tutorial.

+
+

Launch a Recce Instance to validate your change

+

In this section, you will launch a Recce Instance, create validation checks, and sync those checks with Recce Cloud so they can be reviewed by your PR reviewer.

+

Prepare a GitHub Token and Recce State password

+

To access the repository, your local Recce Instance will require a GitHub Token (Classic).

+
    +
  1. Prepare a GitHub Token (Classic) in your account. Ensure you provide repo permission for the new token. + Create a GitHub Token (Classic)
  2. +
  3. Ensure you have configured these environment variables. +
    export GITHUB_TOKEN=<github-token>
    +export RECCE_STATE_PASSWORD=mypassword
    +
  4. +
+

Run Recce in Cloud Mode

+

Run Recce instance in the cloud mode

+
recce server --cloud
+
+

Open the link to the Recce Instance in your browser. By default it should be http://0.0.0.0:8000

+

Create a Recce Check

+

Switch to the Query tab and paste the following query: +

select * from {{ ref("orders") }} order by 1
+
+ Enter the primary key as order_id and click the Run Diff button. + Recce Query Diff +1. Click the Add to Checklist button to add the query result to your Checklist +1. On the Checklist page you'll find that there are three checks. The Row count diff and Schema diff are default Preset Checks, and the Query diff is your newly added check. Leave the checks as unapproved. +1. Go back to the command line and terminate the Recce instance. Your Recce State file, containing your checklist and other artifacts will be encrypted and uploaded to Recce Cloud. +1. Go to the PR page in your GitHub repository and scroll to the bottom. Notice that Recce Cloud shows that check are not approved: + GitHub PR with unapproved Recce Checks

+
+

Note

+

Recce checks sync in realtime. However, due to the overhead of encrypting, compressing, and tranferring the State file, the sync may be slightly delayed. Ensure that you always terminate your Recce Instance on the CLI, and wait for the State to be synced. This will ensure your checks are saved to Recce Cloud.

+
+

Review the PR

+

As a PR author, your job is to review and approve the Checks created by the PR author. Once approved, the PR can be merged.

+
+

Note

+

As this tutorial uses DuckDB, which is a file-based database, the reviewer needs to have the same DuckDB file to continue the reviewers journey.

+
+

Run Recce is Review mode

+

The PR reviewer should prepare their own GitHub token, but ensure to use the same password as the PR author. (The password is used to unencrypt the State file and so must be the same.)

+
    +
  1. Checkout the PR branch +
    git checkout feature/recce-getting-started
    +
  2. +
  3. Configured the required environment variables. +
    export GITHUB_TOKEN=<github-token>
    +export RECCE_STATE_PASSWORD=mypassword
    +
  4. +
  5. Run Recce in --cloud and --review mode. +
    recce server --cloud --review
    +
  6. +
+

Approve Recce Checks

+

When Recce loads, click the Checklist tab to review the Checks that have been prepared by the PR author.

+

Approve all the checks if everything looks good to you

+

Recce Checklist showing approved Checks

+

The approval status of the check is automatically synced to Recce Cloud.

+

Merge the PR

+

Back on the GitHub PR page, you'll notice that the Recce Cloud check status has automatically been updated showing that "All checks are approved".

+

Recce Cloud - All Checks are Approved

+

In a real-world situation you'd now be able to merge the PR with the confidence that the PR author had checked their work, and the reviewer both understands and has signed-off on any changes.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/index.html b/docs/recce-cloud/index.html new file mode 100644 index 00000000..8179303f --- /dev/null +++ b/docs/recce-cloud/index.html @@ -0,0 +1,2722 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Overview - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Overview

+ +
+

Note

+

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

+
+

What is Recce Cloud?

+

Recce Cloud is a service specifically designed for streamlining the DBT PR Review workflow.

+

Recce Cloud primarily operates through Recce and integrates GitHub Pull Requests, consolidating the review status of PRs within the Cloud. Without Recce Cloud, we use the state file to store PR review states. However, this method is not very suitable for collaboration or integration with CI because our review states are not stored in a fixed location. Recce Cloud is designed to solve this problem.

+

Prerequisite

+
    +
  1. Recce Cloud requires Recce. Please make sure that you have understood how use Recce in your dbt project.
  2. +
  3. Prepare the github personal access token with the repo permission. Please see the GitHub document. And set it to your environment variable. +
    export GITHUB_TOKEN=<token>
    +
    + Or you can set the --cloud-token <GITHUB_TOKEN> command option.
  4. +
  5. Prepare the Recce state password. The Recce state password is used to encrypt/decrypt the state file before uploading/downloading. The password is not stored in Recce Cloud, so you need to keep it safe. +
    export RECCE_STATE_PASSWORD=<password>
    +
    + Or you can set the --password <password> or -p <password> command option.
  6. +
+

Getting Started

+

The following instructions give an overview of the process of using Recce in your dbt project. For a hands-on tutorial, please check the Jaffle Shop Tutorial for Cloud.

+

Sign Up the Recce Cloud

+
    +
  1. Go to the recce cloud
  2. +
  3. Sign in by github account
  4. +
  5. Click Install button to install Recce Cloud github app to your personal or organization account.
  6. +
  7. Authorize the repositories to the github app.
  8. +
+

Launch the recce server in the cloud mode

+
    +
  1. Create a branch for developing. +
    git checkout -b <my-awesome-feature>
    +
  2. +
  3. Develop your features and prepare the dbt artifacts for the base (target-base/) and current (target/) environments.
  4. +
  5. Create a pull request for this branch. Recce Cloud requires an open pull request in your GitHub repository. It also stores the latest Recce state for each pull request.
  6. +
  7. Launch the Recce instance in the cloud mode. It will use the dbt artifacts in the local target and target-base and initiate a new review state if necessary. +
    recce server --cloud
    +
  8. +
+
+

Note

+

Here we assume the you have set the GITHUB_TOKEN and RECCE_STATE_PASSWORD in your environment variables.

+
+

Review in the cloud mode

+

If the review state is already available for this PR, you can open the Recce instance to review.

+
    +
  1. Checkout the branch for the reviewed PR.
  2. +
  3. Launch the Recce instance to review this PR +
    recce server --review --cloud
    +
  4. +
+

Usage

+

All the commands requires the following settings.

+ + + + + + + + + + + + + + + + + + + + + + + +
NameEnvironment VariablesCLI OptionsDescription
Cloud tokenGITHUB_TOKEN--cloud-tokenUsed for
1. Get the pull request from github
2. Used as the access token to the recce cloud
State passwordRECCE_STATE_PASSWORD--passwordUsed to encrypt/decrypt the state in the recce cloud
+

Recce Cloud is used for pull request reviews. Before interacting with the cloud state, you should switch to a branch that has an open PR on the remote.

+

Recce server

+

Initiate the review session of the PR. It would use the local dbt artifacts in the target/ and target-base/ directories to sync the state with the cloud.

+
git checkout <pr-branch>
+recce sever --cloud
+
+

Recce server (Review mode)

+

Review a PR with the remote state.

+
git checkout <pr-branch>
+recce sever --cloud --review
+
+

Recce run

+

Run or rerun the PR's checks and sync the state with cloud.

+
git checkout <pr-branch>
+recce run --cloud
+
+

Recce summary

+

Generate the summary markdown

+
git checkout <pr-check>
+recce summary --cloud > summary.md
+
+

Recce cloud

+

The cloud subcommand in recce provides functionality for managing state files in cloud storage.

+

purge

+

You can purge the state from your current PR. It is useful when

+
    +
  1. You forgot the password
  2. +
  3. You would like to reset the state of this PR.
  4. +
+
git checkout <pr-branch>
+recce cloud purge
+
+

upload

+

If you already have the state file for the PR, you can upload it to the cloud.

+
git checkout <pr-branch>
+recce cloud upload <recce-state-file>
+
+

download

+

You can download the recce state file of the PR from cloud as well.

+
git checkout <pr-branch>
+recce cloud download
+
+

GitHub Pull Request Status Check

+

Recce Cloud integrate with the GitHub Pull Request Status Check. If there is recce review state synced to a PR, the PR would has a recce cloud check status. Once all checks in recce are approved, the check status would change to passed and ready to be merged.

+

alt text

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/setup-gh-actions/index.html b/docs/recce-cloud/setup-gh-actions/index.html new file mode 100644 index 00000000..2c0263cf --- /dev/null +++ b/docs/recce-cloud/setup-gh-actions/index.html @@ -0,0 +1,2640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Continue Integration (CI) - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Continue Integration (CI)

+ +
+

Note

+

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

+
+

Continuous Integration(CI) and Continuous Delivery(CD) are best practices in software development. Through CI automation, a dbt project can systematically and continuously deliver and integrate high-quality results.

+

To automate the process, we can use GitHub Actions and GitHub Codespaces to provide an automated and reusable workspace. The following diagram describes the entire ci/cd architecture.

+

alt text

+

We suggest setting up two GitHub Actions workflows in your GitHub repository. One for the base environment and another for the PR environment.

+
    +
  • +

    Base environment workflow: Triggered on every merge to the main branch. This ensures that base artifacts are readily available for use when a PR is opened.

    +
  • +
  • +

    PR environment workflow: Triggered on every push to the pull-request branch. This workflow will compare base models with the current PR environment.

    +
  • +
+

Prerequisites

+
    +
  1. +

    Per-PR Environment: To ensure that each PR has its own isolated environment, it is recommended to put profile.yml under source control in the repository and use environment variables to change the schema name. In the workflow, we can generate the corresponding schema name based on the PR number.

    +
    myprofile:
    +  outputs:
    +    pr:
    +      type: snowflake
    +      ...
    +      schema: "{{ env_var('DBT_SCHEMA') | as_text }}"
    +    prod:
    +      type: snowflake
    +      ...
    +      schema: PUBLIC
    +
    +
  2. +
  3. +

    GitHub Token and Recce State Password: As mentioned here, please ensure that the two secrets are available when running recce commands. You can add GH_TOKEN and RECCE_STATE_PASSWORD to the GitHub Actions Secrets. Then we can use them in the Github Actions workflow file. +

    env:
    +  GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
    +  RECCE_STATE_PASSWORD: ${{ secrets.RECCE_STATE_PASSWORD }}
    +

    +
  4. +
+
+

Warning

+

You cannot use the automatic generated token here, because we need the personal access token (PAT) to verify if the user has PUSH permission of the repository.

+
env:
+  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Don't use 'secrets.GITHUB_TOKEN' here
+
+
+

Set up Recce with GitHub Actions

+

Base Workflow (Main Branch)

+

This workflow will perform the following actions:

+
    +
  1. Run dbt on the base environment.
  2. +
  3. Upload the generated DBT artifacts to github workflow artifacts for later use.
  4. +
+
+

Note

+

Please place the above file in .github/workflows/dbt_base.yml. This workflow path will also be used in the next PR workflow. If you place it in a different location, please remember to make the corresponding changes in the next step.

+
+
name: Daily Job
+
+on:
+  workflow_dispatch:
+  schedule:
+    - cron: "0 0 * * *"
+  push:
+    branches:
+      - main
+
+concurrency:
+  group: recce-ci-base
+  cancel-in-progress: true
+env:
+  # Credentials used by dbt profiles.yml
+  DBT_USER: ${{ secrets.DBT_USER }}
+  DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.10.x"
+          cache: "pip"
+
+      - name: Install dependencies
+        run: |
+          pip install -r requirements.txt
+
+      - name: Run DBT
+        run: |
+          dbt deps
+          dbt seed --target ${{ env.DBT_BASE_TARGET }}
+          dbt run --target ${{ env.DBT_BASE_TARGET }}
+          dbt docs generate --target ${{ env.DBT_BASE_TARGET }}
+        env:
+          DBT_BASE_TARGET: "prod"
+
+      - name: Upload DBT Artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: target
+          path: target/
+
+

If executed successfully, the dbt target will be placed in the run artifacts and will be named target. +alt text

+

PR Workflow (Pull Request Branch)

+

This workflow will perform the following actions:

+
    +
  1. Run dbt on the PR environment.
  2. +
  3. Download previously generated base artifacts from base workflow.
  4. +
  5. Use Recce to compare the PR environment with the downloaded base artifacts.
  6. +
  7. Use Recce to generate the summary of the current changes and post it as a comment on the pull request. Please refer to the Recce Summary for more information.
  8. +
+
name: Recce CI PR Branch
+
+on:
+  pull_request:
+    branches: [main]
+
+env:
+  WORKFLOW_BASE: ".github/workflows/dbt_base.yml"
+  # Credentials used by dbt profiles.yml
+  DBT_USER: ${{ secrets.DBT_USER }}
+  DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}
+  DBT_SCHEMA: "PR_${{ github.event.pull_request.number }}"
+  # Credentials used by recce
+  GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
+  RECCE_STATE_PASSWORD: ${{ secrets.RECCE_STATE_PASSWORD }}
+jobs:
+  check-pull-request:
+    name: Check pull request by Recce CI
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - name: Merge Base Branch into PR
+        uses: DataRecce/PR-Update@v1
+        with:
+          baseBranch: ${{ github.event.pull_request.base.ref }}
+          autoMerge: false
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.10.x"
+          cache: pip
+      - name: Install dependencies
+        run: |
+          pip install -r requirements.txt
+          pip install recce~=0.34
+      - name: Download artifacts for the base environment
+        run: |
+          gh repo set-default ${{ github.repository }}
+          base_branch=${{ github.base_ref }}
+          run_id=$(gh run list --workflow ${WORKFLOW_BASE} --branch ${base_branch} --status success --limit 1 --json databaseId --jq '.[0].databaseId')
+          echo "Download artifacts from run $run_id"
+          gh run download ${run_id} -n target -D target-base
+      - name: Prepare the PR environment
+        run: |
+          dbt deps
+          dbt seed --target ${{ env.DBT_CURRENT_TARGET}}
+          dbt run --target ${{ env.DBT_CURRENT_TARGET}}
+          dbt docs generate --target ${{ env.DBT_CURRENT_TARGET}}
+        env:
+          DBT_CURRENT_TARGET: "pr"
+      - name: Run Recce
+        run: |
+          recce run --cloud
+      - name: Upload DBT Artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: target
+          path: target/
+
+      - name: Prepare Recce Summary
+        id: recce-summary
+        run: |
+          recce summary --cloud > recce_summary.md
+          cat recce_summary.md >> $GITHUB_STEP_SUMMARY
+          echo '${{ env.NEXT_STEP_MESSAGE }}' >> recce_summary.md
+
+          # Handle the case when the recce summary is too long to be displayed in the GitHub PR comment
+          if [[ `wc -c recce_summary.md | awk '{print $1}'` -ge '65535' ]]; then
+            echo '# Recce Summary
+          The recce summary is too long to be displayed in the GitHub PR comment.
+          Please check the summary detail in the [Job Summary](${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}) page.
+          ${{ env.NEXT_STEP_MESSAGE }}' > recce_summary.md
+          fi
+
+        env:
+          RECCE_STATE_PASSWORD: ${{ secrets.RECCE_STATE_PASSWORD }}
+          NEXT_STEP_MESSAGE: |
+            ## Next Steps          
+            If you want to check more detail information about the recce result, please follow this instruction.
+
+            ```bash
+            # Checkout to the PR branch
+            git checkout ${{ github.event.pull_request.head.ref }}
+
+            # Launch the recce server based on the state file
+            recce server --cloud --review
+
+            # Open the recce server http://localhost:8000 by your browser
+            ```
+      - name: Comment on pull request
+        uses: thollander/actions-comment-pull-request@v2
+        with:
+          filePath: recce_summary.md
+          comment_tag: recce
+
+

PR workflow with dbt Cloud

+

We can download the dbt artifacts from dbt Cloud for Recce if CI/CD on dbt Cloud is configured. The basic scenario is to download the latest artifacts of the deploy job for the base environment and the artifacts of the CI job for the current environment. We can archieve it via dbt Cloud API and we need:

+
    +
  1. dbt Cloud Token
  2. +
  3. dbt Cloud Account ID: Check out your "Account settings" on dbt Cloud
  4. +
  5. dbt Cloud Deploy Job ID: Check out "API trigger" of the deploy job
  6. +
  7. dbt Cloud CI Job ID: Check out "API trigger" of the CI job
  8. +
+

alt text

+

We prepare a GitHub Action "Recce dbt Cloud Action" to do the following steps:

+
    +
  1. Trigger the CI job on dbt Cloud
  2. +
  3. Wait the CI job to finish
  4. +
  5. Download the dbt artifacts from the deploy job to ./target-base directory
  6. +
  7. Download the dbt artifacts from the deploy job to ./target directory
  8. +
+

Check out the GitHub Action to configure the GitHub workflow.

+
+

Note

+

Please ensure Generate docs on run is toggled in the "Execution settings" of deploy job and "Advanced settings" of CI job. +alt text

+
+

Review the Recce State File

+

Review locally

+
git checkout <pr-branch>
+recce server --cloud --review
+
+

Review in the GitHub codespace

+

Please see GitHub Codespaces integration

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/recce-cloud/setup-gh-codespaces/index.html b/docs/recce-cloud/setup-gh-codespaces/index.html new file mode 100644 index 00000000..58416dc5 --- /dev/null +++ b/docs/recce-cloud/setup-gh-codespaces/index.html @@ -0,0 +1,2487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + GitHub Codespaces - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

GitHub Codespaces

+ +
+

Note

+

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

+
+

GitHub Codespaces is a development environment provided by GitHub that allows developers to have identical and isolated environments for development. The GitHub Codespaces uses VS Code Server technology. We can launch it from a GitHub pull request page, and once it is started, the Recce instance will run and port forwarding will be set up.

+

Setup Recce Cloud in GitHub Codespaces

+
    +
  1. +

    Prepare the two files in your repository +

    .devcontainer
    +└── recce
    +    ├── Dockerfile
    +    └── devcontainer.json
    +

    +
  2. +
  3. +

    Configure the .devcontainer/recce/devcontainer.json +

    {
    +    "name": "Recce CodeSpace",
    +    "build": {
    +        "dockerfile": "Dockerfile",
    +    },
    +    "containerEnv": {
    +        "RECCE_STATE_PASSWORD": "${{ secrets.RECCE_STATE_PASSWORD }}",
    +        "DBT_USER": "${{ secrets.DBT_USER }}",
    +        "DBT_PASSWORD": "${{ secrets.DBT_PASSWORD }}",
    +    },
    +    "forwardPorts": [8000],
    +    "postStartCommand": "recce server --cloud --review"
    +}
    +
    + The secrets are Github Codespaces secrets. You can configure them in

    + +
  4. +
  5. +

    Prepare the .devcontainer/Dockerfile +

    FROM mcr.microsoft.com/vscode/devcontainers/python:3.11
    +
    +RUN pip install dbt-bigquery~=1.7.0 recce~=0.34
    +

    +
  6. +
+
+

Note

+

The GitHub token generated by codespace is sufficient for Recce's use. It's not necessary to configure the GITHUB_TOKEN separately.

+
+

How to use

+

Once you complete Recce Cloud setup, you can launch GitHub Codespaces from Recce Cloud's pull request page, and once it is started, the Recce instance will run and port forwarding will be set up.

+
    +
  1. Go to Recce Cloud and click the repository you want to make changes. + Recce Cloud Home
  2. +
  3. Click the pull request that you want to use Recce instance.
    +Recce Cloud Open PR
  4. +
  5. Click "Create in GitHub Codespaces." + Create in GitHub Codespaces
  6. +
  7. The Codespaces creation may take 1 to more than 5 mintues depeding on your Codespaces settings. And the Recce instance should take less than 1 minute to launch.
      +
    • Please view FAQ for how to speed up.
    • +
    +
  8. +
  9. You can see the "State" of the progress; And the Action you can take in each state.
      +
    • Codespace Queued: the Codespace is creating + Codespace Queued
    • +
    • Codespace Provisioning: the Codespace is provisioning
    • +
    • Codespace Available: the Codespace is ready
    • +
    • Recce launching: Recce instance is launching
    • +
    • Stop: stop launching Recce instance
    • +
    • Recce active: Recce instance is launched successfully.
        +
      • Open: open the Recce instance
      • +
      • Stop: stop the Codespace +Recce active
      • +
      +
    • +
    • Codespace ShuttingDown: the Codespace is shutting down
    • +
    • Stopped: the Codespace is stopped and the Recce instance is closed.
        +
      • Restart: restart the Codespace
      • +
      • Delete: delete the Codespace
          +
        • It’s recommended to delete the Codespace once your PR is merged.
        • +
        +
      • +
      +
    • +
    +
  10. +
+

Troubleshooting

+

If this is your first time setting up a Codespace, it’s recommended to first test locally with the following commands:

+
git checkout feature/recce-getting-started
+export GITHUB_TOKEN=<github-token>
+export RECCE_STATE_PASSWORD=mypassword
+recce server --cloud --review
+
+

Ensure it runs correctly locally. If it does, then the remaining issues within the Codespace are likely related to its configuration.

+

If your Codespace configration is correct, other common causes might include:

+
    +
  1. The current branch does not have a corresponding pull request. This usually happens if you launch Codespace directly form GitHub main branch. Recce instance cannot assoicate with the main branch.
  2. +
  3. The pull request does not have an uploaded Recce state file. In review mode, the state file must be prepared via CI or locally before proceeding.
  4. +
  5. The RECCE_STATE_PASSWORD mentioned above is not set or the password is incorrect.
  6. +
  7. Other issues are preventing the Recce server from starting at all.
  8. +
+

When you’ve opened a Codespace but are unable to connect to the Recce instance, you can troubleshoot by following these steps:

+
    +
  1. Check Codespace instance in GitHub. + Check Codespace in GitHub
  2. +
  3. If the Codesapce you created from Recce Cloud is active, click "Open in Browser". + Open Codespace in Browser
  4. +
  5. Click on the blue block in the lower left corner of the status bar, which usually shows "Codespaces: instance name"
    +Codespace status bar
  6. +
  7. Select "View creation log" or ppen the VS Code Command Palette and type Codespaces: View Creation Log.
  8. +
  9. At this point, you should be able to see the reason why the Recce server failed to start.
    +alt text
  10. +
  11. If you cannot find any issue from the Codespace creation log, and belive your Codespace configration is correct. Please stop the Codespace and launch Recce instance again.
  12. +
  13. If you still have problem, please contact us via slack or email product@datarecce.io. We are happy to help.
  14. +
+

FAQ

+

Q: How long does Codespace generally take to start?

+

The typical startup time is around 1 to 2 minutes if you have prebuid. If not, it may take more than 5 mintues. However, this depends on how your Dockerfile is configured. Codespace builds your image every time it starts, so if your Dockerfile includes multiple pip install , it may take longer.

+

Once your Codespace instance is already running, you won’t need to wait again when you return to it.

+

Q: Is there a way to optimize the startup speed?

+

Codespace offers a prebuild feature, which can significantly improve startup speed. In our sample project, we found it's helpful to reduce prebuild when we set prebuild available to only sepecific regions. However, you need to ensure that the image is up-to-date. To strike a balance between speed and update frequency, you can consider scheduling a weekly image rebuild. +set prebuild in only specific retion

+

Q: Can a Codespace environment be shared? Can different people access the same Codespace instance?

+

A Codespace environment is tied to each individual GitHub user account. Therefore, a Codespace instance opened by User A cannot be directly accessed by User B. However, User A can set a specific port to be public and share the URL with others. However, the instance is still running under User A's account.

+

Q: Personal Codespace vs. Organization Codespace? How is Codespace billed?

+

By default, Codespace usage is billed to a GitHub personal account. GitHub offers a free tier, allowing each user 120 core hours per month for free.

+

For GitHub organization accounts, you can configure all Codespace charges to be billed to the organization. In this case, billing is attributed to the organization rather than personal accounts.

+

For more details on billing, please refer to the official CodeSpace billing documentation.

+

Q: How to configure codespaces?

+

Codespace utilizes VSCode Dev Containers technology, which can be executed either locally (via Docker) or in the cloud (via GitHub Codespace). The configuration above provided are primarily recommendations for the Recce Cloud setup. For more advanced configuration options, you can refer to the VSCode dev containers or the containers.dev documentation.

+

The default configuration for GitHub Codespace is explained in the documentation. If you're looking to set up a development environment for dbt/Recce, you can also refer to the Python project configuration documentation.

+

Q: Can I have multiple devcontainer configurations? Which one is selected by default?

+

Yes, you can. Please refer to the Codespaces documentation for more details. Recce Cloud will prioritize .devcontainer/recce/devcontainer.json if it is available. If not, it will default to .devcontainer/devcontainer.json, and then any other available configurations.

+

Q: Should I delete Codespace after PR merged?

+

Yes. When you merged a PR, you'll see the Delete codespace message. You can delete the Codespace to save the free usage. +Delete Codespace after PR merged

+

You can also delete Codespace in your GitHub main branch or in Recce Cloud PR page. +Delete Codespace in your GitHub

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/reference/configuration/index.html b/docs/reference/configuration/index.html new file mode 100644 index 00000000..0a5036e7 --- /dev/null +++ b/docs/reference/configuration/index.html @@ -0,0 +1,2373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Configuration - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Configuration

+ +

The config file for Recce is located in recce.yml. Currently, only preset checks are configurable.

+

Preset Checks

+

Preset checks can be generated when executing recce server or recce run.

+
# recce.yml
+checks:
+  - name: Query diff of customers
+    description: |
+        This is the demo preset check.
+
+        Please run the query and paste the screenshot to the PR comment.
+    type: query_diff
+    params:
+        sql_template: select * from {{ ref("customers") }}
+    view_options:
+        primary_keys:
+        - customer_id
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionType
namethe title of the checkstring
descriptionthe description of the checkstring
typethe type of the checkstring
paramsthe parameters for running the checkobject
view_optionsthe options for presenting the run resultobject
+
+

Note

+

Regarding the supported types and settings for each type, the documentation does not currently provide this information. Please obtain the settings by acquiring the preset checks template through the Web UI.

+
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/start-with-dbt-cloud/index.html b/docs/start-with-dbt-cloud/index.html new file mode 100644 index 00000000..234f3b1b --- /dev/null +++ b/docs/start-with-dbt-cloud/index.html @@ -0,0 +1,2646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Start with dbt Cloud - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Start with dbt Cloud

+

dbt Cloud is a hosted service that provides a managed environment for running dbt projects by dbt Labs. This document provides a step-by-step guide to get started recce with dbt Cloud.

+

Prerequisites

+

Recce will compare the data models between two environments. That means you need to have two environments in your dbt Cloud project. For example, one for production and another for development. +Also, you need to provide the credentials profile for both environments in your profiles.yml file to let Recce access your data warehouse.

+

Suggestions for setting up dbt Cloud

+

To integrate the dbt Cloud with Recce, we suggest to set up two run jobs in your dbt Cloud project.

+

Production Run Job

+

The production run should be the main branch of your dbt project. You can trigger the dbt Cloud job on every merge to the main branch or schedule it to run at a daily specific time.

+

Development Run Job

+

The development run should be a separate branch of your dbt project. You can trigger the dbt Cloud job on every merge to the pull-request branch.

+

Set up dbt profiles with credentials

+

You need to provide the credentials profile for both environments in your profiles.yml file. Here is an example of how your profiles.yml file might look like:

+
dbt-example-project:
+  target: dev
+  outputs:
+    dev:
+      type: snowflake
+      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
+
+      # User/password auth
+      user: "{{ env_var('SNOWFLAKE_USER') | as_text }}"
+      password: "{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}"
+
+      role: DEVELOPER
+      database: cloud_database
+      warehouse: LOAD_WH
+      schema: "{{ env_var('SNOWFLAKE_SCHEMA') | as_text }}"
+      threads: 4
+    prod:
+      type: snowflake
+      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
+
+      # User/password auth
+      user: "{{ env_var('SNOWFLAKE_USER') | as_text }}"
+      password: "{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}"
+
+      role: DEVELOPER
+      database: cloud_database
+      warehouse: LOAD_WH
+      schema: PUBLIC
+      threads: 4
+
+

Install Recce

+

Install Recce using pip:

+
pip install -U recce
+
+

Execute Recce with dbt Cloud

+

To compare the data models between two environments, you need to download the dbt Cloud artifacts for both environments. The artifacts include the manifest.json file and the catalog.json file. You can download the artifacts from the dbt Cloud UI.

+

Login to your dbt Cloud account

+

dbt Cloud login

+

Go to the project you want to compare

+

dbt Cloud login

+

Download the dbt artifacts

+

Download the artifacts from the latest run of both run jobs. You can download the artifacts from the Artifacts tab.

+

dbt Cloud login +dbt Cloud login

+

Setup the dbt artifacts folders

+

Extract the downloaded artifacts and keep them in a separate folder. The production artifacts should be in the target-base folder and the development artifacts should be in the target folder.

+
$ tree target target-base
+target
+├── catalog.json
+└── manifest.json
+target-base/
+├── catalog.json
+└── manifest.json
+
+

Setup dbt project

+

Move the target and target-base folders to the root of your dbt project. +You should also have the profiles.yml file in the root of your dbt project with the credentials profile for both environments.

+

Start the Recce server

+

Run the recce command to compare the data models between the two environments.

+
recce server
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/start-with-sqlmesh/index.html b/docs/start-with-sqlmesh/index.html new file mode 100644 index 00000000..7ccdcd5d --- /dev/null +++ b/docs/start-with-sqlmesh/index.html @@ -0,0 +1,2484 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Start with SQLMesh (Experimental) - Recce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Start with SQLMesh (Experimental)

+ +
+

Note

+

The integration of SQLMesh is still in the experimental stage, and some features are not yet fully supported.

+
+

SQLMesh is an alternative to dbt. Unlike dbt, it records the state of each environment within the data warehouse. This feature simplifies the use of Recce.

+

Usage

+
pip install -U recce
+
+

Start the Recce server with the follow command:

+
recce server --sqlmesh --sqlmesh-envs prod:dev
+
+

Start with specific config name +

recce server --sqlmesh --sqlmesh-envs prod:dev --sqlmesh-config local_config
+

+

Tutorial: The Sushi Example Project

+

Here, the official example project from SQLMesh was used to demonstrate how to use recce.

+
    +
  1. +

    Clone the SQLMesh repo +

    git clone git@github.com:TobikoData/sqlmesh.git
    +cd sqlmesh/examples/sushi   
    +

    +
  2. +
  3. +

    Prepare the python venv and install the SQLMesh.

    +
    python -m venv venv
    +source ./venv/bin/activate
    +pip install sqlmesh
    +
    +
  4. +
  5. +

    Plan the prod environment

    +
    sqlmesh --config local_config plan
    +
    +
  6. +
  7. +

    Modify the model models/customers.sql model

    +

      ...
    +  SELECT DISTINCT
    +    o.customer_id::INT AS customer_id, -- customer_id uniquely identifies customers
    +    m.status,
    +    d.zip
    +  FROM sushi.orders AS o
    +  LEFT JOIN current_marketing AS m
    +    ON o.customer_id = m.customer_id
    +  LEFT JOIN raw.demographics AS d
    +    ON o.customer_id = d.customer_id
    ++ WHERE status is not NULL
    +
    + and apply this change to the dev environment +
    sqlmesh --config local_config plan dev    
    +
    +output +
    New environment `dev` will be created from `prod`
    +Summary of differences against `dev`:
    +Models:
    +├── Directly Modified:
    +   └── sushi__dev.customers
    +└── Indirectly Modified:
    +    └── sushi__dev.waiter_as_customer_by_day
    +---
    +
    ++++
    +
    +@@ -31,3 +31,5 @@
    +
    +ON o.customer_id = m.customer_id
    +LEFT JOIN raw.demographics AS d
    +ON o.customer_id = d.customer_id
    ++WHERE
    ++  NOT status IS NULL
    +Directly Modified: sushi__dev.customers (Breaking)
    +└── Indirectly Modified Children:
    +    └── sushi__dev.waiter_as_customer_by_day (Indirect Breaking)
    +Apply - Virtual Update [y/n]:
    +

    +
  8. +
  9. +

    Use sqlmesh table-diff to check the change. +

    sqlmesh --config local_config table_diff prod:dev sushi.customers    
    +
    + output +
    Schema Diff Between 'PROD' and 'DEV' environments for model 'sushi.customers':
    +└── Schemas match
    +
    +
    +Row Counts:
    +├──  COMMON: 55 rows
    +├──  PROD ONLY: 18 rows
    +└──  DEV ONLY: 0 rows
    +
    +COMMON ROWS column comparison stats:
    +        pct_match
    +status      100.0
    +zip         100.0
    +

    +
  10. +
  11. +

    Install Recce +

    pip install recce    
    +

    +
  12. +
  13. +

    Launch the recce server +

    recce server --sqlmesh --sqlmesh-envs prod:dev --sqlmesh-config local_config   
    +

    +

    You can see the lineage DAG diff

    +

    Lineage Diff

    +
  14. +
  15. +

    In the Query page, you can diff with ad-hoc query. Enter the following SQL script and click the Run Diff button.

    +
    select 
    +  status,
    +  count(*) as c
    +from sushi.customers
    +group by status
    +order by status
    +
    +

    Set the primary key as status and click the Run Diff button

    +

    Query Diff Result

    +

    You will see that there is only one record where status=NULL that differs. In the original version, there were two records with status=NULL, but in the new version, there is no record with status=NULL.

    +
  16. +
+

Supported Recce Features

+ + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/firesidechat-watch/index.html b/firesidechat-watch/index.html new file mode 100644 index 00000000..0076d7a6 --- /dev/null +++ b/firesidechat-watch/index.html @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + Recce Fireside Chat - Data Productivity: Beyond DevOps and dbt + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+ See what our users are saying about Recce + +

+
+
+
+
+ +
+
+
+ + + +

Recce

+
+ + +
+
+ + + + + +
+ +
+
+

Data Productivity

+

Beyond DevOps and dbt

+ +
+
+ +

The following video is a recording of the fireside chat hosted by CL Kao, founder of DataRecce.io, and Noel Gomez, co-founder of Datacoves.com on August 27, 2024.

+

Thanks to all those who signed up and joined the event.

+ +
+
+ +
+ + +
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ + + + +
+ +
+
+

Build Better Data.

+
+ + +
+ +
+
+

Build data relationships
through team collaboration

+

Work together on checking and validating data

+
+ +
+

Build data trust
in your data and your team

+

Visualizing and interpreting change with stakeholders builds trust

+
+ +
+

Build data foundations
for data analytics

+

Confidence in data results in a solid foundation for downstream usage

+
+
+ +
+
+
+ + + + +
+ +
+

Book a demo

+

Find out more about how Recce can improve your data PR review process

+
+ + +
+ + + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/firesidechat/index.html b/firesidechat/index.html new file mode 100644 index 00000000..448ea4aa --- /dev/null +++ b/firesidechat/index.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + Recce Fireside Chat - Data Productivity: Beyond DevOps and dbt + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+ See what our users are saying about Recce + +

+
+
+
+
+ +
+
+
+ + + +

Recce

+
+ + +
+
+ + + + + + + + + + + +
+ +
+
+

Build Better Data.

+
+ + +
+ +
+
+

Build data relationships
through team collaboration

+

Work together on checking and validating data

+
+ +
+

Build data trust
in your data and your team

+

Visualizing and interpreting change with stakeholders builds trust

+
+ +
+

Build data foundations
for data analytics

+

Confidence in data results in a solid foundation for downstream usage

+
+
+ +
+
+
+ + + + +
+ +
+

Book a demo

+

Find out more about how Recce can improve your data PR review process

+
+ + +
+ + + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..ef3cbf76 --- /dev/null +++ b/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + Recce - Data-Modeling Validation Toolkit for Analytics and Data Engineers using dbt + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+ See what our users are saying about Recce + +

+
+
+
+
+ +
+
+
+ + + +

Recce

+
+ + +
+
+ + + + +
+
+
+ +
+

Self‑Serve Review for
Self‑Serve Data

+

Contextual data impact analysis for comprehensive PR review. Validate data impact with reproducible data testing environments during development and as part of continuous integration. +

+ + Open Source + + Recce Cloud +
+ + +
+
+
+ +
+ +
+
+ +
+

Level-up your data project PR review and stop merging breaking changes into production

+ +
+ +
+ +

Sign up for a Recce Cloud invite.

+
+
+
+ +
+ + +
+ +
+ + + + +
+ + + +
+
+ +
+
+
+ + +
+ + +
+
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+

News & Updates

+ +
+ +
+
+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ +
Case Study
+

Rio de Janeiro Department of Health

+

Prefeitura do Rio de Janeiro uses Recce to ensure data correctness of the health records for 7 million people

+ Read Case Study + +
+ +
+ +
+ + +
+ +
+ + + +
+
+ +
+ +

Data Productivity:
Beyond DevOps & dbt

+

The fireside chat recording is now available to watch online. CL Kao of Recce and Noel Gomez of Datacoves discussed how to apply and adapt DevOps to modern DataOps. Plus, how to improve your team's:

+ +
    +
  • agility and speed
  • +
  • cross-functional Collaboration
  • +
  • data quality and reliability
  • +
+ +
+ + Watch Now + +
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+
+
+ +
+
+ +
+
+

Faster QA for pull request reviews

+

Cross-environment diffs with Recce help speed up the QA process for pull request review. Quickly check data impact by comparing modeling changes with production data - No need to craft complex queries yourself, or deal with configurations for multiple tools.

+ +
+
+
+ +
+
+
+ +
+
+ +
+
+

Focused Data Reconnaissance

+

Recce is feature-packed with a diff for every type of validation you'll need to ensure proof-of-correctness of your modeling changes. Starting from the Lineage DAG-Diff interface, Recce shows you the part of your DAG that has been impacted by your changes. It's the interface for your data-change reconnaissance mission. +

+
+
+
+ +
+
+
+ +
+
+ +
+
+

Pull Request Comment on Steroids

+

Curate the checks you need into a list of reproducible validations for use in your pull request comment.

+
+
+
+ +
+ + +
+ +
+
+ +
+

Testimonials

+

What our users are Saying

+ +
+ +
+ +
+ + +
+ +
+ +
+ +

+ In my job I have to understand data change, and Recce was a game changer. It made our PR review much quicker - From a day to less than an hour to merge. +

+
+ +
+
+ +
+

Thiago Trabach

+
Head of Data Science & Engineering
Prefeitura do Rio de Janeiro
+
+
+
+ +
+ +
+ +
+ +

+ My entire team uses Recce as part of their PR flow... we use it almost on every PR. I changed a major table in our DB that impacted over 20 down the funnel processes, and (Recce) helped me QA it very efficiently. +

+
+ +
+
+ +
+

Omri Antman

+
Head of Data and Analytics
Imagen
+
+
+
+ +
+ +
+ +
+ +

+ (Recce) helps visualize... everything from the lineage to the single value. You have a visual representation of the change in values for a PR available to everyone. +

+
+ +
+
+ +
+

Igor Dal Bo

+
Data Engineer
FactoryPal
+
+
+
+ +
+ +
+ +
+ +

+ Recce provides a powerful set of features out of the box - Perfect for teams without dedicated engineers who can build and maintain bespoke solutions +

+
+ +
+
+ +
+

Bruce Huang

+
Data Engineer
Migo
+
+
+
+ +
+ +
+ +
+ +

+ Recce... can simplify your workflow and boost your productivity...work smarter, not harder. +

+
+ +
+
+ +
+

Andy Sawyer

+
Strategy Leader & Advisor
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+ +
+

Book a demo

+

Find out more about how Recce can improve your data PR review process

+
+ + +
+ + + +

+ + Book us with Cal.com + +

+ + +
+ + +
+
+
+
+
+

Find out how to level-up your pull request review for dbt.

+

+ Sign up to be notified when Recce Cloud is available. +

+ + Sign Up +
+ + +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..06c51d49 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"blog/","title":"The Recce Blog","text":"

Here you can find articles about dbt best practices and how to get the most out of Recce. For more articles, don't forget to also check out our Medium publication.

"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/","title":"Next-Level Data Validation Toolkit for dbt Data Projects \u2014 Introducing Recce","text":"Recce: Data Validation Toolkit for dbt

Validating data modeling changes and reviewing pull requests for dbt projects can be a challenging task. The difficulty of performing a proper \u2018code review\u2019 for data projects, due to both the code and data needing review, means the data validation stage is often omitted, poorly implemented, or drastically slows down time-to-merge for your time sensitive data updates.

How can you maintain data best practices, but speed up the validation and review process?

"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#recce-the-data-validation-toolkit-for-dbt-projects","title":"Recce \u2014 The data validation toolkit for dbt projects","text":"

Recce (short for reconnaissance) is a data modeling validation toolkit with a focus on environment diffing. Take two dbt environments, such as dev and prod, and compare them using the suite of diff tools in Recce.

"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#your-diffing-toolkit","title":"Your Diffing Toolkit","text":"

With Recce you\u2019re able to validate your data modeling changes against a known-good baseline. The only real way to verify modeling changes is to check it against historical/production data.

"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#lineage-dag-diff","title":"Lineage DAG Diff","text":"

Start from the zone of impact of your changes, and see which models have been modified, added, and removed. The dbt docs lineage DAG only shows you the current state of the DAG, Recce shows you how the DAG differs from before you made any changes.

Lineage DAG Diff in Recce"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#data-profile-diff-value-diff","title":"Data Profile Diff & Value Diff","text":"

Perform holistic checks by diffing the data profile stats for your development branch, then check the percentage of values matching for each column in a model.

Data Profile in Recce"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#query-diff","title":"Query Diff","text":"

If something needs further investigation, drill down and query the data. One query will run on both environments, and you\u2019ll be able to see the difference on a row-by-row basis. Enable change-only view to see just what\u2019s changed.

SQL Query Diff in Recce"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#schema-and-row-count","title":"Schema and Row Count","text":"

In addition to the above diffs, you can also check the schema and row count, just to be sure you didn\u2019t lose any data, or an important column.

Schema and Row Count in Recce"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#create-your-checklist","title":"Create your checklist","text":"

As you create validations, add them to your checklist with notes about what you found, and re-re-run checks if the data changes. When you\u2019re ready, export the checks to your PR comment.

Data Project PR Checklist in Recce"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#create-your-all-signal-no-noise-pr-comment","title":"Create your all signal, no noise PR comment","text":"

When you\u2019re ready to share your validations as proof-of-correctness for your work, you can export checks into your PR comment template. You can copy your notes, and export a screenshot of the check as it appears in Recce. By curating the validations for your PR comment, you can create an \u2018all-signal, no noise\u2019 comment with the validations that are relevant to the context of your changes.

Data Modeling Validations in PR Comment

The reviewer will be able to see the query and results of your data spot-checks and have the comprehensive information required to request further investigation, or sign-off on your changes.

"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#why-recce","title":"Why Recce?","text":"

As mentioned above, whether you\u2019re the pull request author, or reviewer, you\u2019ve got the difficult task of understanding what is going on and trying to verify if the intentions of the PR were realized without screwing up production data. Here\u2019s some common issues we\u2019ve heard about working on large, or business critical, dbt data projects:

  • QA for pull requests takes too long \u2014 stakeholders want to merge new data features faster.
  • dbt build worked, but the data was actually wrong.
  • I\u2019m sick of downtime from silent errors making it into prod.
  • CI takes too long and current data quality tools are costly to run.
  • I just want to see a summary of what changed for modified models.

If any of these pain points ring true, Recce can help with the code review on your data project. Open-source and available now

Recce OSS is available on GitHub now. Follow the instructions in our Getting Started guide to start using Recce to validate your data modeling changes.

  • GitHub: DataRecce/Recce
  • Docs: DataRecce.io/docs
  • Discord: Recce Community
"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#try-recce-online","title":"Try Recce Online","text":"

If you want to try Recce out without having to install, check out the demo instance below.

"},{"location":"blog/data-validaton-toolkit-for-dbt-data-projects/#demo","title":"Demo","text":"

The demo PR makes a simple change to the dbt\u2019s Jaffle Shop project and changes how customer_lifetime_value (CLV) is calculated (fixes it to only calculated completed orders).

Can you validate this code change using Recce?

The expectation from this change is that CLV will be reduced overall, and that this will also impact the customer segments downstream model, With that in mind, see if you can determine if the if the PR has any issues by checking the data in Recce:

  • The PR: https://github.com/DataRecce/jaffle_shop_duckdb/pull/1
  • Recce Demo instance: https://pr1.cloud.datarecce.io/

Hint: Run a Profile Diff, then a Query Diff, on the customers model, then check for downstream impact.

"},{"location":"blog/hands-on-data-impact-analysis-recce/","title":"Hands-On Data Impact Analysis for dbt Data Projects with Recce","text":"

dbt data projects aren\u2019t getting any smaller and, with the increasing complexity of DAGs, properly validating your data modeling changes has become a difficult task. The adoption of best practices such as data project pull request templates, and other \u2018pull request guard rails\u2019 has increased merge times and prolonged the QA process for pull requests.

Validate data modeling changes in dbt projects by comparing two environments with Recce

The difficulty comes from your responsibility to check not only the model SQL code, but also the data, which is a product of your code. Even when code looks right, silent errors and hard to notice bugs can make their way into the data. A proper pull request review is not complete with data validation.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#what-is-data-validation-and-why-you-need-to-do-it","title":"What is data validation and why you need to do it","text":"

Validation is the process of checking that your work is correct and achieves your intention. Common forms of validation are data change impact analysis and regression testing.

With impact analysis checks you want to verify that any impact observed is desirable, such as a change in business logic for which you would expect to see data change. Regression testing, on the other hand, is about confirming that data change did not take place. This is useful for cases in which any change to modeling code should not impact the data.

For both of these cases comparing the resultant data from your dev branch with a known-good baseline is the key - Show me what\u2019s different, or help me validate that nothing changed. This is where Recce can help.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#data-validation-toolkit-recce","title":"Data Validation Toolkit: Recce","text":"

Recce is a toolkit especially designed to validate data modeling changes in dbt projects by comparing two dbt environments, such as your dev branch and production data. Recce can help you validate your changes during development, and then use those validations in your PR comment for better PR review - the ultimate purpose of modeling validation.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#use-case-data-impact-analysis-in-dbts-jaffle-shop","title":"Use Case: Data Impact Analysis in dbt\u2019s Jaffle Shop","text":"

Jaffle Shop is dbt\u2019s standard intro project for dbt, so hopefully everyone is familiar with it. For this use-case demo, I\u2019ll show how to go from a modeling code change in Jaffle Shop and validate that change using the check suite diffing of tools in Recce.

Follow along with the drill down steps detailed below by using the online Recce demo, and the actual PR:

  • Online Recce Demo: https://pr1.cloud.datarecce.io/
  • PR: Fix: customer lifetime value calculation in customers
"},{"location":"blog/hands-on-data-impact-analysis-recce/#the-code-change","title":"The code change","text":"

The code change for this example is simple. It\u2019s a one-liner in the Customers model that modifies the calculation used for customer_lifetime_value to only include completed orders.

Business logic change to the customers model"},{"location":"blog/hands-on-data-impact-analysis-recce/#the-expected-impact","title":"The expected impact","text":"

This is a change to business logic (maybe more a bug fix), so we would expect to see data impact. As the engineer working on the PR, you\u2019ll already have an expectation of the type of impact you should see.

Previously, the calculation for custom_lifetime_value included all orders, regardless of status, meaning returned, and return_pending were erroneously included. After adjusting the calculation we would expect to see:

  • An overall reduction in the customer_lifetime_value figures in this table.
  • Impact to downstream tables that use this figure

We don\u2019t know the scale of the impact, yet. Let\u2019s first validate our expectations and then check the impact.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#data-drill-down-process","title":"Data drill-down process","text":"

For a small change such as this, you might go straight to some smaller data spot-checks, but in larger projects, or with more complex updates, it\u2019s better to follow a process of \u2018drilling down\u2019 into the data from high level checks, and then narrowing the focus to spot-checks. This will give you the full overview of impact to model and then give you the clues about what to check next.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#impact-overview","title":"Impact Overview","text":"

Lineage DAG Diff shows how the DAG has changed based on your branch code and shows added, removed, and modified models.

Lineage Diff in Recce with Row Count and Schema Diff

In the Lineage Diff for this example, you can see that only the customers model has been modified.

Clicking the model pulls up the schema and you can check the row count. If there were schema changes they would be shown here (added, removed, and renamed columns). We don\u2019t see any indication of change to the schema, the row count shows no change, and the only modified model is the customers model, so you have validated that there is no undesired change to the high level structure of the project.

Note: Lineage Diff differs (ha!) from the regular dbt lineage DAG because it shows the state of the DAG compared to another state. Whereas the dbt docs lineage DAG shows only the current state.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#high-level-data-impact-assessment","title":"High-level data impact assessment","text":"

The next stage of validation is high-level assessment. There are a number of checks that you can employ for this, currently Profile Diff, Value-Diff, and Top-K Diff. Each providing a different statistical comparison to your baseline data.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#profile-diff","title":"Profile Diff","text":"

With the Customers model still selected, click the Advanced Diffs button and click Profile Diff.

Data Profile Diff in Recce

The Profile Diff shows lots of stats for each column but, for this modeling change, check the min, max, and avg. You can see that the average value for the customer_lifetime_value column has dropped, which confirms our expectation, because a lot of orders have been excluded from the calculation now.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#value-diff","title":"Value Diff","text":"

A Value Diff is also a good way to get a high level overview of change to a model. With the Customers model selected in the Lineage, click Value Diff from the Advanced Diffs. Select customer_id as the primary key, and check the All Columns checkbox, then click the Execute button.

Choose the primary key and which columns to diff

The Value Diff shows the percentage match for each column:

Value Diff results in Recce

All columns are 100% match, except the customer_lifetime_value column, which now shows only 1.19% match (2 rows) - That\u2019s quite a lot of impact, you better spot-check some of the data! ;-)

"},{"location":"blog/hands-on-data-impact-analysis-recce/#fine-grained-data-spot-checks","title":"Fine-grained data spot-checks","text":"

Query Diff enables you to run a query against both environments, and diff the difference. It\u2019s the best way to spot-check data and see what\u2019s different. You can use any macros that are installed in your dbt project, and in a lot of ways this is like creating an ephemeral model especially for the purpose of data validation. In the simplest form, we can just select a subset of rows and see where the change is.

With the Customers model still selected in the Lineage Diff, click the Query button at the bottom right and then adjust the query to select a subset of data:

select * from {{ ref(\"customers\") }} where customer_id < 100;\n

Note: Avoid using LIMIT in Query Diff because LIMIT does not return the same rows each time, which is required form a useful query diff.

Click Run Diff to query both dbt environments.

When the results are returned click on the key icon in the customer_id column to confirm the primary key. Any columns with a mismatched value will be displayed in red.

Query Diff results in Recce

Spot-checking the customer_lifetime_value column you can see that the current value (the dev branch) are all decreased. This is correct as the calculation we adjusted should only result in a decrease in total order value.

Also, from the Value Diff we ran earlier, we know that only the customer_lifetime_value column should have changed, and that\u2019s confirmed from this Query Diff.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#checking-downstream-impact","title":"Checking downstream impact","text":"

This code change updates a key model, and the customer_lifetime_value column is used downstream in the customer_segments table. With the impact being so great (~99% of customer_lifetime_value has changed) we should check how this affects downstream models.

The customer_segments model uses customer_lifetime_value

Running a Top-K Diff on the value_segment column of customer_segments shows how the distribution has changed with a notable reduction in customers categorized as \u2018High value\u2019.

Top-K Diff on customer_segments.value_segment

Given the change in distribution, if this was a real world situation, you might choose to notify downstream stakeholders.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#the-checklist","title":"The Checklist","text":"

The process above demonstrates how you would validate your work during development but, as mentioned, the ultimate purpose of data validation is to provide proof-of-correctness of your work for PR review.

At each stage in the validation process Recce allows you to add a validation check to the checklist, along with your notes. Just click the Add to Checklist or Add Check button when you see it, and the current validation check will be added to your checklist.

Validation checklist in Recce

In the checklist you can add a note for your validation to explain your findings and provide context. Then, when you\u2019re ready to create a PR, you can copy the checklist items into your PR to help the reviewer understand and sign-off on your work.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#conclusion","title":"Conclusion","text":"

In the above steps, you performed a data impact analysis and successfully validated the code changes by inspecting the data at varying grains of detail.

Check back for more articles in the future that look at other use cases such as validating a refactoring job, and also how to make the most of Recce validations in your PR comment.

"},{"location":"blog/hands-on-data-impact-analysis-recce/#get-started-with-recce","title":"Get started with Recce","text":"

Recce OSS is available on GitHub now. Follow the instructions in our Getting Started guide to start using Recce to validate your data modeling changes in your project.

  • GitHub: DataRecce/Recce
  • Docs: DataRecce.io/docs
  • Discord: Recce Community
"},{"location":"blog/histogram-overlay-top-k-dbt/","title":"Use Histogram Overlay and Top-K Charts to Understand Data Change in dbt","text":"

Data profiling stats are a really efficient way to get an understanding of the distribution of data in a dbt model. You can immediately see skewed data and spot data outliers, something which is difficult to do when checking data at the row level. Here's how Recce can help you make the most of these high-level data stats:

"},{"location":"blog/histogram-overlay-top-k-dbt/#visualize-data-change-with-histogram-and-top-k-charts","title":"Visualize data change with histogram and top-k charts","text":"

Profiling stats become even more useful when applied to data change validation. Let\u2019s say you\u2019ve updated a data model in dbt and changed the calculation logic for a column \u2014 how can you get an overview of how the data was changed or impacted? This is where checking the top-k values, or the histogram, of before-and-after you made the changes, comes in handy \u2014 But there\u2019s one major issue...

The best way to visualize data change in a histogram chart"},{"location":"blog/histogram-overlay-top-k-dbt/#somethings-not-right","title":"Something\u2019s not right","text":"

If you generate a histogram graph from prod data, then do the same for your dev branch, you\u2019ve got two distinct graphs. The axes don\u2019t match, and it\u2019s difficult to compare:

Comparing to distinct histogram charts is impossible

You might be able to spot some differences, such as at the top end of the graph, but the overall impact is mostly hidden.

The same is true for top-k. Cross-referencing categories might be doable when there are only a handful, but it\u2019s still not a meaningful way to visualize the differences:

Cross-referencing Top-K will become difficult with more categories

There\u2019s a real possibility that you\u2019ll miss some edge cases when you can\u2019t compare precisely and accurately. That means silent errors, or even pipeline-breaking errors, will make it into prod. Errors that you won\u2019t find out about until the client or stakeholder calls to asks what\u2019s up with the data.

"},{"location":"blog/histogram-overlay-top-k-dbt/#how-to-meaningfully-compare-histograms-and-top-k","title":"How to meaningfully compare histograms and top-k","text":"

The best way to diff profile stats like histogram and top-k stats, is to plot them on a single chart, overlaid on top of each other using shared axes.

Here\u2019s the same histogram charts as the image above, but with the histograms plotted on a single chart:

An overlaid histogram makes the charts actually useful!

In this overlay histogram chart, you can quickly see the distribution change in a meaningful way. At each value range the scale of the data change is clear. The same is true for top-k. When the values are compared directly next to each other the impact is immediately understandable.

Side-by-side Top-K values for direct comparison

Doing this manually isn\u2019t straight forward. You\u2019d need to code it in a Jupyter notebook with python and would take a lot of configuration, especially given that for dbt data projects you\u2019ll be pulling data from two different schemas if you\u2019re diffing dev and prod. Checking this kind of data profile diff for each PR in your project would require an unreasonable amount of work.

"},{"location":"blog/histogram-overlay-top-k-dbt/#easily-diff-histogram-top-k-and-other-profiling-stats-in-recce","title":"Easily diff histogram, top-k and other profiling stats in Recce","text":"

You can get histogram and top-k diffs, especially designed for dbt data projects, as part of the suite of data modeling validation tools in Recce.

Recce compares your development and production datasets (or any two dbt envs) and enables a visual representation of data change through multiple diffing tools and query comparisons.

Get an overlaid histogram diff for your dbt PR Review with Recce"},{"location":"blog/histogram-overlay-top-k-dbt/#improved-visibility-of-data-change-for-dbt","title":"Improved visibility of data change for dbt","text":"

As a data or analytics engineer, the improved visibility of data change makes validating your work quicker, easier, and more accurate.

As a PR reviewer you can do your job more efficiently and confidently sign off on data changes knowing that an edge case won\u2019t come back to bite you.

Recce is open-source and available now, so you can start properly validating your dev branch right away.

"},{"location":"blog/histogram-overlay-top-k-dbt/#get-recce","title":"Get Recce","text":"
  • GitHub
  • Recce Docs
  • dbt Slack Channel #tools-Recce
"},{"location":"blog/check-critical-models/","title":"Identify and Automate Data Checks on Critical dbt Models","text":"

Do you know which are the critical models in your data project?

I\u2019m sure the answer is yes. Even if you don\u2019t rank models, you can definitely point to which models you should tread carefully around.

Do you check these critical models for data impact with every pull request?

Maybe some, but it\u2019s probably on a more ad-hoc basis. If they really are critical models, you need to be aware of unintended impact. The last thing you want to do is mistakenly change historical metrics, or lose data.

Impacted Lineage DAG from Recce showing modified and impacted models on the California Integrated Travel Project dbt project"},{"location":"blog/check-critical-models/#identifying-critical-models","title":"Identifying critical models","text":"

Knowing the critical models in your project comes from your domain knowledge. You know these models have:

  • a particular significance to business,
  • a ton of downstream models,
  • or, from experience, you\u2019ll get a call about that data if something goes wrong

If you check these models in an ad-hoc way, it might be time to apply a more formal ranking system to models, which will also help the triage process if something does go wrong.

"},{"location":"blog/check-critical-models/#when-to-check-critical-models","title":"When to check critical models","text":"

Checking that these critical models didn\u2019t change is really important, and you need to do this before merging your updated models into prod. You might have a data observability tool in place to monitor prod data but, if you merge a breaking change, it\u2019s already too late. The downstream damage is already done.

You can check the data in two ways before merging:

  • During development: This is when you\u2019re making SQL changes, and editing models and metrics, checking the data and proactively and looking for data impact.
  • Automatically in CI: These checks run automatically in continuous integration (CI). You still need to review the checks, but they are run automatically for each pull request (PR) on your data project.

This mix of manual and automated checks gives you a good chance at comprehensive coverage, but you still need the domain knowledge to identify those critical models.

"},{"location":"blog/check-critical-models/#a-plan-for-checking-critical-models","title":"A plan for checking critical models","text":"

Checking the data in critical models involves comparing the data generated from your development branch with production (or staging data). The process looks like:

  • Identify critical models \u2014 Models which are bottlenecks or are important to business
  • Decide which checks to run on these models \u2014 Data should not change in these models so you should run structural and data profiling checks
  • Automate the checks \u2014 Run the checks in CI with each PR
  • Review check results \u2014 As part of PR review check if there is impact, and if it\u2019s is intended or not
"},{"location":"blog/check-critical-models/#automating-critical-model-checks","title":"Automating Critical Model Checks","text":"

Critical models checks should run with each PR, here\u2019s how you can do that with the preset-checks feature in Recce, by committing a checklist to your data project repo.

Let\u2019s say you want to have the following structural checks run with each PR for your customers model:

  • Row count \u2014 You shouldn\u2019t lose any data.
  • Schema \u2014 The schema should not change unexpectedly.

These are fundamental checks that all critical models should have, regardless of the type of PR.

Schema and row count checks performed in Data Recce

You can run these checks in the Recce UI, and any of these checks can also be automated in CI. Here\u2019s how:

"},{"location":"blog/check-critical-models/#1-generate-your-check-file-recceyml-and-commit-to-your-project","title":"1. Generate your check file (recce.yml) and commit to your project","text":"

To add the row count and schema checks to your dbt CI job you just need to commit a Recce checklist (recce.yml) to your dbt project. From the Recce UI you would:

  1. Add the check to your checklist.
  2. Copy the preset check template to your recce.yml
Recce.yml in the dbt project root

Then commit the recce.yml in the root of your dbt data project. Each branch will get a copy of these checks, and Recce will run the checks each time a PR is opened.

"},{"location":"blog/check-critical-models/#2-run-recce-in-ci","title":"2. Run Recce in CI","text":"

When Recce runs in CI, the checks in recce.yml will be automatically run. Recce also provides a command to generate a PR summary with your results, which can also be automatically added to the PR comments for reviewers to check

Run Recce in CI"},{"location":"blog/check-critical-models/#3-review-the-results","title":"3. Review the results","text":"

The Recce summary, posted to your PR comments, shows you the following things:

  • A diagram of the impacted lineage \u2014 this is just the part of the lineage that has modifications, or is downstream of modified models
  • A list of checks that detected a data mismatch
Review Recce CI Summary

If there was a difference in the row count, or the schema changed, on the customers model, then those checks will be listed here.

The idea behind the Recce Summary is \u2018all signal, no noise\u2019, which means you only want actionable or useful information \u2014 You only want to know when there\u2019s a difference, if everything is the same there\u2019s no point telling you that, it\u2019s just noise that would crowd out the signal.

"},{"location":"blog/check-critical-models/#4-optional-data-impact-exploration","title":"4. (optional) Data impact exploration","text":"

If one of the checks indicated a data-mismatch, or you\u2019ve otherwise seen something that you want to inspect, you can download the Recce state file and run Recce in review mode to see the results of the checks.

Download the Recce state file and execute Recce in Review Mode

To perform live checks on the data, you need only add a dbt_project.yml and profiles.yml, there\u2019s no need to checkout the whole dbt project. Then you can perform live checks to compare dev with prod/staging.

"},{"location":"blog/check-critical-models/#conclusion","title":"Conclusion","text":"

Every data project will have those critical models. It\u2019s fine to check them ad-hoc while you are working on the data project and validating the data generated from your changes. But it\u2019s best practice to identify and automate checks on these critical models.

  • Identify your critical models
  • Curate a checklist for things you know should not change in these models (project specific)
  • Run this checklist in your PRs with Recce
  • Review impact if necessary
"},{"location":"blog/check-critical-models/#get-recce","title":"Get Recce","text":"
  • GitHub
  • Recce Docs
  • dbt Slack Channel #tools-Recce
"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/","title":"From DevOps to DataOps: A Fireside Chat on Practical Strategies for Effective Data Productivity","text":"

Top priorities for data-driven organizations are data productivity, cost reduction, and error prevention. The four strategies to improve DataOps are:

  1. start with small, manageable improvements,
  2. follow a clear blueprint,
  3. conduct regular data reviews, and
  4. gradually introduce best practices across the team.

In a recent fireside chat, CL Kao, founder of Recce, and Noel Gomez, co-founder of Datacoves, shared their combined experience of over two decades in the data and software industry. They discussed practical strategies to tackle these challenges, the evolution from DevOps to DataOps, and the need for companies to focus on data quality to avoid costly mistakes.

Data Productivity - Beyonig DevOps & dbt

Noel Gomez began his journey at Amgen, where he worked extensively on data governance and digital transformation. He learned that data quality is essential to preventing errors. With his software development background, he realized that many software practices weren\u2019t applied to analytics\u2014and saw the opportunity to merge these worlds.

CL Kao worked on version control systems predating Git. He was involved more heavily in data when he helped start Taiwan\u2019s civic tech community, focusing on making public data understandable.

Despite their different paths, both CL and Noel agree that many software practices can be adapted to data management, but there are critical differences. What are the similarities, and what sets DataOps apart?

"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#similarities-between-devops-and-dataops","title":"Similarities Between DevOps and DataOps","text":"

DevOps and DataOps stem from a shared philosophy:

  1. DevOps and DataOps both emphasize agility, adapting quickly to changing requirements while maintaining efficiency, rooted in lean manufacturing and agile software development.
  2. Cross-functional collaboration is essential in both practices. By reducing handoffs between teams, they ensure workflows are smooth. This helps keep pipelines running efficiently and deliver faster insights.
  3. A focus on quality is key in both DevOps and DataOps. DevOps ensures clean, bug-free code and DataOps ensures accurate, reliable data for trustworthy decision-making insights.
  4. Infrastructure as code is a foundational principle in both practices. It enables teams to manage systems through version-controlled code to ensure consistency across environments and reduce risks from manual configuration.
  5. Version control in DevOps and DataOps allows teams to track changes, review updates, and roll back to previous versions to prevent errors in production.
  6. Testing and automation are crucial in both practices. They help identify issues early and ensure reliability, with DevOps focusing on code quality and DataOps on data accuracy throughout the pipeline.

DevOps and DataOps share these principles, but applying them to data introduces unique challenges. While DevOps follows the well-established \"preview, decide, deploy\" cycle, DataOps often lacks this maturity. As CL and Noel pointed out, in many data environments, teams \"deploy first, then decide\"\u2014leading to reactive fixes instead of proactive prevention.

Why is DataOps so different from DevOps, despite having the same underlying principles?

"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#key-differences-between-devops-and-dataops","title":"Key Differences Between DevOps and DataOps","text":"

DevOps focuses on smooth software deployment without downtime, while DataOps ensures the data driving business decisions is accurate and trustworthy, while revisiting how we extract insights.

In DataOps, it\u2019s not just about system uptime; the real challenge is preventing incorrect or incomplete data from leading to flawed decisions or costly errors. This introduces a new layer of complexity, as ensuring data accuracy can be more difficult than ensuring software stability.

A key distinction between DevOps and DataOps is the confidence teams have before releasing changes. In DevOps, you can deploy code changes confidently because everything has been tested in a controlled environment before production. In DataOps, even with the right tools, validating data is trickier because real-world data constantly changes and behaves unpredictably. It\u2019s not just about running tests; it\u2019s about verifying that the insights or automation from the data still make sense after transformations and processes.

"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#1-different-goals","title":"1. Different Goals","text":"
  • DevOps: Focus on Reliable Software Delivery The primary goal of DevOps is to ensure software functions properly and consistently for users. Teams strive to deploy small, frequent changes confidently, without disruptions or compromising user experience. A significant part of this is validating the application performs as expected\u2014whether it's a website or backend system\u2014without crashes or performance degradation. Even if DevOps achieves perfect functionality delivery, the data generated or consumed by the software may not be accurate. Noel pointed out, \u201cTheir website isn\u2019t going down... but it\u2019s still not delivering the data for analytics and decision-making.\u201d This highlights a key limitation of DevOps: ensuring software stability doesn\u2019t guarantee data correctness.

  • DataOps: Focus on Data Accuracy and Validity DataOps focuses on validating data. The goal is to ensure systems are running and to confirm the data being used or processed is accurate and reliable. This means checking if the data makes sense\u2014whether the joins, transformations, and calculations are correct and result in actionable insights. If data is incorrect or missing, even the best-maintained software won\u2019t provide value, and businesses could make poor decisions based on faulty insights.

"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#2-the-validation-process","title":"2. The Validation Process","text":"
  • DevOps: Validating Applications In DevOps, validation focuses on application functionality. Automated tests ensure the application runs correctly, features behave as expected, and the infrastructure remains stable. The primary concern is application stability while introducing changes\u2014ensuring services are consistently available for users without unexpected crashes or interruptions.

  • DataOps: Validating Data DataOps validation focuses on data correctness. Teams must ensure data transformations, aggregations, and insights are accurate before use in decision-making or customer-facing products. This involves automated tests and in-depth data checks to ensure changes in one part of the pipeline don\u2019t affect other areas. For example, an innocent change could lead to missing or duplicated data, affecting downstream dashboards and reports. This makes DataOps more complex and crucial, as faulty data can lead to poor business decisions or financial losses, worsening with more automated systems.

"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#why-dataops-is-more-complex","title":"Why DataOps is More Complex","text":"

Ensuring data correctness introduces more complexity than ensuring software stability due to unique data management challenges, making DataOps a more intricate process.

  1. High Costs of Bad Data Poor data quality can lead to significant financial losses, inefficiencies, and missed opportunities. Minor data errors can propagate through decision-making processes, leading to flawed insights and costly consequences. This makes the stakes in DataOps much higher than traditional DevOps.

  2. Data-Centric Software Stacks As software systems become more data-driven, especially with AI and automated decision-making, managing data quality becomes complex. Without a coherent strategy, errors in data pipelines or transformations can lead to inaccurate decisions and a greater need for robust data management processes.

  3. Disconnected Teams and Legacy Processes A common issue in DataOps is the disconnect between teams responsible for producing data and those using it, which leads to misunderstandings and inconsistencies. Many organizations still use outdated, manual processes with little awareness of modern data management practices. This absence of governance, code review, and version control makes it challenging to implement best practices and increases the risk of errors.

  4. Underrated Data Pipelines Data pipelines are the backbone of decision-making processes, yet they often don't get the respect they deserve. While they may not be as flashy as AI models or analytics tools, everything depends on the quality and reliability of the data flowing through them. Inconsistent or poorly managed pipelines can lead to data issues, undermining the decision-making framework.

As the software stack becomes more data-centric and data plays a larger role in decision-making, especially in AI trends, this complexity increases. Without a structured approach to managing data, companies risk relying on flawed or inaccurate data, leading to costly mistakes.

bad data cost 15%~25% of Revenue"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#practical-dataops-strategies","title":"Practical DataOps Strategies","text":"

As the discussion dove into the root causes of DataOps challenges, CL and Noel shared four practical strategies from their work with various companies. Attendees were eager to ask: How do you encourage teams using legacy systems and entrenched processes to adopt best practices? How do you recommend data professionals secure buy-in for implementing modern solutions?

The four strategies to improve DataOps are: start with small, manageable improvements, follow a clear blueprint, conduct regular data reviews, and gradually introduce best practices across the team. Each step helps build a strong foundation for effective DataOps implementation.

If you're interested in their strategies and answers to these questions, as well as detailed advice on implementing DataOps in your organization,

Sign up using the link below to access the full video recording: https://datarecce.io/firesidechat/

"},{"location":"blog/from-devops-to-dataops-effective-data-productivity/#closing-thoughts-a-brief-look-at-datacoves-and-recce","title":"Closing Thoughts: A Brief Look at Datacoves and Recce","text":"

The fireside chat ended with something that stood out\u2014the passion of these founders. After understanding the challenges teams face with data, CL and Noel focused on different key points, leading them to build two distinct but complementary products.

Datacoves, co-founded by Noel Gomez, focuses on accelerating DataOps workflows by integrating open-source tools like dbt. It's designed to be flexible, scalable, and provides teams a clear path to improve data quality without rigid systems. As Noel put it, \"It's really about explaining to people and teaching them what's possible out there because we're very passionate about this kind of stuff.\"

Recce, founded by CL Kao, ensures data correctness across the pipeline. It simplifies validating data changes by curating proof of correctness, helping teams maintain confidence in their data as things get more complex. CL's vision is clear: \"We are very passionate about the future of data-driven software and its development workflow.\"

What data problems resonate with you? Let's connect and chat!

"},{"location":"blog/self-serve-analytics-pr-review/","title":"The Guide to Supporting Self-Serve Data and Analytics with Comprehensive PR Review","text":"

dbt, the platform that popularized ELT, has revolutionized the way data teams create and maintain data pipelines. The key is in the \u2018T\u2019, of ELT. Rather than transforming data before it hits the data warehouse, as in traditional ETL, dbt flips this and promotes loading raw data into your data warehouse and transforming it there, thus ELT.

This, along with bringing analytics inside the data project, makes dbt an interesting solution for data teams looking to maintain a single-source-of-truth(SSoT) for their data, ensuring data quality and integrity.

Move Fast and DON'T Break Prod

dbt transformations are stored as a series of SQL models that transform the data through raw, staging, intermediate, and then mart stages, in which the data is ready for production use. Keeping all of these transformations in the same place, as the SSoT for how your data is generated, ensures that data validation and data quality checks can be systematically applied at every stage. This also facilitates PR review to maintain data integrity.

  • Version control - the dbt project can be managed in a version control system such as git, essential for dbt CI workflows and PR review.
  • Modular SQL - dbt encourages a modular design to data pipelines. This better facilitates reviewing data models and transformed data, and enabling the re-use and referencing of data models.
  • Reproducible pipelines - the dbt project contains everything required to run the pipeline, which means anyone can checkout the project from version control and build the data. This is a fundamental part of data best practices and dataops.

If you\u2019ve come from a software engineering (SE) background, then these benefits may be familiar to you. Version control, modularity, and reproducibility are the tenets of DevOps, and have benefited software development for years. Through dbt, you\u2019re able to adapt them as part of a new practice of DataOps, and the pull request (PR) is at the center of the process.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#data-incidents-still-happen","title":"Data Incidents still happen","text":"

Given the benefits that dbt brings, you\u2019d be forgiven for thinking that the data problem was solved - That data was always accurate, and bad data was never merged into production. Unfortunately, due to the relative infancy of DataOps, that\u2019s still not the case. Data incidents still happen.

As CL Kao, founder of Data Recce put it in a recent fireside chat with Noel Gomez of Datacoves:

\u201cThe construct that turned devops from a best practice into a viable, productionized, practice, is the pull request\u2026 preview, decide, deploy. (However) In data, we deploy, and then decide if we want to fix it later\u201d- CL Kao, Data Productivity - Beyond DevOps and dbt

The \u201cdeploy and then fix it later\u201d quote probably rings true for a lot of data teams that are working under tight deadlines, with many simultaneous PRs, and find it difficult to perform due diligence on every PR before merging.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#look-ma-no-guardrails","title":"Look ma, no guardrails!","text":"

The version-controlled nature of dbt makes code review straightforward, but data review remains a challenge. It\u2019s vital to build guardrails around the PR review process to ensure that data quality is upheld and the data impact of changes is thoroughly assessed.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#how-much-does-a-data-incident-cost","title":"How much does a data incident cost?","text":"

Merging bad data, whether that be from accidentally changing historic data, to introducing silent errors into production data, can be very costly. Data impact assessment prevents such incidents through rigorous data validation, data quality checks, and structured PR review - Avoiding substantial financial losses caused by bad data.

The Cost of Data Incidents

Mikkel Dengsoe of Sync recently calculated that if the cost of a data incident ranges from $1,000 to $100,000 per-incident, that for a large data team that could result in costs of \u201cbetween $500,000 and $4,000,000 per year\u201d.

As Mikkel puts it, the value of data for business critical applications is high:

\u201cA few hours of incorrect data for an eCommerce ad bidding machine learning model can easily cost $100,000.\u201d Mikkel Dengsoe, The cost of data incidents

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#why-dbt-data-prs-are-hard-to-review","title":"Why dbt data PRs are hard to reviewDownload the Deck","text":"

Firstly, data change is inherently hard to review. The pull request review process from software practices is not suited to reviewing data. PR review is all about reviewing code, not data. When you open a pull request on a data project, you can easily see how the SQL code and transformation logic has changed, but the data is still a black box, and makes data impact assessment and data validation a unique challenge.

New Challenges to Data Integrity

Secondly, dbt has introduced \u2018self-serve data\u2019., where more users within an organization can access and manipulate data. This shift means that dataops practices much evolve to ensure that data quality and data integrity is maintained as the number of data users grows.

* indicates required Enter your email address and we'll send you the presentation deck: *","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#what-is-self-serve-data","title":"What is self-serve data?","text":"

The core principle of self-serve data is to enable wider access to data without the need for specialized knowledge, or the help of data specialists. As atlan says, users should be able to \u201caccess, analyze, and manipulate the data\u201d by themselves:

Self-serve data refers to tools\u2026 that enable \u2026users to access, analyze, and manipulate data without requiring the direct intervention of data specialists \u2014 atlan

dbt takes it a step further, in that users should not require specialized knowledge to create new data products:

A self-serve data platform\u2026 supports creating new data products without the need for custom tooling or specialized knowledge. \u2014 dbt

All you need to know is SQL, and you can start using dbt and access and manipulate the data to your needs.

The actual implementation of self-serve data will differ from organization to organization, but at the very least you would expect that analysts will be shifting some analytics out of BI and directly into dbt. In other cases, non-technical (in engineering terms) teams, such as finance or marketing, may also be directly accessing the data project.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#data-meet-integrity","title":"Data, meet integrity","text":"

An initial worry from data engineering teams is that self-serve data may introduce data integrity issues, and undermine the work of data engineers who are traditionally tasked with maintaining the stability and integrity of data and data pipelines. Indeed, to successfully adopt self-serve analytics, a shift in internal culture is required.

Self-serve data doesn\u2019t have to mean self-serve chaos

Rather than gatekeeping access to data, data engineering teams should actively support the self-serve nature of data. The pull request process is central to maintaining data integrity. It\u2019s the stage in which data quality is upheld, and the intention of the PR author defined and validated.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#how-to-support-a-self-serve-data-culture","title":"How to support a self-serve data culture","text":"

With the PR review being central to ensuring data integrity, the team must validate both the code and the data changes to understand the data impact. Following dbt best practices for data validation and data review is critical to building trust in self-serve data environments.

If the author of the PR records the steps that they took to validate the data generated from the models in the PR, this enables:

  1. The PR author is able to review their own work, ensuring data quality.
  2. The data team is able to review the work of others (peer review), enhancing DataOps processes.
  3. The data validation checks can be shared with others for approval ensuring data integrity by enabling business-context review.

To enable this PR review workflow for data, the PR process for data projects needs to be augmented with suitable tools, enabling 'self-serve review' for self-serve data.

Data impact assessment is more important than ever","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#communication-overhead","title":"Communication overhead","text":"

As more people modify the data project, more PR review is required, bringing with it communication overhead. The team needs to communicate their adjustments and modifications to the data pipeline that require review, and the data team needs to be able to support this review.

To enable better comunication between data teams and stakeholders, we must first understand what the review process for data actually looks like, and understand how to review data.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#how-to-perform-data-impact-assessment-for-pr-review","title":"How to perform data impact assessment for PR review","text":"

Data impact is difficult to assess. You\u2019re faced with the problem of having to interpret if the numbers you\u2019re looking at are correct. How can you know that with confidence, ensuring data quality?

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#1-show-me-the-change","title":"1. Show me the change","text":"

The best way to perform data impact assessment is to see exactly how the data has changed.

Benn Stancil, of Mode Analytics, takes a humorous look at this in a recent blog post. Benn provides two methods. The first, a complicated, convoluted process following the data through contract alerts, and log files. The second, simply:

\u201cCheck if historical values of the metric are the same today as they were yesterday.\u201d - Benn Stancil, All I want to know is what\u2019s different

It\u2019s that simple. You want to be able to easily compare data from before-and-after making data model changes. Doing this will, and confirming that metrics or key figures didn\u2019t change will give you the confidence to merge a PR knowing that unintended impact did not occur, therefore maintaining data integrity.

Just show me what's different","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#2-help-me-validate-change","title":"2. Help me validate change","text":"

Confirming that data didn\u2019t change is good for refactoring work and detecting unintended impact, but there are also instances when you would expect to see data change - You want to confirm that the data is correct, and that the SQL (data model logic change) has the intended data impact.

Comparing data, and the structure of the data pipeline, before-and-after making changes will enable you to see data impact clearly. You want to be able to easily see newly added, removed, and modified data models, and perform data validation checks to ensure accuracy. By doing this, you can assess data quality and look into the models to see how that data has changed, ensuring data integrity throughout the PR review process.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#whats-your-intention-and-expectation","title":"What\u2019s your intention and expectation","text":"

To understand if data impact is correct, you need to already have an expectation of what to look for. This expectation is based on your intention, or, what you are trying to achieve. This is why the pull request template is such a fundamental tool, it helps the PR author clarify these things.

  • Intention - What you trying to do, such as fix a bug, add a new column, refactor a group of models etc.
  • Expectation - Given you intention, what is the expected data impact once the work is completed? For example, no rows should change on models X and Y, or the average value of Z column should remain constant.

Once these are defined, the process of validating data impact becomes a lot easier. The main problem is then the mechanism to perform the data validation checks to prove your expectation, and the groundwork that is needed in your data project to enable that.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#clarity-through-comparison","title":"Clarity through comparison","text":"

Verifying your work before pushing to production is the only way to be absolutely confident that there are no breaking changes. In order to do this, you need to have a duplication of your production environment, or a method to reproduce, or replicate, a subset of production, so that you have a known-correct baseline to check your work against.

Noel Gomez, of Datacoves, describes the importance of having reproducible environments:

\u201cWe want to make sure that we have reproducible environments\u2026 this allows you to experiment. \u2026you\u2019re working in a different environment, you\u2019re not going to break something. (then) there should be a review before (it goes to production). The goal is \u2018Does this data make sense?\u2019\u201d - Noel Gomez, Data Productivity - Beyond DevOps and dbt

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#reproducible-environments-in-dbt","title":"Reproducible environments in dbt","text":"

Following dbt best practices for how to replicate data environments include:

  • Per-developer schemas - this allows the person modifying the pipeline to check their work by building models into their own schema. Enabling the developer (data engineer, analyst etc) to perform data quality checks before opening a PR.
  • Per-PR schemas - Each time a PR is opened, the new project code should build the project into a PR-specific schema. This enables automated, dbt CI-time (continuous integration) checks to run (more on that below), helping with data validation and ensuring the PR review process is efficient.
  • Staging schema (subset of prod) - As part of CD (continuous delivery) build a subset of production data into a separate schema. This acts as the known-good baseline and can be used to check developer and PR schemas against. The benefit is that you don\u2019t need to replicate the whole of production, which can be time consuming and expensive.

As Noel mentions in the quote above, having the ability to replicate environments empowers developers\u2014those modifying the data pipeline and validating changes\u2014to confidently assess data quality and data impact without compromising production data. A strategy that streamlines the development process and also adheres to data integrity principles within a self-serve data culture.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#data-validation-with-recce","title":"Data validation with Recce","text":"

Once you have a suitable system in place to manage your database environments during development and deployment, you\u2019re then able to perform comparisons between these environments. This is where Recce can help enhance your data validation process and improve your dbt PR review.

Recce provides a suite of tools that are specially designed to assess data impact by comparing the data in two dbt environments.

  • You\u2019re clear what your intention is
  • You\u2019re clear what your expectation is
  • Now, prove it

Recce helps you to prove that your data model changes have the intended impact through its data impact check framework. Recce data checks, and enable you to:

  • compare your data
  • record your findings
  • share those findings
","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#recce-checks","title":"Recce Checks","text":"

Recce checks are individual data validations that show the results of a specific type of comparison between your data environments. These checks help to prove that your intention was successful, and your expectation has been realized in the data. You use these check results for:

  • validating your work, as you work
  • sharing as part of discussion between colleagues or stakeholders
  • sharing as proof-of-correctness of your work
  • allowing others to approve the results for data validation

There are various check types that range from high level profiling, right down to row level data inspection. Each type of check provides an insight into how data differs, or not, compared to the previous state. By recording the results of your checks, you can demonstrate that proper impact assessment has been carried out and share the results with others for data QA.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#structural-checks","title":"Structural Checks","text":"

Structural checks provide an overview of structural impact to the data project.

  • Lineage Diff - Shows how the lineage has changed in the PR and the dependencies between models.
  • Row Count Diff - Shows the row count change for models that gives a high level indication that the amount of data is the same.
  • Schema Diff - Shows if any models have added, removed, or renamed columns, providing insight into data structure modifications.
","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#statistical-checks","title":"Statistical Checks","text":"

Statistical checks provide a more detailed look at the data which helps to determine the type of data impact that may have occurred.

  • Profile Diff - Shows the comparison of statistical profiles, such as mean, median, standard deviation, and distinct count, between the current and previous data.
  • Value Diff - Shows the percentage matched rate for each column between the current and previous data.
  • Top-K Diff - Shows the most frequent values in categorical fields between current and previous data.
  • Histogram Diff (distribution) - Shows an overlay comparison of data distribution between current and previous data.
Detect statistical anomalies with Data Profile Diff","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#row-level-checks","title":"Row-Level Checks","text":"

Row-level checks enable you to do data spot-checks on specific rows.

  • Query Diff - Run any query and see how the results of that query differ between current and previous data.

Checks can be added to the Recce Checklist as part of your validation work, or they can be added automatically (see below).

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#recce-checklist","title":"Recce checklist","text":"

Organized into a checklist, Recce checks help to collectively validate a PR, ensuring data quality and data integrity throughout the development process. Each check contains:

  • Check results - The results of a specific check that helps to validate the success of the PR.
  • Author\u2019s annotation - A description that explains what is being shown and why the check results are vital for understanding the data quality and data integrity of the PR.

The checklist helps to guide the reviewer through the process that was used to validate the PR. When accompanied by a detailed PR comment, will enable the reviewer to understand the author's intention and expectation, allowing for informed sign-off on the PR or requests for further data checks.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#automate-checks-for-critical-models-preset-checks","title":"Automate checks for critical models (preset checks)","text":"

In addition to checks that fall within the scope of the PR, there should also be global checks that run regardless of the work being done for the PR. These global checks are called Preset Checks in Recce, are designed to ensure complete data quality coverage of your project, and target models that have:

  • Significance importance to the business,
  • Numerous downstream dependencies,
  • Historical issues that may prompt inquiries about data integrity if problems arise (in other words, you'll get a call about that data if something goes wrong!)

Read more about the importance of identifying and checking critical dbt models as part of data validation strategry for dbt and maintaining data quality in dbt PR reviews.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#share-validation-check-results","title":"Share validation check results","text":"

Once you\u2019ve curated a checklist of data validation checks, it\u2019s time to share those checks in your PR comment. With Recce you can do this in two ways:

  1. Share individual checks for focused data impact discussions
  2. Share the Recce File, which contains the complete checklist and allows other team members to continue review or help validate impact.
  3. Use Recce Cloud for to sync checks across Recce instances and block PR merging until all data impact has been assessed
","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#sharing-individual-checks","title":"Sharing individual checks","text":"

Sharing individual checks can be useful for smaller PRs, or when you want to have discussion around the results. For more complex PRs, with many checks, the Recce File is the more powerful choice for collaborating on data validation.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#sharing-the-recce-file","title":"Sharing the Recce File","text":"

Exporting the Recce File offers two key advantages:

  1. The full checklist is made available for review, enhancing transparency in your data validation processes.
  2. Reviewers can interact with data checks and perform and save additional checks if necessary.

The PR author can export the Recce File, then attach it to the PR, or save it in your organization\u2019s shared storage platform. Another colleague can then run Recce in \u2018review mode\u2019 to start the PR review, facilitating thorough data validation and promoting teamwork in reviewing data PRs.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#recce-cloud","title":"Recce Cloud","text":"

For those using Recce Cloud, the process is a lot smoother, with checks synced between Recce Instances, and the ability to launch a Recce Instance and review a PR within minutes from the Recce Cloud interface.

With Recce Cloud, there\u2019s no need to export and share the Recce File, or deal with manually exporting checks.

Recce Cloud integrates with your GitHub PR and can block merging until all checks have been reviewed.

Download the Deck * indicates required Enter your email address and we'll send you the presentation deck: *","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/self-serve-analytics-pr-review/#conclusion","title":"Conclusion","text":"

dbt brings a lot of benefits to data projects by incorporating essential software engineering practices into the data analytics workflow. By embracing and adopting organizations can support a self-serve data culture that empowers teams to access and leverage data effectively, while also maintaining data integrity.

For discussion on how to properly adopt devops best practices for datatops, check out the Data Productivity - Beyond DevOps and dbt Fireside Chat between the founders of Recce and Datacoves.

","tags":["Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","dbt PR Review","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/","title":"Meet Recce at Coalesce 2024 and The Data Renegade Happy Hour","text":"

The Recce team will be joining Coalesce 2024 in Las Vegas! Meet our founder, CL Kao, and product manager, Karen Hsieh, who has also been hosting the Taipei dbt meetups. As a company focused on helping data teams prevent bad merges and improve data quality, we believe Coalesce is the perfect venue to connect with fellow data professionals, share insights, and gain fresh perspectives.

We are attending Coalesce 2024

At Recce, our mission is to transform the data PR review process, ensuring that data pipelines not only run smoothly but also deliver accurate, validated results. We believe that data should be correct, collaborative, and continuously improved. Coalesce 2024 offers an ideal platform for these crucial conversations, gathering experts across the field to discuss the future of data management. Whether it\u2019s gaining new insights into best practices or forging valuable partnerships, Coalesce is where we aim to make an impact.

"},{"location":"blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/#coalesce-provides-invaluable-insights","title":"Coalesce provides invaluable insights","text":"

This journey didn\u2019t start overnight. The idea for Recce was born from deep conversations with data practitioners at Coalesce 2023 in San Diego. At the time, CL Kao was working on PipeRider, a data impact assessment tool for dbt data projects.

It became clear from the conversations that the need for a more comprehensive solution\u2014something beyond data comparisons and observability\u2014was critical. From those learnings, CL pivoted PipeRider into what is now Recce, focused on improving the entire data PR review process. Coalesce 2023 provided invaluable insights, and this year, we\u2019re returning with an even stronger vision.

The Analytics Development Lifecycle (ADLC) white paper sets the stage for the discussions at Coalesce, and we\u2019re eager to dive in. Trends like DataOps, automation, collaboration and data quality resonates deeply with Recce\u2019s goals. While modern data practices draw significant inspiration from software engineering, data has its own unique challenges. One such challenge is that simply applying software methods directly to data isn\u2019t always effective. Data review, for example, requires more than just SQL code reviews; it demands a thorough examination of the actual data.

"},{"location":"blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/#the-data-renegade-happy-hour-where-data-power-meets-happy-hour","title":"The Data Renegade Happy Hour: Where Data Power Meets Happy Hour","text":"

It\u2019s not just all work! Coalesce also gives us the chance to let loose and connect in a more relaxed setting. That\u2019s why we\u2019re teaming up with some of the brightest minds in data to co-host a special event:

Join the team from Recce and Tobiko, Cube, Paradime.io, Datacoves, and Steep on October 8th at the Data Renegade Happy Hour.

Data Renegade Happy Hour

It\u2019s set to be a fun-filled evening where you can connect with industry pros over witty banter, enjoy data-themed cocktails, and be amazed by world-class magic from international champion John George. Don\u2019t miss the perfect blend of insights and entertainment. RSVP Soon, space is limited!

RSVP Today!

"},{"location":"blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/#chat-with-us","title":"Chat with us","text":"

We are looking forward to engaging with the data community on some of the most pressing challenges facing teams today, including how to prevent bad merges, validate results, and perform robust impact analysis. Every data practitioner who\u2019s ever created or reviewed a PR knows the frustration of spot checks and incomplete data analysis.

We\u2019re also curious to understand what\u2019s holding teams back from addressing these issues. Why do 74% of business stakeholders report revenue loss due to poor data quality, yet many data teams still struggle to prioritize improvements? Everyone talks about the need for high-quality data, but not everyone is taking action. We want to hear how different industries, company sizes, and data teams approach these challenges and discuss how Recce can play a role in solving them\u2014or even understand why Recce might not be the right fit in some cases.

Stay tuned for more details about where to find us during the event! We can\u2019t wait to meet you all, exchange ideas, and explore how we can transform the way data is managed\u2014stickers and limited swags are waiting for you, too.

"},{"location":"blog/dbt-data-pr-comment-template/","title":"The Ultimate PR Comment Template Boilerplate for dbt data projects","text":"

If you\u2019re looking to level-up your dbt game and improve the PR review process for your team, then using a PR comment template is essential.

A PR template is a markdown-formatted boilerplate comment that you copy and paste into the comment box when opening a PR. You then fill out the relevant sections and submit the comment with your PR.

Example of a PR comment with comprehensive data validation checks","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#the-benefits-of-using-a-pr-comment-template","title":"The benefits of using a PR comment template","text":"

The sections in the template help by providing a systematic approach to defining and checking your work. Having a template also helps to avoid ambiguous or superficial comments which make PRs difficult to review. The benefits of using a PR comment template for modern dbt dataops are:

  1. As the PR author, the structure of the template helps you to define and describe your work related to this PR, which helps to you to perform data validation and understand data impact.
  2. As the PR reviewer, the sections help you to review the PR by following the logical steps that the author took by seeing exactly what work as done, and how it was checked. Making data quality and data integrity checks integral to the PR review process.
  3. For future you, it provides a historical record of what was done and why, which is invaluable as part of data best practices.

Check the end of the article for some examples from the data projects of Prefeitura do Rio de Janeiro and the California Integrated Travel Project (Cal-ITP), who both make deft use or PR comment templates to improve their pull request process.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#where-can-you-get-a-dbt-pr-comment-template-from","title":"Where can you get a dbt PR comment template from?","text":"

dbt provides an official pull request template that you can paste into your comment when you open a PR. It\u2019s a great template, and an essential foundation to dbt best practices but, due to the complicated nature of reviewing dbt data project pull requests, there's still room for improvement.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#the-official-dbt-pr-comment-template","title":"The official dbt PR comment template","text":"

The official dbt template includes the following sections:

  • Description & motivation
  • To-do before merge
  • Screenshots
  • Validation of models
  • Changes to existing models
  • Checklist (of small things that are sometimes overlooked)

At Recce, we\u2019re all about validating your work - providing the proof-of-correctness that shows why your work is complete and the PR can be merged. So, we feel this PR comment template can be improved to make it even better suited to this job.

Let\u2019s add a few new sections, and update/re-define some of the existing sections to make them clearer.

What sections should the perfect PR comment contain?","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#making-a-better-pr-comment-template-for-dbt-data-projects","title":"Making a better PR comment template for dbt data projects","text":"

To improve the dbt PR comment template and make your data quality check processes even better, we can start with adding some new sections that will clarify the purpose of the PR and make it easier to review.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#type-of-change","title":"Type of change","text":"

Classifying the type of change helps to frame the work. It guides the PR author toward the type of validations they should run based on the category of work, and the reviewer will have an initial idea how to properly review the work

A sensible set of change types to start with could be as follows:

  • [ ] New model
  • [ ] Bugfix
  • [ ] Refactoring
  • [ ] Breaking change
  • [ ] Documentation
  • [ ] (Other project-specific item)
","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#related-issues","title":"Related Issues","text":"

Discussion surrounding a piece of work can happen anywhere, and likely starts with a ticket, chat, or Github issue etc. Provide links to any related discussion that can help clarify your work. This is essential to validating data quality with context. (dbt covers this in the \u2018description\u2019 section, but I prefer to have this in its own section.)

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#impact-considerations","title":"Impact considerations","text":"

In this section, include validation on how downstream models have/have not been impacted and what considerations are required. This is crucial for understanding data impact and helps ensure that critical models or exposures remain unaffected. As with validation of models, use screenshots and queries to illustrate the impact.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#updated-sections","title":"Updated Sections","text":"

Some of the sections could also be better defined, and potential use clarified:

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#screenshots-lineage-dagdiff","title":"Screenshots \u2192 Lineage DAG/Diff","text":"

You can use screenshots anywhere in the PR comment, so let\u2019s change this section to be \u2018Lineage DAG\u2019 and use it for specifically showing the parts of the DAG that have been impacted by this PR. This is where you\u2019d share the Lineage Diff screenshot from Recce that shows only the modified+ part of the DAG after comparing it to before you made any changes.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#validation-of-models","title":"Validation of models","text":"

This is how you prove that your work is correct and complete. Arguably the most important section of the PR comment as it\u2019s how the reviewer will know that you\u2019ve conducted a thorough data validation.

The dbt template mentions sharing dbt test results in this section, but depending on the size of the your project, the test results might crowd out important, PR-specific, data validations. So we\u2019ve moved dbt test results to their own section below.

This section should consist of custom queries, profiling stats, data quality checks, data comparisons, and diffs, examples of data impact, anything that helps to validate your work and serve as proof-of-correctness. Use the Recce data validation toolkit to check your work, and include any of the relevant check results, such as

  • Ad-hoc queries \u2014 Spot-check queries to confirm the results are as expected
  • Profiling stats \u2014 Do you see the expected impact in the overall profile of the data
  • Value diff stats \u2014 Compare the percentage of matched rows between dev and prod
  • Profile diff \u2014 A statistical comparison of dev and prod
  • Schema diff \u2014 If the schema has changed and if it\u2019s intended

Take screenshots of the checks that you ran and post them as validation of models.

Pro Tip

Export the Recce State File and attach it to PR comment. The Recce State File contains your checks and the PR reviewer will be able to load the checks in Review Mode to view the results.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#dbt-test-results","title":"dbt test results","text":"

As mentioned above, depending on the size of your project, you may consider moving dbt test results to their own section. This enables your validation of models section to focus on the checks that help to validate your PR-specific work.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#the-updated-pr-comment-template-for-dbt-data-projects","title":"The updated PR comment template for dbt data projects","text":"

Here\u2019s the updated PR comment template that you can use to adapt for your needs. As mentioned, this is adapted directly from the official dbt example, with the changes that were detailed above applied. By aligning with dbt best practices, and augmenting with Recce data checks, this template enhances data integrity and supports a more streamlined PR review process.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#what-a-pr-template-looks-like-in-action","title":"What a PR Template looks like in-action","text":"

To see what this PR comment looks like in action check out this demo PR comment that shows how using this template together with Recce data validation checks, can create the ultimate PR comment. The data quality check results listed help ensure data impact is accurately assessed.

If every PR comment had the data validation check results listed, you\u2019d be able to sign-off a PR and merge in no time.

A demo showing a PR comment template in use with Recce data validations","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#whos-using-pr-comment-templates-like-this","title":"Who\u2019s using PR comment templates like this?","text":"

For teams who collaborate on data changes and hold data integrity in high regard, the use of a PR comment templates can streamline the PR review process. As is demonstrated in these real-world examples.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#municipal-government-of-rio-de-janeiro","title":"Municipal government of Rio de Janeiro","text":"

A great example of a PR template in action is by data team for the municipal government of Rio de Janeiro (Prefeitura do Rio de Janeiro). The team there uses a PR template and Recce checks for their validation of models, ensuring data quality and data integrity throughout the data review process:

Prefeitura do Rio de Janeiro uses Recce in their pr comment","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#the-california-integrated-travel-project-cal-itp","title":"The California Integrated Travel Project (Cal-ITP)","text":"

Cal-ITP is a textbook example of dbt best practices with their extensively detailed PR comments, and supplemental data impact assessments.

  • Previous and resolved issues are linked.
  • The work is clearly described with reasoning.
  • The reviewer is tagged with questions about changes.
  • Post-merge actions are defined, adhering to dataops best practices.
Cal-ITP is a textbook example of data due diligence","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/dbt-data-pr-comment-template/#conclusion","title":"Conclusion","text":"

Using a PR comment template for dbt projects is an excellent way to streamline the pull request process, ensuring that both authors and reviewers are aligned on what changes have been made and how those changes have been validated.

The updated PR comment template not only helps to define the work contained in the PR, but also helps the PR author to consider and anticipates the types of data impact that may occur. Using Recce\u2019s data validation checks, such as spot-check queries, profile diffs, and schema comparisons, the validation process is standardized, which helps speed up the PR review and time-to-merge.

Teams like Cal-ITP and the municipal government of Rio de Janeiro have adopted PR comment templates and use them as part of their PR process to improve reviewability and ensure data integrity.

For your dbt projects, use this structured approach to PR comments to achieve faster, more thorough reviews, and ensure that any changes you introduce follow dbt best practices and are well-documented and correctly validated before merging.

","tags":["dbt PR Review","Data Validaton","Data Integrity","Self-Serve Data","Self-Serve Analytics","dbt CI","Data Review","Best Practices","Data Quality","DataOps"]},{"location":"blog/explore-data-impact-with-focus/","title":"Explore data impact and focus on tracking data validations with Recce's new interface","text":"

There\u2019s nothing worse than being distracted while you\u2019re in the middle of working on a complex task or debugging an issue. All the things that you were juggling in your mind come crashing down, and you have to pick up the pieces and start again.

Recce's updated interface lets you stay on track while assessing and exploring data impact in your dbt project when making dbt data model changes, and performing dbt PR review.

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#track-and-record-data-impact-assessment-results","title":"Track and record data impact assessment results","text":"

Staying in context is especially important when you\u2019re running data validations to verify your dbt data models, exploring data change and impact, or finding the root cause of a data incident. You need to:

  • Keep track of the data models that you are checking.
  • Keep track of the type of data validations that you are running.
  • Record the results of your data validations and impact checks.
  • Record the meaning of the results.

It\u2019s important to record and track these things as you validate your work during data modeling, and then also after opening a PR on your dbt data project.

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#how-to-explore-data-impact-without-being-distracted","title":"How to explore data impact without being distracted","text":"

This is why, in the latest version of Recce, you can stay in context while adding data validation checks and continue from where you left off after adding a check.

  1. Explore changes in your dbt data project by running data validations.
  2. Add a data validation check to your checklist.
  3. Continue where you left off.
Explore data impact and focus on tracking data validations with Recce's updated interface","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#data-comparison-results-show-potential-impact","title":"Data comparison results show potential impact","text":"

When exploring data impact with Recce, the Lineage Diff is the main interface. The default view is modified+ and so represents the potential impact radius of your changes.

Lineage Diff is where you\u2019ll focus your data impact assessment efforts, so it\u2019s essential that it stays visible during your exploration. This means you can stay focused on your job of validating the correctness of your work, without having to leave your train-of-thought to view results on another page.

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#perform-multiple-checks-without-losing-context","title":"Perform multiple checks without losing context","text":"

Now, when you run a diff, the results are shown in the the panel below. There\u2019s no need to leave the lineage diff interface; and you can see the lineage, schema, and diff results, all displayed together.

If you do add the results of a diff to your checklist, when you return to the Lineage view, everything is exactly as you left it, so you can continue your work.

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#model-and-row-level-diffs","title":"Model and row-level diffs","text":"

You can also run model-level diffs such as data profiling, or column-level diffs like histogram and top-k, right from the main interface. The results for each diff will be shown right below the lineage.

The new interface means you can find and explore data impact without losing your flow state. Then, when you\u2019ve got the check results you need, add them to your checklist.

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#save-and-share-data-checks-with-your-team-for-pr-review","title":"Save and share data checks with your team for PR review","text":"

To save a diff to your checklist, click the \u2018Add to Checklist\u2019 button and you\u2019ll be taken to the checklist to add your check annotation. Then go back to the Lineage tab to continue where you left off.

When you\u2019ve finished your data validation work, you can export the checklist as a Recce File and share with your colleagues, or add checks to your PR comment. If you\u2019re a Recce Cloud user, your checks and approval status will be automatically synced.

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"blog/explore-data-impact-with-focus/#join-us-to-make-data-productive","title":"Join us to make data productive","text":"

For early access to Recce Cloud, and a hand in making Recce better meet your needs, book a meeting for a demo and chat. We're looking forward to hearing from you!

","tags":["data validaton","dbt data model validation","root cause analysis","data exploration","DataOps","dbt models","data impact","impact assessment"]},{"location":"docs/","title":"What is Recce?","text":"

Recce is a data validation toolkit designed to enhance the pull request (PR) review process for dbt projects. Recce provides enhanced visibility into the data impact from dbt modeling changes by comparing the data in dev and prod environments. Using Recce for data impact assessment before merging a PR ensures that production data remains stable and accurate.

"},{"location":"docs/#key-features","title":"Key Features","text":""},{"location":"docs/#manual-and-automated-data-checks","title":"Manual and Automated Data Checks","text":"

Recce checks help you to assess data impact and explore data change both manually and automatically.

  • Manual checks - Create a Recce Checklist of data checks that help to validate your data modeling work during development, including data profile comparisons, structural comparisons, and row-level data checks.
  • Automated checks - Integrate Recce Checks into your CI process and post a data impact summary automatically to your PR thread when opening a PR.
"},{"location":"docs/#collaboration-and-replication","title":"Collaboration and Replication","text":"

Share Recce checks with your team for stakeholder and PR review. Checks results can be either shared individually, or your full Recce environment can be exported and replicated with one command.

"},{"location":"docs/#why-recce","title":"Why Recce","text":"

dbt has brought software engineering best practices to data projects, but \u201cbad merges\u201d still happen, allowing erroneous data and silent errors to make their way into prod data.

"},{"location":"docs/#understand-data-impact","title":"Understand data impact","text":"

Recce provides data and analytics engineers with a toolkit to explore data impact caused by dbt data modeling changes. The varying levels of Recce checks enable holistic or fine grained impact assessment so you can drill down to find the root cause of data change.

"},{"location":"docs/#improved-confidence-merging","title":"Improved confidence merging","text":"

The improved visibility into data impact gives PR reviewers the confidence to sign-off PRs knowing that prod data will not change unexpectedly.

"},{"location":"docs/#how-recce-works","title":"How Recce Works","text":"

Recce compares dbt environments using the dbt artifacts from both dev and prod environments.

  1. Generate artifacts for the prod environment:

    # Build prod and generate dbt docs into ./target-base\ndbt seed --target prod\ndbt run --target prod\ndbt docs generate --target prod --target-path ./target-base\n
  2. Switch to your dev branch and generate dev artifacts:

    # Switch to your dev branch\ngit switch my-awesome-branch\n\n# build your dev environment\ndbt seed\ndbt run\ndbt docs generate\n
  3. Start your Recce Instance:

    recce server\n

Open your the Recce web UI to start exploring and understanding data impact, and validating your work.

"},{"location":"docs/#what-you-get","title":"What you get","text":""},{"location":"docs/#interactive-impact-assessment-environment","title":"Interactive impact assessment environment","text":"

recce server launches a web UI with an interactive impact assessment environment. Use the tools in Recce to explore the impact to your data models from your branch changes.

"},{"location":"docs/#focused-data-impact-exploration","title":"Focused data impact exploration","text":"

The main interface to Recce is the lineage DAG, which shows modified nodes and potentially impacted downstream nodes. You can quickly see if critical nodes are within the impact radius and focus your data validation efforts.

"},{"location":"docs/#getting-started","title":"Getting Started","text":"

Try the 5-minute tutorial that uses dbt\u2019s Jaffle Shop project, or take the online demo for a test run, which includes an actual PR and related Recce Instance.

"},{"location":"docs/#what-does-recce-mean","title":"What does Recce mean?","text":"

Recce (/\u02c8r\u025bki/), pronounced 'reh-kee', is short for 'reconnaissance'. We chose this name as it's the perfect fit for a tool you'll use to perform a 'data reconnaissance' to discover and assess the impact of data modeling changes. Add a Data Recce to your pull request workflow and stop pushing breaking changes to production!

"},{"location":"docs/demo/","title":"Recce Online Demo","text":"

Try Recce without installing using this online demo.

Recce Online Demo

The demo showcases a pull request that fixes the customer_lifetime_value calculation in dbt's Jaffle Shop project to only included completed orders.

Jaffle Shop customers.sql"},{"location":"docs/demo/#examples","title":"Examples","text":"

Some example validation checks you might create include:

"},{"location":"docs/demo/#value-diff","title":"Value Diff","text":"

Run a Value Diff to check the percentage match of the customer_lifetime_value column between production and the development branch.

Value Diff - Customers Model"},{"location":"docs/demo/#profile-diff","title":"Profile Diff","text":"

Check the Profile Diff of the customers table to see how the customer_lifetime_value has been impacted.

Profile Diff - Customers Model"},{"location":"docs/demo/#query-diff","title":"Query Diff","text":"

Run a Query Diff to compare the the actual values in prod and dev.

select customer_id, customer_lifetime_value from {{ ref(\"customers\") }} where customer_id < 50;\n

Query Diff - Customers Model"},{"location":"docs/get-started-jaffle-shop/","title":"5 Minute Tutorial","text":"

Jaffle Shop is an example project officially provided by dbt-labs. This document uses jaffle_shop_duckdb to enable you to start using recce locally from scratch within five minutes.

  1. Clone the \u201cJaffle Shop\u201d dbt data project
    git clone git@github.com:dbt-labs/jaffle_shop_duckdb.git\ncd jaffle_shop_duckdb\n
  2. Prepare virtual env
    python -m venv venv\nsource venv/bin/activate\n
  3. Installation
    pip install -r requirements.txt\npip install recce\n
  4. Provide additional environment to compare. Edit ./profiles.yml to add one more target.
    jaffle_shop:\n  target: dev\n  outputs:\n  dev:\n    type: duckdb\n    path: 'jaffle_shop.duckdb'\n    threads: 24\n+ prod:\n+   type: duckdb\n+   path: 'jaffle_shop.duckdb'\n+   schema: prod\n+   threads: 24\n
  5. Prepare production environment
    dbt seed --target prod\ndbt run --target prod\ndbt docs generate --target prod --target-path ./target-base\n
  6. Prepare development environment. First, edit an existing model ./models/staging/stg_payments.sql.
    ...\n\nrenamed as (\n         payment_method,\n\n-        -- `amount` is currently stored in cents, so we convert it to dollars\n-        amount / 100 as amount\n+        amount\n\n         from source\n)\n
    run on development environment.
    dbt seed\ndbt run\ndbt docs generate\n
  7. Run the recce server
    recce server\n
    Open the link http://0.0.0.0:8000, you can see the lineage diff
  8. Switch to the Query tab, run this query
    select * from {{ ref(\"orders\") }} order by 1\n
    Click the Run Diff or press Cmd + Shift + Enter Click on the \ud83d\udd11 icon next to the order_id column to compare records that are uniquely identified by their order_id.
  9. Click the + to add the query result to checklist
"},{"location":"docs/get-started/","title":"Getting Started","text":""},{"location":"docs/get-started/#prerequisites","title":"Prerequisites","text":"

Recce requires that your dbt project has two environments to compare. For example, one for production and another for development.

Prepare two targets with separate schemas in your dbt profile. Your profiles.yml might look something like this:

jaffle_shop:\n  target: dev\n  outputs:\n    dev:\n      type: duckdb\n      path: jaffle_shop.duckdb\n      schema: dev\n    prod:\n      type: duckdb\n      path: jaffle_shop.duckdb\n      schema: main\n
"},{"location":"docs/get-started/#install-recce","title":"Install Recce","text":"

Install Recce using pip:

pip install -U recce\n

"},{"location":"docs/get-started/#use-recce-in-your-dbt-project","title":"Use Recce in your dbt project","text":"

The following instructions give an overview of the process of using Recce in your dbt project. For a hands-on tutorial, please check the Jaffle Shop Tutorial.

Navigate to your dbt project.

cd your-dbt-project/\n
"},{"location":"docs/get-started/#prepare-dbt-artifacts","title":"Prepare dbt artifacts","text":"

Recce expects two sets of dbt artifacts to be present:

  • target-base/ - dbt artifacts for to be used as the base for the comparison e.g. production
  • target/ - dbt artifacts for your development branch
"},{"location":"docs/get-started/#generate-artifacts-for-the-base-environment","title":"Generate artifacts for the base environment","text":"

Checkout the main branch of your project and generate the required artifacts into target-base. You can skip dbt build if this environment already exists.

git checkout main\n\ndbt run --target prod\ndbt docs generate --target prod --target-path target-base/\n
"},{"location":"docs/get-started/#generate-artifacts-for-the-target-environment","title":"Generate artifacts for the target environment","text":"
git checkout feature/my-awesome-feature\n\ndbt run\ndbt docs generate\n
"},{"location":"docs/get-started/#start-the-recce-server","title":"Start the Recce server","text":"

Start the Recce server with the follow command:

recce server\n

Recce use dbt artifacts, which is generated when every invocation. You can find these files in the target/ folder.

artifacts dbt command manifest.json dbt docs generate, dbt run, .. catalog.json dbt docs generate

Tip

The regeneration of the catalog.json file is not required after every dbt run. it is only required to regenerate this file when models or columns are added or updated.

"},{"location":"docs/get-started/#first-time-guide-for-recce-instance","title":"First Time Guide for Recce instance","text":"

After you start the Recce server, you can see the Recce instance, the Web UI of the active Recce server.

Here are the 3 steps to use Recce: (see the image below)

  1. Click the model you want to check
  2. Click \u201cExplore Change\u201d
  3. Click \u201cAdd to Checklist\u201d
"},{"location":"docs/installation/","title":"Installation","text":"

Install Recce in your dbt project with pip:

pip install recce\n

To take full advantage of all the features of Recce, ensure that dbt_profiler and audit-helper are installed via the packages.yml file in your dbt project .

packages:\n  - package: dbt-labs/audit_helper\n    version: <version>\n  - package: data-mie/dbt_profiler\n    version: <version>\n

For full instructions on using Recce, check the Getting Started guide.

"},{"location":"docs/start-with-dbt-cloud/","title":"Start with dbt Cloud","text":"

dbt Cloud is a hosted service that provides a managed environment for running dbt projects by dbt Labs. This document provides a step-by-step guide to get started recce with dbt Cloud.

"},{"location":"docs/start-with-dbt-cloud/#prerequisites","title":"Prerequisites","text":"

Recce will compare the data models between two environments. That means you need to have two environments in your dbt Cloud project. For example, one for production and another for development. Also, you need to provide the credentials profile for both environments in your profiles.yml file to let Recce access your data warehouse.

"},{"location":"docs/start-with-dbt-cloud/#suggestions-for-setting-up-dbt-cloud","title":"Suggestions for setting up dbt Cloud","text":"

To integrate the dbt Cloud with Recce, we suggest to set up two run jobs in your dbt Cloud project.

"},{"location":"docs/start-with-dbt-cloud/#production-run-job","title":"Production Run Job","text":"

The production run should be the main branch of your dbt project. You can trigger the dbt Cloud job on every merge to the main branch or schedule it to run at a daily specific time.

"},{"location":"docs/start-with-dbt-cloud/#development-run-job","title":"Development Run Job","text":"

The development run should be a separate branch of your dbt project. You can trigger the dbt Cloud job on every merge to the pull-request branch.

"},{"location":"docs/start-with-dbt-cloud/#set-up-dbt-profiles-with-credentials","title":"Set up dbt profiles with credentials","text":"

You need to provide the credentials profile for both environments in your profiles.yml file. Here is an example of how your profiles.yml file might look like:

dbt-example-project:\n  target: dev\n  outputs:\n    dev:\n      type: snowflake\n      account: \"{{ env_var('SNOWFLAKE_ACCOUNT') }}\"\n\n      # User/password auth\n      user: \"{{ env_var('SNOWFLAKE_USER') | as_text }}\"\n      password: \"{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}\"\n\n      role: DEVELOPER\n      database: cloud_database\n      warehouse: LOAD_WH\n      schema: \"{{ env_var('SNOWFLAKE_SCHEMA') | as_text }}\"\n      threads: 4\n    prod:\n      type: snowflake\n      account: \"{{ env_var('SNOWFLAKE_ACCOUNT') }}\"\n\n      # User/password auth\n      user: \"{{ env_var('SNOWFLAKE_USER') | as_text }}\"\n      password: \"{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}\"\n\n      role: DEVELOPER\n      database: cloud_database\n      warehouse: LOAD_WH\n      schema: PUBLIC\n      threads: 4\n
"},{"location":"docs/start-with-dbt-cloud/#install-recce","title":"Install Recce","text":"

Install Recce using pip:

pip install -U recce\n
"},{"location":"docs/start-with-dbt-cloud/#execute-recce-with-dbt-cloud","title":"Execute Recce with dbt Cloud","text":"

To compare the data models between two environments, you need to download the dbt Cloud artifacts for both environments. The artifacts include the manifest.json file and the catalog.json file. You can download the artifacts from the dbt Cloud UI.

"},{"location":"docs/start-with-dbt-cloud/#login-to-your-dbt-cloud-account","title":"Login to your dbt Cloud account","text":""},{"location":"docs/start-with-dbt-cloud/#go-to-the-project-you-want-to-compare","title":"Go to the project you want to compare","text":""},{"location":"docs/start-with-dbt-cloud/#download-the-dbt-artifacts","title":"Download the dbt artifacts","text":"

Download the artifacts from the latest run of both run jobs. You can download the artifacts from the Artifacts tab.

"},{"location":"docs/start-with-dbt-cloud/#setup-the-dbt-artifacts-folders","title":"Setup the dbt artifacts folders","text":"

Extract the downloaded artifacts and keep them in a separate folder. The production artifacts should be in the target-base folder and the development artifacts should be in the target folder.

$ tree target target-base\ntarget\n\u251c\u2500\u2500 catalog.json\n\u2514\u2500\u2500 manifest.json\ntarget-base/\n\u251c\u2500\u2500 catalog.json\n\u2514\u2500\u2500 manifest.json\n
"},{"location":"docs/start-with-dbt-cloud/#setup-dbt-project","title":"Setup dbt project","text":"

Move the target and target-base folders to the root of your dbt project. You should also have the profiles.yml file in the root of your dbt project with the credentials profile for both environments.

"},{"location":"docs/start-with-dbt-cloud/#start-the-recce-server","title":"Start the Recce server","text":"

Run the recce command to compare the data models between the two environments.

recce server\n
"},{"location":"docs/start-with-sqlmesh/","title":"Start with SQLMesh (Experimental)","text":"

Note

The integration of SQLMesh is still in the experimental stage, and some features are not yet fully supported.

SQLMesh is an alternative to dbt. Unlike dbt, it records the state of each environment within the data warehouse. This feature simplifies the use of Recce.

"},{"location":"docs/start-with-sqlmesh/#usage","title":"Usage","text":"
pip install -U recce\n

Start the Recce server with the follow command:

recce server --sqlmesh --sqlmesh-envs prod:dev\n

Start with specific config name

recce server --sqlmesh --sqlmesh-envs prod:dev --sqlmesh-config local_config\n

"},{"location":"docs/start-with-sqlmesh/#tutorial-the-sushi-example-project","title":"Tutorial: The Sushi Example Project","text":"

Here, the official example project from SQLMesh was used to demonstrate how to use recce.

  1. Clone the SQLMesh repo

    git clone git@github.com:TobikoData/sqlmesh.git\ncd sqlmesh/examples/sushi   \n

  2. Prepare the python venv and install the SQLMesh.

    python -m venv venv\nsource ./venv/bin/activate\npip install sqlmesh\n
  3. Plan the prod environment

    sqlmesh --config local_config plan\n
  4. Modify the model models/customers.sql model

      ...\n  SELECT DISTINCT\n    o.customer_id::INT AS customer_id, -- customer_id uniquely identifies customers\n    m.status,\n    d.zip\n  FROM sushi.orders AS o\n  LEFT JOIN current_marketing AS m\n    ON o.customer_id = m.customer_id\n  LEFT JOIN raw.demographics AS d\n    ON o.customer_id = d.customer_id\n+ WHERE status is not NULL\n
    and apply this change to the dev environment
    sqlmesh --config local_config plan dev    \n
    output
    New environment `dev` will be created from `prod`\nSummary of differences against `dev`:\nModels:\n\u251c\u2500\u2500 Directly Modified:\n\u2502   \u2514\u2500\u2500 sushi__dev.customers\n\u2514\u2500\u2500 Indirectly Modified:\n    \u2514\u2500\u2500 sushi__dev.waiter_as_customer_by_day\n---\n\n+++\n\n@@ -31,3 +31,5 @@\n\nON o.customer_id = m.customer_id\nLEFT JOIN raw.demographics AS d\nON o.customer_id = d.customer_id\n+WHERE\n+  NOT status IS NULL\nDirectly Modified: sushi__dev.customers (Breaking)\n\u2514\u2500\u2500 Indirectly Modified Children:\n    \u2514\u2500\u2500 sushi__dev.waiter_as_customer_by_day (Indirect Breaking)\nApply - Virtual Update [y/n]:\n

  5. Use sqlmesh table-diff to check the change.

    sqlmesh --config local_config table_diff prod:dev sushi.customers    \n
    output
    Schema Diff Between 'PROD' and 'DEV' environments for model 'sushi.customers':\n\u2514\u2500\u2500 Schemas match\n\n\nRow Counts:\n\u251c\u2500\u2500  COMMON: 55 rows\n\u251c\u2500\u2500  PROD ONLY: 18 rows\n\u2514\u2500\u2500  DEV ONLY: 0 rows\n\nCOMMON ROWS column comparison stats:\n        pct_match\nstatus      100.0\nzip         100.0\n

  6. Install Recce

    pip install recce    \n

  7. Launch the recce server

    recce server --sqlmesh --sqlmesh-envs prod:dev --sqlmesh-config local_config   \n

    You can see the lineage DAG diff

  8. In the Query page, you can diff with ad-hoc query. Enter the following SQL script and click the Run Diff button.

    select \n  status,\n  count(*) as c\nfrom sushi.customers\ngroup by status\norder by status\n

    Set the primary key as status and click the Run Diff button

    You will see that there is only one record where status=NULL that differs. In the original version, there were two records with status=NULL, but in the new version, there is no record with status=NULL.

"},{"location":"docs/start-with-sqlmesh/#supported-recce-features","title":"Supported Recce Features","text":"
  • Web UI: For SQLMesh integration, Recce currently supports lineage diff, schema diff, row count diff, and query diff

  • Command line interface: Not supported yet

"},{"location":"docs/architecture/overview/","title":"Overview","text":"

The core concepts in Recce are Tasks, Runs, and Checks.

"},{"location":"docs/architecture/overview/#tasks","title":"Tasks","text":"

A task represents the implementation of specific functionality. For example, tasks such as QueryDiffTask and ValueDiffTask demonstrate different types of operational processes.

def submit_run(task_type, params) -> Run:\n    task = find_task(task_type)\n    result, error = task(params)\n\n    return Run(\n        type=task_type,\n        params=params,\n        result=result,\n        error=error,\n    )\n

A task is responsible for:

  1. Implementing the logic of the task, primarily involving accepting parameters and ultimately generating a result.
  2. Implementing cancellation.
  3. Implementing multi-threading.
  4. Reporting progress, including progress percentage and messages.
"},{"location":"docs/architecture/overview/#runs","title":"Runs","text":"

A run documents a single execution of a Task, capturing details such as the type of execution, parameters used, and the final outcomes. There are two methods to execute a run:

  1. Directly specify the type along with the corresponding parameters.
    curl 'http://localhost:8000/api/runs' \\\n-H \"Content-Type: application/json\" \\\n-d @- <<EOF\n{\n    \"type\": \"query_diff\",\n    \"params\": {\n        \"sql_template\": \"select * from {{ ref('customers') }}\"\n    }\n}\nEOF\n
  2. Execute via a specific check.
    curl 'http://localhost:8000/api/checks/e2a44206-8568-44e9-8d36-da8856df477d/run' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{}'\n

In the Recce instance, each run is recorded, so in theory, it is possible to access the run history. However, the current UI does not offer this feature. You can see the complete run history from the exported recce state.

"},{"location":"docs/architecture/overview/#checks","title":"Checks","text":"

A Check is an item recorded in the user\u2019s Checklist. A Check can be generated through a run or automatically created from preset checks when the Recce is initiated.

A single Check can execute multiple runs; therefore, in theory, it is possible to obtain the run history of a Check, although the current UI does not yet implement this feature.

Additionally, a Check records information in view_options, which is used to determine how the run results are displayed.

"},{"location":"docs/features/checklist/","title":"Checklist","text":"

Save your validation checks to the Recce checklist with a description of your findings.

These checks can later be added to your pull request comment as proof-of-correctness for your modeling changes.

Checklist"},{"location":"docs/features/lineage/","title":"Lineage","text":"

The Lineage Diff is the main interface to Recce and allows you to quickly see the potential area of impact from your dbt data modeling changes.

"},{"location":"docs/features/lineage/#lineage-diff","title":"Lineage Diff","text":"

It's from the Lineage Diff that you will determine which models to investigate further; and also perform the various data validation checks that will serve as proof-of-correctness of your work.

Lineage Diff"},{"location":"docs/features/lineage/#node-summary","title":"Node Summary","text":"

Models are color-coded to indicate their status:

  • Added models are green.
  • Removed models are red.
  • Modified models are orange.

The two icons at the bottom right of each node indicate if a row count or schema change has been detected. Grayed out icons indicate no change.

Model with Schema Change detected

Note: A row count changed icon is only shown if there is row count diff executed on this node.

Open the node details panel

Click a model to open the node details panel and perform other data validation checks.

"},{"location":"docs/features/lineage/#filter-nodes","title":"Filter Nodes","text":"

In the top control bar, you can change the rule to filter the nodes:

  1. Mode:
    • Changed Models: Modified nodes and their downstream + 1st degree of their parents.
    • All: Show all nodes.
  2. Package: Filter by dbt package names.
  3. Select: Select nodes by node selection.
  4. Exclude: Exclude nodes by node selection.
"},{"location":"docs/features/lineage/#select-nodes","title":"Select Nodes","text":"

Click a node to select it, or click the Select nodes button at the top-right corner to select multiple nodes for further operations. For detail, see the Multi Nodes Selections section

"},{"location":"docs/features/lineage/#row-count-diff","title":"Row Count Diff","text":"

A row count diff can be performed on nodes selected using the select and exclude options:

After selecting nodes, run the row count diff by:

  1. Clicking the 3 dots (...) button at the top-right corner.
  2. Clicking Row Count Diff by Selector.
"},{"location":"docs/features/lineage/#node-details","title":"Node Details","text":"

The node details panel shows information about a node, such as node type, schema and row count changes, and allows you to perform diffs on the node using the options accessed via the Explore Change button.

"},{"location":"docs/features/lineage/#schema-diff","title":"Schema Diff","text":"

Schema Diff shows added, removed, and renamed columns. Click a model in the Lineage Diff to open the node details and view the Schema Diff.

Note

Schema Diff requires catalog.json in both environments.

Schema Diff

Schema Diff showing renamed column"},{"location":"docs/features/lineage/#row-count-diff_1","title":"Row Count Diff","text":"

Row Count Diff shows the difference in row count between the base and current environments.

  1. Click the model in the Lineage DAG.
  2. Click the Explore Change button in the node details panel.
  3. Click Row Count Diff.

Row Count Diff - Single model"},{"location":"docs/features/lineage/#code-diff","title":"Code Diff","text":"

Code Diff shows the model code that has changed for a particular model.

  1. Click the model in the Lineage DAG.
  2. Click the Explore Change button in the node details panel.
  3. Click Code Diff.
"},{"location":"docs/features/lineage/#value-diff","title":"Value Diff","text":"

Value Diff shows the matched count and percentage for each column in the table. It uses the primary key(s) to uniquely identify the records between the model in both environments.

The primary key is automatically inferred by the first column with the unique test. If no primary key is detected at least one column is required to be specified as the primary key.

Value Diff
  • Added: Newly added PKs.
  • Removed: Removed PKs.
  • Matched: For a column, the count of matched value of common PKs.
  • Matched %: For a column, the ratio of matched over common PKs.

Note

Value Diff uses the compare_column_values from audit-helper. To use Value Diff, ensure that audit-helper is installed in your project.

packages:\n  - package: dbt-labs/audit_helper\n    version: <version>\n

View mismatched values at the row level by clicking the show mismatched values option on a column name:

"},{"location":"docs/features/lineage/#profile-diff","title":"Profile Diff","text":"

Profile Diff compares the basic statistic (e.g. count, distinct count, min, max, average) for each column in models between two environments.

  1. Select the model from the Lineage DAG.
  2. Click the Expore Change button.
  3. Click Profile Diff.

Profile Diff

Please refer to the dbt-profiler documentation for the definitions of profiling stats.

Note

Profile diff uses the get_profile from dbt-profiler. To use Profile Diff, ensure that dbt-profiler is installed in your project.

packages:\n  - package: data-mie/dbt_profiler\n    version: <version>\n
"},{"location":"docs/features/lineage/#histogram-diff","title":"Histogram Diff","text":"

Histogram Diff compares the distribution of a numeric column in an overlay histogram chart.

Histogram Diff

A Histogram Diff can be generated in two ways.

Via the Explore Change button menu:

  1. Select the model from the Lineage DAG.
  2. Click the Explore Change button.
  3. Click Histogram Diff.
  4. Select a column to diff.
  5. Click Execute.

Via the column options menu:

  1. Select the model from the Lineage DAG.
  2. Hover over the column in the Node Details panel.
  3. Click the vertical 3 dots ...
  4. Click Histogram Diff.

Generate a Recce Histogram Diff from the column options"},{"location":"docs/features/lineage/#top-k-diff","title":"Top-K Diff","text":"

Top-K Diff compares the distribution of a categorical column. The top 10 elements are shown by default, which can be expanded to the top 50 elements.

Recce Top-K Diff

A Top-K Diff can be generated in two ways.

Via the Explore Change button menu:

  1. Select the model from the Lineage DAG.
  2. Click the Explore Change button.
  3. Click Top-K Diff.
  4. Select a column to diff.
  5. Click Execute.

Via the column options menu:

  1. Select the model from the Lineage DAG.
  2. Hover over the column in the Node Details panel.
  3. Click the vertical 3 dots ...
  4. Click Top-K Diff.

Generate a Recce Top-K Diff"},{"location":"docs/features/lineage/#multi-node-selection","title":"Multi-Node Selection","text":"

Multiple nodes can be selected in the Lineage DAG. This enables actions to be performed on multiple nodes at the same time such as Row Count Diff, or Value Diff.

"},{"location":"docs/features/lineage/#select-nodes-individually","title":"Select Nodes Individually","text":"

To select multiple nodes individually, click the check box on the nodes you wish to select.

Select multiple nodes individually"},{"location":"docs/features/lineage/#select-parent-or-child-nodes","title":"Select Parent or Child nodes","text":"

To select a node and all of its parents or children:

  1. Click the checkbox on the node
  2. Right click the node
  3. Click to select either parent or child nodes

Select a node and its parents or children"},{"location":"docs/features/lineage/#perform-actions-on-multiple-nodes","title":"Perform actions on multiple nodes","text":"

After selecting the desired nodes, use the Actions menu at the top right of the screen to perform diffs or add checks.

Perform actions on multiple nodes"},{"location":"docs/features/lineage/#example-row-count-diff","title":"Example - Row Count Diff","text":"

An example of selecting multiple nodes to perform a multi-node row count diff:

Perform a Row Count Diff on multiple nodes"},{"location":"docs/features/lineage/#example-value-diff","title":"Example - Value Diff","text":"

An example of selecting multiple nodes to perform a multi-node Value Diff:

Perform a Value Diff on multiple nodes"},{"location":"docs/features/lineage/#screenshot","title":"Screenshot","text":"

In the diff result, we can find a Copy to Clipboard button. it's a handy feature to copy the result image to clipboard and paste in your PR comment.

Copy a diff result screenshot to the clipboard and paste to GitHub

Note

FireFox does not support to copy image to clipboard. Recce show a modal instead. You can download the image to local or right-click on the image to copy the image.

"},{"location":"docs/features/lineage/#add-to-checklist","title":"Add to Checklist","text":"

In the lineage page, we can run different type of check. However, for these reason we would like to add to checklist

  1. Keep the check and I can rerun this after my code change
  2. Add my result and interpretation for review purpose
"},{"location":"docs/features/lineage/#lineage-diff_1","title":"Lineage Diff","text":"

Lineage diff by selector

  1. Select nodes by Select and Exclude on the top control.
  2. Click ... at the top-right corner
  3. Click the Lineage diff

Lineage diff by multi nodes selection

  1. Click Select nodes button at the top-right corner
  2. Select nodes
  3. Click the Add lineage diff check button
"},{"location":"docs/features/lineage/#schema-diff_1","title":"Schema Diff","text":"

Schema diff by node selector

  1. Select nodes by Select and Exclude on the top control.
  2. Click ... at the top-right corner
  3. Click the Schema diff button

Schema diff by multi nodes selection

  1. Click Select nodes button at the top-right corner
  2. Select nodes
  3. Click the Add schema check button

Schema diff for single node

  1. Select a node, then the node detail would show.
  2. Click Add check button on the node detail pane.
  3. Click Schema check
"},{"location":"docs/features/lineage/#row-count-diff_2","title":"Row Count Diff","text":"

Row count diff by node selector

  1. Select nodes by Select and Exclude on the top control.
  2. Click ... at the top-right corner
  3. Click the Row Count Diff by Selctor, then it will run the row count diff
  4. Click the Add to checklist in the result page.

Row count diff by multi nodes selection

  1. Click Select nodes button
  2. Select nodes
  3. Click Row count diff, then it will run the row count diff
  4. Select a node, then the run result would show.
  5. Click Add to checklist
"},{"location":"docs/features/lineage/#other-diffs","title":"Other Diffs","text":"
  1. Execute the diff
  2. Click Add to checklist
"},{"location":"docs/features/node-selection/","title":"Node Selection","text":"

Recce supports dbt node selection in the lineage diff. This enables you to target specific resources with data checks by selecting or excluding nodes.

"},{"location":"docs/features/node-selection/#supported-syntax-and-methods","title":"Supported syntax and methods","text":"

Since Recce uses dbt's built-in node selector, it supports most of the selecting methods. Here are some examples:

  • Select a node: my_model
  • select by tag: tag:nightly
  • Select by wildcard: customer*
  • Select by graph operators: my_model+, +my_model, +my_model, 1+my_model+
  • Select by union: model1 model2
  • Select by intersection: stg_invoices+,stg_accounts+
  • Select by state: state:modified, state:modified+
"},{"location":"docs/features/node-selection/#use-state-method","title":"Use state method","text":"

In dbt, you need to specify the --state option in the CLI. In Recce, we use the base environment as the state, allowing you to use the selector on the fly.

"},{"location":"docs/features/node-selection/#removed-nodes","title":"Removed nodes","text":"

Another difference is that in dbt, you cannot select removed nodes. However, in Recce, you can select removed nodes and also find them using the graph operator. This is a notable distinction from dbt's node selection capabilities.

"},{"location":"docs/features/node-selection/#supported-diff","title":"Supported Diff","text":"

In addition to lineage diff, other types of diff also support node selection. You can find these features in the ... button at the top right corner. Currently supported diffs include:

  • Lineage diff
  • Row count diff
  • Schema diff

"},{"location":"docs/features/node-selection/#limitation","title":"Limitation","text":"
  • \"result\" method not supported
  • \"source_status\" method not supported.
  • YAML selectors not supported.
"},{"location":"docs/features/preset-checks/","title":"Preset Checks","text":"

In a dbt project, there may be some checks that need to be conducted for every PR. For example, this could be an SQL query, or checking whether an important model has had a schema change.

Preset checks can be the fixed checks that are generated every time a new Recce instance is initiated.

"},{"location":"docs/features/preset-checks/#configure-the-preset-check","title":"Configure the Preset Check","text":"

To configure the preset checks, add the settings to the recce config file.

  1. Add a check to your checklist
  2. Open the menu for the check and select Get Preset Check Template.
  3. Copy the yaml config from the dialog

  4. Paste the config into the recce.yml file located at the root of the project:

    # recce.yml\nchecks:\n  - name: Query diff of customers\n    description: |\n      This is the demo preset check.\n\n      Please run the query and paste the screenshot to the PR comment.\n    type: query_diff\n    params:\n      sql_template: select * from {{ ref(\"customers\") }}\n    view_options:\n      primary_keys:\n        - customer_id\n
"},{"location":"docs/features/preset-checks/#create-the-preset-checks","title":"Create the Preset Checks","text":""},{"location":"docs/features/preset-checks/#recce-server","title":"Recce Server","text":"
  1. When a new Recce instance is launched, all preset checks are automatically set up, but these checks are not executed at this time.
  2. When the Run Query button is pressed, the check will be executed.
"},{"location":"docs/features/preset-checks/#recce-run","title":"Recce Run","text":"
  1. Running recce run executes all preset checks. The default output file is recce_state.json.
    $ recce run\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 DBT Artifacts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBase:\n    Manifest: 2024-04-10 08:54:41.546402+00:00\n    Catalog:  2024-04-10 08:54:42.251611+00:00\nCurrent:\n    Manifest: 2024-04-22 03:24:11.262489+00:00\n    Catalog:  2024-04-10 06:15:13.813125+00:00\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Preset checks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                            Recce Preset Checks\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nStatus      Name                 Type         Execution Time   Failed Reason\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n[Success]   Query of customers   Query Diff   0.10 seconds     N/A\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nThe state file is stored at [recce_state.json]\n
  2. You can view the check results by launching the recce server.
    recce server recce_state.json\n
  3. You can show the summary of the state by the recce summary command
    recce summary recce_state.json\n
"},{"location":"docs/features/query/","title":"Query","text":"

Query page provides an AdHoc query interface to run arbitrary query or diff the query result between two environments. If you're a dbt user, you can use any dbt macros that are installed in your project.

"},{"location":"docs/features/query/#execute-query","title":"Execute Query","text":"
select * from {{ ref(\"mymodel\") }}\n

Actions

  • Run: performs query in the current environment.
  • Run Diff: performs the same query in both environments and diffs the results.

Form

  • Primary key: select the primary key(s) used to compare the query results.

Note

If the primary key(s) is specified, the query will occur in the warehouse; otherwise, the query will happen across two environments and the comparison will take place on the client side.

Tip

In Mac, you can use \u2318 Enter to run a query or use \u2318 \u21e7 Enter to run a query diff.

"},{"location":"docs/features/query/#query-result","title":"Query Result","text":"
  • Primary Key: When comparison occurs on the client side, we can select the primary key by clicking the key icon. The primary key columns are used to be identified as the same record for both sides. If no primary key is specified, the records is compared by the row's index.
  • Pinned Column: The pinned column would show first in the column list.
  • Changed Only: By selecting the Changed only, we can show only the changed rows and columns. The pinned columns are always shown even they are not changed.
"},{"location":"docs/features/query/#shortcut-to-query-a-model","title":"Shortcut to query a model","text":"
  1. In the lineage page, select a model
  2. Click the Query button
  3. Then the query page is shown and filled with the query for this model
"},{"location":"docs/features/query/#add-to-checklist","title":"Add to Checklist","text":"

Click the + button in the result pane, then you can add the query result to the checklist.

"},{"location":"docs/features/query/#how-query-diff-works","title":"How Query Diff Works","text":"

In the current version, Recce provides two ways to compare the query result between two environments.

Query diff occurs in the client side:

Without providing primary key(s) upfront, AdDoc query compare in the client side. That is, Recce fetches the first 2,000 rows and compare in the client side. The advantage is it has more flexibility to query sql for no PK, especially when column structures differ or no clear primary key exists. However, the limitation is that we cannot find the mismatched rows in a big query result.

Query diff occurs in the warehouse:

With primary key(s) given, it can perform a query diff in the warehouse. It only displays changed, added, or removed rows. Therefore, if only one record is different among a million, that specific record will be visible. Hence, it also reduces the amount of data transferred.

Another similar feature is Value Diff. Value diff is based on a chosen model, so you don\u2019t need to write SQL to operate it, though it naturally offers less flexibility. Additionally, value diff can show a summary or actual diff records, whereas query diff only shows the actual diff records.

"},{"location":"docs/features/recce-run/","title":"Run","text":"

Recce provides a Web UI for interactively exploring and comparing two environments in depth. However, in the CI workflow, using the command line interface is more appropriate.

To execute Recce from the command line:

recce run\n

or to specify an output file:

recce run -o recce_pr123.json\n

This command will:

  1. Collect the dbt artifacts for both the base and current environments.
  2. Run the preset checks: Preset checks are checks that have been configured specifically for this dbt project.
  3. Output the result. The default output path is recce_state.json.
"},{"location":"docs/features/recce-summary-example/","title":"Recce Summary","text":""},{"location":"docs/features/recce-summary-example/#lineage-graph","title":"Lineage Graph","text":"
graph LR\nmodel.jaffle_shop.customers[\"customers\n\n[What's Changed]\nCode, Schema, Value Diff\"]\nstyle model.jaffle_shop.customers stroke:#ffa502\nmodel.jaffle_shop.customers---->model.jaffle_shop.customer_segments\nmodel.jaffle_shop.customers---->model.jaffle_shop.customer_order_pattern\nmodel.jaffle_shop.customer_segments[\"customer_segments\"]\nmodel.jaffle_shop.customer_order_pattern[\"customer_order_pattern\"]\n
"},{"location":"docs/features/recce-summary-example/#checks-summary","title":"Checks Summary","text":"Total Checks Data Mismatch Detected 5 3"},{"location":"docs/features/recce-summary-example/#checks-of-data-mismatch-detected","title":"Checks of Data Mismatch Detected","text":"Name Type Related Models Model schema of customers Schema Diff customers Value diff of customers Value Diff customers Query diff of customers avg lifetime value Query Diff N/A"},{"location":"docs/features/recce-summary/","title":"Summary","text":"

Recce Summary command is used to generate a summary based on the input state file. In the previous section, the Run command was used to generate a state file based on the two environments. It provides a way to integrate Recce into your CI/CD pipeline. The Summary command is used to generate a summary based on the output of Run command. You can also integrate the Summary command into your CI/CD pipeline to generate a summary based on the state file generated by the Run command. Therefor, the generated summary can be posted to your repository hosting platform, such as GitHub, GitLab, or Bitbucket.

"},{"location":"docs/features/recce-summary/#usage","title":"Usage","text":"
recce summary <Path-of-recce-state-file>\n
"},{"location":"docs/features/recce-summary/#example","title":"Example","text":"
recce summary recce-state.json\n
"},{"location":"docs/features/recce-summary/#output","title":"Output","text":"

The output of the summary command will be markdown format. The markdown output will contain the following sections:

  • Lineage Graph - A graph that shows the lineage of the models that are impacted by the modified models.
  • Checks Summary - A summary of the checks that are detected mismatch between base and current environments.
"},{"location":"docs/features/recce-summary/#example-output","title":"Example Output","text":"
# Recce Summary\n\n## Lineage Graph\n\n```mermaid\ngraph LR\nmodel.jaffle_shop.customers[\"customers\n\n[What's Changed]\nCode, Schema, Value Diff\"]\nstyle model.jaffle_shop.customers stroke:#ffa502\nmodel.jaffle_shop.customers---->model.jaffle_shop.customer_segments\nmodel.jaffle_shop.customers---->model.jaffle_shop.customer_order_pattern\nmodel.jaffle_shop.customer_segments[\"customer_segments\"]\nmodel.jaffle_shop.customer_order_pattern[\"customer_order_pattern\"]\n\n```\n\n## Checks Summary\n\n| Total Checks | Data Mismatch Detected |\n| ------------ | ---------------------- |\n| 5            | 3                      |\n\n### Checks of Data Mismatch Detected\n\n| Name                                       | Type        | Related Models |\n| ------------------------------------------ | ----------- | -------------- |\n| Model schema of customers                  | Schema Diff | customers      |\n| Value diff of customers                    | Value Diff  | customers      |\n| Query diff of customers avg lifetime value | Query Diff  | N/A            |\n

The rendered output will look like this. Example Output

"},{"location":"docs/features/recce-summary/#content-of-the-summary","title":"Content of the Summary","text":""},{"location":"docs/features/recce-summary/#lineage-graph","title":"Lineage Graph","text":"

The lineage graph shows the lineage of the models that are impacted by the modified models. The graph is generated using the mermaid library. The graph is a directed graph that shows the relationship between the models. The graph is generated based on the modified models and their's children models. If the model is modified or impacted by the modified model, it will be highlighted in the [What's Changed] section.

"},{"location":"docs/features/recce-summary/#example-of-lineage-graph","title":"Example of Lineage Graph","text":"
graph LR\nmodel.jaffle_shop.customers[\"customers\n\n[What's Changed]\nCode, Schema, Value Diff\"]\nstyle model.jaffle_shop.customers stroke:#ffa502\nmodel.jaffle_shop.customers---->model.jaffle_shop.customer_segments\nmodel.jaffle_shop.customers---->model.jaffle_shop.customer_order_pattern\nmodel.jaffle_shop.customer_segments[\"customer_segments\"]\nmodel.jaffle_shop.customer_order_pattern[\"customer_order_pattern\"]
"},{"location":"docs/features/recce-summary/#checks-summary","title":"Checks Summary","text":"

Shows the total number of checks and the number of data mismatched checks. The table will contain the following columns:

  • Total Checks - The total number of checks.
  • Data Mismatch Detected - The number of checks which data mismatched between base and current environments.
"},{"location":"docs/features/recce-summary/#checks-of-data-mismatch-detected","title":"Checks of Data Mismatch Detected","text":"

Shows the checks that are detected data mismatch between base and current environments. If the check is detected data mismatch, we will suggest the PR reviewer should take a look at the check and investigate the data mismatch is expected or not. The table will contain the following columns:

  • Name - The name of the check.
  • Type - The type of the check.
  • Related Models - The models that are related to the check. If a check is not related to any models, it will be N/A.

If all the check between base and current environments are matched, the Checks of Data Mismatch Detected section will not be shown.

"},{"location":"docs/features/recce-summary/#example-of-data-mismatch-detected","title":"Example of Data Mismatch Detected","text":"Name Type Related Models Model schema of customers Schema Diff customers Value diff of customers Value Diff customers Query diff of customers avg lifetime value Query Diff N/A"},{"location":"docs/features/recce-summary/#how-to-integrate-with-cicd-pipeline","title":"How to Integrate with CI/CD Pipeline","text":"

The generated summary can be posted to any kinds of repository hosting platform, such as GitHub, GitLab, or Bitbucket. Please refer to the Recce CI integration with GitHub Action section for more information on how to integrate Recce with your CI/CD pipeline.

"},{"location":"docs/features/state-file/","title":"Recce State File","text":""},{"location":"docs/features/state-file/#introduction","title":"Introduction","text":"

The state file is the serialized state of a recce instance. There are two scenarios where we need to export the state file:

  • PR Review: We can include the state file in the PR Review Comment. If the reviewer wants to further connect to the PR environment, they can use this file to see the final results and perform deeper audits.
  • Development: During development, we often need to save the state and even switch between different branches for development.
"},{"location":"docs/features/state-file/#pr-review","title":"PR Review","text":"

In the PR review process, the state file can serve as a medium of communication between the submitter and the reviewer.

"},{"location":"docs/features/state-file/#create-a-state-file-for-review","title":"Create a State File for Review","text":"
  • Export from Web UI: To export the current Recce state, you can use the Export button located in the top right corner of the Web UI to export the state to a file.

  • Output of the Recce Run: A more mature dbt project may have a CI/CD process in place where dbt transformations are run, and the results are placed in a PR-specific environment. In such cases, you can integrate recce run in your automation workflow, making it convenient for reviewers to audit the results to determine whether the merge can proceed.

"},{"location":"docs/features/state-file/#whats-in-the-state-file","title":"What's in the State File","text":"
  • Checks: These are the data from the checks that have been added to the checklist on the Checks page.
  • Runs: Each execution in Recce represents a run, analogous to a query in the warehouse. However, typically, a single run may submit series of queries in the warehouse and get the final run result.
  • Environments' Artifacts: The base and current environments' manifest.json and catalog.json.
  • Runtime Information: Such as git branch information, PR information in the CI runner.
"},{"location":"docs/features/state-file/#review-the-state-file","title":"Review the State File","text":"

To review a PR, you can download the corresponding state file and open the file with additional --review option.

recce server --review recce_state.json\n

In this mode, the Recce instance won't use the dbt artifacts inside target and target-base. Instead, it will use the artifacts from the Recce state file.

"},{"location":"docs/features/state-file/#development","title":"Development","text":"

When running the Recce server, you can specify an additional file argument

recce server recce_issue_1.json\n

If this file exists, Recce will use it as the initial state. If it doesn't exist, Recce will create a new one. When the server is terminated, the final state will be written into this file.

"},{"location":"docs/features/state-file/#recce-cloud","title":"Recce Cloud","text":"

Note

Currently, Recce Cloud is still under development. If you are interested, please sign up for a Recce Cloud invite or contact us in the dbt slack #tool-recce channel

Although a state file can store the state, it is not very suitable for recording the latest review status of a PR. Especially since a PR may include the submitter, reviewer, and the automated processes in the CI workflow. The purpose of Recce Cloud is to solve the PR review status management issue.

Prerequisites

Before uploading the state file to Recce Cloud, you need to define a password to encrypt the state file. The password is used to encrypt the state file before uploading it to Recce Cloud. It will also be used to decrypt the state file when you download it from Recce Cloud. The password is not stored in Recce Cloud, so you need to keep it safe.

"},{"location":"docs/features/state-file/#pr-review-workflow","title":"PR Review Workflow","text":"

This is a most common workflow: the submitter pushes commits to the GitHub PR, and the reviewer is responsible for reviewing/auditing the PR. This includes checking code changes, ensuring requirements are met, and assessing whether there is any impact on existing models.

As a submitter

  1. Push changes to the remote
  2. Create PR for review
  3. Run dbt to prepare the PR review environment
    dbt run\n
  4. Launch recce server for this PR branch
    recce server --cloud --password <password-to-encrypt-state-file>\n
  5. Add recce checks for review
  6. Leave description and screenshots in the PR comments

As a reviewer

  1. Checkout the PR branch
  2. Launch recce server for this PR branch in the review mode
    recce server --review --cloud --password <password-to-encrypt-state-file>\n
  3. If all checks are good, mark all checks as Approved. Otherwise, leave comment in the PR comment or the recce check description.
"},{"location":"docs/features/state-file/#pr-review-workflow-with-ci","title":"PR Review Workflow with CI","text":"

For more mature projects, we introduce CI automation to standardize the process and reduce human-caused variability or errors. In the workflow, we will do the following two things in the CI:

Execute dbt to create a PR environment. Execute recce to update dbt artifacts, rerun check runs, and update the PR status to Recce Cloud.

As a submitter

  1. Push changes to the remote
  2. Create PR for review

In the CI workflow of the PR push event

  1. The github action workflow is triggered by the push event
  2. Checkout the PR branch
  3. Fetch the dbt artifacts for the base environment
  4. Run dbt for the PR environment
    dbt run\n
  5. Run recce for the current PR and upload state to the recce cloud.
    recce run --cloud --password <password-to-encrypt-state-file>\n

As a submitter and reviewer, collaborate the state in the review mode recce server

  1. Checkout the PR branch
  2. Launch recce server for this PR branch in the review mode
    recce server --review --cloud --password <password-to-encrypt-state-file>\n
"},{"location":"docs/guides/best-practices-prep-env/","title":"Best Practices for Preparing Environments","text":"

Recce is designed to compare two environments in your data project. To use it effectively, it is crucial to prepare environments through CI.

However, there are many challenges in preparing environments.

  1. Your source data might be continuously updating.
  2. Your transformations might be time-consuming.
  3. The base branch may have other PRs merged at any time.
  4. The generated environment will leave data in the warehouse, which also needs to be properly managed.

This article will not focus on how to use Recce but rather on how to effectively prepare environments for Recce use.

"},{"location":"docs/guides/best-practices-prep-env/#best-practices","title":"Best Practices","text":""},{"location":"docs/guides/best-practices-prep-env/#use-schema-to-manage-your-environments","title":"Use schema to manage your environments","text":"

In dbt, you can leverage profiles and targets to specify the credentials for your database connections. By using profiles and dynamically setting the schema, you can direct the transformation results to different schemas, effectively creating separate environments. Here's how you can achieve this using env_var or arg to dynamically change the schema:

  1. Define Your Profile and Target: Your profiles.yml should have different targets that you can switch between. Here\u2019s an example of a profiles.yml file:

    my_profile:\n  target: dev\n  outputs:\n    dev:\n      type: postgres\n      host: localhost\n      user: db_user\n      password: db_pass\n      port: 5432\n      dbname: my_db\n      schema: \"{{ env_var('DBT_SCHEMA') }}\"\n    prod:\n      type: postgres\n      host: prod_host\n      user: prod_user\n      password: prod_pass\n      port: 5432\n      dbname: prod_db\n      schema: public\n
  2. Run dbt with the Specified Schema: Now, when you run dbt commands, the schema setting will dynamically use the value of the DBT_SCHEMA environment variable.

    DBT_SCHEMA=pr_env dbt run\n
  3. Using args in dbt: You can also pass arguments directly in your dbt commands to dynamically set variables. For example:

    dbt run --vars '{\"schema_name\": \"pr_123\"}'\n

    And modify your profiles.yml to use this variable:

    my_profile:\n  target: dev\n  outputs:\n    dev:\n      type: postgres\n      host: localhost\n      user: db_user\n      password: db_pass\n      port: 5432\n      dbname: my_db\n      schema: \"{{ var('schema_name') }}\"\n

This approach allows you to dynamically create different environments by changing the schema on-the-fly. This is particularly useful for creating isolated environments for different PRs or testing scenarios, ensuring that your transformations are scoped to the correct schema and avoiding conflicts between different environments.

"},{"location":"docs/guides/best-practices-prep-env/#prepare-single-base-environment-for-all-prs-to-compare","title":"Prepare single base environment for all PRs to compare","text":"

Using the production environment as the base environment is a straightforward choice. However, to make Recce more efficient, using the staging environment might be more suitable.

This staging environment can have the following characteristics:

  1. Ensure that the transformed results reflect the latest commit of the base branch.
  2. Use the same source data as the PR environment.
  3. Use the same transformation logic as the PR environment.

The basic principle is that the staging environment's configuration should be as close as possible to the PR environments, except for using a different git commit.

"},{"location":"docs/guides/best-practices-prep-env/#prepare-per-pr-environment","title":"Prepare per-PR environment","text":"

A moderately sized data project may have multiple branches in development simultaneously. To avoid interference, it is recommended that each PR have its own isolated PR environment. The schema name can be pr_<number>

"},{"location":"docs/guides/best-practices-prep-env/#reduce-the-update-frequency-of-the-source-data-used-by-both-the-base-and-pr-environments","title":"Reduce the update frequency of the source data used by both the base and PR environments","text":"

Some data projects may have source data that updates every hour or even every second. This can result in different transformation outcomes due to varying source data at different times, leading to Recce comparison results lacking discernibility.

Currently, some data warehouses support zero-copy clone (snowflake, bigquery, databricks), which allows us to freeze the source data at a specific point in time. Considering updating the source data weekly can significantly reduce the variability in environments caused by source data changes.

"},{"location":"docs/guides/best-practices-prep-env/#limit-the-data-range-used-in-transformations","title":"Limit the data range used in transformations","text":"

Most data is temporal. When preparing the base and PR environments, we can use only the data from the last month. This can greatly reduce the data volume while still verifying correctness.

If zero-copy clone for the source data is not supported and the source data continues to update, you can consider excluding the current week's data. This approach can ensure that transformations yield consistent results regardless of when they are executed.

For example, you can design the transformation to only use data from the last month, up to Sunday at 00:00. This approach combines the benefits of shorter execution times and reduced data volatility.

SELECT\n    *\nFROM\n    {{ source('your_source_name', 'orders') }}\n{% if target.name != 'prod' %}\nWHERE\n    order_date >= DATEADD(month, -1, CURRENT_DATE)\n    AND order_date < DATE_TRUNC('week', CURRENT_DATE)\n{% endif %}\n
"},{"location":"docs/guides/best-practices-prep-env/#ensure-that-the-base-environment-is-always-up-to-date","title":"Ensure that the base environment is always up-to-date","text":"

There are two scenarios that may cause the base environment to be out of date:

  1. New Source Data Changes: If you update your data weekly, ensure that your base environment is updated at least once a week as well.
  2. New PRs Merged into base branch: You can trigger a base environment update on merge events to ensure it remains current.
"},{"location":"docs/guides/best-practices-prep-env/#ensure-the-pr-branch-is-in-sync-with-the-base-branch","title":"Ensure the PR branch is in sync with the base branch","text":"

If the PR is executed after the base branch has been updated, the comparison with the base environment will mix the changes from the PR with the changes from other PRs merged into the base branch. This results in comparison outcomes that do not accurately reflect the impact of the current PR.

GitHub can automatically detect whether a PR is in sync. You need to enable this feature in the repository settings. Once enabled, you will see whether the PR is up-to-date directly on the PR page.

You can also to check if the PR is up-to-date in the CI workflow before preparing the PR environment. Here is an example in github action

- name: Check if PR is up-to-date\n  if: github.event_name == 'pull_request'\n  run: |\n    git fetch origin main\n    UPSTREAM=${GITHUB_BASE_REF:-'main'}\n    HEAD=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}\n    if [ \"$(git rev-list --left-only --count ${HEAD}...origin/${UPSTREAM})\" -eq 0 ]; then\n      echo \"Branch is up-to-date\"\n    else\n      echo \"Branch is not up-to-date\"\n      exit 1\n    fi\n

"},{"location":"docs/guides/best-practices-prep-env/#consider-how-to-obtain-your-artifacts-for-environments","title":"Consider how to obtain your artifacts for environments","text":"

Recce relies on the base and current environment artifacts to find the corresponding tables in the data warehouse for comparison. So, the question is how to obtain the artifacts of the environments to be compared.

Here are a few methods you can choose:

  1. In CI, upload the generated artifact to the cloud storage (e.g., AWS S3).
  2. For dbt Cloud users, you can download artifacts for the latest run of a given job.
  3. For GitHub Actions users, you can use the GitHub CLI (gh) to download artifacts for the latest run of a given workflow.

If the methods mentioned above are too complex, a stateless approach is to directly check out the base branch and run dbt docs generate to generate the artifacts.

"},{"location":"docs/guides/best-practices-prep-env/#cleaning-up-pr-environments-on-pr-closed","title":"Cleaning up PR environments on PR closed","text":"

As the number of PRs in a project increases, automatically generated environments also grow. To manage this, you can create a workflow that listens for PR close events and performs cleanup actions. Additionally, you can schedule periodic cleanups to remove outdated environments, such as those not used for a week.

In dbt, you can use the dbt run-operation command to clear a specific schema corresponding to an environment. This can be especially useful for environments named in a pattern such as pr_{env}.

Here\u2019s how you can define a macro to clear an environment schema:

{% macro clear_schema(schema_name) %}\n{% set drop_schema_command = \"DROP SCHEMA IF EXISTS \" ~ schema_name ~ \" CASCADE;\" %}\n{% do run_query(drop_schema_command) %}\n{% endmacro %}\n

Run the macro

dbt run-operation clear_schema --args \"{'schema_name': 'pr_123'}\"\n
"},{"location":"docs/guides/best-practices-prep-env/#example","title":"Example","text":"Environments Schema Name When to run # of environments Data range Production public Daily 1 All Staging staging Daily + On Merge 1 1 month, excluding this week PR pr_<number> On Push # of opened PR 1 month, excluding this week
  • Automate environment generation using GitHub Actions.
  • PR Environment will only be generated automatically when the PR is up-to-date.
  • Artifacts will be stored under the workflow\u2019s artifacts.
  • PR environments are removed on PR closed.
  • Use staging environment as the base environment for Recce.
"},{"location":"docs/guides/scenario-ci/","title":"Recce CI integration with GitHub Action","text":"

Recce provides the recce run command for CI/CD pipeline. You can integrate Recce with GitHub Actions (or other CI tools) to compare the data models between two environments when a new pull-request is created. The below image describes the basic architecture.

The following guide demonstrates how to configure Recce in GitHub Actions.

"},{"location":"docs/guides/scenario-ci/#prerequisites","title":"Prerequisites","text":"

Before integrating Recce with GitHub Actions, you will need to configure the following items:

  • Set up two environments in your data warehouse. For example, one for base and another for pull request.

  • Provide the credentials profile for both environments in your profiles.yml so that Recce can access your data warehouse. You can put the credentials in a profiles.yml file, or use environment variables.

  • Set up the data warehouse credentials in your GitHub repository secrets.

"},{"location":"docs/guides/scenario-ci/#set-up-recce-with-github-actions","title":"Set up Recce with GitHub Actions","text":"

We suggest setting up two GitHub Actions workflows in your GitHub repository. One for the base environment and another for the PR environment.

  • Base environment workflow: Triggered on every merge to the main branch. This ensures that base artifacts are readily available for use when a PR is opened.

  • PR environment workflow: Triggered on every push to the pull-request branch. This workflow will compare base models with the current PR environment.

"},{"location":"docs/guides/scenario-ci/#base-workflow-main-branch","title":"Base Workflow (Main Branch)","text":"

This workflow will perform the following actions:

  1. Run dbt on the base environment.
  2. Upload the generated DBT artifacts to github workflow artifacts for later use.
name: Recce CI Base Branch\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n\nconcurrency:\n  group: recce-ci-base\n  cancel-in-progress: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: \"3.10.x\"\n\n      - name: Install dependencies\n        run: |\n          pip install -r requirements.txt\n\n      - name: Run DBT\n        run: |\n          dbt deps\n          dbt seed --target ${{ env.DBT_BASE_TARGET }}\n          dbt run --target ${{ env.DBT_BASE_TARGET }}\n          dbt docs generate --target ${{ env.DBT_BASE_TARGET }}\n        env:\n          DBT_BASE_TARGET: \"prod\"\n\n      - name: Upload DBT Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: target\n          path: target/\n

Note

Please place the above file in .github/workflows/dbt_base.yml. This workflow path will also be used in the next PR workflow. If you place it in a different location, please remember to make the corresponding changes in the next step.

"},{"location":"docs/guides/scenario-ci/#pr-workflow-pull-request-branch","title":"PR Workflow (Pull Request Branch)","text":"

This workflow will perform the following actions:

  1. Run dbt on the PR environment.
  2. Download previously generated base artifacts from base workflow.
  3. Use Recce to compare the PR environment with the downloaded base artifacts.
  4. Use Recce to generate the summary of the current changes and post it as a comment on the pull request. Please refer to the Recce Summary for more information.
name: Recce CI PR Branch\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  check-pull-request:\n    name: Check pull request by Recce CI\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - name: Merge Base Branch into PR\n        uses: DataRecce/PR-Update@v1\n        with:\n          baseBranch: ${{ github.event.pull_request.base.ref }}\n          autoMerge: false\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: \"3.10.x\"\n      - name: Install dependencies\n        run: |\n          pip install -r requirements.txt\n          pip install recce\n      - name: Prepare dbt Base environment\n        run: |\n          gh repo set-default ${{ github.repository }}\n          base_branch=${{ github.base_ref }}\n          run_id=$(gh run list --workflow ${WORKFLOW_BASE} --branch ${base_branch} --status success --limit 1 --json databaseId --jq '.[0].databaseId')\n          echo \"Download artifacts from run $run_id\"\n          gh run download ${run_id} -n target -D target-base\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          WORKFLOW_BASE: \".github/workflows/dbt_base.yml\"\n      - name: Prepare dbt Current environment\n        run: |\n          git checkout ${{ github.event.pull_request.head.sha }}\n          dbt deps\n          dbt seed --target ${{ env.DBT_CURRENT_TARGET}}\n          dbt run --target ${{ env.DBT_CURRENT_TARGET}}\n          dbt docs generate --target ${{ env.DBT_CURRENT_TARGET}}\n        env:\n          DBT_CURRENT_TARGET: \"dev\"\n\n      - name: Run Recce CI\n        run: |\n          recce run --github-pull-request-url ${{ github.event.pull_request.html_url }}\n\n      - name: Upload DBT Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: target\n          path: target/\n\n      - name: Upload Recce State File\n        uses: actions/upload-artifact@v4\n        id: recce-artifact-uploader\n        with:\n          name: recce-state-file\n          path: recce_state.json\n\n      - name: Prepare Recce Summary\n        id: recce-summary\n        run: |\n          recce summary recce_state.json > recce_summary.md\n          cat recce_summary.md >> $GITHUB_STEP_SUMMARY\n          echo '${{ env.NEXT_STEP_MESSAGE }}' >> recce_summary.md\n\n          # Handle the case when the recce summary is too long to be displayed in the GitHub PR comment\n          if [[ `wc -c recce_summary.md | awk '{print $1}'` -ge '65535' ]]; then\n            echo '# Recce Summary\n          The recce summary is too long to be displayed in the GitHub PR comment.\n          Please check the summary detail in the [Job Summary](${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}) page.\n          ${{ env.NEXT_STEP_MESSAGE }}' > recce_summary.md\n          fi\n\n        env:\n          NEXT_STEP_MESSAGE: |\n            ## Next Steps\n            If you want to check more detail information about the recce result, please download the [artifact](${{ steps.recce-artifact-uploader.outputs.artifact-url }}) file and open it by [Recce](https://pypi.org/project/recce/) CLI.\n\n            ### How to check the recce result\n            ```bash\n            # Unzip the downloaded artifact file\n            tar -xf recce-state-file.zip\n\n            # Launch the recce server based on the state file\n            recce server --review recce_state.json\n\n            # Open the recce server http://localhost:8000 by your browser\n            ```\n\n      - name: Comment on pull request\n        uses: thollander/actions-comment-pull-request@v2\n        with:\n          filePath: recce_summary.md\n          comment_tag: recce\n
"},{"location":"docs/guides/scenario-ci/#review-the-recce-state-file","title":"Review the Recce State File","text":"

Review the downloaded Recce state file with the following command:

recce server --review recce_state.json\n

In the Recce server --review mode, you can review the comparison results of the data models between the base and current environments. It will contain the row counts of modified data models, and the results of any Recce Preset Checks.

"},{"location":"docs/guides/scenario-dev/","title":"Development","text":"

In developing a project with dbt, there are numerous methods available to help you query warehouse data for validation. Recce, in particular, allows for further comparison with production or a specific baseline environment.

"},{"location":"docs/guides/scenario-dev/#prepare-the-environments","title":"Prepare the environments","text":"

In order to enable Recce to compare the base and current environment, you need to prepare artifacts for both environments.

For base environment, put the dbt artifacts in your target-base/ path. You can have the following options

  1. Download the artifacts from remote storage: If you use dbt cloud, you can download the latest artifacts in your production environment. For non dbt cloud case, you can upload the latest artifacts to cloud storage (e.g. s3), and write a scripts to download artifacts.
  2. Generate the artifacts for the production environment:
    dbt docs generate --target prod --target-path target-base/\n

For current developing environment, for most of the dbt command, it would generate the manifest.json. If you want to update the schema information, you have to run the dbt docs generate to generate the catalog.json.

Recce also watch the the target/ and target-base/ folders. If there is artifact file changed, the recce web ui would reload to the latest version.

"},{"location":"docs/guides/scenario-dev/#development-cycle","title":"Development Cycle","text":"

The common development cycle is

  1. Write the code, validate the change, commit your code
  2. Push the commits to remote
  3. Review the impacts of your changes

Here, I assume your pull request hasn't been marked as \"ready for review\" yet, and you're still in the process of development, verifying correctness on your own. In this scenario, Recce can assist you in conducting this validation.

"},{"location":"docs/guides/scenario-dev/#check-the-lineage","title":"Check the Lineage","text":"

DBT provides a method to identify modified models using dbt ls -s state:modified+, but this is obviously not usable within dbt docs. While you can determine how many models are affected using this command, you can't visualize these results.

In Recce, you can conduct an initial assessment of your impact scope by Lineage diff, which may help you identify potential unintended impacts.

"},{"location":"docs/guides/scenario-dev/#validate-the-models-metadata","title":"Validate the Models' Metadata","text":"

With lineage diff, you can start from the modified models to confirm your impact. An inexpensive method is to examine the impact scope of the affected models' metadata.

Firstly, you can start by examining Schema diff to see if any changes are detected in each model's schema. Sometimes, a change from Integer to Text, or from Decimal to Numeric, may have subtle impacts on your downstream models.

Additionally, whether the models with schema changes have only added columns is worth noting, as this might not significantly affect your downstream processes. However, if columns are removed, it's essential to pay special attention to ensure it's the expected outcome.

Next, you can examine the Row count diff for the affected models. Typically, row counts are stored in the warehouse's metadata, meaning you can obtain row count information without much cost. This allows you to quickly determine if row counts are the same or if there are significant changes. Common issues may arise from an erroneous join resulting in unexpected data volumes and erroneous outcomes. Row count diff provides a fast method to identify similar errors.

Observing the summary of each node can help you quickly review schema and row count changes. By using the lineage diff graph, conducting basic checks on schema and row count, you can already gain a basic level of confidence in the changes made during your development process.

"},{"location":"docs/guides/scenario-dev/#validate-the-columns-summary","title":"Validate the Column's Summary","text":"

Apparently, model metadata alone is insufficient. Sometimes, we need to assess the magnitude of impact that the changes currently in development have on the critical Marts models.

Recce provides 4 powerful diff tools to compare the data level changes.

  1. Value Diff: You can use value diff to observe the matched percentage for each column.
  2. Profile Diff: You can use profile diff to compare basic statistical values for each column, such as count, distinct count, min, max, and average.
  3. Histogram Diff: You can use histogram diff to examine the distribution changes of numeric columns.
  4. Top-K Diff: You can use top-k diff to analyze distribution changes of categorical columns.

It's important to note that these queries may take longer to execute and require reading larger amounts of data. Please choose the appropriate method based on the data volume of each model.

"},{"location":"docs/guides/scenario-dev/#validate-by-adhoc-query","title":"Validate by Adhoc Query","text":"

If you want to choose the most flexible method, Query diff is the way to go. You can compare individual records, perform complex operations like where, group by, order by. Or even query multiple models with joins.

AdHoc queries also support the use of dbt macros, providing the highest level of flexibility for validation. However, the downside is that you'll need to write the queries yourself.

"},{"location":"docs/guides/scenario-dev/#check-driven-development","title":"Check Driven Development","text":"

Test Driven Development (TDD) is a common development pattern where you write tests first, then begin development, validating until the tests pass.

When developing in dbt, of course, you can implement the TDD process through dbt tests. However, writing tests is not the only method. First, tests require very precise validation logic, and second, sometimes we don't want to impact the original data definitions. In such cases, what we want to verify is that the data doesn't change too much, rather than a specific logic.

For example, if we want to make slight adjustments to the definition of \"revenue\". In the concept of TDD, we would consider what the input data is and what the output should be. But more often, what we want to verify is just ensuring that the changes in revenue for each month are within an expected range.

In recce, we can a simple query for your validation code

SELECT\n    date_trunc('month', order_date) AS month,\n    SUM(amount) AS revenue\nFROM\n    orders\nGROUP BY\n    month\nORDER BY\n    month desc\n

Next, you can add this check to your checklist. After modifying your code each time, rerun this check until it meets your requirements.

"},{"location":"docs/guides/scenario-dev/#store-your-data","title":"Store your data","text":"

During development, we may inevitably switch branches. If you want to preserve the current state, you can use the Export the state file for later loading.

Another technique is to specify the file directly when opening it.

recce server recce_issue_123.json\n

When you close the Recce instance, it saves your current session to this file. The next time you open it using the same file, it will restore to the previous state.

"},{"location":"docs/guides/scenario-pr-review/","title":"PR Review","text":"

When you've finished developing your feature, we should turn the pull request (PR) into ready for review state. At this point, the PR submitter needs to provide the necessary information for the PR reviewer. Some projects may offer a PR template. In the dbt official blog, this article provides an excellent example.

Recce at this stage aims to assist the submitter in gathering more information to ensure that the reviewer can merge the pull request (PR) with greater confidence.

"},{"location":"docs/guides/scenario-pr-review/#screenshots","title":"Screenshots","text":""},{"location":"docs/guides/scenario-pr-review/#lineage","title":"Lineage","text":"

Firstly, the Lineage DAG is crucial information within a dbt project as it helps us understand the dependencies between models. In dbt docs, it provides lineage diagrams. Usually, we can paste this diagram into the PR comment to help the reviewer understand the latest lineage status.

However, during PR reviews, we may be more interested in understanding what changes have been made and presenting them through the Lineage DAG. At this point, you can utilize recce to capture a screenshot of the Lineage diff and embed it within your PR comment.

"},{"location":"docs/guides/scenario-pr-review/#checks","title":"Checks","text":"

Another core feature of Recce is its various checks, which allow us to compare key models with the base environment. The typical workflow is as follows:

  1. Generate the various diffs you need.
  2. Identify the query and its result that you want to present to the reviewer.
  3. Add it to the checklist.
  4. Click the Copy to Clipboard button and paste it in the corresponding position within the PR comment.
  5. Write a description of your check, including explanations and intentions.
  6. Click the Copy markdown button
  7. paste it in the corresponding position within the PR comment.
"},{"location":"docs/guides/scenario-pr-review/#share-the-recce-file","title":"Share the Recce File","text":"

If you want the reviewer to access your environment, you can also attach the Recce state file to the PR comment.

As a Submitter

  1. Export the recce state file
  2. Attach the state file into the PR comment

As a Reviewer

  1. Download the state file
  2. In your dbt project folder, run this command
    recce server --review <recce state file>\n

By adding the --review option, the Recce server will use the DBT artifacts from the state file to connect to both the base and the pull request (PR) environments.

Note

Although the artifacts are from the Recce state, you still need to provide the profiles.yml and dbt_project.yml files so that Recce knows which credentials to use to connect to the data warehouse.

"},{"location":"docs/recce-cloud/","title":"Overview","text":"

Note

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

"},{"location":"docs/recce-cloud/#what-is-recce-cloud","title":"What is Recce Cloud?","text":"

Recce Cloud is a service specifically designed for streamlining the DBT PR Review workflow.

Recce Cloud primarily operates through Recce and integrates GitHub Pull Requests, consolidating the review status of PRs within the Cloud. Without Recce Cloud, we use the state file to store PR review states. However, this method is not very suitable for collaboration or integration with CI because our review states are not stored in a fixed location. Recce Cloud is designed to solve this problem.

"},{"location":"docs/recce-cloud/#prerequisite","title":"Prerequisite","text":"
  1. Recce Cloud requires Recce. Please make sure that you have understood how use Recce in your dbt project.
  2. Prepare the github personal access token with the repo permission. Please see the GitHub document. And set it to your environment variable.
    export GITHUB_TOKEN=<token>\n
    Or you can set the --cloud-token <GITHUB_TOKEN> command option.
  3. Prepare the Recce state password. The Recce state password is used to encrypt/decrypt the state file before uploading/downloading. The password is not stored in Recce Cloud, so you need to keep it safe.
    export RECCE_STATE_PASSWORD=<password>\n
    Or you can set the --password <password> or -p <password> command option.
"},{"location":"docs/recce-cloud/#getting-started","title":"Getting Started","text":"

The following instructions give an overview of the process of using Recce in your dbt project. For a hands-on tutorial, please check the Jaffle Shop Tutorial for Cloud.

"},{"location":"docs/recce-cloud/#sign-up-the-recce-cloud","title":"Sign Up the Recce Cloud","text":"
  1. Go to the recce cloud
  2. Sign in by github account
  3. Click Install button to install Recce Cloud github app to your personal or organization account.
  4. Authorize the repositories to the github app.
"},{"location":"docs/recce-cloud/#launch-the-recce-server-in-the-cloud-mode","title":"Launch the recce server in the cloud mode","text":"
  1. Create a branch for developing.
    git checkout -b <my-awesome-feature>\n
  2. Develop your features and prepare the dbt artifacts for the base (target-base/) and current (target/) environments.
  3. Create a pull request for this branch. Recce Cloud requires an open pull request in your GitHub repository. It also stores the latest Recce state for each pull request.
  4. Launch the Recce instance in the cloud mode. It will use the dbt artifacts in the local target and target-base and initiate a new review state if necessary.
    recce server --cloud\n

Note

Here we assume the you have set the GITHUB_TOKEN and RECCE_STATE_PASSWORD in your environment variables.

"},{"location":"docs/recce-cloud/#review-in-the-cloud-mode","title":"Review in the cloud mode","text":"

If the review state is already available for this PR, you can open the Recce instance to review.

  1. Checkout the branch for the reviewed PR.
  2. Launch the Recce instance to review this PR
    recce server --review --cloud\n
"},{"location":"docs/recce-cloud/#usage","title":"Usage","text":"

All the commands requires the following settings.

Name Environment Variables CLI Options Description Cloud token GITHUB_TOKEN --cloud-token Used for 1. Get the pull request from github2. Used as the access token to the recce cloud State password RECCE_STATE_PASSWORD --password Used to encrypt/decrypt the state in the recce cloud

Recce Cloud is used for pull request reviews. Before interacting with the cloud state, you should switch to a branch that has an open PR on the remote.

"},{"location":"docs/recce-cloud/#recce-server","title":"Recce server","text":"

Initiate the review session of the PR. It would use the local dbt artifacts in the target/ and target-base/ directories to sync the state with the cloud.

git checkout <pr-branch>\nrecce sever --cloud\n
"},{"location":"docs/recce-cloud/#recce-server-review-mode","title":"Recce server (Review mode)","text":"

Review a PR with the remote state.

git checkout <pr-branch>\nrecce sever --cloud --review\n
"},{"location":"docs/recce-cloud/#recce-run","title":"Recce run","text":"

Run or rerun the PR's checks and sync the state with cloud.

git checkout <pr-branch>\nrecce run --cloud\n
"},{"location":"docs/recce-cloud/#recce-summary","title":"Recce summary","text":"

Generate the summary markdown

git checkout <pr-check>\nrecce summary --cloud > summary.md\n
"},{"location":"docs/recce-cloud/#recce-cloud","title":"Recce cloud","text":"

The cloud subcommand in recce provides functionality for managing state files in cloud storage.

"},{"location":"docs/recce-cloud/#purge","title":"purge","text":"

You can purge the state from your current PR. It is useful when

  1. You forgot the password
  2. You would like to reset the state of this PR.
git checkout <pr-branch>\nrecce cloud purge\n
"},{"location":"docs/recce-cloud/#upload","title":"upload","text":"

If you already have the state file for the PR, you can upload it to the cloud.

git checkout <pr-branch>\nrecce cloud upload <recce-state-file>\n
"},{"location":"docs/recce-cloud/#download","title":"download","text":"

You can download the recce state file of the PR from cloud as well.

git checkout <pr-branch>\nrecce cloud download\n
"},{"location":"docs/recce-cloud/#github-pull-request-status-check","title":"GitHub Pull Request Status Check","text":"

Recce Cloud integrate with the GitHub Pull Request Status Check. If there is recce review state synced to a PR, the PR would has a recce cloud check status. Once all checks in recce are approved, the check status would change to passed and ready to be merged.

"},{"location":"docs/recce-cloud/expose-recce-instance-visibility/","title":"Share Recce Instance Access","text":"

Note

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

As a Recce Cloud user, you can launch a Recce Instance in Cloud Mode or use GitHub Codespaces. However, both of these methods require a GitHub Access Token, which restricts the usage of Recce to those with GitHub accounts.

For situations in which you would like to share your Recce Instance with non-GitHub users, such as stakeholders or other teams, we currently recommend the use of one of the following third-party utilities:

  • Ngrok
  • Tailscale

These services provide an endpoint for your Recce Instance, with optional authentication, that will enable other users to access Recce.

These approaches serve as a workaround to expose a Recce instance to non-GitHub users. We are currently working on official support for enabling this feature without the need for third-party tools.

"},{"location":"docs/recce-cloud/expose-recce-instance-visibility/#provide-external-access-to-a-recce-instance","title":"Provide external access to a Recce Instance","text":"

Note

Using these tools requires registering additional accounts, and you may need to subscribe to a paid plan to accommodate your usage and data transfer volume. For details, please refer to the Pricing Plans of ngrok and tailscale.

"},{"location":"docs/recce-cloud/expose-recce-instance-visibility/#ngrok","title":"ngrok","text":"

After installing the Ngrok client, you can create an endpoint for the Recce Instance that will allow other users to participate in the dbt PR review process, without any additional tools or setup.

  1. Setup the ngrok agent

ngrok supports multiple platforms, including macOS, Linux, and Windows. Please refer to the official installation guide for details.

  1. Connect your ngrok agent to your ngrok account.

    ngrok config add-authtoken <TOKEN>\n

  2. Put the Recce Instance online

    ngrok http <recce-instance-port>\n

  3. Secure access with authentication

    ngrok provides a range of authentication options, from basic methods to integration with multiple OAuth providers.

    # basic auth\nngrok http <recce-instance-port> --basic-auth 'username:password'\n\n# OAuth\nngrok http <recce-instance-port> --oauth=google --oauth-allow-email=user@example.com\n

    For the full usage of settings and options, please refer to the ngrok http docs for details.

"},{"location":"docs/recce-cloud/expose-recce-instance-visibility/#tailscale","title":"Tailscale","text":"

Tailscale enables you to create your own private network (called a 'Tailnet') and invite members to join it. Once set up, you can easily expose your Recce instance, making it accessible to all devices within the Tailnet.

  1. Setup tailscale

    To create a Tailnet, first create an account, then download Tailscale and follow the official guide to continue set-up. Once configured, you can then invite other members to join.

    It also supports integration with GitHub Codespaces.

  2. Connect your device to your account

    tailscale up --authkey <AUTH_KEY>\n

  3. Put the Recce Instance online

    The devices within your Tailnet can access the Recce Instance now.

    tailscale serve <recce-instance-port>\n

    If you need a more fine-grained access control policy, please refer to the Tailscale docs.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/","title":"Demo Tutorial","text":"

Estimated Time: 20 minutes

Note

Recce Cloud is currently in private alpha and scheduled for general availability soon. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

The following guide uses the official Jaffle Shop DuckDB project from dbt-labs, and provides everything you need to get started with Recce Cloud. By the end of the guide you'll be able to create and sync Recce checks with a GitHub PR via Recce Cloud.

To see what you'll get, check out the first section from the following Loom:

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#clone-jaffle-shop-to-your-own-private-repository","title":"Clone Jaffle Shop to your own private repository","text":"
  1. Create a private repository in your GitHub account.
  2. Clone the Jaffle Shop DuckDB dbt data project:
    git clone git@github.com:dbt-labs/jaffle_shop_duckdb.git\ncd jaffle_shop_duckdb\n
  3. Change the remote url to the repository you just created:
    git remote set-url origin git@github.com:<owner>/<repo>.git\n
  4. Push to your newly created repository:
    git push\n
"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#authorize-recce-cloud-to-access-the-repository","title":"Authorize Recce Cloud to access the repository","text":"

Recce Cloud needs access to your data project's repository in order to sync your checks status to the pull request thread.

  1. Visit Recce Cloud. If it is your first time logging in, click the Continue with Github button to authorize the Recce Cloud integration to access your GitHub account.
  2. Click the Install button to install the Recce Cloud GitHub app to your personal or organization account.
  3. On the app installation page, authorize Recce Cloud to access the repository you created in the previous section.
  4. Authorized repositories will then be shown in your Recce Cloud account.
"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#configure-the-jaffle-shop-duckdb-data-project","title":"Configure the Jaffle Shop DuckDB data project","text":"

Set up the Jaffle Shop project and install Recce.

  1. Create a new Python virtual env:
    python -m venv venv\nsource venv/bin/activate\n
  2. Install the requirements and Recce:
    pip install -r requirements.txt\npip install recce\n
  3. Add a production environment to the project by editing ./profiles.yml and adding the following target:
    jaffle_shop:\n  target: dev\n  outputs:\n    dev:\n      type: duckdb\n      path: 'jaffle_shop.duckdb'\n      threads: 24\n+   prod:\n+     type: duckdb\n+     path: 'jaffle_shop.duckdb'\n+     schema: prod\n+     threads: 24\n
  4. Add the following packages required by Recce for some features (highly recommended). Create a ./packages.yml file in the root of your project with the following packages:
    packages:\n- package: dbt-labs/audit_helper\n  version: 0.12.0\n- package: data-mie/dbt_profiler\n  version: 0.8.2\n
    Install the packages:
    dbt deps\n
"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#prepare-the-base-environment","title":"Prepare the base environment","text":"

Recce requires to two environments to compare. The base represents your point of reference (the known-good base), and target represents your PR/development branch.

  1. Prepare production (base) environment. (Note the custom --target-path):
    dbt seed --target prod\ndbt run --target prod\ndbt docs generate --target prod --target-path ./target-base\n
  2. Add the target-base/ folder to the .gitignore file:
     target/\n+target-base/\n dbt_packages/\n dbt_modules/\n logs/\n
  3. Remove the existing GitHub action workflows:
    rm -rf .github/\n
  4. Push the changes to remote:
    git add .\ngit commit -m 'Configure project and prep for Recce'\ngit push \n

Important

By default, Recce expects the dbt artifacts for the base environment to be located in a folder named target-base.

The base environment preparation is now complete. The data in the prod schema, and artifacts in the target-base folder, represent stable (production) data.

As a PR author, you'll be working on data models, making changes to the project, and validating your work for correctness.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#prepare-the-review-state-for-the-pr","title":"Prepare the review state for the PR","text":"

In this section, you'll make a new branch, update a data model, and create a pull request.

  1. Checkout a branch:

    git checkout -b feature/recce-getting-started\n

  2. Edit the staging model located in ./models/staging/stg_payments.sql as follows:

    ...\n\nrenamed as (\n         payment_method,\n\n-        -- `amount` is currently stored in cents, so we convert it to dollars\n-        amount / 100 as amount\n+        amount\n\n         from source\n)\n

  3. Run dbt on the development environment (the default target):

    dbt seed\ndbt run\ndbt docs generate\n

  4. Commit the change:

    git add models/staging/stg_payments.sql\ngit commit -m 'Update the model'\ngit push -u origin feature/recce-getting-started \n

  5. Create a pull request for this branch in your GitHub repository.

Important

Don't forget to create a branch for the commit above, before continuing with this tutorial.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#launch-a-recce-instance-to-validate-your-change","title":"Launch a Recce Instance to validate your change","text":"

In this section, you will launch a Recce Instance, create validation checks, and sync those checks with Recce Cloud so they can be reviewed by your PR reviewer.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#prepare-a-github-token-and-recce-state-password","title":"Prepare a GitHub Token and Recce State password","text":"

To access the repository, your local Recce Instance will require a GitHub Token (Classic).

  1. Prepare a GitHub Token (Classic) in your account. Ensure you provide repo permission for the new token.
  2. Ensure you have configured these environment variables.
    export GITHUB_TOKEN=<github-token>\nexport RECCE_STATE_PASSWORD=mypassword\n
"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#run-recce-in-cloud-mode","title":"Run Recce in Cloud Mode","text":"

Run Recce instance in the cloud mode

recce server --cloud\n

Open the link to the Recce Instance in your browser. By default it should be http://0.0.0.0:8000

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#create-a-recce-check","title":"Create a Recce Check","text":"

Switch to the Query tab and paste the following query:

select * from {{ ref(\"orders\") }} order by 1\n
Enter the primary key as order_id and click the Run Diff button. 1. Click the Add to Checklist button to add the query result to your Checklist 1. On the Checklist page you'll find that there are three checks. The Row count diff and Schema diff are default Preset Checks, and the Query diff is your newly added check. Leave the checks as unapproved. 1. Go back to the command line and terminate the Recce instance. Your Recce State file, containing your checklist and other artifacts will be encrypted and uploaded to Recce Cloud. 1. Go to the PR page in your GitHub repository and scroll to the bottom. Notice that Recce Cloud shows that check are not approved:

Note

Recce checks sync in realtime. However, due to the overhead of encrypting, compressing, and tranferring the State file, the sync may be slightly delayed. Ensure that you always terminate your Recce Instance on the CLI, and wait for the State to be synced. This will ensure your checks are saved to Recce Cloud.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#review-the-pr","title":"Review the PR","text":"

As a PR author, your job is to review and approve the Checks created by the PR author. Once approved, the PR can be merged.

Note

As this tutorial uses DuckDB, which is a file-based database, the reviewer needs to have the same DuckDB file to continue the reviewers journey.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#run-recce-is-review-mode","title":"Run Recce is Review mode","text":"

The PR reviewer should prepare their own GitHub token, but ensure to use the same password as the PR author. (The password is used to unencrypt the State file and so must be the same.)

  1. Checkout the PR branch
    git checkout feature/recce-getting-started\n
  2. Configured the required environment variables.
    export GITHUB_TOKEN=<github-token>\nexport RECCE_STATE_PASSWORD=mypassword\n
  3. Run Recce in --cloud and --review mode.
    recce server --cloud --review\n
"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#approve-recce-checks","title":"Approve Recce Checks","text":"

When Recce loads, click the Checklist tab to review the Checks that have been prepared by the PR author.

Approve all the checks if everything looks good to you

The approval status of the check is automatically synced to Recce Cloud.

"},{"location":"docs/recce-cloud/getting-started-recce-cloud/#merge-the-pr","title":"Merge the PR","text":"

Back on the GitHub PR page, you'll notice that the Recce Cloud check status has automatically been updated showing that \"All checks are approved\".

In a real-world situation you'd now be able to merge the PR with the confidence that the PR author had checked their work, and the reviewer both understands and has signed-off on any changes.

"},{"location":"docs/recce-cloud/setup-gh-actions/","title":"Continue Integration (CI)","text":"

Note

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

Continuous Integration(CI) and Continuous Delivery(CD) are best practices in software development. Through CI automation, a dbt project can systematically and continuously deliver and integrate high-quality results.

To automate the process, we can use GitHub Actions and GitHub Codespaces to provide an automated and reusable workspace. The following diagram describes the entire ci/cd architecture.

We suggest setting up two GitHub Actions workflows in your GitHub repository. One for the base environment and another for the PR environment.

  • Base environment workflow: Triggered on every merge to the main branch. This ensures that base artifacts are readily available for use when a PR is opened.

  • PR environment workflow: Triggered on every push to the pull-request branch. This workflow will compare base models with the current PR environment.

"},{"location":"docs/recce-cloud/setup-gh-actions/#prerequisites","title":"Prerequisites","text":"
  1. Per-PR Environment: To ensure that each PR has its own isolated environment, it is recommended to put profile.yml under source control in the repository and use environment variables to change the schema name. In the workflow, we can generate the corresponding schema name based on the PR number.

    myprofile:\n  outputs:\n    pr:\n      type: snowflake\n      ...\n      schema: \"{{ env_var('DBT_SCHEMA') | as_text }}\"\n    prod:\n      type: snowflake\n      ...\n      schema: PUBLIC\n
  2. GitHub Token and Recce State Password: As mentioned here, please ensure that the two secrets are available when running recce commands. You can add GH_TOKEN and RECCE_STATE_PASSWORD to the GitHub Actions Secrets. Then we can use them in the Github Actions workflow file.

    env:\n  GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}\n  RECCE_STATE_PASSWORD: ${{ secrets.RECCE_STATE_PASSWORD }}\n

Warning

You cannot use the automatic generated token here, because we need the personal access token (PAT) to verify if the user has PUSH permission of the repository.

env:\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Don't use 'secrets.GITHUB_TOKEN' here\n
"},{"location":"docs/recce-cloud/setup-gh-actions/#set-up-recce-with-github-actions","title":"Set up Recce with GitHub Actions","text":""},{"location":"docs/recce-cloud/setup-gh-actions/#base-workflow-main-branch","title":"Base Workflow (Main Branch)","text":"

This workflow will perform the following actions:

  1. Run dbt on the base environment.
  2. Upload the generated DBT artifacts to github workflow artifacts for later use.

Note

Please place the above file in .github/workflows/dbt_base.yml. This workflow path will also be used in the next PR workflow. If you place it in a different location, please remember to make the corresponding changes in the next step.

name: Daily Job\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * *\"\n  push:\n    branches:\n      - main\n\nconcurrency:\n  group: recce-ci-base\n  cancel-in-progress: true\nenv:\n  # Credentials used by dbt profiles.yml\n  DBT_USER: ${{ secrets.DBT_USER }}\n  DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.10.x\"\n          cache: \"pip\"\n\n      - name: Install dependencies\n        run: |\n          pip install -r requirements.txt\n\n      - name: Run DBT\n        run: |\n          dbt deps\n          dbt seed --target ${{ env.DBT_BASE_TARGET }}\n          dbt run --target ${{ env.DBT_BASE_TARGET }}\n          dbt docs generate --target ${{ env.DBT_BASE_TARGET }}\n        env:\n          DBT_BASE_TARGET: \"prod\"\n\n      - name: Upload DBT Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: target\n          path: target/\n

If executed successfully, the dbt target will be placed in the run artifacts and will be named target.

"},{"location":"docs/recce-cloud/setup-gh-actions/#pr-workflow-pull-request-branch","title":"PR Workflow (Pull Request Branch)","text":"

This workflow will perform the following actions:

  1. Run dbt on the PR environment.
  2. Download previously generated base artifacts from base workflow.
  3. Use Recce to compare the PR environment with the downloaded base artifacts.
  4. Use Recce to generate the summary of the current changes and post it as a comment on the pull request. Please refer to the Recce Summary for more information.
name: Recce CI PR Branch\n\non:\n  pull_request:\n    branches: [main]\n\nenv:\n  WORKFLOW_BASE: \".github/workflows/dbt_base.yml\"\n  # Credentials used by dbt profiles.yml\n  DBT_USER: ${{ secrets.DBT_USER }}\n  DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}\n  DBT_SCHEMA: \"PR_${{ github.event.pull_request.number }}\"\n  # Credentials used by recce\n  GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}\n  RECCE_STATE_PASSWORD: ${{ secrets.RECCE_STATE_PASSWORD }}\njobs:\n  check-pull-request:\n    name: Check pull request by Recce CI\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Merge Base Branch into PR\n        uses: DataRecce/PR-Update@v1\n        with:\n          baseBranch: ${{ github.event.pull_request.base.ref }}\n          autoMerge: false\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.10.x\"\n          cache: pip\n      - name: Install dependencies\n        run: |\n          pip install -r requirements.txt\n          pip install recce~=0.34\n      - name: Download artifacts for the base environment\n        run: |\n          gh repo set-default ${{ github.repository }}\n          base_branch=${{ github.base_ref }}\n          run_id=$(gh run list --workflow ${WORKFLOW_BASE} --branch ${base_branch} --status success --limit 1 --json databaseId --jq '.[0].databaseId')\n          echo \"Download artifacts from run $run_id\"\n          gh run download ${run_id} -n target -D target-base\n      - name: Prepare the PR environment\n        run: |\n          dbt deps\n          dbt seed --target ${{ env.DBT_CURRENT_TARGET}}\n          dbt run --target ${{ env.DBT_CURRENT_TARGET}}\n          dbt docs generate --target ${{ env.DBT_CURRENT_TARGET}}\n        env:\n          DBT_CURRENT_TARGET: \"pr\"\n      - name: Run Recce\n        run: |\n          recce run --cloud\n      - name: Upload DBT Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: target\n          path: target/\n\n      - name: Prepare Recce Summary\n        id: recce-summary\n        run: |\n          recce summary --cloud > recce_summary.md\n          cat recce_summary.md >> $GITHUB_STEP_SUMMARY\n          echo '${{ env.NEXT_STEP_MESSAGE }}' >> recce_summary.md\n\n          # Handle the case when the recce summary is too long to be displayed in the GitHub PR comment\n          if [[ `wc -c recce_summary.md | awk '{print $1}'` -ge '65535' ]]; then\n            echo '# Recce Summary\n          The recce summary is too long to be displayed in the GitHub PR comment.\n          Please check the summary detail in the [Job Summary](${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}) page.\n          ${{ env.NEXT_STEP_MESSAGE }}' > recce_summary.md\n          fi\n\n        env:\n          RECCE_STATE_PASSWORD: ${{ secrets.RECCE_STATE_PASSWORD }}\n          NEXT_STEP_MESSAGE: |\n            ## Next Steps          \n            If you want to check more detail information about the recce result, please follow this instruction.\n\n            ```bash\n            # Checkout to the PR branch\n            git checkout ${{ github.event.pull_request.head.ref }}\n\n            # Launch the recce server based on the state file\n            recce server --cloud --review\n\n            # Open the recce server http://localhost:8000 by your browser\n            ```\n      - name: Comment on pull request\n        uses: thollander/actions-comment-pull-request@v2\n        with:\n          filePath: recce_summary.md\n          comment_tag: recce\n
"},{"location":"docs/recce-cloud/setup-gh-actions/#pr-workflow-with-dbt-cloud","title":"PR workflow with dbt Cloud","text":"

We can download the dbt artifacts from dbt Cloud for Recce if CI/CD on dbt Cloud is configured. The basic scenario is to download the latest artifacts of the deploy job for the base environment and the artifacts of the CI job for the current environment. We can archieve it via dbt Cloud API and we need:

  1. dbt Cloud Token
  2. dbt Cloud Account ID: Check out your \"Account settings\" on dbt Cloud
  3. dbt Cloud Deploy Job ID: Check out \"API trigger\" of the deploy job
  4. dbt Cloud CI Job ID: Check out \"API trigger\" of the CI job

We prepare a GitHub Action \"Recce dbt Cloud Action\" to do the following steps:

  1. Trigger the CI job on dbt Cloud
  2. Wait the CI job to finish
  3. Download the dbt artifacts from the deploy job to ./target-base directory
  4. Download the dbt artifacts from the deploy job to ./target directory

Check out the GitHub Action to configure the GitHub workflow.

Note

Please ensure Generate docs on run is toggled in the \"Execution settings\" of deploy job and \"Advanced settings\" of CI job.

"},{"location":"docs/recce-cloud/setup-gh-actions/#review-the-recce-state-file","title":"Review the Recce State File","text":"

Review locally

git checkout <pr-branch>\nrecce server --cloud --review\n

Review in the GitHub codespace

Please see GitHub Codespaces integration

"},{"location":"docs/recce-cloud/setup-gh-codespaces/","title":"GitHub Codespaces","text":"

Note

Recce Cloud is currently in private alpha and scheduled for general availability later this year. Sign up to the Recce newsletter to be notified, or email product@datarecce.io to join our design partnership program for early access.

GitHub Codespaces is a development environment provided by GitHub that allows developers to have identical and isolated environments for development. The GitHub Codespaces uses VS Code Server technology. We can launch it from a GitHub pull request page, and once it is started, the Recce instance will run and port forwarding will be set up.

"},{"location":"docs/recce-cloud/setup-gh-codespaces/#setup-recce-cloud-in-github-codespaces","title":"Setup Recce Cloud in GitHub Codespaces","text":"
  1. Prepare the two files in your repository

    .devcontainer\n\u2514\u2500\u2500 recce\n    \u251c\u2500\u2500 Dockerfile\n    \u2514\u2500\u2500 devcontainer.json\n

  2. Configure the .devcontainer/recce/devcontainer.json

    {\n    \"name\": \"Recce CodeSpace\",\n    \"build\": {\n        \"dockerfile\": \"Dockerfile\",\n    },\n    \"containerEnv\": {\n        \"RECCE_STATE_PASSWORD\": \"${{ secrets.RECCE_STATE_PASSWORD }}\",\n        \"DBT_USER\": \"${{ secrets.DBT_USER }}\",\n        \"DBT_PASSWORD\": \"${{ secrets.DBT_PASSWORD }}\",\n    },\n    \"forwardPorts\": [8000],\n    \"postStartCommand\": \"recce server --cloud --review\"\n}\n
    The secrets are Github Codespaces secrets. You can configure them in

    • Account specific codespaces secrets
    • or Repository-level or organization-level codespaces secrets
  3. Prepare the .devcontainer/Dockerfile

    FROM mcr.microsoft.com/vscode/devcontainers/python:3.11\n\nRUN pip install dbt-bigquery~=1.7.0 recce~=0.34\n

Note

The GitHub token generated by codespace is sufficient for Recce's use. It's not necessary to configure the GITHUB_TOKEN separately.

"},{"location":"docs/recce-cloud/setup-gh-codespaces/#how-to-use","title":"How to use","text":"

Once you complete Recce Cloud setup, you can launch GitHub Codespaces from Recce Cloud's pull request page, and once it is started, the Recce instance will run and port forwarding will be set up.

  1. Go to Recce Cloud and click the repository you want to make changes.
  2. Click the pull request that you want to use Recce instance.
  3. Click \"Create in GitHub Codespaces.\"
  4. The Codespaces creation may take 1 to more than 5 mintues depeding on your Codespaces settings. And the Recce instance should take less than 1 minute to launch.
    • Please view FAQ for how to speed up.
  5. You can see the \"State\" of the progress; And the Action you can take in each state.
    • Codespace Queued: the Codespace is creating
    • Codespace Provisioning: the Codespace is provisioning
    • Codespace Available: the Codespace is ready
    • Recce launching: Recce instance is launching
    • Stop: stop launching Recce instance
    • Recce active: Recce instance is launched successfully.
      • Open: open the Recce instance
      • Stop: stop the Codespace
    • Codespace ShuttingDown: the Codespace is shutting down
    • Stopped: the Codespace is stopped and the Recce instance is closed.
      • Restart: restart the Codespace
      • Delete: delete the Codespace
        • It\u2019s recommended to delete the Codespace once your PR is merged.
"},{"location":"docs/recce-cloud/setup-gh-codespaces/#troubleshooting","title":"Troubleshooting","text":"

If this is your first time setting up a Codespace, it\u2019s recommended to first test locally with the following commands:

git checkout feature/recce-getting-started\nexport GITHUB_TOKEN=<github-token>\nexport RECCE_STATE_PASSWORD=mypassword\nrecce server --cloud --review\n

Ensure it runs correctly locally. If it does, then the remaining issues within the Codespace are likely related to its configuration.

If your Codespace configration is correct, other common causes might include:

  1. The current branch does not have a corresponding pull request. This usually happens if you launch Codespace directly form GitHub main branch. Recce instance cannot assoicate with the main branch.
  2. The pull request does not have an uploaded Recce state file. In review mode, the state file must be prepared via CI or locally before proceeding.
  3. The RECCE_STATE_PASSWORD mentioned above is not set or the password is incorrect.
  4. Other issues are preventing the Recce server from starting at all.

When you\u2019ve opened a Codespace but are unable to connect to the Recce instance, you can troubleshoot by following these steps:

  1. Check Codespace instance in GitHub.
  2. If the Codesapce you created from Recce Cloud is active, click \"Open in Browser\".
  3. Click on the blue block in the lower left corner of the status bar, which usually shows \"Codespaces: instance name\"
  4. Select \"View creation log\" or ppen the VS Code Command Palette and type Codespaces: View Creation Log.
  5. At this point, you should be able to see the reason why the Recce server failed to start.
  6. If you cannot find any issue from the Codespace creation log, and belive your Codespace configration is correct. Please stop the Codespace and launch Recce instance again.
  7. If you still have problem, please contact us via slack or email product@datarecce.io. We are happy to help.
"},{"location":"docs/recce-cloud/setup-gh-codespaces/#faq","title":"FAQ","text":"

Q: How long does Codespace generally take to start?

The typical startup time is around 1 to 2 minutes if you have prebuid. If not, it may take more than 5 mintues. However, this depends on how your Dockerfile is configured. Codespace builds your image every time it starts, so if your Dockerfile includes multiple pip install , it may take longer.

Once your Codespace instance is already running, you won\u2019t need to wait again when you return to it.

Q: Is there a way to optimize the startup speed?

Codespace offers a prebuild feature, which can significantly improve startup speed. In our sample project, we found it's helpful to reduce prebuild when we set prebuild available to only sepecific regions. However, you need to ensure that the image is up-to-date. To strike a balance between speed and update frequency, you can consider scheduling a weekly image rebuild.

Q: Can a Codespace environment be shared? Can different people access the same Codespace instance?

A Codespace environment is tied to each individual GitHub user account. Therefore, a Codespace instance opened by User A cannot be directly accessed by User B. However, User A can set a specific port to be public and share the URL with others. However, the instance is still running under User A's account.

Q: Personal Codespace vs. Organization Codespace? How is Codespace billed?

By default, Codespace usage is billed to a GitHub personal account. GitHub offers a free tier, allowing each user 120 core hours per month for free.

For GitHub organization accounts, you can configure all Codespace charges to be billed to the organization. In this case, billing is attributed to the organization rather than personal accounts.

For more details on billing, please refer to the official CodeSpace billing documentation.

Q: How to configure codespaces?

Codespace utilizes VSCode Dev Containers technology, which can be executed either locally (via Docker) or in the cloud (via GitHub Codespace). The configuration above provided are primarily recommendations for the Recce Cloud setup. For more advanced configuration options, you can refer to the VSCode dev containers or the containers.dev documentation.

The default configuration for GitHub Codespace is explained in the documentation. If you're looking to set up a development environment for dbt/Recce, you can also refer to the Python project configuration documentation.

Q: Can I have multiple devcontainer configurations? Which one is selected by default?

Yes, you can. Please refer to the Codespaces documentation for more details. Recce Cloud will prioritize .devcontainer/recce/devcontainer.json if it is available. If not, it will default to .devcontainer/devcontainer.json, and then any other available configurations.

Q: Should I delete Codespace after PR merged?

Yes. When you merged a PR, you'll see the Delete codespace message. You can delete the Codespace to save the free usage.

You can also delete Codespace in your GitHub main branch or in Recce Cloud PR page.

"},{"location":"docs/recce-cloud/architecture/","title":"Recce Cloud Architecture","text":"

In this section, we will describe the architecture of Recce Cloud.

"},{"location":"docs/recce-cloud/architecture/security/","title":"Security","text":"

Recce Cloud is designed to be secure by default. We take security seriously and consider the data privacy from the beginning of the design phase. In this document, we will describe how we handle your data on Recce Cloud.

"},{"location":"docs/recce-cloud/architecture/security/#state-file-in-recce-cloud","title":"State File in Recce Cloud","text":"

When users execute recce run or recce server with option --cloud, Recce will upload the state file into the Recce Cloud. In the meanwhile, Recce will also request users provide the encryption password by option --password or -p. Recce will use the password to encrypt the state file when uploading and decrypt the state file when downloading. The password will not be stored in Recce Cloud, so users need to keep it safe.

"},{"location":"docs/recce-cloud/architecture/security/#how-recce-encrypts-the-state-file","title":"How Recce Encrypts the State File","text":"

Once users choose to upload the state file to Recce Cloud, Recce will store the state file in the cloud storage. Recce Cloud will use AWS S3 to store the state file and enable the server-side encryption with customer-provided keys (SSE-C). Recce Cloud will use the encryption password provided by users to encrypt the state file before uploading it to the S3 bucket. The encryption algorithm is AES-256. The state file will be decrypted with the same password when downloading it from the S3 bucket.

Based on the above flow chart, the state file will be encrypted and decrypted on the AWS S3 side. Recce Cloud server will only in charge of generating the pre-signed URL for uploading and downloading the state file. Which means Recce Cloud server will not have access to the state file content and the password key.

"},{"location":"docs/recce-cloud/architecture/self-hosted/","title":"Self Hosted Recce Instance","text":"

Note

The Self-Hosted Recce Instance is currently under development.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#self-hosted-recce-instance","title":"Self-Hosted Recce Instance","text":"

In a collaborative dbt project, we often want each in-progress PR to have a corresponding Recce server for review, which we call a Recce Instance. There are three solutions to create a Recce Instance.

  • GitHub Codespaces: Run the Recce Instance in the user's GitHub Codespace. Due to design limitations of codespaces, each codespace must be opened under a specific user, so each user needs to launch their own Recce Instance. This method also requires every user to have a GitHub account, which might not be suitable for some teams.

  • Self-Hosted Recce Instance: Run the Recce Instance on your own cloud infrastructure (AWS, GCP, Azure, or on-premise machines). Since it is self-hosted, it offers greater flexibility compared to GitHub Codespaces, and because the server is hosted on your own infrastructure, it can provide better privacy and security. However, this comes at the cost of additional management overhead.

  • Recce-Managed Recce Instance: We would host the Recce Instance on Recce's own cloud infrastructure, directly connecting to your warehouse. The advantage is minimal setup cost, but there are privacy and security considerations. Currently, we do not offer this method.

In this document, we will explain the design and architecture of the Self-hosted Recce Instance solution.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#components","title":"Components","text":"
  1. Recce Cloud: Provides an API to manage instances through an user interface.
  2. Recce Cloud Agent: A service running on your own cloud infrastructure. It communicates with Recce Cloud, creates and manages Recce Instances. The agent requires configuration with the Recce Cloud API token and the necessary data warehouse credentials for the Recce Instances.
  3. Recce Instance: An instance running Recce that requires a publicly accessible URL.
"},{"location":"docs/recce-cloud/architecture/self-hosted/#recce-cloud-agent-type","title":"Recce Cloud Agent Type","text":"
  • Docker: The Docker agent runs instances within Docker containers, making it suitable for a single VM serving multiple Recce Instances through different ports. However, if too many instances are run concurrently, it may lead to insufficient VM resources.

  • GCP Cloud Run: This approach runs the Recce Instances on GCP Cloud Run. Since GCP Cloud Run can scale the number of instances based on demand, it avoids the resource limitations faced by a single VM with Docker agents.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#recce-instance-lifecycle","title":"Recce Instance Lifecycle","text":"
  1. The user creates a Recce Instance for a PR.
  2. The agent receives the request and provisions the Recce Instance.
  3. Once the instance is ready, the URL endpoint becomes accessible.
  4. The user can stop or start an instance as needed.
  5. The user can delete a Recce Instance, and the agent will handle the deletion of the underlying container.
"},{"location":"docs/recce-cloud/architecture/self-hosted/#access-control","title":"Access Control","text":"

Recce Instances allow you to connect directly to the database through the interface, making access control a key consideration. Here are several options:

  • Virtual Private Network: If your Recce Instance is running inside a VPC and is not accessible externally, this is the most secure and reliable approach.
  • Basic Authentication: The Recce Instance is publicly accessible but protected via HTTP Basic Authentication. Accessing the Recce Instance requires providing a user/password.
  • Tunnel-based Authentication: The Recce Instance runs within a VPC but can be exposed for external access using services such as ngrok or Tailscale. These services provide access control features to secure the instance.
  • Recce Cloud Authentication: Utilize Recce Cloud's login state to access the Recce Instance. The Recce Instance acts as an OIDC app within Recce Cloud, authenticating users through the OIDC protocol.
"},{"location":"docs/recce-cloud/architecture/self-hosted/#faq","title":"FAQ","text":""},{"location":"docs/recce-cloud/architecture/self-hosted/#q-does-recce-cloud-make-requests-to-the-agents-endpoint","title":"Q: Does Recce Cloud make requests to the Agent's endpoint?","text":"

No. All actions are initiated by the agent making requests to the Recce Cloud API.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#q-do-i-need-to-store-my-credentials-in-recce-cloud","title":"Q: Do I need to store my credentials in Recce Cloud?","text":"

No. All credentials are configured within the agent, which uses these settings to start a Recce Instance. This ensures you do not encounter any credential leakage issues.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#q-does-recce-agent-manage-the-entire-organization-or-a-single-repository","title":"Q: Does Recce Agent manage the entire organization or a single repository?","text":"

It manages the entire organization. However, support for single repository configurations may be available in the future.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#q-do-i-need-to-define-what-container-image-the-recce-instance-uses-can-i-customize-my-own-container-image","title":"Q: Do I need to define what container image the Recce Instance uses? Can I customize my own container image?","text":"

We provide an official Recce container that will check out your branch and install the necessary dependencies. You can also customize your own container image to speed up startup times or provide more flexible initialization processes.

"},{"location":"docs/recce-cloud/architecture/self-hosted/#q-how-do-i-run-a-recce-cloud-agent-where-should-my-cloud-run-agent-be-hosted","title":"Q: How do I run a Recce Cloud Agent? Where should my Cloud Run Agent be hosted?","text":"

The Recce Cloud Agent is a Python application. The recommended execution method varies depending on the agent type. The Docker agent runs within a Docker container, while the GCP Cloud Run agent runs within a Cloud Run container.

"},{"location":"docs/reference/configuration/","title":"Configuration","text":"

The config file for Recce is located in recce.yml. Currently, only preset checks are configurable.

"},{"location":"docs/reference/configuration/#preset-checks","title":"Preset Checks","text":"

Preset checks can be generated when executing recce server or recce run.

# recce.yml\nchecks:\n  - name: Query diff of customers\n    description: |\n        This is the demo preset check.\n\n        Please run the query and paste the screenshot to the PR comment.\n    type: query_diff\n    params:\n        sql_template: select * from {{ ref(\"customers\") }}\n    view_options:\n        primary_keys:\n        - customer_id\n
Field Description Type name the title of the check string description the description of the check string type the type of the check string params the parameters for running the check object view_options the options for presenting the run result object

Note

Regarding the supported types and settings for each type, the documentation does not currently provide this information. Please obtain the settings by acquiring the preset checks template through the Web UI.

"},{"location":"blog/archive/2024/","title":"2024","text":""},{"location":"blog/category/data-exploration/","title":"Data Exploration","text":""},{"location":"blog/category/impact-assessment/","title":"Impact assessment","text":""},{"location":"blog/category/data-validation/","title":"Data Validation","text":""},{"location":"blog/category/pull-request/","title":"Pull Request","text":""},{"location":"blog/category/pr-review/","title":"PR Review","text":""},{"location":"blog/category/best-practices/","title":"Best Practices","text":""},{"location":"blog/category/announcements/","title":"Announcements","text":""},{"location":"blog/category/concepts/","title":"Concepts","text":""},{"location":"blog/category/self-serve-review/","title":"Self-Serve Review","text":""},{"location":"blog/category/dbt/","title":"dbt","text":""},{"location":"blog/category/webinar/","title":"Webinar","text":""},{"location":"blog/category/usage/","title":"Usage","text":""},{"location":"blog/category/features/","title":"Features","text":""},{"location":"blog/tags/","title":"Tags","text":"

Following is a list of tags for Recce Blog content:

"},{"location":"blog/tags/#best-practices","title":"Best Practices","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#data-integrity","title":"Data Integrity","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#data-quality","title":"Data Quality","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#data-review","title":"Data Review","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#data-validaton","title":"Data Validaton","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#dataops","title":"DataOps","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#self-serve-analytics","title":"Self-Serve Analytics","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#self-serve-data","title":"Self-Serve Data","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#data-exploration","title":"data exploration","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#data-impact","title":"data impact","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#data-validaton_1","title":"data validaton","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#dbt-ci","title":"dbt CI","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#dbt-pr-review","title":"dbt PR Review","text":"
  • Support Self-Serve Data with Comprehensive PR Review
  • The Ultimate PR Comment Template Boilerplate for dbt data projects
"},{"location":"blog/tags/#dbt-data-model-validation","title":"dbt data model validation","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#dbt-models","title":"dbt models","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#impact-assessment","title":"impact assessment","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"},{"location":"blog/tags/#root-cause-analysis","title":"root cause analysis","text":"
  • Explore data impact and focus on tracking data validations with Recce's new interface
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..a0b9db58 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,247 @@ + + + + https://datarecce.io/ + 2024-12-04 + + + https://datarecce.io/case-studies/ + 2024-12-04 + + + https://datarecce.io/cloud/ + 2024-12-04 + + + https://datarecce.io/firesidechat-watch/ + 2024-12-04 + + + https://datarecce.io/firesidechat/ + 2024-12-04 + + + https://datarecce.io/blog/ + 2024-12-04 + + + https://datarecce.io/blog/data-validaton-toolkit-for-dbt-data-projects/ + 2024-12-04 + + + https://datarecce.io/blog/hands-on-data-impact-analysis-recce/ + 2024-12-04 + + + https://datarecce.io/blog/histogram-overlay-top-k-dbt/ + 2024-12-04 + + + https://datarecce.io/blog/check-critical-models/ + 2024-12-04 + + + https://datarecce.io/blog/from-devops-to-dataops-effective-data-productivity/ + 2024-12-04 + + + https://datarecce.io/blog/self-serve-analytics-pr-review/ + 2024-12-04 + + + https://datarecce.io/blog/meet-recce-at-coalesce-2024-and-the-data-renegate-happy-hour/ + 2024-12-04 + + + https://datarecce.io/blog/dbt-data-pr-comment-template/ + 2024-12-04 + + + https://datarecce.io/blog/explore-data-impact-with-focus/ + 2024-12-04 + + + https://datarecce.io/case-studies/rio-department-of-health/ + 2024-12-04 + + + https://datarecce.io/docs/ + 2024-12-04 + + + https://datarecce.io/docs/demo/ + 2024-12-04 + + + https://datarecce.io/docs/get-started-jaffle-shop/ + 2024-12-04 + + + https://datarecce.io/docs/get-started/ + 2024-12-04 + + + https://datarecce.io/docs/installation/ + 2024-12-04 + + + https://datarecce.io/docs/start-with-dbt-cloud/ + 2024-12-04 + + + https://datarecce.io/docs/start-with-sqlmesh/ + 2024-12-04 + + + https://datarecce.io/docs/architecture/overview/ + 2024-12-04 + + + https://datarecce.io/docs/features/checklist/ + 2024-12-04 + + + https://datarecce.io/docs/features/lineage/ + 2024-12-04 + + + https://datarecce.io/docs/features/node-selection/ + 2024-12-04 + + + https://datarecce.io/docs/features/preset-checks/ + 2024-12-04 + + + https://datarecce.io/docs/features/query/ + 2024-12-04 + + + https://datarecce.io/docs/features/recce-run/ + 2024-12-04 + + + https://datarecce.io/docs/features/recce-summary-example/ + 2024-12-04 + + + https://datarecce.io/docs/features/recce-summary/ + 2024-12-04 + + + https://datarecce.io/docs/features/state-file/ + 2024-12-04 + + + https://datarecce.io/docs/guides/best-practices-prep-env/ + 2024-12-04 + + + https://datarecce.io/docs/guides/scenario-ci/ + 2024-12-04 + + + https://datarecce.io/docs/guides/scenario-dev/ + 2024-12-04 + + + https://datarecce.io/docs/guides/scenario-pr-review/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/expose-recce-instance-visibility/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/getting-started-recce-cloud/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/setup-gh-actions/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/setup-gh-codespaces/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/architecture/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/architecture/security/ + 2024-12-04 + + + https://datarecce.io/docs/recce-cloud/architecture/self-hosted/ + 2024-12-04 + + + https://datarecce.io/docs/reference/configuration/ + 2024-12-04 + + + https://datarecce.io/blog/archive/2024/ + 2024-12-04 + + + https://datarecce.io/blog/category/data-exploration/ + 2024-12-04 + + + https://datarecce.io/blog/category/impact-assessment/ + 2024-12-04 + + + https://datarecce.io/blog/category/data-validation/ + 2024-12-04 + + + https://datarecce.io/blog/category/pull-request/ + 2024-12-04 + + + https://datarecce.io/blog/category/pr-review/ + 2024-12-04 + + + https://datarecce.io/blog/category/best-practices/ + 2024-12-04 + + + https://datarecce.io/blog/category/announcements/ + 2024-12-04 + + + https://datarecce.io/blog/category/concepts/ + 2024-12-04 + + + https://datarecce.io/blog/category/self-serve-review/ + 2024-12-04 + + + https://datarecce.io/blog/category/dbt/ + 2024-12-04 + + + https://datarecce.io/blog/category/webinar/ + 2024-12-04 + + + https://datarecce.io/blog/category/usage/ + 2024-12-04 + + + https://datarecce.io/blog/category/features/ + 2024-12-04 + + + https://datarecce.io/blog/tags/ + 2024-12-04 + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 00000000..733190a3 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/styles/extra.css b/styles/extra.css new file mode 100644 index 00000000..34676404 --- /dev/null +++ b/styles/extra.css @@ -0,0 +1,157 @@ +.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 { + font-weight: bold; +} +.md-typeset h1 { + color: --md-default-fg-color; +} + +.md-typeset code:not(.md-typeset pre > code) { + color: #ff6e42; + display: inline-block; + border-radius: 3px; +} + +/**** Nav ****/ + +/* .md-nav { + font-size: 0.8rem; +} */ + +/*.md-nav__title { + display: none; +}*/ + +/* .md-nav__item { + padding: 0.3rem 0; + font-weight: bold; +} */ + +.md-nav__link[href]:hover { + color: #ff6e42; +} + +/**** Blog ****/ + +.md-post--excerpt .md-post__content h2 { + line-height: 1.2; +} + +.md-post__action a { + background-color: #ff6e42; + color: #fff !important; + padding: .3rem 1rem; + border-radius: 1rem; + text-transform: uppercase; + font-weight: bold; + display: inline-block; + margin-top: 1rem; +} +.md-post__action a:hover { + border-bottom: none !important; +} + + +.md-post--excerpt { + padding-bottom: 2.5rem; + margin-bottom: 2.5rem; + border-bottom: dashed black 1px; +} + +.md-post--excerpt:last-of-type { + border-bottom: none; +} + +.md-content__inner .article-cta { + border-top: dashed 1px #d2d2d2; + padding-top: 1rem; + margin-top: 1rem; +} + +.md-content__inner .mailchimp-cta { + background-color: #ff6e42; + border-radius: 1rem; + overflow: hidden; + color: #fff; + margin-top: 1rem; + position: relative; + padding-left: 10%; + padding-right: 10%; +} + +.md-content__inner .mailchimp-cta::before { + content: ""; + position: absolute; + background-image: url('/assets/images/Logo_Recce_white-fs8.png'); + background-size: contain; + background-repeat: no-repeat; + opacity: 20%; + width: 110%; + height: 110%; + top: -5%; + left: 0; +} + +.md-content__inner .mailchimp-cta #mc_embed_signup form { + padding-top: 1rem; + padding-bottom: 1rem; + margin-top: 0; + margin-bottom: 0; + text-align: center; +} + +.md-content__inner .mailchimp-cta #mc_embed_signup form h2 { + text-shadow: 1px 1px 3px rgba(0,0,0,0.3); + text-transform: uppercase; + margin-bottom: 0; +} +.md-content__inner .mailchimp-cta #mc_embed_signup form h3 { + margin-top: 0; +} + +.md-content__inner .mailchimp-cta #mc_embed_signup .button { + text-transform: uppercase; + color: #ff6e42; + font-weight: bold; + background-color: #fff; +} + +.md-content__inner .mailchimp-cta #mc_embed_signup .button:hover { + background-color: #000; +} + +.md-content__inner .mailchimp-cta #mc_embed_signup #mce-success-response { + color: #fff; +} + +.md-content__inner .mailchimp-cta #mc_embed_signup div.response { + width: 100%; +} + + +/**** Links ****/ + +.md-typeset a { + +} +.md-typeset a:hover, .md-typeset a:focus, +.md-post .md-post__meta a:hover, .md-post .md-post__meta a:focus { + border-bottom: solid #ff6e42 1px; + color: #ff6e42; +} + +.md-typeset .md-button:hover, .md-typeset .md-button:focus { + background-color: #e04f23; + border-color: #e04f23; + +} + +h1 a:hover, h2 a:hover a, h3 a:hover { + border-bottom: none; +/* background-color: #ff6e42;*/ +/* color: #fff;*/ +} + +.shadow { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + diff --git a/styles/home.css b/styles/home.css new file mode 100644 index 00000000..3abb684a --- /dev/null +++ b/styles/home.css @@ -0,0 +1,13041 @@ +@charset "UTF-8"; +/*! + * Bootstrap v5.3.2 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root, +[data-bs-theme=light] { + --bs-blue: #0d6efd; + --bs-indigo: #6610f2; + --bs-purple: #6f42c1; + --bs-pink: #d63384; + --bs-red: #dc3545; + --bs-orange: #fd7e14; + --bs-yellow: #ffc107; + --bs-green: #198754; + --bs-teal: #20c997; + --bs-cyan: #0dcaf0; + --bs-black: #000; + --bs-white: #fff; + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-gray-100: #f8f9fa; + --bs-gray-200: #e9ecef; + --bs-gray-300: #dee2e6; + --bs-gray-400: #ced4da; + --bs-gray-500: #adb5bd; + --bs-gray-600: #6c757d; + --bs-gray-700: #495057; + --bs-gray-800: #343a40; + --bs-gray-900: #212529; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #198754; + --bs-info: #0dcaf0; + --bs-warning: #ffc107; + --bs-danger: #dc3545; + --bs-light: #f8f9fa; + --bs-dark: #212529; + --bs-primary-rgb: 13, 110, 253; + --bs-secondary-rgb: 108, 117, 125; + --bs-success-rgb: 25, 135, 84; + --bs-info-rgb: 13, 202, 240; + --bs-warning-rgb: 255, 193, 7; + --bs-danger-rgb: 220, 53, 69; + --bs-light-rgb: 248, 249, 250; + --bs-dark-rgb: 33, 37, 41; + --bs-primary-text-emphasis: #052c65; + --bs-secondary-text-emphasis: #2b2f32; + --bs-success-text-emphasis: #0a3622; + --bs-info-text-emphasis: #055160; + --bs-warning-text-emphasis: #664d03; + --bs-danger-text-emphasis: #58151c; + --bs-light-text-emphasis: #495057; + --bs-dark-text-emphasis: #495057; + --bs-primary-bg-subtle: #cfe2ff; + --bs-secondary-bg-subtle: #e2e3e5; + --bs-success-bg-subtle: #d1e7dd; + --bs-info-bg-subtle: #cff4fc; + --bs-warning-bg-subtle: #fff3cd; + --bs-danger-bg-subtle: #f8d7da; + --bs-light-bg-subtle: #fcfcfd; + --bs-dark-bg-subtle: #ced4da; + --bs-primary-border-subtle: #9ec5fe; + --bs-secondary-border-subtle: #c4c8cb; + --bs-success-border-subtle: #a3cfbb; + --bs-info-border-subtle: #9eeaf9; + --bs-warning-border-subtle: #ffe69c; + --bs-danger-border-subtle: #f1aeb5; + --bs-light-border-subtle: #e9ecef; + --bs-dark-border-subtle: #adb5bd; + --bs-white-rgb: 255, 255, 255; + --bs-black-rgb: 0, 0, 0; + --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); + --bs-body-font-family: var(--bs-font-sans-serif); + --bs-body-font-size: 1rem; + --bs-body-font-weight: 400; + --bs-body-line-height: 1.5; + --bs-body-color: #212529; + --bs-body-color-rgb: 33, 37, 41; + --bs-body-bg: #fff; + --bs-body-bg-rgb: 255, 255, 255; + --bs-emphasis-color: #000; + --bs-emphasis-color-rgb: 0, 0, 0; + --bs-secondary-color: rgba(33, 37, 41, 0.75); + --bs-secondary-color-rgb: 33, 37, 41; + --bs-secondary-bg: #e9ecef; + --bs-secondary-bg-rgb: 233, 236, 239; + --bs-tertiary-color: rgba(33, 37, 41, 0.5); + --bs-tertiary-color-rgb: 33, 37, 41; + --bs-tertiary-bg: #f8f9fa; + --bs-tertiary-bg-rgb: 248, 249, 250; + --bs-heading-color: inherit; + --bs-link-color: #0d6efd; + --bs-link-color-rgb: 13, 110, 253; + --bs-link-decoration: underline; + --bs-link-hover-color: #0a58ca; + --bs-link-hover-color-rgb: 10, 88, 202; + --bs-code-color: #d63384; + --bs-highlight-color: #212529; + --bs-highlight-bg: #fff3cd; + --bs-border-width: 1px; + --bs-border-style: solid; + --bs-border-color: #dee2e6; + --bs-border-color-translucent: rgba(0, 0, 0, 0.175); + --bs-border-radius: 0.375rem; + --bs-border-radius-sm: 0.25rem; + --bs-border-radius-lg: 0.5rem; + --bs-border-radius-xl: 1rem; + --bs-border-radius-xxl: 2rem; + --bs-border-radius-2xl: var(--bs-border-radius-xxl); + --bs-border-radius-pill: 50rem; + --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); + --bs-focus-ring-width: 0.25rem; + --bs-focus-ring-opacity: 0.25; + --bs-focus-ring-color: rgba(13, 110, 253, 0.25); + --bs-form-valid-color: #198754; + --bs-form-valid-border-color: #198754; + --bs-form-invalid-color: #dc3545; + --bs-form-invalid-border-color: #dc3545; +} + +[data-bs-theme=dark] { + color-scheme: dark; + --bs-body-color: #dee2e6; + --bs-body-color-rgb: 222, 226, 230; + --bs-body-bg: #212529; + --bs-body-bg-rgb: 33, 37, 41; + --bs-emphasis-color: #fff; + --bs-emphasis-color-rgb: 255, 255, 255; + --bs-secondary-color: rgba(222, 226, 230, 0.75); + --bs-secondary-color-rgb: 222, 226, 230; + --bs-secondary-bg: #343a40; + --bs-secondary-bg-rgb: 52, 58, 64; + --bs-tertiary-color: rgba(222, 226, 230, 0.5); + --bs-tertiary-color-rgb: 222, 226, 230; + --bs-tertiary-bg: #2b3035; + --bs-tertiary-bg-rgb: 43, 48, 53; + --bs-primary-text-emphasis: #6ea8fe; + --bs-secondary-text-emphasis: #a7acb1; + --bs-success-text-emphasis: #75b798; + --bs-info-text-emphasis: #6edff6; + --bs-warning-text-emphasis: #ffda6a; + --bs-danger-text-emphasis: #ea868f; + --bs-light-text-emphasis: #f8f9fa; + --bs-dark-text-emphasis: #dee2e6; + --bs-primary-bg-subtle: #031633; + --bs-secondary-bg-subtle: #161719; + --bs-success-bg-subtle: #051b11; + --bs-info-bg-subtle: #032830; + --bs-warning-bg-subtle: #332701; + --bs-danger-bg-subtle: #2c0b0e; + --bs-light-bg-subtle: #343a40; + --bs-dark-bg-subtle: #1a1d20; + --bs-primary-border-subtle: #084298; + --bs-secondary-border-subtle: #41464b; + --bs-success-border-subtle: #0f5132; + --bs-info-border-subtle: #087990; + --bs-warning-border-subtle: #997404; + --bs-danger-border-subtle: #842029; + --bs-light-border-subtle: #495057; + --bs-dark-border-subtle: #343a40; + --bs-heading-color: inherit; + --bs-link-color: #6ea8fe; + --bs-link-hover-color: #8bb9fe; + --bs-link-color-rgb: 110, 168, 254; + --bs-link-hover-color-rgb: 139, 185, 254; + --bs-code-color: #e685b5; + --bs-highlight-color: #dee2e6; + --bs-highlight-bg: #664d03; + --bs-border-color: #495057; + --bs-border-color-translucent: rgba(255, 255, 255, 0.15); + --bs-form-valid-color: #75b798; + --bs-form-valid-border-color: #75b798; + --bs-form-invalid-color: #ea868f; + --bs-form-invalid-border-color: #ea868f; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; + } +} + +body { + margin: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-body-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +hr { + margin: 1rem 0; + color: inherit; + border: 0; + border-top: var(--bs-border-width) solid; + opacity: 0.25; +} + +h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; + color: var(--bs-heading-color); +} + +h1, .h1 { + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1, .h1 { + font-size: 2.5rem; + } +} + +h2, .h2 { + font-size: calc(1.325rem + 0.9vw); +} +@media (min-width: 1200px) { + h2, .h2 { + font-size: 2rem; + } +} + +h3, .h3 { + font-size: calc(1.3rem + 0.6vw); +} +@media (min-width: 1200px) { + h3, .h3 { + font-size: 1.75rem; + } +} + +h4, .h4 { + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + h4, .h4 { + font-size: 1.5rem; + } +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title] { + text-decoration: underline dotted; + cursor: help; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul { + padding-left: 2rem; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small, .small { + font-size: 0.875em; +} + +mark, .mark { + padding: 0.1875em; + color: var(--bs-highlight-color); + background-color: var(--bs-highlight-bg); +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); + text-decoration: underline; +} +a:hover { + --bs-link-color-rgb: var(--bs-link-hover-color-rgb); +} + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: var(--bs-font-monospace); + font-size: 1em; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 0.875em; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +code { + font-size: 0.875em; + color: var(--bs-code-color); + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.1875rem 0.375rem; + font-size: 0.875em; + color: var(--bs-body-bg); + background-color: var(--bs-body-color); + border-radius: 0.25rem; +} +kbd kbd { + padding: 0; + font-size: 1em; +} + +figure { + margin: 0 0 1rem; +} + +img, +svg { + vertical-align: middle; +} + +table { + caption-side: bottom; + border-collapse: collapse; +} + +caption { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-secondary-color); + text-align: left; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +label { + display: inline-block; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} +select:disabled { + opacity: 1; +} + +[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { + display: none !important; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: calc(1.275rem + 0.3vw); + line-height: inherit; +} +@media (min-width: 1200px) { + legend { + font-size: 1.5rem; + } +} +legend + * { + clear: left; +} + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-inner-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-color-swatch-wrapper { + padding: 0; +} + +::file-selector-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +iframe { + border: 0; +} + +summary { + display: list-item; + cursor: pointer; +} + +progress { + vertical-align: baseline; +} + +[hidden] { + display: none !important; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: calc(1.625rem + 4.5vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-1 { + font-size: 5rem; + } +} + +.display-2 { + font-size: calc(1.575rem + 3.9vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-2 { + font-size: 4.5rem; + } +} + +.display-3 { + font-size: calc(1.525rem + 3.3vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-3 { + font-size: 4rem; + } +} + +.display-4 { + font-size: calc(1.475rem + 2.7vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-4 { + font-size: 3.5rem; + } +} + +.display-5 { + font-size: calc(1.425rem + 2.1vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-5 { + font-size: 3rem; + } +} + +.display-6 { + font-size: calc(1.375rem + 1.5vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-6 { + font-size: 2.5rem; + } +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 0.875em; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} +.blockquote > :last-child { + margin-bottom: 0; +} + +.blockquote-footer { + margin-top: -1rem; + margin-bottom: 1rem; + font-size: 0.875em; + color: #6c757d; +} +.blockquote-footer::before { + content: "— "; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: var(--bs-body-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 0.875em; + color: var(--bs-secondary-color); +} + +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +:root { + --bs-breakpoint-xs: 0; + --bs-breakpoint-sm: 576px; + --bs-breakpoint-md: 768px; + --bs-breakpoint-lg: 992px; + --bs-breakpoint-xl: 1200px; + --bs-breakpoint-xxl: 1400px; +} + +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-right: calc(-0.5 * var(--bs-gutter-x)); + margin-left: calc(-0.5 * var(--bs-gutter-x)); +} +.row > * { + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0%; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.33333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-left: 8.33333333%; +} + +.offset-2 { + margin-left: 16.66666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333333%; +} + +.offset-5 { + margin-left: 41.66666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333333%; +} + +.offset-8 { + margin-left: 66.66666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333333%; +} + +.offset-11 { + margin-left: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0%; + } + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.33333333%; + } + .offset-sm-2 { + margin-left: 16.66666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333333%; + } + .offset-sm-5 { + margin-left: 41.66666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333333%; + } + .offset-sm-8 { + margin-left: 66.66666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333333%; + } + .offset-sm-11 { + margin-left: 91.66666667%; + } + .g-sm-0, + .gx-sm-0 { + --bs-gutter-x: 0; + } + .g-sm-0, + .gy-sm-0 { + --bs-gutter-y: 0; + } + .g-sm-1, + .gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + .g-sm-1, + .gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + .g-sm-2, + .gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + .g-sm-2, + .gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + .g-sm-3, + .gx-sm-3 { + --bs-gutter-x: 1rem; + } + .g-sm-3, + .gy-sm-3 { + --bs-gutter-y: 1rem; + } + .g-sm-4, + .gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + .g-sm-4, + .gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + .g-sm-5, + .gx-sm-5 { + --bs-gutter-x: 3rem; + } + .g-sm-5, + .gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0%; + } + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.33333333%; + } + .offset-md-2 { + margin-left: 16.66666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333333%; + } + .offset-md-5 { + margin-left: 41.66666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333333%; + } + .offset-md-8 { + margin-left: 66.66666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333333%; + } + .offset-md-11 { + margin-left: 91.66666667%; + } + .g-md-0, + .gx-md-0 { + --bs-gutter-x: 0; + } + .g-md-0, + .gy-md-0 { + --bs-gutter-y: 0; + } + .g-md-1, + .gx-md-1 { + --bs-gutter-x: 0.25rem; + } + .g-md-1, + .gy-md-1 { + --bs-gutter-y: 0.25rem; + } + .g-md-2, + .gx-md-2 { + --bs-gutter-x: 0.5rem; + } + .g-md-2, + .gy-md-2 { + --bs-gutter-y: 0.5rem; + } + .g-md-3, + .gx-md-3 { + --bs-gutter-x: 1rem; + } + .g-md-3, + .gy-md-3 { + --bs-gutter-y: 1rem; + } + .g-md-4, + .gx-md-4 { + --bs-gutter-x: 1.5rem; + } + .g-md-4, + .gy-md-4 { + --bs-gutter-y: 1.5rem; + } + .g-md-5, + .gx-md-5 { + --bs-gutter-x: 3rem; + } + .g-md-5, + .gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0%; + } + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.33333333%; + } + .offset-lg-2 { + margin-left: 16.66666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333333%; + } + .offset-lg-5 { + margin-left: 41.66666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333333%; + } + .offset-lg-8 { + margin-left: 66.66666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333333%; + } + .offset-lg-11 { + margin-left: 91.66666667%; + } + .g-lg-0, + .gx-lg-0 { + --bs-gutter-x: 0; + } + .g-lg-0, + .gy-lg-0 { + --bs-gutter-y: 0; + } + .g-lg-1, + .gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + .g-lg-1, + .gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + .g-lg-2, + .gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + .g-lg-2, + .gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + .g-lg-3, + .gx-lg-3 { + --bs-gutter-x: 1rem; + } + .g-lg-3, + .gy-lg-3 { + --bs-gutter-y: 1rem; + } + .g-lg-4, + .gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + .g-lg-4, + .gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + .g-lg-5, + .gx-lg-5 { + --bs-gutter-x: 3rem; + } + .g-lg-5, + .gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0%; + } + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.33333333%; + } + .offset-xl-2 { + margin-left: 16.66666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333333%; + } + .offset-xl-5 { + margin-left: 41.66666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333333%; + } + .offset-xl-8 { + margin-left: 66.66666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333333%; + } + .offset-xl-11 { + margin-left: 91.66666667%; + } + .g-xl-0, + .gx-xl-0 { + --bs-gutter-x: 0; + } + .g-xl-0, + .gy-xl-0 { + --bs-gutter-y: 0; + } + .g-xl-1, + .gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xl-1, + .gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xl-2, + .gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xl-2, + .gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xl-3, + .gx-xl-3 { + --bs-gutter-x: 1rem; + } + .g-xl-3, + .gy-xl-3 { + --bs-gutter-y: 1rem; + } + .g-xl-4, + .gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xl-4, + .gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xl-5, + .gx-xl-5 { + --bs-gutter-x: 3rem; + } + .g-xl-5, + .gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0%; + } + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xxl-0 { + margin-left: 0; + } + .offset-xxl-1 { + margin-left: 8.33333333%; + } + .offset-xxl-2 { + margin-left: 16.66666667%; + } + .offset-xxl-3 { + margin-left: 25%; + } + .offset-xxl-4 { + margin-left: 33.33333333%; + } + .offset-xxl-5 { + margin-left: 41.66666667%; + } + .offset-xxl-6 { + margin-left: 50%; + } + .offset-xxl-7 { + margin-left: 58.33333333%; + } + .offset-xxl-8 { + margin-left: 66.66666667%; + } + .offset-xxl-9 { + margin-left: 75%; + } + .offset-xxl-10 { + margin-left: 83.33333333%; + } + .offset-xxl-11 { + margin-left: 91.66666667%; + } + .g-xxl-0, + .gx-xxl-0 { + --bs-gutter-x: 0; + } + .g-xxl-0, + .gy-xxl-0 { + --bs-gutter-y: 0; + } + .g-xxl-1, + .gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xxl-1, + .gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xxl-2, + .gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xxl-2, + .gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xxl-3, + .gx-xxl-3 { + --bs-gutter-x: 1rem; + } + .g-xxl-3, + .gy-xxl-3 { + --bs-gutter-y: 1rem; + } + .g-xxl-4, + .gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xxl-4, + .gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xxl-5, + .gx-xxl-5 { + --bs-gutter-x: 3rem; + } + .g-xxl-5, + .gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.table { + --bs-table-color-type: initial; + --bs-table-bg-type: initial; + --bs-table-color-state: initial; + --bs-table-bg-state: initial; + --bs-table-color: var(--bs-emphasis-color); + --bs-table-bg: var(--bs-body-bg); + --bs-table-border-color: var(--bs-border-color); + --bs-table-accent-bg: transparent; + --bs-table-striped-color: var(--bs-emphasis-color); + --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05); + --bs-table-active-color: var(--bs-emphasis-color); + --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1); + --bs-table-hover-color: var(--bs-emphasis-color); + --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075); + width: 100%; + margin-bottom: 1rem; + vertical-align: top; + border-color: var(--bs-table-border-color); +} +.table > :not(caption) > * > * { + padding: 0.5rem 0.5rem; + color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color))); + background-color: var(--bs-table-bg); + border-bottom-width: var(--bs-border-width); + box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); +} +.table > tbody { + vertical-align: inherit; +} +.table > thead { + vertical-align: bottom; +} + +.table-group-divider { + border-top: calc(var(--bs-border-width) * 2) solid currentcolor; +} + +.caption-top { + caption-side: top; +} + +.table-sm > :not(caption) > * > * { + padding: 0.25rem 0.25rem; +} + +.table-bordered > :not(caption) > * { + border-width: var(--bs-border-width) 0; +} +.table-bordered > :not(caption) > * > * { + border-width: 0 var(--bs-border-width); +} + +.table-borderless > :not(caption) > * > * { + border-bottom-width: 0; +} +.table-borderless > :not(:first-child) { + border-top-width: 0; +} + +.table-striped > tbody > tr:nth-of-type(odd) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-striped-columns > :not(caption) > tr > :nth-child(even) { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-active { + --bs-table-color-state: var(--bs-table-active-color); + --bs-table-bg-state: var(--bs-table-active-bg); +} + +.table-hover > tbody > tr:hover > * { + --bs-table-color-state: var(--bs-table-hover-color); + --bs-table-bg-state: var(--bs-table-hover-bg); +} + +.table-primary { + --bs-table-color: #000; + --bs-table-bg: #cfe2ff; + --bs-table-border-color: #a6b5cc; + --bs-table-striped-bg: #c5d7f2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bacbe6; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfd1ec; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-secondary { + --bs-table-color: #000; + --bs-table-bg: #e2e3e5; + --bs-table-border-color: #b5b6b7; + --bs-table-striped-bg: #d7d8da; + --bs-table-striped-color: #000; + --bs-table-active-bg: #cbccce; + --bs-table-active-color: #000; + --bs-table-hover-bg: #d1d2d4; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-success { + --bs-table-color: #000; + --bs-table-bg: #d1e7dd; + --bs-table-border-color: #a7b9b1; + --bs-table-striped-bg: #c7dbd2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bcd0c7; + --bs-table-active-color: #000; + --bs-table-hover-bg: #c1d6cc; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-info { + --bs-table-color: #000; + --bs-table-bg: #cff4fc; + --bs-table-border-color: #a6c3ca; + --bs-table-striped-bg: #c5e8ef; + --bs-table-striped-color: #000; + --bs-table-active-bg: #badce3; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfe2e9; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-warning { + --bs-table-color: #000; + --bs-table-bg: #fff3cd; + --bs-table-border-color: #ccc2a4; + --bs-table-striped-bg: #f2e7c3; + --bs-table-striped-color: #000; + --bs-table-active-bg: #e6dbb9; + --bs-table-active-color: #000; + --bs-table-hover-bg: #ece1be; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-danger { + --bs-table-color: #000; + --bs-table-bg: #f8d7da; + --bs-table-border-color: #c6acae; + --bs-table-striped-bg: #eccccf; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfc2c4; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5c7ca; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-light { + --bs-table-color: #000; + --bs-table-bg: #f8f9fa; + --bs-table-border-color: #c6c7c8; + --bs-table-striped-bg: #ecedee; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfe0e1; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5e6e7; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-dark { + --bs-table-color: #fff; + --bs-table-bg: #212529; + --bs-table-border-color: #4d5154; + --bs-table-striped-bg: #2c3034; + --bs-table-striped-color: #fff; + --bs-table-active-bg: #373b3e; + --bs-table-active-color: #fff; + --bs-table-hover-bg: #323539; + --bs-table-hover-color: #fff; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 767.98px) { + .table-responsive-md { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 991.98px) { + .table-responsive-lg { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1199.98px) { + .table-responsive-xl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1399.98px) { + .table-responsive-xxl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +.form-label { + margin-bottom: 0.5rem; +} + +.col-form-label { + padding-top: calc(0.375rem + var(--bs-border-width)); + padding-bottom: calc(0.375rem + var(--bs-border-width)); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + var(--bs-border-width)); + padding-bottom: calc(0.5rem + var(--bs-border-width)); + font-size: 1.25rem; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + var(--bs-border-width)); + padding-bottom: calc(0.25rem + var(--bs-border-width)); + font-size: 0.875rem; +} + +.form-text { + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-secondary-color); +} + +.form-control { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + appearance: none; + background-color: var(--bs-body-bg); + background-clip: padding-box; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} +.form-control[type=file] { + overflow: hidden; +} +.form-control[type=file]:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control:focus { + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-control::-webkit-date-and-time-value { + min-width: 85px; + height: 1.5em; + margin: 0; +} +.form-control::-webkit-datetime-edit { + display: block; + padding: 0; +} +.form-control::placeholder { + color: var(--bs-secondary-color); + opacity: 1; +} +.form-control:disabled { + background-color: var(--bs-secondary-bg); + opacity: 1; +} +.form-control::file-selector-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + margin-inline-end: 0.75rem; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--bs-border-width); + border-radius: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control::file-selector-button { + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::file-selector-button { + background-color: var(--bs-secondary-bg); +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + line-height: 1.5; + color: var(--bs-body-color); + background-color: transparent; + border: solid transparent; + border-width: var(--bs-border-width) 0; +} +.form-control-plaintext:focus { + outline: 0; +} +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} +.form-control-sm::file-selector-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + margin-inline-end: 0.5rem; +} + +.form-control-lg { + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} +.form-control-lg::file-selector-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + margin-inline-end: 1rem; +} + +textarea.form-control { + min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); +} +textarea.form-control-sm { + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +textarea.form-control-lg { + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); +} + +.form-control-color { + width: 3rem; + height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); + padding: 0.375rem; +} +.form-control-color:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control-color::-moz-color-swatch { + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color::-webkit-color-swatch { + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color.form-control-sm { + height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +.form-control-color.form-control-lg { + height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); +} + +.form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + display: block; + width: 100%; + padding: 0.375rem 2.25rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + appearance: none; + background-color: var(--bs-body-bg); + background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-select { + transition: none; + } +} +.form-select:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-select[multiple], .form-select[size]:not([size="1"]) { + padding-right: 0.75rem; + background-image: none; +} +.form-select:disabled { + background-color: var(--bs-secondary-bg); +} +.form-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 var(--bs-body-color); +} + +.form-select-sm { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} + +.form-select-lg { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} + +[data-bs-theme=dark] .form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); +} + +.form-check { + display: block; + min-height: 1.5rem; + padding-left: 1.5em; + margin-bottom: 0.125rem; +} +.form-check .form-check-input { + float: left; + margin-left: -1.5em; +} + +.form-check-reverse { + padding-right: 1.5em; + padding-left: 0; + text-align: right; +} +.form-check-reverse .form-check-input { + float: right; + margin-right: -1.5em; + margin-left: 0; +} + +.form-check-input { + --bs-form-check-bg: var(--bs-body-bg); + flex-shrink: 0; + width: 1em; + height: 1em; + margin-top: 0.25em; + vertical-align: top; + appearance: none; + background-color: var(--bs-form-check-bg); + background-image: var(--bs-form-check-bg-image); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + border: var(--bs-border-width) solid var(--bs-border-color); + print-color-adjust: exact; +} +.form-check-input[type=checkbox] { + border-radius: 0.25em; +} +.form-check-input[type=radio] { + border-radius: 50%; +} +.form-check-input:active { + filter: brightness(90%); +} +.form-check-input:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-check-input:checked { + background-color: #0d6efd; + border-color: #0d6efd; +} +.form-check-input:checked[type=checkbox] { + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e"); +} +.form-check-input:checked[type=radio] { + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-check-input[type=checkbox]:indeterminate { + background-color: #0d6efd; + border-color: #0d6efd; + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); +} +.form-check-input:disabled { + pointer-events: none; + filter: none; + opacity: 0.5; +} +.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + cursor: default; + opacity: 0.5; +} + +.form-switch { + padding-left: 2.5em; +} +.form-switch .form-check-input { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + width: 2em; + margin-left: -2.5em; + background-image: var(--bs-form-switch-bg); + background-position: left center; + border-radius: 2em; + transition: background-position 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-switch .form-check-input { + transition: none; + } +} +.form-switch .form-check-input:focus { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e"); +} +.form-switch .form-check-input:checked { + background-position: right center; + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-switch.form-check-reverse { + padding-right: 2.5em; + padding-left: 0; +} +.form-switch.form-check-reverse .form-check-input { + margin-right: -2.5em; + margin-left: 0; +} + +.form-check-inline { + display: inline-block; + margin-right: 1rem; +} + +.btn-check { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.btn-check[disabled] + .btn, .btn-check:disabled + .btn { + pointer-events: none; + filter: none; + opacity: 0.65; +} + +[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e"); +} + +.form-range { + width: 100%; + height: 1.5rem; + padding: 0; + appearance: none; + background-color: transparent; +} +.form-range:focus { + outline: 0; +} +.form-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range::-moz-focus-outer { + border: 0; +} +.form-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + appearance: none; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-webkit-slider-thumb { + transition: none; + } +} +.form-range::-webkit-slider-thumb:active { + background-color: #b6d4fe; +} +.form-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: var(--bs-secondary-bg); + border-color: transparent; + border-radius: 1rem; +} +.form-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + appearance: none; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-moz-range-thumb { + transition: none; + } +} +.form-range::-moz-range-thumb:active { + background-color: #b6d4fe; +} +.form-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: var(--bs-secondary-bg); + border-color: transparent; + border-radius: 1rem; +} +.form-range:disabled { + pointer-events: none; +} +.form-range:disabled::-webkit-slider-thumb { + background-color: var(--bs-secondary-color); +} +.form-range:disabled::-moz-range-thumb { + background-color: var(--bs-secondary-color); +} + +.form-floating { + position: relative; +} +.form-floating > .form-control, +.form-floating > .form-control-plaintext, +.form-floating > .form-select { + height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + min-height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + line-height: 1.25; +} +.form-floating > label { + position: absolute; + top: 0; + left: 0; + z-index: 2; + height: 100%; + padding: 1rem 0.75rem; + overflow: hidden; + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: var(--bs-border-width) solid transparent; + transform-origin: 0 0; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-floating > label { + transition: none; + } +} +.form-floating > .form-control, +.form-floating > .form-control-plaintext { + padding: 1rem 0.75rem; +} +.form-floating > .form-control::placeholder, +.form-floating > .form-control-plaintext::placeholder { + color: transparent; +} +.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown), +.form-floating > .form-control-plaintext:focus, +.form-floating > .form-control-plaintext:not(:placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:-webkit-autofill, +.form-floating > .form-control-plaintext:-webkit-autofill { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-select { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label, +.form-floating > .form-control-plaintext ~ label, +.form-floating > .form-select ~ label { + color: rgba(var(--bs-body-color-rgb), 0.65); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:focus ~ label::after, +.form-floating > .form-control:not(:placeholder-shown) ~ label::after, +.form-floating > .form-control-plaintext ~ label::after, +.form-floating > .form-select ~ label::after { + position: absolute; + inset: 1rem 0.375rem; + z-index: -1; + height: 1.5em; + content: ""; + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius); +} +.form-floating > .form-control:-webkit-autofill ~ label { + color: rgba(var(--bs-body-color-rgb), 0.65); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control-plaintext ~ label { + border-width: var(--bs-border-width) 0; +} +.form-floating > :disabled ~ label, +.form-floating > .form-control:disabled ~ label { + color: #6c757d; +} +.form-floating > :disabled ~ label::after, +.form-floating > .form-control:disabled ~ label::after { + background-color: var(--bs-secondary-bg); +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} +.input-group > .form-control, +.input-group > .form-select, +.input-group > .form-floating { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; +} +.input-group > .form-control:focus, +.input-group > .form-select:focus, +.input-group > .form-floating:focus-within { + z-index: 5; +} +.input-group .btn { + position: relative; + z-index: 2; +} +.input-group .btn:focus { + z-index: 5; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + text-align: center; + white-space: nowrap; + background-color: var(--bs-tertiary-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); +} + +.input-group-lg > .form-control, +.input-group-lg > .form-select, +.input-group-lg > .input-group-text, +.input-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} + +.input-group-sm > .form-control, +.input-group-sm > .form-select, +.input-group-sm > .input-group-text, +.input-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} + +.input-group-lg > .form-select, +.input-group-sm > .form-select { + padding-right: 3rem; +} + +.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3), +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control, +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4), +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control, +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + margin-left: calc(var(--bs-border-width) * -1); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group > .form-floating:not(:first-child) > .form-control, +.input-group > .form-floating:not(:first-child) > .form-select { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-form-valid-color); +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: var(--bs-success); + border-radius: var(--bs-border-radius); +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: var(--bs-form-valid-border-color); + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:valid, .form-select.is-valid { + border-color: var(--bs-form-valid-border-color); +} +.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + padding-right: 4.125rem; + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:valid:focus, .form-select.is-valid:focus { + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated .form-control-color:valid, .form-control-color.is-valid { + width: calc(3rem + calc(1.5em + 0.75rem)); +} + +.was-validated .form-check-input:valid, .form-check-input.is-valid { + border-color: var(--bs-form-valid-border-color); +} +.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { + background-color: var(--bs-form-valid-color); +} +.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: var(--bs-form-valid-color); +} + +.form-check-inline .form-check-input ~ .valid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, +.was-validated .input-group > .form-select:not(:focus):valid, +.input-group > .form-select:not(:focus).is-valid, +.was-validated .input-group > .form-floating:not(:focus-within):valid, +.input-group > .form-floating:not(:focus-within).is-valid { + z-index: 3; +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-form-invalid-color); +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: var(--bs-danger); + border-radius: var(--bs-border-radius); +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: var(--bs-form-invalid-border-color); + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:invalid, .form-select.is-invalid { + border-color: var(--bs-form-invalid-border-color); +} +.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + padding-right: 4.125rem; + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated .form-control-color:invalid, .form-control-color.is-invalid { + width: calc(3rem + calc(1.5em + 0.75rem)); +} + +.was-validated .form-check-input:invalid, .form-check-input.is-invalid { + border-color: var(--bs-form-invalid-border-color); +} +.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { + background-color: var(--bs-form-invalid-color); +} +.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: var(--bs-form-invalid-color); +} + +.form-check-inline .form-check-input ~ .invalid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, +.was-validated .input-group > .form-select:not(:focus):invalid, +.input-group > .form-select:not(:focus).is-invalid, +.was-validated .input-group > .form-floating:not(:focus-within):invalid, +.input-group > .form-floating:not(:focus-within).is-invalid { + z-index: 4; +} + +.btn { + --bs-btn-padding-x: 0.75rem; + --bs-btn-padding-y: 0.375rem; + --bs-btn-font-family: ; + --bs-btn-font-size: 1rem; + --bs-btn-font-weight: 400; + --bs-btn-line-height: 1.5; + --bs-btn-color: var(--bs-body-color); + --bs-btn-bg: transparent; + --bs-btn-border-width: var(--bs-border-width); + --bs-btn-border-color: transparent; + --bs-btn-border-radius: var(--bs-border-radius); + --bs-btn-hover-border-color: transparent; + --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + --bs-btn-disabled-opacity: 0.65; + --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5); + display: inline-block; + padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x); + font-family: var(--bs-btn-font-family); + font-size: var(--bs-btn-font-size); + font-weight: var(--bs-btn-font-weight); + line-height: var(--bs-btn-line-height); + color: var(--bs-btn-color); + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + user-select: none; + border: var(--bs-btn-border-width) solid var(--bs-btn-border-color); + border-radius: var(--bs-btn-border-radius); + background-color: var(--bs-btn-bg); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); +} +.btn-check + .btn:hover { + color: var(--bs-btn-color); + background-color: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); +} +.btn:focus-visible { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:focus-visible + .btn { + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { + color: var(--bs-btn-active-color); + background-color: var(--bs-btn-active-bg); + border-color: var(--bs-btn-active-border-color); +} +.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn:disabled, .btn.disabled, fieldset:disabled .btn { + color: var(--bs-btn-disabled-color); + pointer-events: none; + background-color: var(--bs-btn-disabled-bg); + border-color: var(--bs-btn-disabled-border-color); + opacity: var(--bs-btn-disabled-opacity); +} + +.btn-primary { + --bs-btn-color: #fff; + --bs-btn-bg: #0d6efd; + --bs-btn-border-color: #0d6efd; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #0b5ed7; + --bs-btn-hover-border-color: #0a58ca; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #0a58ca; + --bs-btn-active-border-color: #0a53be; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #0d6efd; + --bs-btn-disabled-border-color: #0d6efd; +} + +.btn-secondary { + --bs-btn-color: #fff; + --bs-btn-bg: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #5c636a; + --bs-btn-hover-border-color: #565e64; + --bs-btn-focus-shadow-rgb: 130, 138, 145; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #565e64; + --bs-btn-active-border-color: #51585e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #6c757d; + --bs-btn-disabled-border-color: #6c757d; +} + +.btn-success { + --bs-btn-color: #fff; + --bs-btn-bg: #198754; + --bs-btn-border-color: #198754; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #157347; + --bs-btn-hover-border-color: #146c43; + --bs-btn-focus-shadow-rgb: 60, 153, 110; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #146c43; + --bs-btn-active-border-color: #13653f; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #198754; + --bs-btn-disabled-border-color: #198754; +} + +.btn-info { + --bs-btn-color: #000; + --bs-btn-bg: #0dcaf0; + --bs-btn-border-color: #0dcaf0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #31d2f2; + --bs-btn-hover-border-color: #25cff2; + --bs-btn-focus-shadow-rgb: 11, 172, 204; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #3dd5f3; + --bs-btn-active-border-color: #25cff2; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #0dcaf0; + --bs-btn-disabled-border-color: #0dcaf0; +} + +.btn-warning { + --bs-btn-color: #000; + --bs-btn-bg: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #ffca2c; + --bs-btn-hover-border-color: #ffc720; + --bs-btn-focus-shadow-rgb: 217, 164, 6; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #ffcd39; + --bs-btn-active-border-color: #ffc720; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #ffc107; + --bs-btn-disabled-border-color: #ffc107; +} + +.btn-danger { + --bs-btn-color: #fff; + --bs-btn-bg: #dc3545; + --bs-btn-border-color: #dc3545; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #bb2d3b; + --bs-btn-hover-border-color: #b02a37; + --bs-btn-focus-shadow-rgb: 225, 83, 97; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #b02a37; + --bs-btn-active-border-color: #a52834; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #dc3545; + --bs-btn-disabled-border-color: #dc3545; +} + +.btn-light { + --bs-btn-color: #000; + --bs-btn-bg: #f8f9fa; + --bs-btn-border-color: #f8f9fa; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #d3d4d5; + --bs-btn-hover-border-color: #c6c7c8; + --bs-btn-focus-shadow-rgb: 211, 212, 213; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #c6c7c8; + --bs-btn-active-border-color: #babbbc; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #f8f9fa; + --bs-btn-disabled-border-color: #f8f9fa; +} + +.btn-dark { + --bs-btn-color: #fff; + --bs-btn-bg: #212529; + --bs-btn-border-color: #212529; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #424649; + --bs-btn-hover-border-color: #373b3e; + --bs-btn-focus-shadow-rgb: 66, 70, 73; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #4d5154; + --bs-btn-active-border-color: #373b3e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #212529; + --bs-btn-disabled-border-color: #212529; +} + +.btn-outline-primary { + --bs-btn-color: #0d6efd; + --bs-btn-border-color: #0d6efd; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #0d6efd; + --bs-btn-hover-border-color: #0d6efd; + --bs-btn-focus-shadow-rgb: 13, 110, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #0d6efd; + --bs-btn-active-border-color: #0d6efd; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #0d6efd; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #0d6efd; + --bs-gradient: none; +} + +.btn-outline-secondary { + --bs-btn-color: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #6c757d; + --bs-btn-hover-border-color: #6c757d; + --bs-btn-focus-shadow-rgb: 108, 117, 125; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #6c757d; + --bs-btn-active-border-color: #6c757d; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #6c757d; + --bs-gradient: none; +} + +.btn-outline-success { + --bs-btn-color: #198754; + --bs-btn-border-color: #198754; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #198754; + --bs-btn-hover-border-color: #198754; + --bs-btn-focus-shadow-rgb: 25, 135, 84; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #198754; + --bs-btn-active-border-color: #198754; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #198754; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #198754; + --bs-gradient: none; +} + +.btn-outline-info { + --bs-btn-color: #0dcaf0; + --bs-btn-border-color: #0dcaf0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #0dcaf0; + --bs-btn-hover-border-color: #0dcaf0; + --bs-btn-focus-shadow-rgb: 13, 202, 240; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #0dcaf0; + --bs-btn-active-border-color: #0dcaf0; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #0dcaf0; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #0dcaf0; + --bs-gradient: none; +} + +.btn-outline-warning { + --bs-btn-color: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #ffc107; + --bs-btn-hover-border-color: #ffc107; + --bs-btn-focus-shadow-rgb: 255, 193, 7; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #ffc107; + --bs-btn-active-border-color: #ffc107; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #ffc107; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #ffc107; + --bs-gradient: none; +} + +.btn-outline-danger { + --bs-btn-color: #dc3545; + --bs-btn-border-color: #dc3545; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #dc3545; + --bs-btn-hover-border-color: #dc3545; + --bs-btn-focus-shadow-rgb: 220, 53, 69; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #dc3545; + --bs-btn-active-border-color: #dc3545; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #dc3545; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #dc3545; + --bs-gradient: none; +} + +.btn-outline-light { + --bs-btn-color: #f8f9fa; + --bs-btn-border-color: #f8f9fa; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #f8f9fa; + --bs-btn-hover-border-color: #f8f9fa; + --bs-btn-focus-shadow-rgb: 248, 249, 250; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #f8f9fa; + --bs-btn-active-border-color: #f8f9fa; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #f8f9fa; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #f8f9fa; + --bs-gradient: none; +} + +.btn-outline-dark { + --bs-btn-color: #212529; + --bs-btn-border-color: #212529; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #212529; + --bs-btn-hover-border-color: #212529; + --bs-btn-focus-shadow-rgb: 33, 37, 41; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #212529; + --bs-btn-active-border-color: #212529; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #212529; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #212529; + --bs-gradient: none; +} + +.btn-link { + --bs-btn-font-weight: 400; + --bs-btn-color: var(--bs-link-color); + --bs-btn-bg: transparent; + --bs-btn-border-color: transparent; + --bs-btn-hover-color: var(--bs-link-hover-color); + --bs-btn-hover-border-color: transparent; + --bs-btn-active-color: var(--bs-link-hover-color); + --bs-btn-active-border-color: transparent; + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-border-color: transparent; + --bs-btn-box-shadow: 0 0 0 #000; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + text-decoration: underline; +} +.btn-link:focus-visible { + color: var(--bs-btn-color); +} +.btn-link:hover { + color: var(--bs-btn-hover-color); +} + +.btn-lg, .btn-group-lg > .btn { + --bs-btn-padding-y: 0.5rem; + --bs-btn-padding-x: 1rem; + --bs-btn-font-size: 1.25rem; + --bs-btn-border-radius: var(--bs-border-radius-lg); +} + +.btn-sm, .btn-group-sm > .btn { + --bs-btn-padding-y: 0.25rem; + --bs-btn-padding-x: 0.5rem; + --bs-btn-font-size: 0.875rem; + --bs-btn-border-radius: var(--bs-border-radius-sm); +} + +.fade { + transition: opacity 0.15s linear; +} +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} +.collapsing.collapse-horizontal { + width: 0; + height: auto; + transition: width 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing.collapse-horizontal { + transition: none; + } +} + +.dropup, +.dropend, +.dropdown, +.dropstart, +.dropup-center, +.dropdown-center { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + --bs-dropdown-zindex: 1000; + --bs-dropdown-min-width: 10rem; + --bs-dropdown-padding-x: 0; + --bs-dropdown-padding-y: 0.5rem; + --bs-dropdown-spacer: 0.125rem; + --bs-dropdown-font-size: 1rem; + --bs-dropdown-color: var(--bs-body-color); + --bs-dropdown-bg: var(--bs-body-bg); + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-border-radius: var(--bs-border-radius); + --bs-dropdown-border-width: var(--bs-border-width); + --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-divider-margin-y: 0.5rem; + --bs-dropdown-box-shadow: var(--bs-box-shadow); + --bs-dropdown-link-color: var(--bs-body-color); + --bs-dropdown-link-hover-color: var(--bs-body-color); + --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #0d6efd; + --bs-dropdown-link-disabled-color: var(--bs-tertiary-color); + --bs-dropdown-item-padding-x: 1rem; + --bs-dropdown-item-padding-y: 0.25rem; + --bs-dropdown-header-color: #6c757d; + --bs-dropdown-header-padding-x: 1rem; + --bs-dropdown-header-padding-y: 0.5rem; + position: absolute; + z-index: var(--bs-dropdown-zindex); + display: none; + min-width: var(--bs-dropdown-min-width); + padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); + margin: 0; + font-size: var(--bs-dropdown-font-size); + color: var(--bs-dropdown-color); + text-align: left; + list-style: none; + background-color: var(--bs-dropdown-bg); + background-clip: padding-box; + border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); + border-radius: var(--bs-dropdown-border-radius); +} +.dropdown-menu[data-bs-popper] { + top: 100%; + left: 0; + margin-top: var(--bs-dropdown-spacer); +} + +.dropdown-menu-start { + --bs-position: start; +} +.dropdown-menu-start[data-bs-popper] { + right: auto; + left: 0; +} + +.dropdown-menu-end { + --bs-position: end; +} +.dropdown-menu-end[data-bs-popper] { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-start { + --bs-position: start; + } + .dropdown-menu-sm-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-sm-end { + --bs-position: end; + } + .dropdown-menu-sm-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 768px) { + .dropdown-menu-md-start { + --bs-position: start; + } + .dropdown-menu-md-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-md-end { + --bs-position: end; + } + .dropdown-menu-md-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 992px) { + .dropdown-menu-lg-start { + --bs-position: start; + } + .dropdown-menu-lg-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-lg-end { + --bs-position: end; + } + .dropdown-menu-lg-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 1200px) { + .dropdown-menu-xl-start { + --bs-position: start; + } + .dropdown-menu-xl-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-xl-end { + --bs-position: end; + } + .dropdown-menu-xl-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 1400px) { + .dropdown-menu-xxl-start { + --bs-position: start; + } + .dropdown-menu-xxl-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-xxl-end { + --bs-position: end; + } + .dropdown-menu-xxl-end[data-bs-popper] { + right: 0; + left: auto; + } +} +.dropup .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--bs-dropdown-spacer); +} +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropend .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: var(--bs-dropdown-spacer); +} +.dropend .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} +.dropend .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropend .dropdown-toggle::after { + vertical-align: 0; +} + +.dropstart .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: var(--bs-dropdown-spacer); +} +.dropstart .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} +.dropstart .dropdown-toggle::after { + display: none; +} +.dropstart .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} +.dropstart .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropstart .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-divider { + height: 0; + margin: var(--bs-dropdown-divider-margin-y) 0; + overflow: hidden; + border-top: 1px solid var(--bs-dropdown-divider-bg); + opacity: 1; +} + +.dropdown-item { + display: block; + width: 100%; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + clear: both; + font-weight: 400; + color: var(--bs-dropdown-link-color); + text-align: inherit; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border: 0; + border-radius: var(--bs-dropdown-item-border-radius, 0); +} +.dropdown-item:hover, .dropdown-item:focus { + color: var(--bs-dropdown-link-hover-color); + background-color: var(--bs-dropdown-link-hover-bg); +} +.dropdown-item.active, .dropdown-item:active { + color: var(--bs-dropdown-link-active-color); + text-decoration: none; + background-color: var(--bs-dropdown-link-active-bg); +} +.dropdown-item.disabled, .dropdown-item:disabled { + color: var(--bs-dropdown-link-disabled-color); + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); + margin-bottom: 0; + font-size: 0.875rem; + color: var(--bs-dropdown-header-color); + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + color: var(--bs-dropdown-link-color); +} + +.dropdown-menu-dark { + --bs-dropdown-color: #dee2e6; + --bs-dropdown-bg: #343a40; + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-box-shadow: ; + --bs-dropdown-link-color: #dee2e6; + --bs-dropdown-link-hover-color: #fff; + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #0d6efd; + --bs-dropdown-link-disabled-color: #adb5bd; + --bs-dropdown-header-color: #adb5bd; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + flex: 1 1 auto; +} +.btn-group > .btn-check:checked + .btn, +.btn-group > .btn-check:focus + .btn, +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn-check:checked + .btn, +.btn-group-vertical > .btn-check:focus + .btn, +.btn-group-vertical > .btn:hover, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.btn-toolbar .input-group { + width: auto; +} + +.btn-group { + border-radius: var(--bs-border-radius); +} +.btn-group > :not(.btn-check:first-child) + .btn, +.btn-group > .btn-group:not(:first-child) { + margin-left: calc(var(--bs-border-width) * -1); +} +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn.dropdown-toggle-split:first-child, +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:nth-child(n+3), +.btn-group > :not(.btn-check) + .btn, +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} +.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after { + margin-left: 0; +} +.dropstart .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: calc(var(--bs-border-width) * -1); +} +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn ~ .btn, +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav { + --bs-nav-link-padding-x: 1rem; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-link-color); + --bs-nav-link-hover-color: var(--bs-link-hover-color); + --bs-nav-link-disabled-color: var(--bs-secondary-color); + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x); + font-size: var(--bs-nav-link-font-size); + font-weight: var(--bs-nav-link-font-weight); + color: var(--bs-nav-link-color); + text-decoration: none; + background: none; + border: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .nav-link { + transition: none; + } +} +.nav-link:hover, .nav-link:focus { + color: var(--bs-nav-link-hover-color); +} +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.nav-link.disabled, .nav-link:disabled { + color: var(--bs-nav-link-disabled-color); + pointer-events: none; + cursor: default; +} + +.nav-tabs { + --bs-nav-tabs-border-width: var(--bs-border-width); + --bs-nav-tabs-border-color: var(--bs-border-color); + --bs-nav-tabs-border-radius: var(--bs-border-radius); + --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color); + --bs-nav-tabs-link-active-color: var(--bs-emphasis-color); + --bs-nav-tabs-link-active-bg: var(--bs-body-bg); + --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg); + border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); +} +.nav-tabs .nav-link { + margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width)); + border: var(--bs-nav-tabs-border-width) solid transparent; + border-top-left-radius: var(--bs-nav-tabs-border-radius); + border-top-right-radius: var(--bs-nav-tabs-border-radius); +} +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + isolation: isolate; + border-color: var(--bs-nav-tabs-link-hover-border-color); +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--bs-nav-tabs-link-active-color); + background-color: var(--bs-nav-tabs-link-active-bg); + border-color: var(--bs-nav-tabs-link-active-border-color); +} +.nav-tabs .dropdown-menu { + margin-top: calc(-1 * var(--bs-nav-tabs-border-width)); + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills { + --bs-nav-pills-border-radius: var(--bs-border-radius); + --bs-nav-pills-link-active-color: #fff; + --bs-nav-pills-link-active-bg: #0d6efd; +} +.nav-pills .nav-link { + border-radius: var(--bs-nav-pills-border-radius); +} +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: var(--bs-nav-pills-link-active-color); + background-color: var(--bs-nav-pills-link-active-bg); +} + +.nav-underline { + --bs-nav-underline-gap: 1rem; + --bs-nav-underline-border-width: 0.125rem; + --bs-nav-underline-link-active-color: var(--bs-emphasis-color); + gap: var(--bs-nav-underline-gap); +} +.nav-underline .nav-link { + padding-right: 0; + padding-left: 0; + border-bottom: var(--bs-nav-underline-border-width) solid transparent; +} +.nav-underline .nav-link:hover, .nav-underline .nav-link:focus { + border-bottom-color: currentcolor; +} +.nav-underline .nav-link.active, +.nav-underline .show > .nav-link { + font-weight: 700; + color: var(--bs-nav-underline-link-active-color); + border-bottom-color: currentcolor; +} + +.nav-fill > .nav-link, +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; +} + +.nav-justified > .nav-link, +.nav-justified .nav-item { + flex-basis: 0; + flex-grow: 1; + text-align: center; +} + +.nav-fill .nav-item .nav-link, +.nav-justified .nav-item .nav-link { + width: 100%; +} + +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} + +.navbar { + --bs-navbar-padding-x: 0; + --bs-navbar-padding-y: 0.5rem; + --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65); + --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8); + --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3); + --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-padding-y: 0.3125rem; + --bs-navbar-brand-margin-end: 1rem; + --bs-navbar-brand-font-size: 1.25rem; + --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-nav-link-padding-x: 0.5rem; + --bs-navbar-toggler-padding-y: 0.25rem; + --bs-navbar-toggler-padding-x: 0.75rem; + --bs-navbar-toggler-font-size: 1.25rem; + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); + --bs-navbar-toggler-border-radius: var(--bs-border-radius); + --bs-navbar-toggler-focus-width: 0.25rem; + --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out; + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x); +} +.navbar > .container, +.navbar > .container-fluid, +.navbar > .container-sm, +.navbar > .container-md, +.navbar > .container-lg, +.navbar > .container-xl, +.navbar > .container-xxl { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; +} +.navbar-brand { + padding-top: var(--bs-navbar-brand-padding-y); + padding-bottom: var(--bs-navbar-brand-padding-y); + margin-right: var(--bs-navbar-brand-margin-end); + font-size: var(--bs-navbar-brand-font-size); + color: var(--bs-navbar-brand-color); + text-decoration: none; + white-space: nowrap; +} +.navbar-brand:hover, .navbar-brand:focus { + color: var(--bs-navbar-brand-hover-color); +} + +.navbar-nav { + --bs-nav-link-padding-x: 0; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-navbar-color); + --bs-nav-link-hover-color: var(--bs-navbar-hover-color); + --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color); + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .nav-link.active, .navbar-nav .nav-link.show { + color: var(--bs-navbar-active-color); +} +.navbar-nav .dropdown-menu { + position: static; +} + +.navbar-text { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-navbar-color); +} +.navbar-text a, +.navbar-text a:hover, +.navbar-text a:focus { + color: var(--bs-navbar-active-color); +} + +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} + +.navbar-toggler { + padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x); + font-size: var(--bs-navbar-toggler-font-size); + line-height: 1; + color: var(--bs-navbar-color); + background-color: transparent; + border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color); + border-radius: var(--bs-navbar-toggler-border-radius); + transition: var(--bs-navbar-toggler-transition); +} +@media (prefers-reduced-motion: reduce) { + .navbar-toggler { + transition: none; + } +} +.navbar-toggler:hover { + text-decoration: none; +} +.navbar-toggler:focus { + text-decoration: none; + outline: 0; + box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width); +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + background-image: var(--bs-navbar-toggler-icon-bg); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; +} + +.navbar-nav-scroll { + max-height: var(--bs-scroll-height, 75vh); + overflow-y: auto; +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } + .navbar-expand-sm .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-sm .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-sm .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 768px) { + .navbar-expand-md { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } + .navbar-expand-md .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-md .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-md .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 992px) { + .navbar-expand-lg { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } + .navbar-expand-lg .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-lg .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-lg .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } + .navbar-expand-xl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xl .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-xl .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1400px) { + .navbar-expand-xxl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xxl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xxl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xxl .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-xxl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xxl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xxl .navbar-toggler { + display: none; + } + .navbar-expand-xxl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +.navbar-expand { + flex-wrap: nowrap; + justify-content: flex-start; +} +.navbar-expand .navbar-nav { + flex-direction: row; +} +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-expand .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); +} +.navbar-expand .navbar-nav-scroll { + overflow: visible; +} +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} +.navbar-expand .navbar-toggler { + display: none; +} +.navbar-expand .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; +} +.navbar-expand .offcanvas .offcanvas-header { + display: none; +} +.navbar-expand .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; +} + +.navbar-dark, +.navbar[data-bs-theme=dark] { + --bs-navbar-color: rgba(255, 255, 255, 0.55); + --bs-navbar-hover-color: rgba(255, 255, 255, 0.75); + --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25); + --bs-navbar-active-color: #fff; + --bs-navbar-brand-color: #fff; + --bs-navbar-brand-hover-color: #fff; + --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1); + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +[data-bs-theme=dark] .navbar-toggler-icon { + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.card { + --bs-card-spacer-y: 1rem; + --bs-card-spacer-x: 1rem; + --bs-card-title-spacer-y: 0.5rem; + --bs-card-title-color: ; + --bs-card-subtitle-color: ; + --bs-card-border-width: var(--bs-border-width); + --bs-card-border-color: var(--bs-border-color-translucent); + --bs-card-border-radius: var(--bs-border-radius); + --bs-card-box-shadow: ; + --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-card-cap-padding-y: 0.5rem; + --bs-card-cap-padding-x: 1rem; + --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03); + --bs-card-cap-color: ; + --bs-card-height: ; + --bs-card-color: ; + --bs-card-bg: var(--bs-body-bg); + --bs-card-img-overlay-padding: 1rem; + --bs-card-group-margin: 0.75rem; + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + height: var(--bs-card-height); + color: var(--bs-body-color); + word-wrap: break-word; + background-color: var(--bs-card-bg); + background-clip: border-box; + border: var(--bs-card-border-width) solid var(--bs-card-border-color); + border-radius: var(--bs-card-border-radius); +} +.card > hr { + margin-right: 0; + margin-left: 0; +} +.card > .list-group { + border-top: inherit; + border-bottom: inherit; +} +.card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); +} +.card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); +} +.card > .card-header + .list-group, +.card > .list-group + .card-footer { + border-top: 0; +} + +.card-body { + flex: 1 1 auto; + padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x); + color: var(--bs-card-color); +} + +.card-title { + margin-bottom: var(--bs-card-title-spacer-y); + color: var(--bs-card-title-color); +} + +.card-subtitle { + margin-top: calc(-0.5 * var(--bs-card-title-spacer-y)); + margin-bottom: 0; + color: var(--bs-card-subtitle-color); +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link + .card-link { + margin-left: var(--bs-card-spacer-x); +} + +.card-header { + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + margin-bottom: 0; + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color); +} +.card-header:first-child { + border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0; +} + +.card-footer { + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); +} +.card-footer:last-child { + border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius); +} + +.card-header-tabs { + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-bottom: calc(-1 * var(--bs-card-cap-padding-y)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); + border-bottom: 0; +} +.card-header-tabs .nav-link.active { + background-color: var(--bs-card-bg); + border-bottom-color: var(--bs-card-bg); +} + +.card-header-pills { + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: var(--bs-card-img-overlay-padding); + border-radius: var(--bs-card-inner-border-radius); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); +} + +.card-group > .card { + margin-bottom: var(--bs-card-group-margin); +} +@media (min-width: 576px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group > .card { + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.accordion { + --bs-accordion-color: var(--bs-body-color); + --bs-accordion-bg: var(--bs-body-bg); + --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + --bs-accordion-border-color: var(--bs-border-color); + --bs-accordion-border-width: var(--bs-border-width); + --bs-accordion-border-radius: var(--bs-border-radius); + --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-accordion-btn-padding-x: 1.25rem; + --bs-accordion-btn-padding-y: 1rem; + --bs-accordion-btn-color: var(--bs-body-color); + --bs-accordion-btn-bg: var(--bs-accordion-bg); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-icon-width: 1.25rem; + --bs-accordion-btn-icon-transform: rotate(-180deg); + --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23052c65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-focus-border-color: #86b7fe; + --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-accordion-body-padding-x: 1.25rem; + --bs-accordion-body-padding-y: 1rem; + --bs-accordion-active-color: var(--bs-primary-text-emphasis); + --bs-accordion-active-bg: var(--bs-primary-bg-subtle); +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); + font-size: 1rem; + color: var(--bs-accordion-btn-color); + text-align: left; + background-color: var(--bs-accordion-btn-bg); + border: 0; + border-radius: 0; + overflow-anchor: none; + transition: var(--bs-accordion-transition); +} +@media (prefers-reduced-motion: reduce) { + .accordion-button { + transition: none; + } +} +.accordion-button:not(.collapsed) { + color: var(--bs-accordion-active-color); + background-color: var(--bs-accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); +} +.accordion-button:not(.collapsed)::after { + background-image: var(--bs-accordion-btn-active-icon); + transform: var(--bs-accordion-btn-icon-transform); +} +.accordion-button::after { + flex-shrink: 0; + width: var(--bs-accordion-btn-icon-width); + height: var(--bs-accordion-btn-icon-width); + margin-left: auto; + content: ""; + background-image: var(--bs-accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--bs-accordion-btn-icon-width); + transition: var(--bs-accordion-btn-icon-transition); +} +@media (prefers-reduced-motion: reduce) { + .accordion-button::after { + transition: none; + } +} +.accordion-button:hover { + z-index: 2; +} +.accordion-button:focus { + z-index: 3; + border-color: var(--bs-accordion-btn-focus-border-color); + outline: 0; + box-shadow: var(--bs-accordion-btn-focus-box-shadow); +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--bs-accordion-color); + background-color: var(--bs-accordion-bg); + border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); +} +.accordion-item:first-of-type { + border-top-left-radius: var(--bs-accordion-border-radius); + border-top-right-radius: var(--bs-accordion-border-radius); +} +.accordion-item:first-of-type .accordion-button { + border-top-left-radius: var(--bs-accordion-inner-border-radius); + border-top-right-radius: var(--bs-accordion-inner-border-radius); +} +.accordion-item:not(:first-of-type) { + border-top: 0; +} +.accordion-item:last-of-type { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} +.accordion-item:last-of-type .accordion-button.collapsed { + border-bottom-right-radius: var(--bs-accordion-inner-border-radius); + border-bottom-left-radius: var(--bs-accordion-inner-border-radius); +} +.accordion-item:last-of-type .accordion-collapse { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} + +.accordion-body { + padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); +} + +.accordion-flush .accordion-collapse { + border-width: 0; +} +.accordion-flush .accordion-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} +.accordion-flush .accordion-item:first-child { + border-top: 0; +} +.accordion-flush .accordion-item:last-child { + border-bottom: 0; +} +.accordion-flush .accordion-item .accordion-button, .accordion-flush .accordion-item .accordion-button.collapsed { + border-radius: 0; +} + +[data-bs-theme=dark] .accordion-button::after { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +.breadcrumb { + --bs-breadcrumb-padding-x: 0; + --bs-breadcrumb-padding-y: 0; + --bs-breadcrumb-margin-bottom: 1rem; + --bs-breadcrumb-bg: ; + --bs-breadcrumb-border-radius: ; + --bs-breadcrumb-divider-color: var(--bs-secondary-color); + --bs-breadcrumb-item-padding-x: 0.5rem; + --bs-breadcrumb-item-active-color: var(--bs-secondary-color); + display: flex; + flex-wrap: wrap; + padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x); + margin-bottom: var(--bs-breadcrumb-margin-bottom); + font-size: var(--bs-breadcrumb-font-size); + list-style: none; + background-color: var(--bs-breadcrumb-bg); + border-radius: var(--bs-breadcrumb-border-radius); +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: var(--bs-breadcrumb-item-padding-x); +} +.breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: var(--bs-breadcrumb-item-padding-x); + color: var(--bs-breadcrumb-divider-color); + content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; +} +.breadcrumb-item.active { + color: var(--bs-breadcrumb-item-active-color); +} + +.pagination { + --bs-pagination-padding-x: 0.75rem; + --bs-pagination-padding-y: 0.375rem; + --bs-pagination-font-size: 1rem; + --bs-pagination-color: var(--bs-link-color); + --bs-pagination-bg: var(--bs-body-bg); + --bs-pagination-border-width: var(--bs-border-width); + --bs-pagination-border-color: var(--bs-border-color); + --bs-pagination-border-radius: var(--bs-border-radius); + --bs-pagination-hover-color: var(--bs-link-hover-color); + --bs-pagination-hover-bg: var(--bs-tertiary-bg); + --bs-pagination-hover-border-color: var(--bs-border-color); + --bs-pagination-focus-color: var(--bs-link-hover-color); + --bs-pagination-focus-bg: var(--bs-secondary-bg); + --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-pagination-active-color: #fff; + --bs-pagination-active-bg: #0d6efd; + --bs-pagination-active-border-color: #0d6efd; + --bs-pagination-disabled-color: var(--bs-secondary-color); + --bs-pagination-disabled-bg: var(--bs-secondary-bg); + --bs-pagination-disabled-border-color: var(--bs-border-color); + display: flex; + padding-left: 0; + list-style: none; +} + +.page-link { + position: relative; + display: block; + padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x); + font-size: var(--bs-pagination-font-size); + color: var(--bs-pagination-color); + text-decoration: none; + background-color: var(--bs-pagination-bg); + border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .page-link { + transition: none; + } +} +.page-link:hover { + z-index: 2; + color: var(--bs-pagination-hover-color); + background-color: var(--bs-pagination-hover-bg); + border-color: var(--bs-pagination-hover-border-color); +} +.page-link:focus { + z-index: 3; + color: var(--bs-pagination-focus-color); + background-color: var(--bs-pagination-focus-bg); + outline: 0; + box-shadow: var(--bs-pagination-focus-box-shadow); +} +.page-link.active, .active > .page-link { + z-index: 3; + color: var(--bs-pagination-active-color); + background-color: var(--bs-pagination-active-bg); + border-color: var(--bs-pagination-active-border-color); +} +.page-link.disabled, .disabled > .page-link { + color: var(--bs-pagination-disabled-color); + pointer-events: none; + background-color: var(--bs-pagination-disabled-bg); + border-color: var(--bs-pagination-disabled-border-color); +} + +.page-item:not(:first-child) .page-link { + margin-left: calc(var(--bs-border-width) * -1); +} +.page-item:first-child .page-link { + border-top-left-radius: var(--bs-pagination-border-radius); + border-bottom-left-radius: var(--bs-pagination-border-radius); +} +.page-item:last-child .page-link { + border-top-right-radius: var(--bs-pagination-border-radius); + border-bottom-right-radius: var(--bs-pagination-border-radius); +} + +.pagination-lg { + --bs-pagination-padding-x: 1.5rem; + --bs-pagination-padding-y: 0.75rem; + --bs-pagination-font-size: 1.25rem; + --bs-pagination-border-radius: var(--bs-border-radius-lg); +} + +.pagination-sm { + --bs-pagination-padding-x: 0.5rem; + --bs-pagination-padding-y: 0.25rem; + --bs-pagination-font-size: 0.875rem; + --bs-pagination-border-radius: var(--bs-border-radius-sm); +} + +.badge { + --bs-badge-padding-x: 0.65em; + --bs-badge-padding-y: 0.35em; + --bs-badge-font-size: 0.75em; + --bs-badge-font-weight: 700; + --bs-badge-color: #fff; + --bs-badge-border-radius: var(--bs-border-radius); + display: inline-block; + padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); + font-size: var(--bs-badge-font-size); + font-weight: var(--bs-badge-font-weight); + line-height: 1; + color: var(--bs-badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--bs-badge-border-radius); +} +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.alert { + --bs-alert-bg: transparent; + --bs-alert-padding-x: 1rem; + --bs-alert-padding-y: 1rem; + --bs-alert-margin-bottom: 1rem; + --bs-alert-color: inherit; + --bs-alert-border-color: transparent; + --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color); + --bs-alert-border-radius: var(--bs-border-radius); + --bs-alert-link-color: inherit; + position: relative; + padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x); + margin-bottom: var(--bs-alert-margin-bottom); + color: var(--bs-alert-color); + background-color: var(--bs-alert-bg); + border: var(--bs-alert-border); + border-radius: var(--bs-alert-border-radius); +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; + color: var(--bs-alert-link-color); +} + +.alert-dismissible { + padding-right: 3rem; +} +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 1.25rem 1rem; +} + +.alert-primary { + --bs-alert-color: var(--bs-primary-text-emphasis); + --bs-alert-bg: var(--bs-primary-bg-subtle); + --bs-alert-border-color: var(--bs-primary-border-subtle); + --bs-alert-link-color: var(--bs-primary-text-emphasis); +} + +.alert-secondary { + --bs-alert-color: var(--bs-secondary-text-emphasis); + --bs-alert-bg: var(--bs-secondary-bg-subtle); + --bs-alert-border-color: var(--bs-secondary-border-subtle); + --bs-alert-link-color: var(--bs-secondary-text-emphasis); +} + +.alert-success { + --bs-alert-color: var(--bs-success-text-emphasis); + --bs-alert-bg: var(--bs-success-bg-subtle); + --bs-alert-border-color: var(--bs-success-border-subtle); + --bs-alert-link-color: var(--bs-success-text-emphasis); +} + +.alert-info { + --bs-alert-color: var(--bs-info-text-emphasis); + --bs-alert-bg: var(--bs-info-bg-subtle); + --bs-alert-border-color: var(--bs-info-border-subtle); + --bs-alert-link-color: var(--bs-info-text-emphasis); +} + +.alert-warning { + --bs-alert-color: var(--bs-warning-text-emphasis); + --bs-alert-bg: var(--bs-warning-bg-subtle); + --bs-alert-border-color: var(--bs-warning-border-subtle); + --bs-alert-link-color: var(--bs-warning-text-emphasis); +} + +.alert-danger { + --bs-alert-color: var(--bs-danger-text-emphasis); + --bs-alert-bg: var(--bs-danger-bg-subtle); + --bs-alert-border-color: var(--bs-danger-border-subtle); + --bs-alert-link-color: var(--bs-danger-text-emphasis); +} + +.alert-light { + --bs-alert-color: var(--bs-light-text-emphasis); + --bs-alert-bg: var(--bs-light-bg-subtle); + --bs-alert-border-color: var(--bs-light-border-subtle); + --bs-alert-link-color: var(--bs-light-text-emphasis); +} + +.alert-dark { + --bs-alert-color: var(--bs-dark-text-emphasis); + --bs-alert-bg: var(--bs-dark-bg-subtle); + --bs-alert-border-color: var(--bs-dark-border-subtle); + --bs-alert-link-color: var(--bs-dark-text-emphasis); +} + +@keyframes progress-bar-stripes { + 0% { + background-position-x: 1rem; + } +} +.progress, +.progress-stacked { + --bs-progress-height: 1rem; + --bs-progress-font-size: 0.75rem; + --bs-progress-bg: var(--bs-secondary-bg); + --bs-progress-border-radius: var(--bs-border-radius); + --bs-progress-box-shadow: var(--bs-box-shadow-inset); + --bs-progress-bar-color: #fff; + --bs-progress-bar-bg: #0d6efd; + --bs-progress-bar-transition: width 0.6s ease; + display: flex; + height: var(--bs-progress-height); + overflow: hidden; + font-size: var(--bs-progress-font-size); + background-color: var(--bs-progress-bg); + border-radius: var(--bs-progress-border-radius); +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: var(--bs-progress-bar-color); + text-align: center; + white-space: nowrap; + background-color: var(--bs-progress-bar-bg); + transition: var(--bs-progress-bar-transition); +} +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: var(--bs-progress-height) var(--bs-progress-height); +} + +.progress-stacked > .progress { + overflow: visible; +} + +.progress-stacked > .progress > .progress-bar { + width: 100%; +} + +.progress-bar-animated { + animation: 1s linear infinite progress-bar-stripes; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + animation: none; + } +} + +.list-group { + --bs-list-group-color: var(--bs-body-color); + --bs-list-group-bg: var(--bs-body-bg); + --bs-list-group-border-color: var(--bs-border-color); + --bs-list-group-border-width: var(--bs-border-width); + --bs-list-group-border-radius: var(--bs-border-radius); + --bs-list-group-item-padding-x: 1rem; + --bs-list-group-item-padding-y: 0.5rem; + --bs-list-group-action-color: var(--bs-secondary-color); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-tertiary-bg); + --bs-list-group-action-active-color: var(--bs-body-color); + --bs-list-group-action-active-bg: var(--bs-secondary-bg); + --bs-list-group-disabled-color: var(--bs-secondary-color); + --bs-list-group-disabled-bg: var(--bs-body-bg); + --bs-list-group-active-color: #fff; + --bs-list-group-active-bg: #0d6efd; + --bs-list-group-active-border-color: #0d6efd; + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: var(--bs-list-group-border-radius); +} + +.list-group-numbered { + list-style-type: none; + counter-reset: section; +} +.list-group-numbered > .list-group-item::before { + content: counters(section, ".") ". "; + counter-increment: section; +} + +.list-group-item-action { + width: 100%; + color: var(--bs-list-group-action-color); + text-align: inherit; +} +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: var(--bs-list-group-action-hover-color); + text-decoration: none; + background-color: var(--bs-list-group-action-hover-bg); +} +.list-group-item-action:active { + color: var(--bs-list-group-action-active-color); + background-color: var(--bs-list-group-action-active-bg); +} + +.list-group-item { + position: relative; + display: block; + padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x); + color: var(--bs-list-group-color); + text-decoration: none; + background-color: var(--bs-list-group-bg); + border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); +} +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} +.list-group-item.disabled, .list-group-item:disabled { + color: var(--bs-list-group-disabled-color); + pointer-events: none; + background-color: var(--bs-list-group-disabled-bg); +} +.list-group-item.active { + z-index: 2; + color: var(--bs-list-group-active-color); + background-color: var(--bs-list-group-active-bg); + border-color: var(--bs-list-group-active-border-color); +} +.list-group-item + .list-group-item { + border-top-width: 0; +} +.list-group-item + .list-group-item.active { + margin-top: calc(-1 * var(--bs-list-group-border-width)); + border-top-width: var(--bs-list-group-border-width); +} + +.list-group-horizontal { + flex-direction: row; +} +.list-group-horizontal > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; +} +.list-group-horizontal > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; +} +.list-group-horizontal > .list-group-item.active { + margin-top: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + flex-direction: row; + } + .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 768px) { + .list-group-horizontal-md { + flex-direction: row; + } + .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 992px) { + .list-group-horizontal-lg { + flex-direction: row; + } + .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 1200px) { + .list-group-horizontal-xl { + flex-direction: row; + } + .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 1400px) { + .list-group-horizontal-xxl { + flex-direction: row; + } + .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +.list-group-flush { + border-radius: 0; +} +.list-group-flush > .list-group-item { + border-width: 0 0 var(--bs-list-group-border-width); +} +.list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + --bs-list-group-color: var(--bs-primary-text-emphasis); + --bs-list-group-bg: var(--bs-primary-bg-subtle); + --bs-list-group-border-color: var(--bs-primary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-primary-border-subtle); + --bs-list-group-active-color: var(--bs-primary-bg-subtle); + --bs-list-group-active-bg: var(--bs-primary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-primary-text-emphasis); +} + +.list-group-item-secondary { + --bs-list-group-color: var(--bs-secondary-text-emphasis); + --bs-list-group-bg: var(--bs-secondary-bg-subtle); + --bs-list-group-border-color: var(--bs-secondary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle); + --bs-list-group-active-color: var(--bs-secondary-bg-subtle); + --bs-list-group-active-bg: var(--bs-secondary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis); +} + +.list-group-item-success { + --bs-list-group-color: var(--bs-success-text-emphasis); + --bs-list-group-bg: var(--bs-success-bg-subtle); + --bs-list-group-border-color: var(--bs-success-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-success-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-success-border-subtle); + --bs-list-group-active-color: var(--bs-success-bg-subtle); + --bs-list-group-active-bg: var(--bs-success-text-emphasis); + --bs-list-group-active-border-color: var(--bs-success-text-emphasis); +} + +.list-group-item-info { + --bs-list-group-color: var(--bs-info-text-emphasis); + --bs-list-group-bg: var(--bs-info-bg-subtle); + --bs-list-group-border-color: var(--bs-info-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-info-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-info-border-subtle); + --bs-list-group-active-color: var(--bs-info-bg-subtle); + --bs-list-group-active-bg: var(--bs-info-text-emphasis); + --bs-list-group-active-border-color: var(--bs-info-text-emphasis); +} + +.list-group-item-warning { + --bs-list-group-color: var(--bs-warning-text-emphasis); + --bs-list-group-bg: var(--bs-warning-bg-subtle); + --bs-list-group-border-color: var(--bs-warning-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-warning-border-subtle); + --bs-list-group-active-color: var(--bs-warning-bg-subtle); + --bs-list-group-active-bg: var(--bs-warning-text-emphasis); + --bs-list-group-active-border-color: var(--bs-warning-text-emphasis); +} + +.list-group-item-danger { + --bs-list-group-color: var(--bs-danger-text-emphasis); + --bs-list-group-bg: var(--bs-danger-bg-subtle); + --bs-list-group-border-color: var(--bs-danger-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-danger-border-subtle); + --bs-list-group-active-color: var(--bs-danger-bg-subtle); + --bs-list-group-active-bg: var(--bs-danger-text-emphasis); + --bs-list-group-active-border-color: var(--bs-danger-text-emphasis); +} + +.list-group-item-light { + --bs-list-group-color: var(--bs-light-text-emphasis); + --bs-list-group-bg: var(--bs-light-bg-subtle); + --bs-list-group-border-color: var(--bs-light-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-light-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-light-border-subtle); + --bs-list-group-active-color: var(--bs-light-bg-subtle); + --bs-list-group-active-bg: var(--bs-light-text-emphasis); + --bs-list-group-active-border-color: var(--bs-light-text-emphasis); +} + +.list-group-item-dark { + --bs-list-group-color: var(--bs-dark-text-emphasis); + --bs-list-group-bg: var(--bs-dark-bg-subtle); + --bs-list-group-border-color: var(--bs-dark-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-dark-border-subtle); + --bs-list-group-active-color: var(--bs-dark-bg-subtle); + --bs-list-group-active-bg: var(--bs-dark-text-emphasis); + --bs-list-group-active-border-color: var(--bs-dark-text-emphasis); +} + +.btn-close { + --bs-btn-close-color: #000; + --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e"); + --bs-btn-close-opacity: 0.5; + --bs-btn-close-hover-opacity: 0.75; + --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-btn-close-focus-opacity: 1; + --bs-btn-close-disabled-opacity: 0.25; + --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%); + box-sizing: content-box; + width: 1em; + height: 1em; + padding: 0.25em 0.25em; + color: var(--bs-btn-close-color); + background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat; + border: 0; + border-radius: 0.375rem; + opacity: var(--bs-btn-close-opacity); +} +.btn-close:hover { + color: var(--bs-btn-close-color); + text-decoration: none; + opacity: var(--bs-btn-close-hover-opacity); +} +.btn-close:focus { + outline: 0; + box-shadow: var(--bs-btn-close-focus-shadow); + opacity: var(--bs-btn-close-focus-opacity); +} +.btn-close:disabled, .btn-close.disabled { + pointer-events: none; + user-select: none; + opacity: var(--bs-btn-close-disabled-opacity); +} + +.btn-close-white { + filter: var(--bs-btn-close-white-filter); +} + +[data-bs-theme=dark] .btn-close { + filter: var(--bs-btn-close-white-filter); +} + +.toast { + --bs-toast-zindex: 1090; + --bs-toast-padding-x: 0.75rem; + --bs-toast-padding-y: 0.5rem; + --bs-toast-spacing: 1.5rem; + --bs-toast-max-width: 350px; + --bs-toast-font-size: 0.875rem; + --bs-toast-color: ; + --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-border-width: var(--bs-border-width); + --bs-toast-border-color: var(--bs-border-color-translucent); + --bs-toast-border-radius: var(--bs-border-radius); + --bs-toast-box-shadow: var(--bs-box-shadow); + --bs-toast-header-color: var(--bs-secondary-color); + --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-header-border-color: var(--bs-border-color-translucent); + width: var(--bs-toast-max-width); + max-width: 100%; + font-size: var(--bs-toast-font-size); + color: var(--bs-toast-color); + pointer-events: auto; + background-color: var(--bs-toast-bg); + background-clip: padding-box; + border: var(--bs-toast-border-width) solid var(--bs-toast-border-color); + box-shadow: var(--bs-toast-box-shadow); + border-radius: var(--bs-toast-border-radius); +} +.toast.showing { + opacity: 0; +} +.toast:not(.show) { + display: none; +} + +.toast-container { + --bs-toast-zindex: 1090; + position: absolute; + z-index: var(--bs-toast-zindex); + width: max-content; + max-width: 100%; + pointer-events: none; +} +.toast-container > :not(:last-child) { + margin-bottom: var(--bs-toast-spacing); +} + +.toast-header { + display: flex; + align-items: center; + padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x); + color: var(--bs-toast-header-color); + background-color: var(--bs-toast-header-bg); + background-clip: padding-box; + border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color); + border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); + border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); +} +.toast-header .btn-close { + margin-right: calc(-0.5 * var(--bs-toast-padding-x)); + margin-left: var(--bs-toast-padding-x); +} + +.toast-body { + padding: var(--bs-toast-padding-x); + word-wrap: break-word; +} + +.modal { + --bs-modal-zindex: 1055; + --bs-modal-width: 500px; + --bs-modal-padding: 1rem; + --bs-modal-margin: 0.5rem; + --bs-modal-color: ; + --bs-modal-bg: var(--bs-body-bg); + --bs-modal-border-color: var(--bs-border-color-translucent); + --bs-modal-border-width: var(--bs-border-width); + --bs-modal-border-radius: var(--bs-border-radius-lg); + --bs-modal-box-shadow: var(--bs-box-shadow-sm); + --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width))); + --bs-modal-header-padding-x: 1rem; + --bs-modal-header-padding-y: 1rem; + --bs-modal-header-padding: 1rem 1rem; + --bs-modal-header-border-color: var(--bs-border-color); + --bs-modal-header-border-width: var(--bs-border-width); + --bs-modal-title-line-height: 1.5; + --bs-modal-footer-gap: 0.5rem; + --bs-modal-footer-bg: ; + --bs-modal-footer-border-color: var(--bs-border-color); + --bs-modal-footer-border-width: var(--bs-border-width); + position: fixed; + top: 0; + left: 0; + z-index: var(--bs-modal-zindex); + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: var(--bs-modal-margin); + pointer-events: none; +} +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -50px); +} +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} +.modal.show .modal-dialog { + transform: none; +} +.modal.modal-static .modal-dialog { + transform: scale(1.02); +} + +.modal-dialog-scrollable { + height: calc(100% - var(--bs-modal-margin) * 2); +} +.modal-dialog-scrollable .modal-content { + max-height: 100%; + overflow: hidden; +} +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - var(--bs-modal-margin) * 2); +} + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + color: var(--bs-modal-color); + pointer-events: auto; + background-color: var(--bs-modal-bg); + background-clip: padding-box; + border: var(--bs-modal-border-width) solid var(--bs-modal-border-color); + border-radius: var(--bs-modal-border-radius); + outline: 0; +} + +.modal-backdrop { + --bs-backdrop-zindex: 1050; + --bs-backdrop-bg: #000; + --bs-backdrop-opacity: 0.5; + position: fixed; + top: 0; + left: 0; + z-index: var(--bs-backdrop-zindex); + width: 100vw; + height: 100vh; + background-color: var(--bs-backdrop-bg); +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop.show { + opacity: var(--bs-backdrop-opacity); +} + +.modal-header { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding: var(--bs-modal-header-padding); + border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color); + border-top-left-radius: var(--bs-modal-inner-border-radius); + border-top-right-radius: var(--bs-modal-inner-border-radius); +} +.modal-header .btn-close { + padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5); + margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto; +} + +.modal-title { + margin-bottom: 0; + line-height: var(--bs-modal-title-line-height); +} + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: var(--bs-modal-padding); +} + +.modal-footer { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5); + background-color: var(--bs-modal-footer-bg); + border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color); + border-bottom-right-radius: var(--bs-modal-inner-border-radius); + border-bottom-left-radius: var(--bs-modal-inner-border-radius); +} +.modal-footer > * { + margin: calc(var(--bs-modal-footer-gap) * 0.5); +} + +@media (min-width: 576px) { + .modal { + --bs-modal-margin: 1.75rem; + --bs-modal-box-shadow: var(--bs-box-shadow); + } + .modal-dialog { + max-width: var(--bs-modal-width); + margin-right: auto; + margin-left: auto; + } + .modal-sm { + --bs-modal-width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + --bs-modal-width: 800px; + } +} +@media (min-width: 1200px) { + .modal-xl { + --bs-modal-width: 1140px; + } +} +.modal-fullscreen { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; +} +.modal-fullscreen .modal-content { + height: 100%; + border: 0; + border-radius: 0; +} +.modal-fullscreen .modal-header, +.modal-fullscreen .modal-footer { + border-radius: 0; +} +.modal-fullscreen .modal-body { + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .modal-fullscreen-sm-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-sm-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-header, + .modal-fullscreen-sm-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 767.98px) { + .modal-fullscreen-md-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-md-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-md-down .modal-header, + .modal-fullscreen-md-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-md-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 991.98px) { + .modal-fullscreen-lg-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-lg-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-header, + .modal-fullscreen-lg-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 1199.98px) { + .modal-fullscreen-xl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-header, + .modal-fullscreen-xl-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 1399.98px) { + .modal-fullscreen-xxl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xxl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-header, + .modal-fullscreen-xxl-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-body { + overflow-y: auto; + } +} +.tooltip { + --bs-tooltip-zindex: 1080; + --bs-tooltip-max-width: 200px; + --bs-tooltip-padding-x: 0.5rem; + --bs-tooltip-padding-y: 0.25rem; + --bs-tooltip-margin: ; + --bs-tooltip-font-size: 0.875rem; + --bs-tooltip-color: var(--bs-body-bg); + --bs-tooltip-bg: var(--bs-emphasis-color); + --bs-tooltip-border-radius: var(--bs-border-radius); + --bs-tooltip-opacity: 0.9; + --bs-tooltip-arrow-width: 0.8rem; + --bs-tooltip-arrow-height: 0.4rem; + z-index: var(--bs-tooltip-zindex); + display: block; + margin: var(--bs-tooltip-margin); + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + white-space: normal; + word-spacing: normal; + line-break: auto; + font-size: var(--bs-tooltip-font-size); + word-wrap: break-word; + opacity: 0; +} +.tooltip.show { + opacity: var(--bs-tooltip-opacity); +} +.tooltip .tooltip-arrow { + display: block; + width: var(--bs-tooltip-arrow-width); + height: var(--bs-tooltip-arrow-height); +} +.tooltip .tooltip-arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { + bottom: calc(-1 * var(--bs-tooltip-arrow-height)); +} +.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { + top: -1px; + border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-top-color: var(--bs-tooltip-bg); +} + +/* rtl:begin:ignore */ +.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { + left: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); +} +.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { + right: -1px; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-right-color: var(--bs-tooltip-bg); +} + +/* rtl:end:ignore */ +.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { + top: calc(-1 * var(--bs-tooltip-arrow-height)); +} +.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { + bottom: -1px; + border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-bottom-color: var(--bs-tooltip-bg); +} + +/* rtl:begin:ignore */ +.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { + right: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); +} +.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { + left: -1px; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-left-color: var(--bs-tooltip-bg); +} + +/* rtl:end:ignore */ +.tooltip-inner { + max-width: var(--bs-tooltip-max-width); + padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x); + color: var(--bs-tooltip-color); + text-align: center; + background-color: var(--bs-tooltip-bg); + border-radius: var(--bs-tooltip-border-radius); +} + +.popover { + --bs-popover-zindex: 1070; + --bs-popover-max-width: 276px; + --bs-popover-font-size: 0.875rem; + --bs-popover-bg: var(--bs-body-bg); + --bs-popover-border-width: var(--bs-border-width); + --bs-popover-border-color: var(--bs-border-color-translucent); + --bs-popover-border-radius: var(--bs-border-radius-lg); + --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width)); + --bs-popover-box-shadow: var(--bs-box-shadow); + --bs-popover-header-padding-x: 1rem; + --bs-popover-header-padding-y: 0.5rem; + --bs-popover-header-font-size: 1rem; + --bs-popover-header-color: inherit; + --bs-popover-header-bg: var(--bs-secondary-bg); + --bs-popover-body-padding-x: 1rem; + --bs-popover-body-padding-y: 1rem; + --bs-popover-body-color: var(--bs-body-color); + --bs-popover-arrow-width: 1rem; + --bs-popover-arrow-height: 0.5rem; + --bs-popover-arrow-border: var(--bs-popover-border-color); + z-index: var(--bs-popover-zindex); + display: block; + max-width: var(--bs-popover-max-width); + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + white-space: normal; + word-spacing: normal; + line-break: auto; + font-size: var(--bs-popover-font-size); + word-wrap: break-word; + background-color: var(--bs-popover-bg); + background-clip: padding-box; + border: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-radius: var(--bs-popover-border-radius); +} +.popover .popover-arrow { + display: block; + width: var(--bs-popover-arrow-width); + height: var(--bs-popover-arrow-height); +} +.popover .popover-arrow::before, .popover .popover-arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; + border-width: 0; +} + +.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { + bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { + bottom: 0; + border-top-color: var(--bs-popover-arrow-border); +} +.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + bottom: var(--bs-popover-border-width); + border-top-color: var(--bs-popover-bg); +} + +/* rtl:begin:ignore */ +.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { + left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { + left: 0; + border-right-color: var(--bs-popover-arrow-border); +} +.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + left: var(--bs-popover-border-width); + border-right-color: var(--bs-popover-bg); +} + +/* rtl:end:ignore */ +.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { + top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { + top: 0; + border-bottom-color: var(--bs-popover-arrow-border); +} +.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + top: var(--bs-popover-border-width); + border-bottom-color: var(--bs-popover-bg); +} +.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: var(--bs-popover-arrow-width); + margin-left: calc(-0.5 * var(--bs-popover-arrow-width)); + content: ""; + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); +} + +/* rtl:begin:ignore */ +.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { + right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { + right: 0; + border-left-color: var(--bs-popover-arrow-border); +} +.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + right: var(--bs-popover-border-width); + border-left-color: var(--bs-popover-bg); +} + +/* rtl:end:ignore */ +.popover-header { + padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x); + margin-bottom: 0; + font-size: var(--bs-popover-header-font-size); + color: var(--bs-popover-header-color); + background-color: var(--bs-popover-header-bg); + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-top-left-radius: var(--bs-popover-inner-border-radius); + border-top-right-radius: var(--bs-popover-inner-border-radius); +} +.popover-header:empty { + display: none; +} + +.popover-body { + padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x); + color: var(--bs-popover-body-color); +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + transition: transform 0.6s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; +} +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-start, +.carousel-fade .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; +} +.carousel-fade .active.carousel-item-start, +.carousel-fade .active.carousel-item-end { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-start, + .carousel-fade .active.carousel-item-end { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + padding: 0; + color: #fff; + text-align: center; + background: none; + border: 0; + opacity: 0.5; + transition: opacity 0.15s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 2rem; + height: 2rem; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +/* rtl:options: { + "autoRename": true, + "stringMap":[ { + "name" : "prev-next", + "search" : "prev", + "replace" : "next" + } ] +} */ +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + margin-right: 15%; + margin-bottom: 1rem; + margin-left: 15%; +} +.carousel-indicators [data-bs-target] { + box-sizing: content-box; + flex: 0 1 auto; + width: 30px; + height: 3px; + padding: 0; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + transition: opacity 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-indicators [data-bs-target] { + transition: none; + } +} +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 1.25rem; + left: 15%; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + color: #fff; + text-align: center; +} + +.carousel-dark .carousel-control-prev-icon, +.carousel-dark .carousel-control-next-icon { + filter: invert(1) grayscale(100); +} +.carousel-dark .carousel-indicators [data-bs-target] { + background-color: #000; +} +.carousel-dark .carousel-caption { + color: #000; +} + +[data-bs-theme=dark] .carousel .carousel-control-prev-icon, +[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon, +[data-bs-theme=dark].carousel .carousel-control-next-icon { + filter: invert(1) grayscale(100); +} +[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] { + background-color: #000; +} +[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption { + color: #000; +} + +.spinner-grow, +.spinner-border { + display: inline-block; + width: var(--bs-spinner-width); + height: var(--bs-spinner-height); + vertical-align: var(--bs-spinner-vertical-align); + border-radius: 50%; + animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); +} + +@keyframes spinner-border { + to { + transform: rotate(360deg) /* rtl:ignore */; + } +} +.spinner-border { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-border-width: 0.25em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-border; + border: var(--bs-spinner-border-width) solid currentcolor; + border-right-color: transparent; +} + +.spinner-border-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; + --bs-spinner-border-width: 0.2em; +} + +@keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } +} +.spinner-grow { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-grow; + background-color: currentcolor; + opacity: 0; +} + +.spinner-grow-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; +} + +@media (prefers-reduced-motion: reduce) { + .spinner-border, + .spinner-grow { + --bs-spinner-animation-speed: 1.5s; + } +} +.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm { + --bs-offcanvas-zindex: 1045; + --bs-offcanvas-width: 400px; + --bs-offcanvas-height: 30vh; + --bs-offcanvas-padding-x: 1rem; + --bs-offcanvas-padding-y: 1rem; + --bs-offcanvas-color: var(--bs-body-color); + --bs-offcanvas-bg: var(--bs-body-bg); + --bs-offcanvas-border-width: var(--bs-border-width); + --bs-offcanvas-border-color: var(--bs-border-color-translucent); + --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm); + --bs-offcanvas-transition: transform 0.3s ease-in-out; + --bs-offcanvas-title-line-height: 1.5; +} + +@media (max-width: 575.98px) { + .offcanvas-sm { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-sm { + transition: none; + } +} +@media (max-width: 575.98px) { + .offcanvas-sm.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-sm.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-sm.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-sm.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) { + transform: none; + } + .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show { + visibility: visible; + } +} +@media (min-width: 576px) { + .offcanvas-sm { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-sm .offcanvas-header { + display: none; + } + .offcanvas-sm .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 767.98px) { + .offcanvas-md { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-md { + transition: none; + } +} +@media (max-width: 767.98px) { + .offcanvas-md.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-md.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-md.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-md.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) { + transform: none; + } + .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show { + visibility: visible; + } +} +@media (min-width: 768px) { + .offcanvas-md { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-md .offcanvas-header { + display: none; + } + .offcanvas-md .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 991.98px) { + .offcanvas-lg { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-lg { + transition: none; + } +} +@media (max-width: 991.98px) { + .offcanvas-lg.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-lg.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-lg.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-lg.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) { + transform: none; + } + .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show { + visibility: visible; + } +} +@media (min-width: 992px) { + .offcanvas-lg { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-lg .offcanvas-header { + display: none; + } + .offcanvas-lg .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1199.98px) { + .offcanvas-xl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xl { + transition: none; + } +} +@media (max-width: 1199.98px) { + .offcanvas-xl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) { + transform: none; + } + .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show { + visibility: visible; + } +} +@media (min-width: 1200px) { + .offcanvas-xl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xl .offcanvas-header { + display: none; + } + .offcanvas-xl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1399.98px) { + .offcanvas-xxl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xxl { + transition: none; + } +} +@media (max-width: 1399.98px) { + .offcanvas-xxl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xxl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xxl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xxl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) { + transform: none; + } + .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show { + visibility: visible; + } +} +@media (min-width: 1400px) { + .offcanvas-xxl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xxl .offcanvas-header { + display: none; + } + .offcanvas-xxl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +.offcanvas { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); +} +@media (prefers-reduced-motion: reduce) { + .offcanvas { + transition: none; + } +} +.offcanvas.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); +} +.offcanvas.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); +} +.offcanvas.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); +} +.offcanvas.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); +} +.offcanvas.showing, .offcanvas.show:not(.hiding) { + transform: none; +} +.offcanvas.showing, .offcanvas.hiding, .offcanvas.show { + visibility: visible; +} + +.offcanvas-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} +.offcanvas-backdrop.fade { + opacity: 0; +} +.offcanvas-backdrop.show { + opacity: 0.5; +} + +.offcanvas-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); +} +.offcanvas-header .btn-close { + padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); + margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y)); +} + +.offcanvas-title { + margin-bottom: 0; + line-height: var(--bs-offcanvas-title-line-height); +} + +.offcanvas-body { + flex-grow: 1; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); + overflow-y: auto; +} + +.placeholder { + display: inline-block; + min-height: 1em; + vertical-align: middle; + cursor: wait; + background-color: currentcolor; + opacity: 0.5; +} +.placeholder.btn::before { + display: inline-block; + content: ""; +} + +.placeholder-xs { + min-height: 0.6em; +} + +.placeholder-sm { + min-height: 0.8em; +} + +.placeholder-lg { + min-height: 1.2em; +} + +.placeholder-glow .placeholder { + animation: placeholder-glow 2s ease-in-out infinite; +} + +@keyframes placeholder-glow { + 50% { + opacity: 0.2; + } +} +.placeholder-wave { + mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + mask-size: 200% 100%; + animation: placeholder-wave 2s linear infinite; +} + +@keyframes placeholder-wave { + 100% { + mask-position: -200% 0%; + } +} +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.text-bg-primary { + color: #fff !important; + background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-secondary { + color: #fff !important; + background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-success { + color: #fff !important; + background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-info { + color: #000 !important; + background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-warning { + color: #000 !important; + background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-danger { + color: #fff !important; + background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-light { + color: #000 !important; + background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-dark { + color: #fff !important; + background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.link-primary { + color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-primary:hover, .link-primary:focus { + color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-secondary { + color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-secondary:hover, .link-secondary:focus { + color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-success { + color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-success:hover, .link-success:focus { + color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-info { + color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-info:hover, .link-info:focus { + color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-warning { + color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-warning:hover, .link-warning:focus { + color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-danger { + color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-danger:hover, .link-danger:focus { + color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-light { + color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-light:hover, .link-light:focus { + color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-dark { + color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-dark:hover, .link-dark:focus { + color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-body-emphasis { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-body-emphasis:hover, .link-body-emphasis:focus { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; +} + +.focus-ring:focus { + outline: 0; + box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); +} + +.icon-link { + display: inline-flex; + gap: 0.375rem; + align-items: center; + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-underline-offset: 0.25em; + backface-visibility: hidden; +} +.icon-link > .bi { + flex-shrink: 0; + width: 1em; + height: 1em; + fill: currentcolor; + transition: 0.2s ease-in-out transform; +} +@media (prefers-reduced-motion: reduce) { + .icon-link > .bi { + transition: none; + } +} + +.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi { + transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0)); +} + +.ratio { + position: relative; + width: 100%; +} +.ratio::before { + display: block; + padding-top: var(--bs-aspect-ratio); + content: ""; +} +.ratio > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.ratio-1x1 { + --bs-aspect-ratio: 100%; +} + +.ratio-4x3 { + --bs-aspect-ratio: 75%; +} + +.ratio-16x9 { + --bs-aspect-ratio: 56.25%; +} + +.ratio-21x9 { + --bs-aspect-ratio: 42.8571428571%; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +.sticky-top { + position: sticky; + top: 0; + z-index: 1020; +} + +.sticky-bottom { + position: sticky; + bottom: 0; + z-index: 1020; +} + +@media (min-width: 576px) { + .sticky-sm-top { + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-sm-bottom { + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 768px) { + .sticky-md-top { + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-md-bottom { + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 992px) { + .sticky-lg-top { + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-lg-bottom { + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 1200px) { + .sticky-xl-top { + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-xl-bottom { + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 1400px) { + .sticky-xxl-top { + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-xxl-bottom { + position: sticky; + bottom: 0; + z-index: 1020; + } +} +.hstack { + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; +} + +.vstack { + display: flex; + flex: 1 1 auto; + flex-direction: column; + align-self: stretch; +} + +.visually-hidden, +.visually-hidden-focusable:not(:focus):not(:focus-within) { + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} +.visually-hidden:not(caption), +.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) { + position: absolute !important; +} + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vr { + display: inline-block; + align-self: stretch; + width: var(--bs-border-width); + min-height: 1em; + background-color: currentcolor; + opacity: 0.25; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.float-start { + float: left !important; +} + +.float-end { + float: right !important; +} + +.float-none { + float: none !important; +} + +.object-fit-contain { + object-fit: contain !important; +} + +.object-fit-cover { + object-fit: cover !important; +} + +.object-fit-fill { + object-fit: fill !important; +} + +.object-fit-scale { + object-fit: scale-down !important; +} + +.object-fit-none { + object-fit: none !important; +} + +.opacity-0 { + opacity: 0 !important; +} + +.opacity-25 { + opacity: 0.25 !important; +} + +.opacity-50 { + opacity: 0.5 !important; +} + +.opacity-75 { + opacity: 0.75 !important; +} + +.opacity-100 { + opacity: 1 !important; +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.overflow-visible { + overflow: visible !important; +} + +.overflow-scroll { + overflow: scroll !important; +} + +.overflow-x-auto { + overflow-x: auto !important; +} + +.overflow-x-hidden { + overflow-x: hidden !important; +} + +.overflow-x-visible { + overflow-x: visible !important; +} + +.overflow-x-scroll { + overflow-x: scroll !important; +} + +.overflow-y-auto { + overflow-y: auto !important; +} + +.overflow-y-hidden { + overflow-y: hidden !important; +} + +.overflow-y-visible { + overflow-y: visible !important; +} + +.overflow-y-scroll { + overflow-y: scroll !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-inline-grid { + display: inline-grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.shadow { + box-shadow: var(--bs-box-shadow) !important; +} + +.shadow-sm { + box-shadow: var(--bs-box-shadow-sm) !important; +} + +.shadow-lg { + box-shadow: var(--bs-box-shadow-lg) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.focus-ring-primary { + --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-secondary { + --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-success { + --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-info { + --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-warning { + --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-danger { + --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-light { + --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-dark { + --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity)); +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: sticky !important; +} + +.top-0 { + top: 0 !important; +} + +.top-50 { + top: 50% !important; +} + +.top-100 { + top: 100% !important; +} + +.bottom-0 { + bottom: 0 !important; +} + +.bottom-50 { + bottom: 50% !important; +} + +.bottom-100 { + bottom: 100% !important; +} + +.start-0 { + left: 0 !important; +} + +.start-50 { + left: 50% !important; +} + +.start-100 { + left: 100% !important; +} + +.end-0 { + right: 0 !important; +} + +.end-50 { + right: 50% !important; +} + +.end-100 { + right: 100% !important; +} + +.translate-middle { + transform: translate(-50%, -50%) !important; +} + +.translate-middle-x { + transform: translateX(-50%) !important; +} + +.translate-middle-y { + transform: translateY(-50%) !important; +} + +.border { + border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top { + border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-end { + border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-end-0 { + border-right: 0 !important; +} + +.border-bottom { + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-start { + border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-start-0 { + border-left: 0 !important; +} + +.border-primary { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important; +} + +.border-secondary { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important; +} + +.border-success { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important; +} + +.border-info { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important; +} + +.border-warning { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important; +} + +.border-danger { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; +} + +.border-light { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; +} + +.border-dark { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important; +} + +.border-black { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important; +} + +.border-white { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; +} + +.border-primary-subtle { + border-color: var(--bs-primary-border-subtle) !important; +} + +.border-secondary-subtle { + border-color: var(--bs-secondary-border-subtle) !important; +} + +.border-success-subtle { + border-color: var(--bs-success-border-subtle) !important; +} + +.border-info-subtle { + border-color: var(--bs-info-border-subtle) !important; +} + +.border-warning-subtle { + border-color: var(--bs-warning-border-subtle) !important; +} + +.border-danger-subtle { + border-color: var(--bs-danger-border-subtle) !important; +} + +.border-light-subtle { + border-color: var(--bs-light-border-subtle) !important; +} + +.border-dark-subtle { + border-color: var(--bs-dark-border-subtle) !important; +} + +.border-1 { + border-width: 1px !important; +} + +.border-2 { + border-width: 2px !important; +} + +.border-3 { + border-width: 3px !important; +} + +.border-4 { + border-width: 4px !important; +} + +.border-5 { + border-width: 5px !important; +} + +.border-opacity-10 { + --bs-border-opacity: 0.1; +} + +.border-opacity-25 { + --bs-border-opacity: 0.25; +} + +.border-opacity-50 { + --bs-border-opacity: 0.5; +} + +.border-opacity-75 { + --bs-border-opacity: 0.75; +} + +.border-opacity-100 { + --bs-border-opacity: 1; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.vw-100 { + width: 100vw !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.vh-100 { + height: 100vh !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-right: 0 !important; +} + +.me-1 { + margin-right: 0.25rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.me-3 { + margin-right: 1rem !important; +} + +.me-4 { + margin-right: 1.5rem !important; +} + +.me-5 { + margin-right: 3rem !important; +} + +.me-auto { + margin-right: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-left: 0 !important; +} + +.ms-1 { + margin-left: 0.25rem !important; +} + +.ms-2 { + margin-left: 0.5rem !important; +} + +.ms-3 { + margin-left: 1rem !important; +} + +.ms-4 { + margin-left: 1.5rem !important; +} + +.ms-5 { + margin-left: 3rem !important; +} + +.ms-auto { + margin-left: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-right: 0 !important; +} + +.pe-1 { + padding-right: 0.25rem !important; +} + +.pe-2 { + padding-right: 0.5rem !important; +} + +.pe-3 { + padding-right: 1rem !important; +} + +.pe-4 { + padding-right: 1.5rem !important; +} + +.pe-5 { + padding-right: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-left: 0 !important; +} + +.ps-1 { + padding-left: 0.25rem !important; +} + +.ps-2 { + padding-left: 0.5rem !important; +} + +.ps-3 { + padding-left: 1rem !important; +} + +.ps-4 { + padding-left: 1.5rem !important; +} + +.ps-5 { + padding-left: 3rem !important; +} + +.gap-0 { + gap: 0 !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 1rem !important; +} + +.gap-4 { + gap: 1.5rem !important; +} + +.gap-5 { + gap: 3rem !important; +} + +.row-gap-0 { + row-gap: 0 !important; +} + +.row-gap-1 { + row-gap: 0.25rem !important; +} + +.row-gap-2 { + row-gap: 0.5rem !important; +} + +.row-gap-3 { + row-gap: 1rem !important; +} + +.row-gap-4 { + row-gap: 1.5rem !important; +} + +.row-gap-5 { + row-gap: 3rem !important; +} + +.column-gap-0 { + column-gap: 0 !important; +} + +.column-gap-1 { + column-gap: 0.25rem !important; +} + +.column-gap-2 { + column-gap: 0.5rem !important; +} + +.column-gap-3 { + column-gap: 1rem !important; +} + +.column-gap-4 { + column-gap: 1.5rem !important; +} + +.column-gap-5 { + column-gap: 3rem !important; +} + +.font-monospace { + font-family: var(--bs-font-monospace) !important; +} + +.fs-1 { + font-size: calc(1.375rem + 1.5vw) !important; +} + +.fs-2 { + font-size: calc(1.325rem + 0.9vw) !important; +} + +.fs-3 { + font-size: calc(1.3rem + 0.6vw) !important; +} + +.fs-4 { + font-size: calc(1.275rem + 0.3vw) !important; +} + +.fs-5 { + font-size: 1.25rem !important; +} + +.fs-6 { + font-size: 1rem !important; +} + +.fst-italic { + font-style: italic !important; +} + +.fst-normal { + font-style: normal !important; +} + +.fw-lighter { + font-weight: lighter !important; +} + +.fw-light { + font-weight: 300 !important; +} + +.fw-normal { + font-weight: 400 !important; +} + +.fw-medium { + font-weight: 500 !important; +} + +.fw-semibold { + font-weight: 600 !important; +} + +.fw-bold { + font-weight: 700 !important; +} + +.fw-bolder { + font-weight: bolder !important; +} + +.lh-1 { + line-height: 1 !important; +} + +.lh-sm { + line-height: 1.25 !important; +} + +.lh-base { + line-height: 1.5 !important; +} + +.lh-lg { + line-height: 2 !important; +} + +.text-start { + text-align: left !important; +} + +.text-end { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-decoration-underline { + text-decoration: underline !important; +} + +.text-decoration-line-through { + text-decoration: line-through !important; +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +/* rtl:begin:remove */ +.text-break { + word-wrap: break-word !important; + word-break: break-word !important; +} + +/* rtl:end:remove */ +.text-primary { + --bs-text-opacity: 1; + color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; +} + +.text-secondary { + --bs-text-opacity: 1; + color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; +} + +.text-success { + --bs-text-opacity: 1; + color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; +} + +.text-info { + --bs-text-opacity: 1; + color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; +} + +.text-warning { + --bs-text-opacity: 1; + color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; +} + +.text-danger { + --bs-text-opacity: 1; + color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; +} + +.text-light { + --bs-text-opacity: 1; + color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; +} + +.text-dark { + --bs-text-opacity: 1; + color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; +} + +.text-black { + --bs-text-opacity: 1; + color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; +} + +.text-white { + --bs-text-opacity: 1; + color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; +} + +.text-body { + --bs-text-opacity: 1; + color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; +} + +.text-muted { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-black-50 { + --bs-text-opacity: 1; + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + --bs-text-opacity: 1; + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-body-secondary { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-body-tertiary { + --bs-text-opacity: 1; + color: var(--bs-tertiary-color) !important; +} + +.text-body-emphasis { + --bs-text-opacity: 1; + color: var(--bs-emphasis-color) !important; +} + +.text-reset { + --bs-text-opacity: 1; + color: inherit !important; +} + +.text-opacity-25 { + --bs-text-opacity: 0.25; +} + +.text-opacity-50 { + --bs-text-opacity: 0.5; +} + +.text-opacity-75 { + --bs-text-opacity: 0.75; +} + +.text-opacity-100 { + --bs-text-opacity: 1; +} + +.text-primary-emphasis { + color: var(--bs-primary-text-emphasis) !important; +} + +.text-secondary-emphasis { + color: var(--bs-secondary-text-emphasis) !important; +} + +.text-success-emphasis { + color: var(--bs-success-text-emphasis) !important; +} + +.text-info-emphasis { + color: var(--bs-info-text-emphasis) !important; +} + +.text-warning-emphasis { + color: var(--bs-warning-text-emphasis) !important; +} + +.text-danger-emphasis { + color: var(--bs-danger-text-emphasis) !important; +} + +.text-light-emphasis { + color: var(--bs-light-text-emphasis) !important; +} + +.text-dark-emphasis { + color: var(--bs-dark-text-emphasis) !important; +} + +.link-opacity-10 { + --bs-link-opacity: 0.1; +} + +.link-opacity-10-hover:hover { + --bs-link-opacity: 0.1; +} + +.link-opacity-25 { + --bs-link-opacity: 0.25; +} + +.link-opacity-25-hover:hover { + --bs-link-opacity: 0.25; +} + +.link-opacity-50 { + --bs-link-opacity: 0.5; +} + +.link-opacity-50-hover:hover { + --bs-link-opacity: 0.5; +} + +.link-opacity-75 { + --bs-link-opacity: 0.75; +} + +.link-opacity-75-hover:hover { + --bs-link-opacity: 0.75; +} + +.link-opacity-100 { + --bs-link-opacity: 1; +} + +.link-opacity-100-hover:hover { + --bs-link-opacity: 1; +} + +.link-offset-1 { + text-underline-offset: 0.125em !important; +} + +.link-offset-1-hover:hover { + text-underline-offset: 0.125em !important; +} + +.link-offset-2 { + text-underline-offset: 0.25em !important; +} + +.link-offset-2-hover:hover { + text-underline-offset: 0.25em !important; +} + +.link-offset-3 { + text-underline-offset: 0.375em !important; +} + +.link-offset-3-hover:hover { + text-underline-offset: 0.375em !important; +} + +.link-underline-primary { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-secondary { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-success { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-info { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-warning { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-danger { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-light { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-dark { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline { + --bs-link-underline-opacity: 1; + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} + +.link-underline-opacity-0 { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-0-hover:hover { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-10 { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-10-hover:hover { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-25 { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-25-hover:hover { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-50 { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-50-hover:hover { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-75 { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-75-hover:hover { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-100 { + --bs-link-underline-opacity: 1; +} + +.link-underline-opacity-100-hover:hover { + --bs-link-underline-opacity: 1; +} + +.bg-primary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-success { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-info { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-warning { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-danger { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-light { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-dark { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-black { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-white { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-transparent { + --bs-bg-opacity: 1; + background-color: transparent !important; +} + +.bg-body-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body-tertiary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-opacity-10 { + --bs-bg-opacity: 0.1; +} + +.bg-opacity-25 { + --bs-bg-opacity: 0.25; +} + +.bg-opacity-50 { + --bs-bg-opacity: 0.5; +} + +.bg-opacity-75 { + --bs-bg-opacity: 0.75; +} + +.bg-opacity-100 { + --bs-bg-opacity: 1; +} + +.bg-primary-subtle { + background-color: var(--bs-primary-bg-subtle) !important; +} + +.bg-secondary-subtle { + background-color: var(--bs-secondary-bg-subtle) !important; +} + +.bg-success-subtle { + background-color: var(--bs-success-bg-subtle) !important; +} + +.bg-info-subtle { + background-color: var(--bs-info-bg-subtle) !important; +} + +.bg-warning-subtle { + background-color: var(--bs-warning-bg-subtle) !important; +} + +.bg-danger-subtle { + background-color: var(--bs-danger-bg-subtle) !important; +} + +.bg-light-subtle { + background-color: var(--bs-light-bg-subtle) !important; +} + +.bg-dark-subtle { + background-color: var(--bs-dark-bg-subtle) !important; +} + +.bg-gradient { + background-image: var(--bs-gradient) !important; +} + +.user-select-all { + user-select: all !important; +} + +.user-select-auto { + user-select: auto !important; +} + +.user-select-none { + user-select: none !important; +} + +.pe-none { + pointer-events: none !important; +} + +.pe-auto { + pointer-events: auto !important; +} + +.rounded { + border-radius: var(--bs-border-radius) !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.rounded-1 { + border-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-2 { + border-radius: var(--bs-border-radius) !important; +} + +.rounded-3 { + border-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-4 { + border-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-5 { + border-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-top { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-0 { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +.rounded-top-1 { + border-top-left-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-top-2 { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-3 { + border-top-left-radius: var(--bs-border-radius-lg) !important; + border-top-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-top-4 { + border-top-left-radius: var(--bs-border-radius-xl) !important; + border-top-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-top-5 { + border-top-left-radius: var(--bs-border-radius-xxl) !important; + border-top-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-top-circle { + border-top-left-radius: 50% !important; + border-top-right-radius: 50% !important; +} + +.rounded-top-pill { + border-top-left-radius: var(--bs-border-radius-pill) !important; + border-top-right-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-end { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-0 { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.rounded-end-1 { + border-top-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-end-2 { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-3 { + border-top-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-end-4 { + border-top-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-end-5 { + border-top-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-end-circle { + border-top-right-radius: 50% !important; + border-bottom-right-radius: 50% !important; +} + +.rounded-end-pill { + border-top-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-right-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-bottom { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-0 { + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-bottom-1 { + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-bottom-2 { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-3 { + border-bottom-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-bottom-4 { + border-bottom-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-bottom-5 { + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-bottom-circle { + border-bottom-right-radius: 50% !important; + border-bottom-left-radius: 50% !important; +} + +.rounded-bottom-pill { + border-bottom-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-left-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-start { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-0 { + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; +} + +.rounded-start-1 { + border-bottom-left-radius: var(--bs-border-radius-sm) !important; + border-top-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-start-2 { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-3 { + border-bottom-left-radius: var(--bs-border-radius-lg) !important; + border-top-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-start-4 { + border-bottom-left-radius: var(--bs-border-radius-xl) !important; + border-top-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-start-5 { + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; + border-top-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-start-circle { + border-bottom-left-radius: 50% !important; + border-top-left-radius: 50% !important; +} + +.rounded-start-pill { + border-bottom-left-radius: var(--bs-border-radius-pill) !important; + border-top-left-radius: var(--bs-border-radius-pill) !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +.z-n1 { + z-index: -1 !important; +} + +.z-0 { + z-index: 0 !important; +} + +.z-1 { + z-index: 1 !important; +} + +.z-2 { + z-index: 2 !important; +} + +.z-3 { + z-index: 3 !important; +} + +@media (min-width: 576px) { + .float-sm-start { + float: left !important; + } + .float-sm-end { + float: right !important; + } + .float-sm-none { + float: none !important; + } + .object-fit-sm-contain { + object-fit: contain !important; + } + .object-fit-sm-cover { + object-fit: cover !important; + } + .object-fit-sm-fill { + object-fit: fill !important; + } + .object-fit-sm-scale { + object-fit: scale-down !important; + } + .object-fit-sm-none { + object-fit: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-grid { + display: grid !important; + } + .d-sm-inline-grid { + display: inline-grid !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: flex !important; + } + .d-sm-inline-flex { + display: inline-flex !important; + } + .d-sm-none { + display: none !important; + } + .flex-sm-fill { + flex: 1 1 auto !important; + } + .flex-sm-row { + flex-direction: row !important; + } + .flex-sm-column { + flex-direction: column !important; + } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + .flex-sm-wrap { + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-sm-start { + justify-content: flex-start !important; + } + .justify-content-sm-end { + justify-content: flex-end !important; + } + .justify-content-sm-center { + justify-content: center !important; + } + .justify-content-sm-between { + justify-content: space-between !important; + } + .justify-content-sm-around { + justify-content: space-around !important; + } + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + .align-items-sm-start { + align-items: flex-start !important; + } + .align-items-sm-end { + align-items: flex-end !important; + } + .align-items-sm-center { + align-items: center !important; + } + .align-items-sm-baseline { + align-items: baseline !important; + } + .align-items-sm-stretch { + align-items: stretch !important; + } + .align-content-sm-start { + align-content: flex-start !important; + } + .align-content-sm-end { + align-content: flex-end !important; + } + .align-content-sm-center { + align-content: center !important; + } + .align-content-sm-between { + align-content: space-between !important; + } + .align-content-sm-around { + align-content: space-around !important; + } + .align-content-sm-stretch { + align-content: stretch !important; + } + .align-self-sm-auto { + align-self: auto !important; + } + .align-self-sm-start { + align-self: flex-start !important; + } + .align-self-sm-end { + align-self: flex-end !important; + } + .align-self-sm-center { + align-self: center !important; + } + .align-self-sm-baseline { + align-self: baseline !important; + } + .align-self-sm-stretch { + align-self: stretch !important; + } + .order-sm-first { + order: -1 !important; + } + .order-sm-0 { + order: 0 !important; + } + .order-sm-1 { + order: 1 !important; + } + .order-sm-2 { + order: 2 !important; + } + .order-sm-3 { + order: 3 !important; + } + .order-sm-4 { + order: 4 !important; + } + .order-sm-5 { + order: 5 !important; + } + .order-sm-last { + order: 6 !important; + } + .m-sm-0 { + margin: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-sm-0 { + margin-top: 0 !important; + } + .mt-sm-1 { + margin-top: 0.25rem !important; + } + .mt-sm-2 { + margin-top: 0.5rem !important; + } + .mt-sm-3 { + margin-top: 1rem !important; + } + .mt-sm-4 { + margin-top: 1.5rem !important; + } + .mt-sm-5 { + margin-top: 3rem !important; + } + .mt-sm-auto { + margin-top: auto !important; + } + .me-sm-0 { + margin-right: 0 !important; + } + .me-sm-1 { + margin-right: 0.25rem !important; + } + .me-sm-2 { + margin-right: 0.5rem !important; + } + .me-sm-3 { + margin-right: 1rem !important; + } + .me-sm-4 { + margin-right: 1.5rem !important; + } + .me-sm-5 { + margin-right: 3rem !important; + } + .me-sm-auto { + margin-right: auto !important; + } + .mb-sm-0 { + margin-bottom: 0 !important; + } + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + .mb-sm-3 { + margin-bottom: 1rem !important; + } + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + .mb-sm-5 { + margin-bottom: 3rem !important; + } + .mb-sm-auto { + margin-bottom: auto !important; + } + .ms-sm-0 { + margin-left: 0 !important; + } + .ms-sm-1 { + margin-left: 0.25rem !important; + } + .ms-sm-2 { + margin-left: 0.5rem !important; + } + .ms-sm-3 { + margin-left: 1rem !important; + } + .ms-sm-4 { + margin-left: 1.5rem !important; + } + .ms-sm-5 { + margin-left: 3rem !important; + } + .ms-sm-auto { + margin-left: auto !important; + } + .p-sm-0 { + padding: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-sm-0 { + padding-top: 0 !important; + } + .pt-sm-1 { + padding-top: 0.25rem !important; + } + .pt-sm-2 { + padding-top: 0.5rem !important; + } + .pt-sm-3 { + padding-top: 1rem !important; + } + .pt-sm-4 { + padding-top: 1.5rem !important; + } + .pt-sm-5 { + padding-top: 3rem !important; + } + .pe-sm-0 { + padding-right: 0 !important; + } + .pe-sm-1 { + padding-right: 0.25rem !important; + } + .pe-sm-2 { + padding-right: 0.5rem !important; + } + .pe-sm-3 { + padding-right: 1rem !important; + } + .pe-sm-4 { + padding-right: 1.5rem !important; + } + .pe-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-0 { + padding-bottom: 0 !important; + } + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + .pb-sm-3 { + padding-bottom: 1rem !important; + } + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + .pb-sm-5 { + padding-bottom: 3rem !important; + } + .ps-sm-0 { + padding-left: 0 !important; + } + .ps-sm-1 { + padding-left: 0.25rem !important; + } + .ps-sm-2 { + padding-left: 0.5rem !important; + } + .ps-sm-3 { + padding-left: 1rem !important; + } + .ps-sm-4 { + padding-left: 1.5rem !important; + } + .ps-sm-5 { + padding-left: 3rem !important; + } + .gap-sm-0 { + gap: 0 !important; + } + .gap-sm-1 { + gap: 0.25rem !important; + } + .gap-sm-2 { + gap: 0.5rem !important; + } + .gap-sm-3 { + gap: 1rem !important; + } + .gap-sm-4 { + gap: 1.5rem !important; + } + .gap-sm-5 { + gap: 3rem !important; + } + .row-gap-sm-0 { + row-gap: 0 !important; + } + .row-gap-sm-1 { + row-gap: 0.25rem !important; + } + .row-gap-sm-2 { + row-gap: 0.5rem !important; + } + .row-gap-sm-3 { + row-gap: 1rem !important; + } + .row-gap-sm-4 { + row-gap: 1.5rem !important; + } + .row-gap-sm-5 { + row-gap: 3rem !important; + } + .column-gap-sm-0 { + column-gap: 0 !important; + } + .column-gap-sm-1 { + column-gap: 0.25rem !important; + } + .column-gap-sm-2 { + column-gap: 0.5rem !important; + } + .column-gap-sm-3 { + column-gap: 1rem !important; + } + .column-gap-sm-4 { + column-gap: 1.5rem !important; + } + .column-gap-sm-5 { + column-gap: 3rem !important; + } + .text-sm-start { + text-align: left !important; + } + .text-sm-end { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} +@media (min-width: 768px) { + .float-md-start { + float: left !important; + } + .float-md-end { + float: right !important; + } + .float-md-none { + float: none !important; + } + .object-fit-md-contain { + object-fit: contain !important; + } + .object-fit-md-cover { + object-fit: cover !important; + } + .object-fit-md-fill { + object-fit: fill !important; + } + .object-fit-md-scale { + object-fit: scale-down !important; + } + .object-fit-md-none { + object-fit: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-grid { + display: grid !important; + } + .d-md-inline-grid { + display: inline-grid !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: flex !important; + } + .d-md-inline-flex { + display: inline-flex !important; + } + .d-md-none { + display: none !important; + } + .flex-md-fill { + flex: 1 1 auto !important; + } + .flex-md-row { + flex-direction: row !important; + } + .flex-md-column { + flex-direction: column !important; + } + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + .flex-md-grow-0 { + flex-grow: 0 !important; + } + .flex-md-grow-1 { + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + .flex-md-wrap { + flex-wrap: wrap !important; + } + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-md-start { + justify-content: flex-start !important; + } + .justify-content-md-end { + justify-content: flex-end !important; + } + .justify-content-md-center { + justify-content: center !important; + } + .justify-content-md-between { + justify-content: space-between !important; + } + .justify-content-md-around { + justify-content: space-around !important; + } + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + .align-items-md-start { + align-items: flex-start !important; + } + .align-items-md-end { + align-items: flex-end !important; + } + .align-items-md-center { + align-items: center !important; + } + .align-items-md-baseline { + align-items: baseline !important; + } + .align-items-md-stretch { + align-items: stretch !important; + } + .align-content-md-start { + align-content: flex-start !important; + } + .align-content-md-end { + align-content: flex-end !important; + } + .align-content-md-center { + align-content: center !important; + } + .align-content-md-between { + align-content: space-between !important; + } + .align-content-md-around { + align-content: space-around !important; + } + .align-content-md-stretch { + align-content: stretch !important; + } + .align-self-md-auto { + align-self: auto !important; + } + .align-self-md-start { + align-self: flex-start !important; + } + .align-self-md-end { + align-self: flex-end !important; + } + .align-self-md-center { + align-self: center !important; + } + .align-self-md-baseline { + align-self: baseline !important; + } + .align-self-md-stretch { + align-self: stretch !important; + } + .order-md-first { + order: -1 !important; + } + .order-md-0 { + order: 0 !important; + } + .order-md-1 { + order: 1 !important; + } + .order-md-2 { + order: 2 !important; + } + .order-md-3 { + order: 3 !important; + } + .order-md-4 { + order: 4 !important; + } + .order-md-5 { + order: 5 !important; + } + .order-md-last { + order: 6 !important; + } + .m-md-0 { + margin: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-md-0 { + margin-top: 0 !important; + } + .mt-md-1 { + margin-top: 0.25rem !important; + } + .mt-md-2 { + margin-top: 0.5rem !important; + } + .mt-md-3 { + margin-top: 1rem !important; + } + .mt-md-4 { + margin-top: 1.5rem !important; + } + .mt-md-5 { + margin-top: 3rem !important; + } + .mt-md-auto { + margin-top: auto !important; + } + .me-md-0 { + margin-right: 0 !important; + } + .me-md-1 { + margin-right: 0.25rem !important; + } + .me-md-2 { + margin-right: 0.5rem !important; + } + .me-md-3 { + margin-right: 1rem !important; + } + .me-md-4 { + margin-right: 1.5rem !important; + } + .me-md-5 { + margin-right: 3rem !important; + } + .me-md-auto { + margin-right: auto !important; + } + .mb-md-0 { + margin-bottom: 0 !important; + } + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + .mb-md-3 { + margin-bottom: 1rem !important; + } + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + .mb-md-5 { + margin-bottom: 3rem !important; + } + .mb-md-auto { + margin-bottom: auto !important; + } + .ms-md-0 { + margin-left: 0 !important; + } + .ms-md-1 { + margin-left: 0.25rem !important; + } + .ms-md-2 { + margin-left: 0.5rem !important; + } + .ms-md-3 { + margin-left: 1rem !important; + } + .ms-md-4 { + margin-left: 1.5rem !important; + } + .ms-md-5 { + margin-left: 3rem !important; + } + .ms-md-auto { + margin-left: auto !important; + } + .p-md-0 { + padding: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-md-0 { + padding-top: 0 !important; + } + .pt-md-1 { + padding-top: 0.25rem !important; + } + .pt-md-2 { + padding-top: 0.5rem !important; + } + .pt-md-3 { + padding-top: 1rem !important; + } + .pt-md-4 { + padding-top: 1.5rem !important; + } + .pt-md-5 { + padding-top: 3rem !important; + } + .pe-md-0 { + padding-right: 0 !important; + } + .pe-md-1 { + padding-right: 0.25rem !important; + } + .pe-md-2 { + padding-right: 0.5rem !important; + } + .pe-md-3 { + padding-right: 1rem !important; + } + .pe-md-4 { + padding-right: 1.5rem !important; + } + .pe-md-5 { + padding-right: 3rem !important; + } + .pb-md-0 { + padding-bottom: 0 !important; + } + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + .pb-md-3 { + padding-bottom: 1rem !important; + } + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + .pb-md-5 { + padding-bottom: 3rem !important; + } + .ps-md-0 { + padding-left: 0 !important; + } + .ps-md-1 { + padding-left: 0.25rem !important; + } + .ps-md-2 { + padding-left: 0.5rem !important; + } + .ps-md-3 { + padding-left: 1rem !important; + } + .ps-md-4 { + padding-left: 1.5rem !important; + } + .ps-md-5 { + padding-left: 3rem !important; + } + .gap-md-0 { + gap: 0 !important; + } + .gap-md-1 { + gap: 0.25rem !important; + } + .gap-md-2 { + gap: 0.5rem !important; + } + .gap-md-3 { + gap: 1rem !important; + } + .gap-md-4 { + gap: 1.5rem !important; + } + .gap-md-5 { + gap: 3rem !important; + } + .row-gap-md-0 { + row-gap: 0 !important; + } + .row-gap-md-1 { + row-gap: 0.25rem !important; + } + .row-gap-md-2 { + row-gap: 0.5rem !important; + } + .row-gap-md-3 { + row-gap: 1rem !important; + } + .row-gap-md-4 { + row-gap: 1.5rem !important; + } + .row-gap-md-5 { + row-gap: 3rem !important; + } + .column-gap-md-0 { + column-gap: 0 !important; + } + .column-gap-md-1 { + column-gap: 0.25rem !important; + } + .column-gap-md-2 { + column-gap: 0.5rem !important; + } + .column-gap-md-3 { + column-gap: 1rem !important; + } + .column-gap-md-4 { + column-gap: 1.5rem !important; + } + .column-gap-md-5 { + column-gap: 3rem !important; + } + .text-md-start { + text-align: left !important; + } + .text-md-end { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} +@media (min-width: 992px) { + .float-lg-start { + float: left !important; + } + .float-lg-end { + float: right !important; + } + .float-lg-none { + float: none !important; + } + .object-fit-lg-contain { + object-fit: contain !important; + } + .object-fit-lg-cover { + object-fit: cover !important; + } + .object-fit-lg-fill { + object-fit: fill !important; + } + .object-fit-lg-scale { + object-fit: scale-down !important; + } + .object-fit-lg-none { + object-fit: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-grid { + display: grid !important; + } + .d-lg-inline-grid { + display: inline-grid !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: flex !important; + } + .d-lg-inline-flex { + display: inline-flex !important; + } + .d-lg-none { + display: none !important; + } + .flex-lg-fill { + flex: 1 1 auto !important; + } + .flex-lg-row { + flex-direction: row !important; + } + .flex-lg-column { + flex-direction: column !important; + } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + .flex-lg-wrap { + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-lg-start { + justify-content: flex-start !important; + } + .justify-content-lg-end { + justify-content: flex-end !important; + } + .justify-content-lg-center { + justify-content: center !important; + } + .justify-content-lg-between { + justify-content: space-between !important; + } + .justify-content-lg-around { + justify-content: space-around !important; + } + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + .align-items-lg-start { + align-items: flex-start !important; + } + .align-items-lg-end { + align-items: flex-end !important; + } + .align-items-lg-center { + align-items: center !important; + } + .align-items-lg-baseline { + align-items: baseline !important; + } + .align-items-lg-stretch { + align-items: stretch !important; + } + .align-content-lg-start { + align-content: flex-start !important; + } + .align-content-lg-end { + align-content: flex-end !important; + } + .align-content-lg-center { + align-content: center !important; + } + .align-content-lg-between { + align-content: space-between !important; + } + .align-content-lg-around { + align-content: space-around !important; + } + .align-content-lg-stretch { + align-content: stretch !important; + } + .align-self-lg-auto { + align-self: auto !important; + } + .align-self-lg-start { + align-self: flex-start !important; + } + .align-self-lg-end { + align-self: flex-end !important; + } + .align-self-lg-center { + align-self: center !important; + } + .align-self-lg-baseline { + align-self: baseline !important; + } + .align-self-lg-stretch { + align-self: stretch !important; + } + .order-lg-first { + order: -1 !important; + } + .order-lg-0 { + order: 0 !important; + } + .order-lg-1 { + order: 1 !important; + } + .order-lg-2 { + order: 2 !important; + } + .order-lg-3 { + order: 3 !important; + } + .order-lg-4 { + order: 4 !important; + } + .order-lg-5 { + order: 5 !important; + } + .order-lg-last { + order: 6 !important; + } + .m-lg-0 { + margin: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-lg-0 { + margin-top: 0 !important; + } + .mt-lg-1 { + margin-top: 0.25rem !important; + } + .mt-lg-2 { + margin-top: 0.5rem !important; + } + .mt-lg-3 { + margin-top: 1rem !important; + } + .mt-lg-4 { + margin-top: 1.5rem !important; + } + .mt-lg-5 { + margin-top: 3rem !important; + } + .mt-lg-auto { + margin-top: auto !important; + } + .me-lg-0 { + margin-right: 0 !important; + } + .me-lg-1 { + margin-right: 0.25rem !important; + } + .me-lg-2 { + margin-right: 0.5rem !important; + } + .me-lg-3 { + margin-right: 1rem !important; + } + .me-lg-4 { + margin-right: 1.5rem !important; + } + .me-lg-5 { + margin-right: 3rem !important; + } + .me-lg-auto { + margin-right: auto !important; + } + .mb-lg-0 { + margin-bottom: 0 !important; + } + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + .mb-lg-3 { + margin-bottom: 1rem !important; + } + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + .mb-lg-5 { + margin-bottom: 3rem !important; + } + .mb-lg-auto { + margin-bottom: auto !important; + } + .ms-lg-0 { + margin-left: 0 !important; + } + .ms-lg-1 { + margin-left: 0.25rem !important; + } + .ms-lg-2 { + margin-left: 0.5rem !important; + } + .ms-lg-3 { + margin-left: 1rem !important; + } + .ms-lg-4 { + margin-left: 1.5rem !important; + } + .ms-lg-5 { + margin-left: 3rem !important; + } + .ms-lg-auto { + margin-left: auto !important; + } + .p-lg-0 { + padding: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-lg-0 { + padding-top: 0 !important; + } + .pt-lg-1 { + padding-top: 0.25rem !important; + } + .pt-lg-2 { + padding-top: 0.5rem !important; + } + .pt-lg-3 { + padding-top: 1rem !important; + } + .pt-lg-4 { + padding-top: 1.5rem !important; + } + .pt-lg-5 { + padding-top: 3rem !important; + } + .pe-lg-0 { + padding-right: 0 !important; + } + .pe-lg-1 { + padding-right: 0.25rem !important; + } + .pe-lg-2 { + padding-right: 0.5rem !important; + } + .pe-lg-3 { + padding-right: 1rem !important; + } + .pe-lg-4 { + padding-right: 1.5rem !important; + } + .pe-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-0 { + padding-bottom: 0 !important; + } + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + .pb-lg-3 { + padding-bottom: 1rem !important; + } + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + .pb-lg-5 { + padding-bottom: 3rem !important; + } + .ps-lg-0 { + padding-left: 0 !important; + } + .ps-lg-1 { + padding-left: 0.25rem !important; + } + .ps-lg-2 { + padding-left: 0.5rem !important; + } + .ps-lg-3 { + padding-left: 1rem !important; + } + .ps-lg-4 { + padding-left: 1.5rem !important; + } + .ps-lg-5 { + padding-left: 3rem !important; + } + .gap-lg-0 { + gap: 0 !important; + } + .gap-lg-1 { + gap: 0.25rem !important; + } + .gap-lg-2 { + gap: 0.5rem !important; + } + .gap-lg-3 { + gap: 1rem !important; + } + .gap-lg-4 { + gap: 1.5rem !important; + } + .gap-lg-5 { + gap: 3rem !important; + } + .row-gap-lg-0 { + row-gap: 0 !important; + } + .row-gap-lg-1 { + row-gap: 0.25rem !important; + } + .row-gap-lg-2 { + row-gap: 0.5rem !important; + } + .row-gap-lg-3 { + row-gap: 1rem !important; + } + .row-gap-lg-4 { + row-gap: 1.5rem !important; + } + .row-gap-lg-5 { + row-gap: 3rem !important; + } + .column-gap-lg-0 { + column-gap: 0 !important; + } + .column-gap-lg-1 { + column-gap: 0.25rem !important; + } + .column-gap-lg-2 { + column-gap: 0.5rem !important; + } + .column-gap-lg-3 { + column-gap: 1rem !important; + } + .column-gap-lg-4 { + column-gap: 1.5rem !important; + } + .column-gap-lg-5 { + column-gap: 3rem !important; + } + .text-lg-start { + text-align: left !important; + } + .text-lg-end { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .float-xl-start { + float: left !important; + } + .float-xl-end { + float: right !important; + } + .float-xl-none { + float: none !important; + } + .object-fit-xl-contain { + object-fit: contain !important; + } + .object-fit-xl-cover { + object-fit: cover !important; + } + .object-fit-xl-fill { + object-fit: fill !important; + } + .object-fit-xl-scale { + object-fit: scale-down !important; + } + .object-fit-xl-none { + object-fit: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-grid { + display: grid !important; + } + .d-xl-inline-grid { + display: inline-grid !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: flex !important; + } + .d-xl-inline-flex { + display: inline-flex !important; + } + .d-xl-none { + display: none !important; + } + .flex-xl-fill { + flex: 1 1 auto !important; + } + .flex-xl-row { + flex-direction: row !important; + } + .flex-xl-column { + flex-direction: column !important; + } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xl-wrap { + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xl-start { + justify-content: flex-start !important; + } + .justify-content-xl-end { + justify-content: flex-end !important; + } + .justify-content-xl-center { + justify-content: center !important; + } + .justify-content-xl-between { + justify-content: space-between !important; + } + .justify-content-xl-around { + justify-content: space-around !important; + } + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + .align-items-xl-start { + align-items: flex-start !important; + } + .align-items-xl-end { + align-items: flex-end !important; + } + .align-items-xl-center { + align-items: center !important; + } + .align-items-xl-baseline { + align-items: baseline !important; + } + .align-items-xl-stretch { + align-items: stretch !important; + } + .align-content-xl-start { + align-content: flex-start !important; + } + .align-content-xl-end { + align-content: flex-end !important; + } + .align-content-xl-center { + align-content: center !important; + } + .align-content-xl-between { + align-content: space-between !important; + } + .align-content-xl-around { + align-content: space-around !important; + } + .align-content-xl-stretch { + align-content: stretch !important; + } + .align-self-xl-auto { + align-self: auto !important; + } + .align-self-xl-start { + align-self: flex-start !important; + } + .align-self-xl-end { + align-self: flex-end !important; + } + .align-self-xl-center { + align-self: center !important; + } + .align-self-xl-baseline { + align-self: baseline !important; + } + .align-self-xl-stretch { + align-self: stretch !important; + } + .order-xl-first { + order: -1 !important; + } + .order-xl-0 { + order: 0 !important; + } + .order-xl-1 { + order: 1 !important; + } + .order-xl-2 { + order: 2 !important; + } + .order-xl-3 { + order: 3 !important; + } + .order-xl-4 { + order: 4 !important; + } + .order-xl-5 { + order: 5 !important; + } + .order-xl-last { + order: 6 !important; + } + .m-xl-0 { + margin: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xl-0 { + margin-top: 0 !important; + } + .mt-xl-1 { + margin-top: 0.25rem !important; + } + .mt-xl-2 { + margin-top: 0.5rem !important; + } + .mt-xl-3 { + margin-top: 1rem !important; + } + .mt-xl-4 { + margin-top: 1.5rem !important; + } + .mt-xl-5 { + margin-top: 3rem !important; + } + .mt-xl-auto { + margin-top: auto !important; + } + .me-xl-0 { + margin-right: 0 !important; + } + .me-xl-1 { + margin-right: 0.25rem !important; + } + .me-xl-2 { + margin-right: 0.5rem !important; + } + .me-xl-3 { + margin-right: 1rem !important; + } + .me-xl-4 { + margin-right: 1.5rem !important; + } + .me-xl-5 { + margin-right: 3rem !important; + } + .me-xl-auto { + margin-right: auto !important; + } + .mb-xl-0 { + margin-bottom: 0 !important; + } + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xl-3 { + margin-bottom: 1rem !important; + } + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xl-5 { + margin-bottom: 3rem !important; + } + .mb-xl-auto { + margin-bottom: auto !important; + } + .ms-xl-0 { + margin-left: 0 !important; + } + .ms-xl-1 { + margin-left: 0.25rem !important; + } + .ms-xl-2 { + margin-left: 0.5rem !important; + } + .ms-xl-3 { + margin-left: 1rem !important; + } + .ms-xl-4 { + margin-left: 1.5rem !important; + } + .ms-xl-5 { + margin-left: 3rem !important; + } + .ms-xl-auto { + margin-left: auto !important; + } + .p-xl-0 { + padding: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xl-0 { + padding-top: 0 !important; + } + .pt-xl-1 { + padding-top: 0.25rem !important; + } + .pt-xl-2 { + padding-top: 0.5rem !important; + } + .pt-xl-3 { + padding-top: 1rem !important; + } + .pt-xl-4 { + padding-top: 1.5rem !important; + } + .pt-xl-5 { + padding-top: 3rem !important; + } + .pe-xl-0 { + padding-right: 0 !important; + } + .pe-xl-1 { + padding-right: 0.25rem !important; + } + .pe-xl-2 { + padding-right: 0.5rem !important; + } + .pe-xl-3 { + padding-right: 1rem !important; + } + .pe-xl-4 { + padding-right: 1.5rem !important; + } + .pe-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-0 { + padding-bottom: 0 !important; + } + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xl-3 { + padding-bottom: 1rem !important; + } + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xl-5 { + padding-bottom: 3rem !important; + } + .ps-xl-0 { + padding-left: 0 !important; + } + .ps-xl-1 { + padding-left: 0.25rem !important; + } + .ps-xl-2 { + padding-left: 0.5rem !important; + } + .ps-xl-3 { + padding-left: 1rem !important; + } + .ps-xl-4 { + padding-left: 1.5rem !important; + } + .ps-xl-5 { + padding-left: 3rem !important; + } + .gap-xl-0 { + gap: 0 !important; + } + .gap-xl-1 { + gap: 0.25rem !important; + } + .gap-xl-2 { + gap: 0.5rem !important; + } + .gap-xl-3 { + gap: 1rem !important; + } + .gap-xl-4 { + gap: 1.5rem !important; + } + .gap-xl-5 { + gap: 3rem !important; + } + .row-gap-xl-0 { + row-gap: 0 !important; + } + .row-gap-xl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xl-3 { + row-gap: 1rem !important; + } + .row-gap-xl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xl-5 { + row-gap: 3rem !important; + } + .column-gap-xl-0 { + column-gap: 0 !important; + } + .column-gap-xl-1 { + column-gap: 0.25rem !important; + } + .column-gap-xl-2 { + column-gap: 0.5rem !important; + } + .column-gap-xl-3 { + column-gap: 1rem !important; + } + .column-gap-xl-4 { + column-gap: 1.5rem !important; + } + .column-gap-xl-5 { + column-gap: 3rem !important; + } + .text-xl-start { + text-align: left !important; + } + .text-xl-end { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} +@media (min-width: 1400px) { + .float-xxl-start { + float: left !important; + } + .float-xxl-end { + float: right !important; + } + .float-xxl-none { + float: none !important; + } + .object-fit-xxl-contain { + object-fit: contain !important; + } + .object-fit-xxl-cover { + object-fit: cover !important; + } + .object-fit-xxl-fill { + object-fit: fill !important; + } + .object-fit-xxl-scale { + object-fit: scale-down !important; + } + .object-fit-xxl-none { + object-fit: none !important; + } + .d-xxl-inline { + display: inline !important; + } + .d-xxl-inline-block { + display: inline-block !important; + } + .d-xxl-block { + display: block !important; + } + .d-xxl-grid { + display: grid !important; + } + .d-xxl-inline-grid { + display: inline-grid !important; + } + .d-xxl-table { + display: table !important; + } + .d-xxl-table-row { + display: table-row !important; + } + .d-xxl-table-cell { + display: table-cell !important; + } + .d-xxl-flex { + display: flex !important; + } + .d-xxl-inline-flex { + display: inline-flex !important; + } + .d-xxl-none { + display: none !important; + } + .flex-xxl-fill { + flex: 1 1 auto !important; + } + .flex-xxl-row { + flex-direction: row !important; + } + .flex-xxl-column { + flex-direction: column !important; + } + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xxl-start { + justify-content: flex-start !important; + } + .justify-content-xxl-end { + justify-content: flex-end !important; + } + .justify-content-xxl-center { + justify-content: center !important; + } + .justify-content-xxl-between { + justify-content: space-between !important; + } + .justify-content-xxl-around { + justify-content: space-around !important; + } + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + .align-items-xxl-start { + align-items: flex-start !important; + } + .align-items-xxl-end { + align-items: flex-end !important; + } + .align-items-xxl-center { + align-items: center !important; + } + .align-items-xxl-baseline { + align-items: baseline !important; + } + .align-items-xxl-stretch { + align-items: stretch !important; + } + .align-content-xxl-start { + align-content: flex-start !important; + } + .align-content-xxl-end { + align-content: flex-end !important; + } + .align-content-xxl-center { + align-content: center !important; + } + .align-content-xxl-between { + align-content: space-between !important; + } + .align-content-xxl-around { + align-content: space-around !important; + } + .align-content-xxl-stretch { + align-content: stretch !important; + } + .align-self-xxl-auto { + align-self: auto !important; + } + .align-self-xxl-start { + align-self: flex-start !important; + } + .align-self-xxl-end { + align-self: flex-end !important; + } + .align-self-xxl-center { + align-self: center !important; + } + .align-self-xxl-baseline { + align-self: baseline !important; + } + .align-self-xxl-stretch { + align-self: stretch !important; + } + .order-xxl-first { + order: -1 !important; + } + .order-xxl-0 { + order: 0 !important; + } + .order-xxl-1 { + order: 1 !important; + } + .order-xxl-2 { + order: 2 !important; + } + .order-xxl-3 { + order: 3 !important; + } + .order-xxl-4 { + order: 4 !important; + } + .order-xxl-5 { + order: 5 !important; + } + .order-xxl-last { + order: 6 !important; + } + .m-xxl-0 { + margin: 0 !important; + } + .m-xxl-1 { + margin: 0.25rem !important; + } + .m-xxl-2 { + margin: 0.5rem !important; + } + .m-xxl-3 { + margin: 1rem !important; + } + .m-xxl-4 { + margin: 1.5rem !important; + } + .m-xxl-5 { + margin: 3rem !important; + } + .m-xxl-auto { + margin: auto !important; + } + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xxl-0 { + margin-top: 0 !important; + } + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + .mt-xxl-3 { + margin-top: 1rem !important; + } + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + .mt-xxl-5 { + margin-top: 3rem !important; + } + .mt-xxl-auto { + margin-top: auto !important; + } + .me-xxl-0 { + margin-right: 0 !important; + } + .me-xxl-1 { + margin-right: 0.25rem !important; + } + .me-xxl-2 { + margin-right: 0.5rem !important; + } + .me-xxl-3 { + margin-right: 1rem !important; + } + .me-xxl-4 { + margin-right: 1.5rem !important; + } + .me-xxl-5 { + margin-right: 3rem !important; + } + .me-xxl-auto { + margin-right: auto !important; + } + .mb-xxl-0 { + margin-bottom: 0 !important; + } + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + .mb-xxl-auto { + margin-bottom: auto !important; + } + .ms-xxl-0 { + margin-left: 0 !important; + } + .ms-xxl-1 { + margin-left: 0.25rem !important; + } + .ms-xxl-2 { + margin-left: 0.5rem !important; + } + .ms-xxl-3 { + margin-left: 1rem !important; + } + .ms-xxl-4 { + margin-left: 1.5rem !important; + } + .ms-xxl-5 { + margin-left: 3rem !important; + } + .ms-xxl-auto { + margin-left: auto !important; + } + .p-xxl-0 { + padding: 0 !important; + } + .p-xxl-1 { + padding: 0.25rem !important; + } + .p-xxl-2 { + padding: 0.5rem !important; + } + .p-xxl-3 { + padding: 1rem !important; + } + .p-xxl-4 { + padding: 1.5rem !important; + } + .p-xxl-5 { + padding: 3rem !important; + } + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xxl-0 { + padding-top: 0 !important; + } + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + .pt-xxl-3 { + padding-top: 1rem !important; + } + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + .pt-xxl-5 { + padding-top: 3rem !important; + } + .pe-xxl-0 { + padding-right: 0 !important; + } + .pe-xxl-1 { + padding-right: 0.25rem !important; + } + .pe-xxl-2 { + padding-right: 0.5rem !important; + } + .pe-xxl-3 { + padding-right: 1rem !important; + } + .pe-xxl-4 { + padding-right: 1.5rem !important; + } + .pe-xxl-5 { + padding-right: 3rem !important; + } + .pb-xxl-0 { + padding-bottom: 0 !important; + } + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + .ps-xxl-0 { + padding-left: 0 !important; + } + .ps-xxl-1 { + padding-left: 0.25rem !important; + } + .ps-xxl-2 { + padding-left: 0.5rem !important; + } + .ps-xxl-3 { + padding-left: 1rem !important; + } + .ps-xxl-4 { + padding-left: 1.5rem !important; + } + .ps-xxl-5 { + padding-left: 3rem !important; + } + .gap-xxl-0 { + gap: 0 !important; + } + .gap-xxl-1 { + gap: 0.25rem !important; + } + .gap-xxl-2 { + gap: 0.5rem !important; + } + .gap-xxl-3 { + gap: 1rem !important; + } + .gap-xxl-4 { + gap: 1.5rem !important; + } + .gap-xxl-5 { + gap: 3rem !important; + } + .row-gap-xxl-0 { + row-gap: 0 !important; + } + .row-gap-xxl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xxl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xxl-3 { + row-gap: 1rem !important; + } + .row-gap-xxl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xxl-5 { + row-gap: 3rem !important; + } + .column-gap-xxl-0 { + column-gap: 0 !important; + } + .column-gap-xxl-1 { + column-gap: 0.25rem !important; + } + .column-gap-xxl-2 { + column-gap: 0.5rem !important; + } + .column-gap-xxl-3 { + column-gap: 1rem !important; + } + .column-gap-xxl-4 { + column-gap: 1.5rem !important; + } + .column-gap-xxl-5 { + column-gap: 3rem !important; + } + .text-xxl-start { + text-align: left !important; + } + .text-xxl-end { + text-align: right !important; + } + .text-xxl-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .fs-1 { + font-size: 2.5rem !important; + } + .fs-2 { + font-size: 2rem !important; + } + .fs-3 { + font-size: 1.75rem !important; + } + .fs-4 { + font-size: 1.5rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-grid { + display: grid !important; + } + .d-print-inline-grid { + display: inline-grid !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: flex !important; + } + .d-print-inline-flex { + display: inline-flex !important; + } + .d-print-none { + display: none !important; + } +} +.color-brand { + color: #ff6e42; +} + +.color-brand-background { + background-color: #ff6e42; +} + +html { + font-size: 62.5%; +} + +body { + font-size: 1.7rem; + line-height: 1.6; + font-weight: normal; + font-family: "Open Sans", sans-serif; + line-height: 1.6; +} + +h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5 { + font-family: "Rubik", sans-serif; + font-weight: 600; +} + +h1, .h1 { + font-size: 4rem; + margin-bottom: 2rem; +} + +h2, .h2 { + font-size: 3.5rem; + margin-bottom: 1.8rem; +} + +h3, .h3 { + font-size: 3rem; + margin-bottom: 1.6rem; +} + +h4, .h4 { + font-size: 2.5rem; + margin-bottom: 1.5rem; +} + +h5, .h5 { + font-size: 2rem; + margin-bottom: 1.5rem; +} + +p { + margin-bottom: 2rem; + line-height: 1.6; +} + +a { + color: #ff6e42; +} + +.form-control { + font-size: 1.8rem; +} + +.responsive-video { + position: relative; + padding-bottom: 56.25%; + height: 0; + overflow: hidden; + max-width: 100%; + background: #000; + border-radius: 1rem; + overflow: hidden; +} +.responsive-video iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.announcement-bar { + background-color: #ff6e42; + color: #fff; + position: relative; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + text-align: center; +} +.announcement-bar span { + text-decoration: underline; +} +.announcement-bar:hover span { + color: #420f00; +} +.announcement-bar p { + margin-bottom: 0; + padding: 0; +} +.announcement-bar a { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: #fff; +} + +.btn { + font-size: 1.6rem; + border-radius: 0.5rem; + padding: 0.5rem 2rem; + font-weight: bold; + transition: none; + text-transform: uppercase; + border-left: none; + border-right: none; + font-family: "Rubik", sans-serif; + font-weight: 600; +} +@media (min-width: 992px) { + .btn { + font-size: 2rem; + } +} +.btn.btn-primary { + background-color: #ff6e42; + border-color: #ff6e42; + border-bottom: solid #a82700 4px; +} +.btn.btn-primary:hover, .btn.btn-primary:focus, .btn.btn-primary:active { + background-color: #ff6e42 !important; + border-color: #ff6e42 !important; +} +.btn.btn-secondary { + background-color: #3B3B3B; + border-color: #3B3B3B; + border-bottom: solid black 4px; + color: #fff; +} +.btn.btn-secondary:hover, .btn.btn-secondary:focus, .btn.btn-secondary:active { + background-color: #3B3B3B !important; + border-color: #3B3B3B !important; +} +.btn a { + color: #fff; +} +.btn:hover, .btn:focus, .btn:active { + border-bottom: 0px; + position: relative; + bottom: -4px; + margin-bottom: 4px; +} + +section { + padding: 4rem 0; +} +section .section-header { + text-align: center; + padding-bottom: 2rem; +} +section .section-header h3, section .section-header .h3 { + font-size: 1.8rem; + margin-bottom: 0; + letter-spacing: 1px; + color: #ff6e42; +} +section .section-header h2, section .section-header .h2 { + font-size: 3.6rem; + text-transform: uppercase; +} + +section.hero { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + position: relative; +} +section.hero .section-content { + display: flex; + flex-direction: column; + position: relative; +} +@media (min-width: 992px) { + section.hero .section-content { + flex-direction: row; + height: 70rem; + justify-content: space-between; + align-items: center; + } +} +section.hero .section-content .message { + width: 100%; + width: 100%; + padding: 3rem; + z-index: 99; + text-align: left; +} +@media (min-width: 992px) { + section.hero .section-content .message { + text-align: left; + } +} +@media (min-width: 992px) { + section.hero .section-content .message { + width: 100%; + position: relative; + top: -8rem; + } +} +section.hero .section-content .message h1, section.hero .section-content .message .h1 { + font-size: 3rem; + font-weight: 800; + color: #3B3B3B; + margin-bottom: 1rem; + line-height: 1.2; + margin-bottom: 2.5rem; +} +section.hero .section-content .message h1 span, section.hero .section-content .message .h1 span { + color: #ff6e42; +} +@media (min-width: 576px) { + section.hero .section-content .message h1, section.hero .section-content .message .h1 { + font-size: 4rem; + } +} +@media (min-width: 768px) { + section.hero .section-content .message h1, section.hero .section-content .message .h1 { + font-size: 5rem; + } + section.hero .section-content .message h1 br, section.hero .section-content .message .h1 br { + display: none; + } +} +@media (min-width: 992px) { + section.hero .section-content .message h1, section.hero .section-content .message .h1 { + font-size: 5.5rem; + text-align: left; + } + section.hero .section-content .message h1 br, section.hero .section-content .message .h1 br { + display: block; + } +} +section.hero .section-content .message h2, section.hero .section-content .message .h2 { + font-size: 2rem; + font-weight: 300; + line-height: 1.6; +} +section.hero .section-content .message h2 strong, section.hero .section-content .message .h2 strong { + font-weight: 600; +} +@media (min-width: 768px) { + section.hero .section-content .message h2, section.hero .section-content .message .h2 { + text-align: left; + } +} +@media (min-width: 992px) { + section.hero .section-content .message h2, section.hero .section-content .message .h2 { + text-align: left; + max-width: 500px; + } +} +section.hero .section-content .message h3, section.hero .section-content .message .h3 { + color: #ff6e42; + text-transform: uppercase; + text-transform: uppercase; + font-family: "Montserrat", sans-serif; + font-weight: 800; + font-size: 2.6rem; +} +section.hero .section-content .featured-image { + width: 100%; + height: 100%; + right: 0; +} +@media (min-width: 992px) { + section.hero .section-content .featured-image { + width: 60%; + position: absolute; + } +} +section.hero .section-content .featured-image img { + width: 100%; + height: auto; +} +@media (min-width: 992px) { + section.hero .section-content .featured-image img { + width: 120%; + position: absolute; + top: 0rem; + right: -20rem; + } +} + +section.hero.cloud-hero { + overflow: visible; +} +section.hero.cloud-hero .section-content { + height: auto; + min-height: 50rem; +} +@media (min-width: 768px) { + section.hero.cloud-hero .section-content { + align-items: flex-start; + } +} +section.hero.cloud-hero .section-content .message { + width: 100%; + width: 100%; + top: 0; +} +section.hero.cloud-hero .section-content .message h1, section.hero.cloud-hero .section-content .message .h1 { + font-size: 3rem; + font-weight: 800; + color: #3B3B3B; + margin-bottom: 1rem; + line-height: 1.2; + margin-bottom: 2.5rem; +} +section.hero.cloud-hero .section-content .message h1 span, section.hero.cloud-hero .section-content .message .h1 span { + color: #ff6e42; +} +@media (min-width: 576px) { + section.hero.cloud-hero .section-content .message h1, section.hero.cloud-hero .section-content .message .h1 { + font-size: 4rem; + } +} +@media (min-width: 768px) { + section.hero.cloud-hero .section-content .message h1, section.hero.cloud-hero .section-content .message .h1 { + font-size: 5rem; + } + section.hero.cloud-hero .section-content .message h1 br, section.hero.cloud-hero .section-content .message .h1 br { + display: none; + } +} +@media (min-width: 992px) { + section.hero.cloud-hero .section-content .message h1, section.hero.cloud-hero .section-content .message .h1 { + font-size: 4.5rem; + text-align: left; + } + section.hero.cloud-hero .section-content .message h1 br, section.hero.cloud-hero .section-content .message .h1 br { + display: block; + } +} +section.hero.cloud-hero .section-content .message h2, section.hero.cloud-hero .section-content .message .h2 { + font-size: 2rem; + font-weight: 300; + line-height: 1.6; +} +section.hero.cloud-hero .section-content .message h2 strong, section.hero.cloud-hero .section-content .message .h2 strong { + font-weight: 600; +} +@media (min-width: 768px) { + section.hero.cloud-hero .section-content .message h2, section.hero.cloud-hero .section-content .message .h2 { + text-align: center; + } +} +@media (min-width: 992px) { + section.hero.cloud-hero .section-content .message h2, section.hero.cloud-hero .section-content .message .h2 { + text-align: left; + max-width: 500px; + } +} +section.hero.cloud-hero .section-content .message h3, section.hero.cloud-hero .section-content .message .h3 { + color: #ff6e42; + text-transform: uppercase; + text-transform: uppercase; + font-family: "Montserrat", sans-serif; + font-weight: 800; + font-size: 2.6rem; +} +section.hero.cloud-hero .featured-image { + width: 100%; + height: 100%; + right: 0; + display: flex; + justify-content: center; + align-items: center; + padding-left: 0; + padding-right: 0; +} +@media (min-width: 576px) { + section.hero.cloud-hero .featured-image { + padding-left: 10%; + padding-right: 10%; + } +} +@media (min-width: 768px) { + section.hero.cloud-hero .featured-image { + padding-left: 20%; + padding-right: 20%; + } +} +@media (min-width: 992px) { + section.hero.cloud-hero .featured-image { + width: 60%; + } +} +section.hero.cloud-hero .featured-image img { + width: 100%; + height: auto; +} +@media (min-width: 992px) { + section.hero.cloud-hero .featured-image img { + width: 80%; + position: absolute; + top: 13rem; + right: 0rem; + } +} +@media (min-width: 1200px) { + section.hero.cloud-hero .featured-image img { + top: 10rem; + } +} +@media (min-width: 1400px) { + section.hero.cloud-hero .featured-image img { + top: 4rem; + } +} + +section.concept-highlights { + text-align: center; +} +section.concept-highlights .section-header { + padding-bottom: 8rem; +} +section.concept-highlights .section-header h2, section.concept-highlights .section-header .h2 { + font-size: 4rem; + font-weight: 800; + position: relative; +} +section.concept-highlights .section-header h2::after, section.concept-highlights .section-header .h2::after { + content: ""; + height: 0.8rem; + width: 50%; + max-width: 100px; + background-color: #ff6e42; + bottom: -2rem; + position: absolute; + left: 50%; + transform: translateX(-50%); + border-radius: 1rem; +} +section.concept-highlights .col { + margin-bottom: 2rem; +} +@media (min-width: 768px) { + section.concept-highlights .col { + margin-bottom: 0; + } +} +@media (min-width: 992px) { + section.concept-highlights .col { + padding-left: 2rem; + padding-right: 2rem; + } +} +section.concept-highlights h2, section.concept-highlights .h2 { + font-size: 2.4rem; +} +section.concept-highlights h2 span, section.concept-highlights .h2 span { + color: #ff6e42; +} + +section.cta { + background-color: #3B3B3B; + padding: 3rem 2rem; +} +@media (min-width: 992px) { + section.cta { + padding: 5rem 2rem; + } +} +@media (min-width: 1200px) { + section.cta { + padding: 6rem 0; + } +} +section.cta h2, section.cta .h2 { + color: #fff; + font-size: 2.8rem; + font-weight: 400; + line-height: 1.3; + margin-bottom: 0; + padding: 3rem 0; +} +@media (min-width: 992px) { + section.cta h2, section.cta .h2 { + font-size: 3rem; + padding: 3rem 4rem 3rem 0; + } +} +section.cta h3, section.cta .h3 { + font-weight: 300; + color: #fff; + font-size: 2.4rem; +} +section.cta .form-holder { + padding-top: 4rem; +} +section.cta .form-holder .form-control { + font-size: 2.3rem; +} +section.cta .form-holder .btn { + padding: 0.6rem 2rem; +} +section.cta .form-holder #mce-responses { + padding: 1rem 0; + color: #ff6e42; +} + +section.cta2 { + background-color: #3B3B3B; + padding: 6rem 0; + color: #fff; +} +section.cta2 h4, section.cta2 .h4 { + font-weight: 300; + margin-bottom: 3rem; +} +section.cta2 .btn { + padding: 0.5rem 2rem; + font-size: 2.2rem; +} + +section.features { + padding: 0; + display: flex; + flex-direction: column; +} +section.features .feature { + display: flex; + flex-direction: column-reverse; +} +section.features .feature .feature-image { + width: 100%; + overflow: hidden; +} +@media (min-width: 768px) { + section.features .feature .feature-image { + width: 50%; + display: flex; + justify-content: flex-end; + align-items: center; + } +} +section.features .feature .feature-image .feature-image-holder { + width: 100%; + max-width: 800px; + padding: 8rem 4rem; +} +@media (min-width: 1200px) { + section.features .feature .feature-image .feature-image-holder { + padding: 8rem; + } +} +section.features .feature .feature-image .feature-image-holder img { + width: 100%; + height: auto; +} +section.features .feature .feature-image.overlap { + position: relative; + min-height: 50rem; +} +section.features .feature .feature-image.overlap .feature-image-holder { + position: absolute; + overflow: hidden; + padding-top: 0; + top: 0; + width: 120%; + max-width: 900px; +} +section.features .feature .feature-description { + box-shadow: 0 0 44px 15px rgba(0, 0, 0, 0.06); + width: 100%; +} +section.features .feature .feature-description.brand { + background-color: #ff6e42; + color: #fff; +} +section.features .feature .feature-description.brand ::selection { + background-color: #fff; + color: #ff6e42; +} +section.features .feature .feature-description.alt { + background-color: #797aaa; + color: #fff; +} +section.features .feature .feature-description.light { + background-color: #fff8f0; +} +@media (min-width: 768px) { + section.features .feature .feature-description { + width: 50%; + display: flex; + justify-content: flex-start; + align-items: center; + } +} +section.features .feature .feature-description .description-holder { + width: 100%; + max-width: 800px; + padding: 6rem 4rem; +} +@media (min-width: 1200px) { + section.features .feature .feature-description .description-holder { + padding: 8rem; + } +} +@media (min-width: 992px) { + section.features .feature .feature-description .description-holder h2, section.features .feature .feature-description .description-holder .h2 { + font-size: 3rem; + } +} +@media (min-width: 1200px) { + section.features .feature .feature-description .description-holder h2, section.features .feature .feature-description .description-holder .h2 { + font-size: 4rem; + font-weight: 800; + } +} +section.features .feature .feature-description .description-holder p { + font-size: 1.8rem; +} +@media (min-width: 1200px) { + section.features .feature .feature-description .description-holder p { + font-size: 2rem; + } +} +@media (min-width: 768px) { + section.features .feature { + flex-direction: row; + } + section.features .feature:nth-of-type(even) { + flex-flow: row-reverse; + } + section.features .feature:nth-of-type(even) .feature-image { + justify-content: flex-start; + } + section.features .feature:nth-of-type(even) .feature-description { + justify-content: flex-end; + } +} + +section.features.cloud-features { + padding: 4rem 0; +} + +section.testimonials { + padding: 8rem 0; + background-color: #f8f9fa; +} +section.testimonials .testimonials-holder { + display: grid; + grid-template-columns: 1fr; + grid-column-gap: 4rem; + grid-row-gap: 4rem; + padding-left: 5%; + padding-right: 5%; +} +@media (min-width: 992px) { + section.testimonials .testimonials-holder { + grid-template-columns: 1fr 1fr; + padding-left: 0; + padding-right: 0; + } +} + +.testimonial { + display: flex; + flex-direction: column; +} +.testimonial .testimonial-body { + display: flex; + position: relative; + flex-direction: column; + flex-grow: 1; +} +@media (min-width: 768px) { + .testimonial .testimonial-body { + flex-direction: row; + } +} +.testimonial .testimonial-body i.quote-icon { + color: #ff6e42; + display: block; + position: absolute; + font-size: 16rem; + display: block; + top: -3rem; + line-height: 1; + position: absolute; + opacity: 0.2; + left: -5rem; + line-height: 1; +} +@media (min-width: 768px) { + .testimonial .testimonial-body i.quote-icon { + font-size: 10rem; + position: relative; + opacity: 1; + top: 0; + left: 0; + } +} +.testimonial .testimonial-body p.testimonial-text { + font-size: 2rem; + font-family: "Lora", sans-serif; + font-style: italic; + padding-left: 2rem; + padding-top: 4rem; + margin-bottom: 1rem; +} +@media (min-width: 768px) { + .testimonial .testimonial-body p.testimonial-text { + padding-top: 4rem; + font-size: 2.2rem; + padding-left: 0; + } +} +.testimonial .testimonial-meta { + display: flex; + justify-content: center; +} +@media (min-width: 768px) { + .testimonial .testimonial-meta { + justify-content: flex-end; + } +} +.testimonial .testimonial-meta .testimonial-meta-content { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-end; + align-content: center; + text-align: right; +} +@media (min-width: 768px) { + .testimonial .testimonial-meta .testimonial-meta-content { + flex-direction: row-reverse; + align-items: center; + align-content: center; + text-align: right; + } +} +.testimonial .testimonial-meta .testimonial-meta-content img { + width: 7rem; + height: 7rem; + border-radius: 100%; + box-shadow: 0 0 30px -20px rgba(0, 0, 0, 0.4); + margin-bottom: 1rem; +} +@media (min-width: 768px) { + .testimonial .testimonial-meta .testimonial-meta-content img { + margin-bottom: 0; + margin-left: 2rem; + } +} +.testimonial .testimonial-meta .testimonial-meta-content h4, .testimonial .testimonial-meta .testimonial-meta-content .h4 { + font-size: 1.8rem; + margin: 0; +} +.testimonial .testimonial-meta .testimonial-meta-content h5, .testimonial .testimonial-meta .testimonial-meta-content .h5 { + font-size: 1.6rem; + margin: 0; + width: 300px; +} +.testimonial .testimonial-meta .testimonial-meta-content h5 span, .testimonial .testimonial-meta .testimonial-meta-content .h5 span { + font-weight: normal; +} +.cloud-form { + background-color: #ff6e42; + padding: 3rem 0; + position: relative; +} +.cloud-form .large-logo { + position: absolute; + height: 150%; + width: auto; + top: -25%; + opacity: 0.3; + left: 50%; + -webkit-transform: translateX(-115%); + transform: translateX(-115%); +} +@media (min-width: 768px) { + .cloud-form .large-logo { + -webkit-transform: translateX(-140%); + transform: translateX(-140%); + } +} +.cloud-form .section-header h2, .cloud-form .section-header .h2 { + color: #fff; + font-size: 2.6rem; + text-transform: uppercase; +} +.cloud-form #mc_embed_shell { + display: flex; + justify-content: center; + align-items: center; +} +.cloud-form #mc_embed_shell #mc_embed_signup { + border-radius: 1rem; +} +.cloud-form #mc_embed_shell #mc_embed_signup form { + padding: 1rem 2rem; + text-align: center; + margin: 0; +} +@media (min-width: 768px) { + .cloud-form #mc_embed_shell #mc_embed_signup form { + padding: 1rem 5rem; + } +} +.cloud-form #mc_embed_shell #mc_embed_signup form h2, .cloud-form #mc_embed_shell #mc_embed_signup form .h2 { + text-transform: uppercase; +} +.cloud-form #mc_embed_shell #mc_embed_signup form .mc-field-group { + width: 100%; +} +.cloud-form #mc_embed_shell #mc_embed_signup form #mc-embedded-subscribe { + background-color: #ff6e42; + text-transform: uppercase; + font-family: "Rubik", sans-serif; + letter-spacing: 0.08rem; + height: auto; + padding: 0.5rem 2.5rem; +} +.cloud-form #mc_embed_shell #mc_embed_signup div.response { + width: 100%; +} + +.fireside-chat-signup { + clear: left; + font-size: 1.4rem; +} +.fireside-chat-signup #mc_embed_shell { + width: 100%; + max-width: 650px; + padding: 1rem; + background-color: #ffe3db; + border-radius: 1rem; + margin: 0 auto; +} +.fireside-chat-signup #mc_embed_shell .helper_text { + background-color: transparent; +} +.fireside-chat-signup #mc_embed_shell .button { + background-color: #ff6e42; + padding: 0.5rem 1rem; + height: auto; + text-transform: uppercase; +} +.fireside-chat-signup #mc_embed_shell .button:hover { + background-color: #ff470f; +} + +section.cal-book { + background-color: #1f1f1f; +} +section.cal-book .section-header { + color: #fff; + text-transform: initial; +} +section.cal-book .section-header span { + text-transform: lowercase; +} + +section.footer { + background-color: #222222; + background-color: #ff6e42; + color: #fff; +} +section.footer .footer-holder { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + align-content: center; +} +section.footer .footer-holder .brand { + display: flex; + flex-direction: row; + align-content: center; + justify-content: center; + align-items: center; + text-decoration: none; + margin-right: 1rem; +} +section.footer .footer-holder .brand .logo { + margin-right: 1rem; + display: flex; + justify-content: center; + align-items: center; + align-content: center; + width: 35px; +} +section.footer .footer-holder .brand .logo img { + width: 100%; + max-width: 50px; + height: auto; +} +section.footer .footer-holder .brand h1, section.footer .footer-holder .brand .h1 { + margin: 0; + padding: 0; + font-size: 2.6rem; + text-transform: uppercase; + font-family: "Montserrat", sans-serif; + color: #fff; + font-weight: 800; +} +section.footer .footer-holder p.copy { + margin-bottom: 0; +} + +section.news { + padding: 6rem 0; +} +@media (min-width: 992px) { + section.news .news-content .row:nth-child(even) { + flex-direction: row-reverse; + } +} +@media (min-width: 992px) { + section.news .event-details { + padding-left: 2rem; + padding-right: 2rem; + } +} + +section.case-study .case-study-header h5, section.case-study .case-study-header .h5 { + color: #fff; + background-color: #adb5bd; + background-color: #ff6e42; + border-radius: 2rem; + display: inline-block; + padding: 0.5rem 2rem; + font-size: 1.8rem; + text-transform: uppercase; +} +section.case-study .case-study-stats { + margin-top: 8rem; + margin-bottom: 8rem; + position: relative; +} +section.case-study .case-study-stats .stats-header { + position: absolute; + left: 50%; + transform: translateX(-50%); + top: -1.5rem; + background-color: #ff6e42; + color: #fff; + border-radius: 2rem; + display: inline-block; + padding: 0.5rem 2rem; + font-size: 1.8rem; + text-transform: uppercase; +} +section.case-study .case-study-stats .row-stats { + background-color: #f3f3f3; + border-radius: 2rem; + padding-top: 6rem; + padding-bottom: 6rem; +} +section.case-study .case-study-stats .row-stats .stat { + padding-left: 3rem; + padding-right: 3rem; + padding-bottom: 5rem; + text-align: center; + display: flex; + flex-direction: column; +} +section.case-study .case-study-stats .row-stats .stat:last-of-type { + padding-bottom: 0; +} +@media (min-width: 992px) { + section.case-study .case-study-stats .row-stats .stat { + padding-bottom: 0; + padding-left: 2rem; + padding-right: 2rem; + } +} +section.case-study .case-study-stats .row-stats .stat h2, section.case-study .case-study-stats .row-stats .stat .h2 { + font-size: 2.2rem; + margin-bottom: 1rem; +} +@media (min-width: 992px) { + section.case-study .case-study-stats .row-stats .stat h2, section.case-study .case-study-stats .row-stats .stat .h2 { + font-size: 2.4rem; + } +} +@media (min-width: 992px) { + section.case-study .case-study-stats .row-stats .stat h2 span, section.case-study .case-study-stats .row-stats .stat .h2 span { + display: none; + } +} +@media (min-width: 1200px) { + section.case-study .case-study-stats .row-stats .stat h2 span, section.case-study .case-study-stats .row-stats .stat .h2 span { + display: inline; + } +} +section.case-study .case-study-stats .row-stats .stat p { + margin-bottom: 0; +} +section.case-study .case-study-body h2, section.case-study .case-study-body .h2, section.case-study .case-study-body h3, section.case-study .case-study-body .h3, section.case-study .case-study-body h4, section.case-study .case-study-body .h4 { + margin-top: 5rem; + margin-bottom: 2rem; +} +section.case-study .case-study-body h2:first-of-type, section.case-study .case-study-body .h2:first-of-type { + margin-top: 0; +} +section.case-study .case-study-body ul li { + max-width: 80%; + margin-bottom: 2rem; +} +section.case-study .case-study-body .testimonial { + margin-top: 4rem; + margin-bottom: 6rem; +} +@media (min-width: 992px) { + section.case-study .case-study-body .testimonial { + width: 75%; + margin-left: 5%; + } +} + +body { + position: relative; + min-height: 100vh; +} +body:before { + content: ""; + background-image: url("../assets/images/dotted-bg.png"); + background-repeat: repeat; + opacity: 0.5; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; +} +body ::selection { + background-color: #ff6e42; + color: #fff; +} + +header { + padding: 4rem 0; + position: relative; +} +header:after { + position: absolute; + content: ""; + width: 80%; + display: block; + left: 50%; + transform: translateX(-50%); + bottom: 0; +} +@media (min-width: 768px) { + header:after { + display: none; + } +} +header .header-content { + display: flex; + flex-direction: column; + padding-left: 2rem; + padding-right: 2rem; +} +@media (min-width: 992px) { + header .header-content { + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + } +} +header .header-content .brand { + display: flex; + flex-direction: row; + align-content: center; + justify-content: center; + align-items: center; + text-decoration: none; + margin-right: 1rem; + margin-bottom: 3rem; +} +@media (min-width: 992px) { + header .header-content .brand { + margin-bottom: 0; + } +} +header .header-content .brand .logo { + margin-right: 1rem; + display: flex; + justify-content: center; + align-items: center; + align-content: center; + width: 35px; +} +header .header-content .brand .logo img { + width: 100%; + max-width: 50px; + height: auto; +} +header .header-content .brand h1, header .header-content .brand .h1 { + margin: 0; + padding: 0; + font-size: 2.6rem; + text-transform: uppercase; + font-family: "Montserrat", sans-serif; + font-weight: 800; +} +header .nav-holder .nav { + display: flex; + align-items: center; + align-content: center; + flex-wrap: wrap; + justify-content: center; + font-family: "Rubik", sans-serif; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; +} +@media (min-width: 992px) { + header .nav-holder .nav { + justify-content: flex-end; + } +} +header .nav-holder .nav .nav-item { + margin-left: 1rem; + font-size: 1.8rem; +} +@media (min-width: 576px) { + header .nav-holder .nav .nav-item { + margin-left: 2rem; + } +} +@media (min-width: 768px) { + header .nav-holder .nav .nav-item { + margin-left: 3rem; + } +} +header .nav-holder .nav .nav-item:first-of-type { + margin-left: 0; +} +header .nav-holder .nav .nav-item a { + color: #3B3B3B; +} +header .nav-holder .nav .nav-item a:hover { + color: #ff6e42; +} +header .nav-holder .nav .nav-item.signup { + display: none; +} +@media (min-width: 768px) { + header .nav-holder .nav .nav-item.signup { + display: inline-block; + } +} +header .nav-holder .nav .nav-item.btn { + font-size: 1.6rem; + padding: 0.1rem 0.5rem; +} +header .nav-holder .nav .nav-item.btn a { + color: #fff; +} +header .nav-holder .quotes-cta { + text-align: center; + padding-top: 2rem; + font-family: "Rubik", sans-serif; + font-weight: 600; + font-size: 2rem; +} +@media (min-width: 992px) { + header .nav-holder .quotes-cta { + text-align: right; + } +} + +/*# sourceMappingURL=home.css.map */ diff --git a/styles/home.css.map b/styles/home.css.map new file mode 100644 index 00000000..20e4ecd2 --- /dev/null +++ b/styles/home.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../../node_modules/bootstrap/scss/mixins/_banner.scss","../../node_modules/bootstrap/scss/_root.scss","../../node_modules/bootstrap/scss/vendor/_rfs.scss","../../node_modules/bootstrap/scss/mixins/_color-mode.scss","../../node_modules/bootstrap/scss/_reboot.scss","../../node_modules/bootstrap/scss/_variables.scss","../../node_modules/bootstrap/scss/mixins/_border-radius.scss","../../node_modules/bootstrap/scss/_type.scss","../../node_modules/bootstrap/scss/mixins/_lists.scss","../../node_modules/bootstrap/scss/_images.scss","../../node_modules/bootstrap/scss/mixins/_image.scss","../../node_modules/bootstrap/scss/_containers.scss","../../node_modules/bootstrap/scss/mixins/_container.scss","../../node_modules/bootstrap/scss/mixins/_breakpoints.scss","../../node_modules/bootstrap/scss/_grid.scss","../../node_modules/bootstrap/scss/mixins/_grid.scss","../../node_modules/bootstrap/scss/_tables.scss","../../node_modules/bootstrap/scss/mixins/_table-variants.scss","../../node_modules/bootstrap/scss/forms/_labels.scss","../../node_modules/bootstrap/scss/forms/_form-text.scss","../../node_modules/bootstrap/scss/forms/_form-control.scss","../../node_modules/bootstrap/scss/mixins/_transition.scss","../../node_modules/bootstrap/scss/mixins/_gradients.scss","../../node_modules/bootstrap/scss/forms/_form-select.scss","../../node_modules/bootstrap/scss/forms/_form-check.scss","../../node_modules/bootstrap/scss/forms/_form-range.scss","../../node_modules/bootstrap/scss/forms/_floating-labels.scss","../../node_modules/bootstrap/scss/forms/_input-group.scss","../../node_modules/bootstrap/scss/mixins/_forms.scss","../../node_modules/bootstrap/scss/_buttons.scss","../../node_modules/bootstrap/scss/mixins/_buttons.scss","../../node_modules/bootstrap/scss/_transitions.scss","../../node_modules/bootstrap/scss/_dropdown.scss","../../node_modules/bootstrap/scss/mixins/_caret.scss","../../node_modules/bootstrap/scss/_button-group.scss","../../node_modules/bootstrap/scss/_nav.scss","../../node_modules/bootstrap/scss/_navbar.scss","../../node_modules/bootstrap/scss/_card.scss","../../node_modules/bootstrap/scss/_accordion.scss","../../node_modules/bootstrap/scss/_breadcrumb.scss","../../node_modules/bootstrap/scss/_pagination.scss","../../node_modules/bootstrap/scss/mixins/_pagination.scss","../../node_modules/bootstrap/scss/_badge.scss","../../node_modules/bootstrap/scss/_alert.scss","../../node_modules/bootstrap/scss/_progress.scss","../../node_modules/bootstrap/scss/_list-group.scss","../../node_modules/bootstrap/scss/_close.scss","../../node_modules/bootstrap/scss/_toasts.scss","../../node_modules/bootstrap/scss/_modal.scss","../../node_modules/bootstrap/scss/mixins/_backdrop.scss","../../node_modules/bootstrap/scss/_tooltip.scss","../../node_modules/bootstrap/scss/mixins/_reset-text.scss","../../node_modules/bootstrap/scss/_popover.scss","../../node_modules/bootstrap/scss/_carousel.scss","../../node_modules/bootstrap/scss/mixins/_clearfix.scss","../../node_modules/bootstrap/scss/_spinners.scss","../../node_modules/bootstrap/scss/_offcanvas.scss","../../node_modules/bootstrap/scss/_placeholders.scss","../../node_modules/bootstrap/scss/helpers/_color-bg.scss","../../node_modules/bootstrap/scss/helpers/_colored-links.scss","../../node_modules/bootstrap/scss/helpers/_focus-ring.scss","../../node_modules/bootstrap/scss/helpers/_icon-link.scss","../../node_modules/bootstrap/scss/helpers/_ratio.scss","../../node_modules/bootstrap/scss/helpers/_position.scss","../../node_modules/bootstrap/scss/helpers/_stacks.scss","../../node_modules/bootstrap/scss/helpers/_visually-hidden.scss","../../node_modules/bootstrap/scss/mixins/_visually-hidden.scss","../../node_modules/bootstrap/scss/helpers/_stretched-link.scss","../../node_modules/bootstrap/scss/helpers/_text-truncation.scss","../../node_modules/bootstrap/scss/mixins/_text-truncate.scss","../../node_modules/bootstrap/scss/helpers/_vr.scss","../../node_modules/bootstrap/scss/mixins/_utilities.scss","../../node_modules/bootstrap/scss/utilities/_api.scss","../../sass/config/variables.sass","../../sass/common/global.sass","../../sass/common/buttons.sass","../../sass/sections/sections.sass","../../sass/sections/hero.sass","../../sass/sections/concept-highlights.sass","../../sass/sections/cta.sass","../../sass/sections/features.sass","../../sass/sections/testimonials.sass","../../sass/sections/forms.sass","../../sass/sections/calendar.sass","../../sass/sections/footer.sass","../../sass/sections/news.sass","../../sass/sections/case-study.sass","../../sass/home.sass"],"names":[],"mappings":";AACE;AAAA;AAAA;AAAA;AAAA;ACDF;AAAA;EASI;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAGF;EACA;EAMA;EACA;EACA;EAOA;EC2OI,qBALI;EDpOR;EACA;EAKA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EAGA;EAEA;EACA;EACA;EAEA;EACA;EAMA;EACA;EACA;EAGA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EAIA;EACA;EACA;EAIA;EACA;EACA;EACA;;;AEhHE;EFsHA;EAGA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EAGE;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAIA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAAA;EAGF;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;;;AGxKJ;AAAA;AAAA;EAGE;;;AAeE;EANJ;IAOM;;;;AAcN;EACE;EACA;EF6OI,WALI;EEtOR;EACA;EACA;EACA;EACA;EACA;EACA;;;AASF;EACE;EACA,OCmnB4B;EDlnB5B;EACA;EACA,SCynB4B;;;AD/mB9B;EACE;EACA,eCwjB4B;EDrjB5B,aCwjB4B;EDvjB5B,aCwjB4B;EDvjB5B;;;AAGF;EFuMQ;;AA5JJ;EE3CJ;IF8MQ;;;;AEzMR;EFkMQ;;AA5JJ;EEtCJ;IFyMQ;;;;AEpMR;EF6LQ;;AA5JJ;EEjCJ;IFoMQ;;;;AE/LR;EFwLQ;;AA5JJ;EE5BJ;IF+LQ;;;;AE1LR;EF+KM,WALI;;;AErKV;EF0KM,WALI;;;AE1JV;EACE;EACA,eCwV0B;;;AD9U5B;EACE;EACA;EACA;;;AAMF;EACE;EACA;EACA;;;AAMF;AAAA;EAEE;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE;;;AAGF;EACE,aC6b4B;;;ADxb9B;EACE;EACA;;;AAMF;EACE;;;AAQF;AAAA;EAEE,aCsa4B;;;AD9Z9B;EF6EM,WALI;;;AEjEV;EACE,SCqf4B;EDpf5B;EACA;;;AASF;AAAA;EAEE;EFwDI,WALI;EEjDR;EACA;;;AAGF;EAAM;;;AACN;EAAM;;;AAKN;EACE;EACA,iBCgNwC;;AD9MxC;EACE;;;AAWF;EAEE;EACA;;;AAOJ;AAAA;AAAA;AAAA;EAIE,aCgV4B;EHlUxB,WALI;;;AEDV;EACE;EACA;EACA;EACA;EFEI,WALI;;AEQR;EFHI,WALI;EEUN;EACA;;;AAIJ;EFVM,WALI;EEiBR;EACA;;AAGA;EACE;;;AAIJ;EACE;EFtBI,WALI;EE6BR,OCu5CkC;EDt5ClC,kBCu5CkC;EC5rDhC;;AFwSF;EACE;EF7BE,WALI;;;AE6CV;EACE;;;AAMF;AAAA;EAEE;;;AAQF;EACE;EACA;;;AAGF;EACE,aC4X4B;ED3X5B,gBC2X4B;ED1X5B,OC4Z4B;ED3Z5B;;;AAOF;EAEE;EACA;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;;;AAQF;EACE;;;AAMF;EAEE;;;AAQF;EACE;;;AAKF;AAAA;AAAA;AAAA;AAAA;EAKE;EACA;EF5HI,WALI;EEmIR;;;AAIF;AAAA;EAEE;;;AAKF;EACE;;;AAGF;EAGE;;AAGA;EACE;;;AAOJ;EACE;;;AAQF;AAAA;AAAA;AAAA;EAIE;;AAGE;AAAA;AAAA;AAAA;EACE;;;AAON;EACE;EACA;;;AAKF;EACE;;;AAUF;EACE;EACA;EACA;EACA;;;AAQF;EACE;EACA;EACA;EACA,eCmN4B;EHpatB;EEoNN;;AFhXE;EEyWJ;IFtMQ;;;AE+MN;EACE;;;AAOJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;;;AAGF;EACE;;;AASF;EACE;EACA;;;AAQF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA;EACE;;;AAKF;EACE;;;AAOF;EACE;EACA;;;AAKF;EACE;;;AAKF;EACE;;;AAOF;EACE;EACA;;;AAQF;EACE;;;AAQF;EACE;;;AGrkBF;ELmQM,WALI;EK5PR,aFwoB4B;;;AEnoB5B;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AKvQN;ELgQM;EK5PJ,aFynBkB;EExnBlB,aFwmB0B;;AHzgB1B;EKpGF;ILuQM;;;;AK/OR;ECvDE;EACA;;;AD2DF;EC5DE;EACA;;;AD8DF;EACE;;AAEA;EACE,cFsoB0B;;;AE5nB9B;EL8MM,WALI;EKvMR;;;AAIF;EACE,eFiUO;EH1HH,WALI;;AK/LR;EACE;;;AAIJ;EACE;EACA,eFuTO;EH1HH,WALI;EKtLR,OFtFS;;AEwFT;EACE;;;AEhGJ;ECIE;EAGA;;;ADDF;EACE,SJ6jDkC;EI5jDlC,kBJ6jDkC;EI5jDlC;EHGE;EIRF;EAGA;;;ADcF;EAEE;;;AAGF;EACE;EACA;;;AAGF;EPyPM,WALI;EOlPR,OJgjDkC;;;AMllDlC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECHA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACsDE;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;AQvbnB;EF5CE;IACE,WNkee;;;ASlfvB;EAEI;EAAA;EAAA;EAAA;EAAA;EAAA;;;AAKF;ECNA;EACA;EACA;EACA;EAEA;EACA;EACA;;ADEE;ECOF;EACA;EACA;EACA;EACA;EACA;;;AA+CI;EACE;;;AAGF;EApCJ;EACA;;;AAcA;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AA+BE;EAhDJ;EACA;;;AAqDQ;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AA+DM;EAhEN;EACA;;;AAuEQ;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAwDU;EAxDV;;;AAmEM;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AAPF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AAPF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AAPF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AAPF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AAPF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AF1DN;EEUE;IACE;;EAGF;IApCJ;IACA;;EAcA;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EA+BE;IAhDJ;IACA;;EAqDQ;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EAuEQ;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAmEM;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;;AF1DN;EEUE;IACE;;EAGF;IApCJ;IACA;;EAcA;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EA+BE;IAhDJ;IACA;;EAqDQ;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EAuEQ;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAmEM;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;;AF1DN;EEUE;IACE;;EAGF;IApCJ;IACA;;EAcA;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EA+BE;IAhDJ;IACA;;EAqDQ;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EAuEQ;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAmEM;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;;AF1DN;EEUE;IACE;;EAGF;IApCJ;IACA;;EAcA;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EA+BE;IAhDJ;IACA;;EAqDQ;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EAuEQ;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAmEM;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;;AF1DN;EEUE;IACE;;EAGF;IApCJ;IACA;;EAcA;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EAFF;IACE;IACA;;EA+BE;IAhDJ;IACA;;EAqDQ;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EA+DM;IAhEN;IACA;;EAuEQ;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAwDU;IAxDV;;EAmEM;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;EAPF;AAAA;IAEE;;EAGF;AAAA;IAEE;;;ACrHV;EAEE;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA,eXkYO;EWjYP,gBXusB4B;EWtsB5B;;AAOA;EACE;EAEA;EACA;EACA,qBX+sB0B;EW9sB1B;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;;;AAOF;EACE;;;AAUA;EACE;;;AAeF;EACE;;AAGA;EACE;;;AAOJ;EACE;;AAGF;EACE;;;AAUF;EACE;EACA;;;AAMF;EACE;EACA;;;AAQJ;EACE;EACA;;;AAQA;EACE;EACA;;;AC5IF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAlBF;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;ADiJA;EACE;EACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AH3FF;EGyFA;IACE;IACA;;;AEnKN;EACE,ebu2BsC;;;Aa91BxC;EACE;EACA;EACA;EhB8QI,WALI;EgBrQR,ab+lB4B;;;Aa3lB9B;EACE;EACA;EhBoQI,WALI;;;AgB3PV;EACE;EACA;EhB8PI,WALI;;;AiBtRV;EACE,Yd+1BsC;EHrkBlC,WALI;EiBjRR,Od+1BsC;;;Aep2BxC;EACE;EACA;EACA;ElBwRI,WALI;EkBhRR,afkmB4B;EejmB5B,afymB4B;EexmB5B,Of43BsC;Ee33BtC;EACA,kBfq3BsC;Eep3BtC;EACA;EdGE;EeHE,YDMJ;;ACFI;EDhBN;ICiBQ;;;ADGN;EACE;;AAEA;EACE;;AAKJ;EACE,Ofs2BoC;Eer2BpC,kBfg2BoC;Ee/1BpC,cf82BoC;Ee72BpC;EAKE,YfkhBkB;;Ae9gBtB;EAME;EAMA;EAKA;;AAKF;EACE;EACA;;AAIF;EACE,Of40BoC;Ee10BpC;;AAQF;EAEE,kBf8yBoC;Ee3yBpC;;AAIF;EACE;EACA;EACA,mBforB0B;EenrB1B,OfsyBoC;EiBp4BtC,kBjBqiCgC;Eer8B9B;EACA;EACA;EACA;EACA,yBfgsB0B;Ee/rB1B;ECzFE,YD0FF;;ACtFE;ED0EJ;ICzEM;;;ADwFN;EACE,kBf47B8B;;;Aen7BlC;EACE;EACA;EACA;EACA;EACA,afwf4B;Eevf5B,Of2xBsC;Ee1xBtC;EACA;EACA;;AAEA;EACE;;AAGF;EAEE;EACA;;;AAWJ;EACE,Yf4wBsC;Ee3wBtC;ElByII,WALI;EIvQN;;AcuIF;EACE;EACA;EACA,mBfooB0B;;;AehoB9B;EACE,YfgwBsC;Ee/vBtC;ElB4HI,WALI;EIvQN;;AcoJF;EACE;EACA;EACA,mBf2nB0B;;;AennB5B;EACE,Yf6uBoC;;Ae1uBtC;EACE,Yf0uBoC;;AevuBtC;EACE,YfuuBoC;;;AeluBxC;EACE,OfquBsC;EepuBtC,Qf8tBsC;Ee7tBtC,SfilB4B;;Ae/kB5B;EACE;;AAGF;EACE;EdvLA;;Ac2LF;EACE;Ed5LA;;AcgMF;EAAoB,Qf8sBkB;;Ae7sBtC;EAAoB,Qf8sBkB;;;AkB75BxC;EACE;EAEA;EACA;EACA;ErBqRI,WALI;EqB7QR,alB+lB4B;EkB9lB5B,alBsmB4B;EkBrmB5B,OlBy3BsC;EkBx3BtC;EACA,kBlBk3BsC;EkBj3BtC;EACA;EACA,qBlB+9BkC;EkB99BlC,iBlB+9BkC;EkB99BlC;EjBHE;EeHE,YESJ;;AFLI;EEfN;IFgBQ;;;AEMN;EACE,clBs3BoC;EkBr3BpC;EAKE,YlBi+B4B;;AkB79BhC;EAEE,elB6uB0B;EkB5uB1B;;AAGF;EAEE,kBlBu1BoC;;AkBl1BtC;EACE;EACA;;;AAIJ;EACE,alBsuB4B;EkBruB5B,gBlBquB4B;EkBpuB5B,clBquB4B;EHlgBxB,WALI;EIvQN;;;AiB8CJ;EACE,alBkuB4B;EkBjuB5B,gBlBiuB4B;EkBhuB5B,clBiuB4B;EHtgBxB,WALI;EIvQN;;;AiBwDA;EACE;;;ACxEN;EACE;EACA,YnBq6BwC;EmBp6BxC,cnBq6BwC;EmBp6BxC,enBq6BwC;;AmBn6BxC;EACE;EACA;;;AAIJ;EACE,enB25BwC;EmB15BxC;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EAEA;EACA,OnB04BwC;EmBz4BxC,QnBy4BwC;EmBx4BxC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QnB24BwC;EmB14BxC;;AAGA;ElB3BE;;AkB+BF;EAEE,enBm4BsC;;AmBh4BxC;EACE,QnB03BsC;;AmBv3BxC;EACE,cnBs1BoC;EmBr1BpC;EACA,YnB8foB;;AmB3ftB;EACE,kBnB5BM;EmB6BN,cnB7BM;;AmB+BN;EAII;;AAIJ;EAII;;AAKN;EACE,kBnBjDM;EmBkDN,cnBlDM;EmBuDJ;;AAIJ;EACE;EACA;EACA,SnBk2BuC;;AmB31BvC;EACE;EACA,SnBy1BqC;;;AmB30B3C;EACE,cnBo1BgC;;AmBl1BhC;EACE;EAEA,OnB80B8B;EmB70B9B;EACA;EACA;ElBjHA;EeHE,YGsHF;;AHlHE;EG0GJ;IHzGM;;;AGmHJ;EACE;;AAGF;EACE,qBnB60B4B;EmBx0B1B;;AAKN;EACE,enBwzB8B;EmBvzB9B;;AAEA;EACE;EACA;;;AAKN;EACE;EACA,cnBsyBgC;;;AmBnyBlC;EACE;EACA;EACA;;AAIE;EACE;EACA;EACA,SnBspBwB;;;AmB/oB1B;EACE;;;ACnLN;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIA;EAA0B,YpB8gCa;;AoB7gCvC;EAA0B,YpB6gCa;;AoB1gCzC;EACE;;AAGF;EACE,OpB+/BuC;EoB9/BvC,QpB8/BuC;EoB7/BvC;EACA;EH1BF,kBjBkCQ;EoBNN,QpB6/BuC;EC1gCvC;EeHE,YImBF;;AJfE;EIMJ;IJLM;;;AIgBJ;EHjCF,kBjB8hCyC;;AoBx/BzC;EACE,OpBw+B8B;EoBv+B9B,QpBw+B8B;EoBv+B9B;EACA,QpBu+B8B;EoBt+B9B,kBpBu+B8B;EoBt+B9B;EnB7BA;;AmBkCF;EACE,OpBo+BuC;EoBn+BvC,QpBm+BuC;EoBl+BvC;EHpDF,kBjBkCQ;EoBoBN,QpBm+BuC;EC1gCvC;EeHE,YI6CF;;AJzCE;EIiCJ;IJhCM;;;AI0CJ;EH3DF,kBjB8hCyC;;AoB99BzC;EACE,OpB88B8B;EoB78B9B,QpB88B8B;EoB78B9B;EACA,QpB68B8B;EoB58B9B,kBpB68B8B;EoB58B9B;EnBvDA;;AmB4DF;EACE;;AAEA;EACE,kBpBg9BqC;;AoB78BvC;EACE,kBpB48BqC;;;AqBniC3C;EACE;;AAEA;AAAA;AAAA;EAGE,QrBwiCoC;EqBviCpC,YrBuiCoC;EqBtiCpC,arBuiCoC;;AqBpiCtC;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ELRE,YKSF;;ALLE;EKTJ;ILUM;;;AKON;AAAA;EAEE;;AAEA;AAAA;EACE;;AAGF;AAAA;AAAA;EAEE,arB4gCkC;EqB3gClC,gBrB4gCkC;;AqBzgCpC;AAAA;EACE,arBugCkC;EqBtgClC,gBrBugCkC;;AqBngCtC;EACE,arBigCoC;EqBhgCpC,gBrBigCoC;;AqB1/BpC;AAAA;AAAA;AAAA;EACE;EACA,WrB2/BkC;;AqBz/BlC;AAAA;AAAA;AAAA;EACE;EACA;EACA;EACA,QrBm/BgC;EqBl/BhC;EACA,kBrBg0BgC;ECh3BpC;;AoBuDA;EACE;EACA,WrB0+BkC;;AqBr+BpC;EACE;;AAIJ;AAAA;EAEE,OrB1EO;;AqB4EP;AAAA;EACE,kBrB0yBkC;;;AsBj4BxC;EACE;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EAGE;EACA;EACA;EACA;;AAIF;AAAA;AAAA;EAGE;;AAMF;EACE;EACA;;AAEA;EACE;;;AAWN;EACE;EACA;EACA;EzB8OI,WALI;EyBvOR,atByjB4B;EsBxjB5B,atBgkB4B;EsB/jB5B,OtBm1BsC;EsBl1BtC;EACA;EACA,kBtB06BsC;EsBz6BtC;ErBtCE;;;AqBgDJ;AAAA;AAAA;AAAA;EAIE;EzBwNI,WALI;EIvQN;;;AqByDJ;AAAA;AAAA;AAAA;EAIE;EzB+MI,WALI;EIvQN;;;AqBkEJ;AAAA;EAEE;;;AAaE;AAAA;AAAA;AAAA;ErBjEA;EACA;;AqByEA;AAAA;AAAA;AAAA;ErB1EA;EACA;;AqBsFF;EACE;ErB1EA;EACA;;AqB6EF;AAAA;ErB9EE;EACA;;;AsBxBF;EACE;EACA;EACA,YvBu0BoC;EHrkBlC,WALI;E0B1PN,OvBkjCqB;;;AuB/iCvB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;E1BqPE,WALI;E0B7ON,OvBqiCqB;EuBpiCrB,kBvBoiCqB;EC/jCrB;;;AsBgCA;AAAA;AAAA;AAAA;EAEE;;;AA/CF;EAqDE,cvBuhCmB;EuBphCjB,evB81BgC;EuB71BhC;EACA;EACA;EACA;;AAGF;EACE,cvB4gCiB;EuB3gCjB,YvB2gCiB;;;AuB5kCrB;EA0EI,evB40BgC;EuB30BhC;;;AA3EJ;EAkFE,cvB0/BmB;;AuBv/BjB;EAEE;EACA,evB05B8B;EuBz5B9B;EACA;;AAIJ;EACE,cvB6+BiB;EuB5+BjB,YvB4+BiB;;;AuB5kCrB;EAwGI;;;AAxGJ;EA+GE,cvB69BmB;;AuB39BnB;EACE,kBvB09BiB;;AuBv9BnB;EACE,YvBs9BiB;;AuBn9BnB;EACE,OvBk9BiB;;;AuB78BrB;EACE;;;AAhIF;AAAA;AAAA;AAAA;AAAA;EA0IM;;;AAtHR;EACE;EACA;EACA,YvBu0BoC;EHrkBlC,WALI;E0B1PN,OvBkjCqB;;;AuB/iCvB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;E1BqPE,WALI;E0B7ON,OvBqiCqB;EuBpiCrB,kBvBoiCqB;EC/jCrB;;;AsBgCA;AAAA;AAAA;AAAA;EAEE;;;AA/CF;EAqDE,cvBuhCmB;EuBphCjB,evB81BgC;EuB71BhC;EACA;EACA;EACA;;AAGF;EACE,cvB4gCiB;EuB3gCjB,YvB2gCiB;;;AuB5kCrB;EA0EI,evB40BgC;EuB30BhC;;;AA3EJ;EAkFE,cvB0/BmB;;AuBv/BjB;EAEE;EACA,evB05B8B;EuBz5B9B;EACA;;AAIJ;EACE,cvB6+BiB;EuB5+BjB,YvB4+BiB;;;AuB5kCrB;EAwGI;;;AAxGJ;EA+GE,cvB69BmB;;AuB39BnB;EACE,kBvB09BiB;;AuBv9BnB;EACE,YvBs9BiB;;AuBn9BnB;EACE,OvBk9BiB;;;AuB78BrB;EACE;;;AAhIF;AAAA;AAAA;AAAA;AAAA;EA4IM;;;AC9IV;EAEE;EACA;EACA;E3BuRI,oBALI;E2BhRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;E3BsQI,WALI;E2B/PR;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EvBjBE;EgBfF,kBOkCqB;ERtBjB,YQwBJ;;ARpBI;EQhBN;IRiBQ;;;AQqBN;EACE;EAEA;EACA;;AAGF;EAEE;EACA;EACA;;AAGF;EACE;EPrDF,kBOsDuB;EACrB;EACA;EAKE;;AAIJ;EACE;EACA;EAKE;;AAIJ;EAKE;EACA;EAGA;;AAGA;EAKI;;AAKN;EAGE;EACA;EACA;EAEA;EACA;;;AAYF;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADyFA;ECtGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADmHA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AD0FA;ECvGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ADsGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,iBxBuRwC;;AwB7QxC;EACE;;AAGF;EACE;;;AAWJ;ECxIE;EACA;E5B8NI,oBALI;E4BvNR;;;ADyIF;EC5IE;EACA;E5B8NI,oBALI;E4BvNR;;;ACnEF;EVgBM,YUfJ;;AVmBI;EUpBN;IVqBQ;;;AUlBN;EACE;;;AAMF;EACE;;;AAIJ;EACE;EACA;EVDI,YUEJ;;AVEI;EULN;IVMQ;;;AUDN;EACE;EACA;EVNE,YUOF;;AVHE;EUAJ;IVCM;;;;AWpBR;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAGF;EACE;;ACwBE;EACE;EACA,a5B6hBwB;E4B5hBxB,gB5B2hBwB;E4B1hBxB;EArCJ;EACA;EACA;EACA;;AA0DE;EACE;;;AD9CN;EAEE;EACA;EACA;EACA;EACA;E9BuQI,yBALI;E8BhQR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;E9B0OI,WALI;E8BnOR;EACA;EACA;EACA;EACA;EACA;E1BzCE;;A0B6CF;EACE;EACA;EACA;;;AAwBA;EACE;;AAEA;EACE;EACA;;;AAIJ;EACE;;AAEA;EACE;EACA;;;AnB1CJ;EmB4BA;IACE;;EAEA;IACE;IACA;;EAIJ;IACE;;EAEA;IACE;IACA;;;AnB1CJ;EmB4BA;IACE;;EAEA;IACE;IACA;;EAIJ;IACE;;EAEA;IACE;IACA;;;AnB1CJ;EmB4BA;IACE;;EAEA;IACE;IACA;;EAIJ;IACE;;EAEA;IACE;IACA;;;AnB1CJ;EmB4BA;IACE;;EAEA;IACE;IACA;;EAIJ;IACE;;EAEA;IACE;IACA;;;AnB1CJ;EmB4BA;IACE;;EAEA;IACE;IACA;;EAIJ;IACE;;EAEA;IACE;IACA;;;AAUN;EACE;EACA;EACA;EACA;;ACpFA;EACE;EACA,a5B6hBwB;E4B5hBxB,gB5B2hBwB;E4B1hBxB;EA9BJ;EACA;EACA;EACA;;AAmDE;EACE;;;ADgEJ;EACE;EACA;EACA;EACA;EACA;;AClGA;EACE;EACA,a5B6hBwB;E4B5hBxB,gB5B2hBwB;E4B1hBxB;EAvBJ;EACA;EACA;EACA;;AA4CE;EACE;;AD0EF;EACE;;;AAMJ;EACE;EACA;EACA;EACA;EACA;;ACnHA;EACE;EACA,a5B6hBwB;E4B5hBxB,gB5B2hBwB;E4B1hBxB;;AAWA;EACE;;AAGF;EACE;EACA,c5B0gBsB;E4BzgBtB,gB5BwgBsB;E4BvgBtB;EAnCN;EACA;EACA;;AAsCE;EACE;;AD2FF;EACE;;;AAON;EACE;EACA;EACA;EACA;EACA;;;AAMF;EACE;EACA;EACA;EACA;EACA,a3Byb4B;E2Bxb5B;EACA;EACA;EACA;EACA;EACA;E1BtKE;;A0ByKF;EAEE;EV1LF,kBU4LuB;;AAGvB;EAEE;EACA;EVlMF,kBUmMuB;;AAGvB;EAEE;EACA;EACA;;;AAMJ;EACE;;;AAIF;EACE;EACA;EACA;E9BmEI,WALI;E8B5DR;EACA;;;AAIF;EACE;EACA;EACA;;;AAIF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AEtPF;AAAA;EAEE;EACA;EACA;;AAEA;AAAA;EACE;EACA;;AAKF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAKJ;EACE;EACA;EACA;;AAEA;EACE;;;AAIJ;E5BhBI;;A4BoBF;AAAA;EAEE;;AAIF;AAAA;AAAA;E5BVE;EACA;;A4BmBF;AAAA;AAAA;E5BNE;EACA;;;A4BwBJ;EACE;EACA;;AAEA;EAGE;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAoBF;EACE;EACA;EACA;;AAEA;AAAA;EAEE;;AAGF;AAAA;EAEE;;AAIF;AAAA;E5B1FE;EACA;;A4B8FF;AAAA;E5B7GE;EACA;;;A6BxBJ;EAEE;EACA;EAEA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EjCsQI,WALI;EiC/PR;EACA;EACA;EACA;EACA;EdfI,YcgBJ;;AdZI;EcGN;IdFQ;;;AcaN;EAEE;;AAIF;EACE;EACA,Y9BkhBoB;;A8B9gBtB;EAEE;EACA;EACA;;;AAQJ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;;AAEA;EACE;EACA;E7B7CA;EACA;;A6B+CA;EAGE;EACA;;AAIJ;AAAA;EAEE;EACA;EACA;;AAGF;EAEE;E7BjEA;EACA;;;A6B2EJ;EAEE;EACA;EACA;;AAGA;E7B5FE;;A6BgGF;AAAA;EAEE;EbjHF,kBakHuB;;;AASzB;EAEE;EACA;EACA;EAGA;;AAEA;EACE;EACA;EACA;;AAEA;EAEE;;AAIJ;AAAA;EAEE,a9B0d0B;E8Bzd1B;EACA;;;AAUF;AAAA;EAEE;EACA;;;AAKF;AAAA;EAEE;EACA;EACA;;;AAMF;AAAA;EACE;;;AAUF;EACE;;AAEF;EACE;;;AC7LJ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;;AAMA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;EACA;EACA;EACA;;AAoBJ;EACE;EACA;EACA;ElC4NI,WALI;EkCrNR;EACA;EACA;;AAEA;EAEE;;;AAUJ;EAEE;EACA;EAEA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;;AAGE;EAEE;;AAIJ;EACE;;;AASJ;EACE,a/B8gCkC;E+B7gClC,gB/B6gCkC;E+B5gClC;;AAEA;AAAA;AAAA;EAGE;;;AAaJ;EACE;EACA;EAGA;;;AAIF;EACE;ElCyII,WALI;EkClIR;EACA;EACA;EACA;E9BxIE;EeHE,Ye6IJ;;AfzII;EeiIN;IfhIQ;;;Ae0IN;EACE;;AAGF;EACE;EACA;EACA;;;AAMJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AvB1HE;EuBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;If9NJ,YegOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AvB5LR;EuBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;If9NJ,YegOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AvB5LR;EuBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;If9NJ,YegOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AvB5LR;EuBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;If9NJ,YegOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AvB5LR;EuBsIA;IAEI;IACA;;EAEA;IACE;;EAEA;IACE;;EAGF;IACE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;EAGF;IAEE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;If9NJ,YegOI;;EAGA;IACE;;EAGF;IACE;IACA;IACA;IACA;;;AAtDR;EAEI;EACA;;AAEA;EACE;;AAEA;EACE;;AAGF;EACE;EACA;;AAIJ;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;Ef9NJ,YegOI;;AAGA;EACE;;AAGF;EACE;EACA;EACA;EACA;;;AAiBZ;AAAA;EAGE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAME;EACE;;;ACzRN;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;E/BjBE;;A+BqBF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;E/BtBF;EACA;;A+ByBA;EACE;E/BbF;EACA;;A+BmBF;AAAA;EAEE;;;AAIJ;EAGE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAQA;EACE;;;AAQJ;EACE;EACA;EACA;EACA;EACA;;AAEA;E/B7FE;;;A+BkGJ;EACE;EACA;EACA;EACA;;AAEA;E/BxGE;;;A+BkHJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;E/B1IE;;;A+B8IJ;AAAA;AAAA;EAGE;;;AAGF;AAAA;E/B3II;EACA;;;A+B+IJ;AAAA;E/BlII;EACA;;;A+B8IF;EACE;;AxB3HA;EwBuHJ;IAQI;IACA;;EAGA;IAEE;IACA;;EAEA;IACE;IACA;;EAKA;I/B3KJ;IACA;;E+B6KM;AAAA;IAGE;;EAEF;AAAA;IAGE;;EAIJ;I/B5KJ;IACA;;E+B8KM;AAAA;IAGE;;EAEF;AAAA;IAGE;;;;ACpOZ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EpC2PI,WALI;EoCpPR;EACA;EACA;EACA;EhCtBE;EgCwBF;EjB3BI,YiB4BJ;;AjBxBI;EiBWN;IjBVQ;;;AiByBN;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EjBlDE,YiBmDF;;AjB/CE;EiBsCJ;IjBrCM;;;AiBiDN;EACE;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EhC/DE;EACA;;AgCiEA;EhClEA;EACA;;AgCsEF;EACE;;AAIF;EhC9DE;EACA;;AgCiEE;EhClEF;EACA;;AgCsEA;EhCvEA;EACA;;;AgC4EJ;EACE;;;AASA;EACE;;AAGF;EACE;EACA;EhCpHA;;AgCuHA;EAAgB;;AAChB;EAAe;;AAGb;EhC3HF;;;AgCqIA;EACE;EACA;;;AC1JN;EAEE;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;ErC+QI,WALI;EqCxQR;EACA;EjCAE;;;AiCMF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;;;ACrCJ;EAEE;EACA;EtC4RI,2BALI;EsCrRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EhCpBA;EACA;;;AgCuBF;EACE;EACA;EACA;EtCgQI,WALI;EsCzPR;EACA;EACA;EACA;EnBpBI,YmBqBJ;;AnBjBI;EmBQN;InBPQ;;;AmBkBN;EACE;EACA;EAEA;EACA;;AAGF;EACE;EACA;EACA;EACA,SnC2uCgC;EmC1uChC;;AAGF;EAEE;EACA;ElBtDF,kBkBuDuB;EACrB;;AAGF;EAEE;EACA;EACA;EACA;;;AAKF;EACE,anC8sCgC;;AmCzsC9B;ElC9BF;EACA;;AkCmCE;ElClDF;EACA;;;AkCkEJ;EClGE;EACA;EvC0RI,2BALI;EuCnRR;;;ADmGF;ECtGE;EACA;EvC0RI,2BALI;EuCnRR;;;ACFF;EAEE;EACA;ExCuRI,sBALI;EwChRR;EACA;EACA;EAGA;EACA;ExC+QI,WALI;EwCxQR;EACA;EACA;EACA;EACA;EACA;EpCJE;;AoCSF;EACE;;;AAKJ;EACE;EACA;;;AChCF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;ErCHE;;;AqCQJ;EAEE;;;AAIF;EACE,atC6kB4B;EsC5kB5B;;;AAQF;EACE,etCo+C8B;;AsCj+C9B;EACE;EACA;EACA;EACA;EACA;;;AAQF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AAJF;EACE;EACA;EACA;EACA;;;AC5DF;EACE;IAAK,uBvCuhD2B;;;AuClhDpC;AAAA;EAGE;E1CkRI,yBALI;E0C3QR;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;E1CsQI,WALI;E0C/PR;EtCRE;;;AsCaJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EvBxBI,YuByBJ;;AvBrBI;EuBYN;IvBXQ;;;;AuBuBR;EtBAE;EsBEA;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;;AAGE;EAJJ;IAKM;;;;AC3DR;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EAGA;EACA;EvCXE;;;AuCeJ;EACE;EACA;;AAEA;EAEE;EACA;;;AASJ;EACE;EACA;EACA;;AAGA;EAEE;EACA;EACA;EACA;;AAGF;EACE;EACA;;;AAQJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EvCvDE;EACA;;AuC0DF;EvC7CE;EACA;;AuCgDF;EAEE;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAIF;EACE;;AAEA;EACE;EACA;;;AAaF;EACE;;AAGE;EvCvDJ;EAZA;;AuCwEI;EvCxEJ;EAYA;;AuCiEI;EACE;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AhCtFR;EgC8DA;IACE;;EAGE;IvCvDJ;IAZA;;EuCwEI;IvCxEJ;IAYA;;EuCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AhCtFR;EgC8DA;IACE;;EAGE;IvCvDJ;IAZA;;EuCwEI;IvCxEJ;IAYA;;EuCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AhCtFR;EgC8DA;IACE;;EAGE;IvCvDJ;IAZA;;EuCwEI;IvCxEJ;IAYA;;EuCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AhCtFR;EgC8DA;IACE;;EAGE;IvCvDJ;IAZA;;EuCwEI;IvCxEJ;IAYA;;EuCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AhCtFR;EgC8DA;IACE;;EAGE;IvCvDJ;IAZA;;EuCwEI;IvCxEJ;IAYA;;EuCiEI;IACE;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;;AAcZ;EvChJI;;AuCmJF;EACE;;AAEA;EACE;;;AAaJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAVF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AC5LJ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA,OzCmpD2B;EyClpD3B,QzCkpD2B;EyCjpD3B;EACA;EACA;EACA;ExCJE;EwCMF;;AAGA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EAEE;EACA;EACA;;;AAQJ;EAHE;;;AASE;EATF;;;ACjDF;EAEE;EACA;EACA;EACA;EACA;E7CyRI,sBALI;E6ClRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;E7C2QI,WALI;E6CpQR;EACA;EACA;EACA;EACA;EACA;EzCRE;;AyCWF;EACE;;AAGF;EACE;;;AAIJ;EACE;EAEA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EzChCE;EACA;;AyCkCF;EACE;EACA;;;AAIJ;EACE;EACA;;;AC9DF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;;;AAOF;EACE;EACA;EACA;EAEA;;AAGA;E3B5CI,Y2B6CF;EACA,W3Cg8CgC;;AgB1+C9B;E2BwCJ;I3BvCM;;;A2B2CN;EACE,W3C87CgC;;A2C17ClC;EACE,W3C27CgC;;;A2Cv7CpC;EACE;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;E1CrFE;E0CyFF;;;AAIF;EAEE;EACA;EACA;EClHA;EACA;EACA;EACA,SDkH0B;ECjH1B;EACA;EACA,kBD+G4D;;AC5G5D;EAAS;;AACT;EAAS,SD2GiF;;;AAK5F;EACE;EACA;EACA;EACA;EACA;EACA;E1CtGE;EACA;;A0CwGF;EACE;EACA;;;AAKJ;EACE;EACA;;;AAKF;EACE;EAGA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;E1C1HE;EACA;;A0C+HF;EACE;;;AnC5GA;EmCkHF;IACE;IACA;;EAIF;IACE;IACA;IACA;;EAGF;IACE;;;AnC/HA;EmCoIF;AAAA;IAEE;;;AnCtIA;EmC2IF;IACE;;;AAUA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;E1C1MJ;;A0C8ME;AAAA;E1C9MF;;A0CmNE;EACE;;;AnC3JJ;EmCyIA;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;I1C1MJ;;E0C8ME;AAAA;I1C9MF;;E0CmNE;IACE;;;AnC3JJ;EmCyIA;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;I1C1MJ;;E0C8ME;AAAA;I1C9MF;;E0CmNE;IACE;;;AnC3JJ;EmCyIA;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;I1C1MJ;;E0C8ME;AAAA;I1C9MF;;E0CmNE;IACE;;;AnC3JJ;EmCyIA;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;I1C1MJ;;E0C8ME;AAAA;I1C9MF;;E0CmNE;IACE;;;AnC3JJ;EmCyIA;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;I1C1MJ;;E0C8ME;AAAA;I1C9MF;;E0CmNE;IACE;;;AEtOR;EAEE;EACA;EACA;EACA;EACA;EhDwRI,wBALI;EgDjRR;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EClBA,a9C+lB4B;E8C7lB5B;EACA,a9CwmB4B;E8CvmB5B,a9C+mB4B;E8C9mB5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EjDgRI,WALI;EgDhQR;EACA;;AAEA;EAAS;;AAET;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;;AAKN;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;AACA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;AACA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;AAkBA;EACE;EACA;EACA;EACA;EACA;E5CjGE;;;A8CnBJ;EAEE;EACA;ElD4RI,wBALI;EkDrRR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ElDmRI,+BALI;EkD5QR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EDzBA,a9C+lB4B;E8C7lB5B;EACA,a9CwmB4B;E8CvmB5B,a9C+mB4B;E8C9mB5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EjDgRI,WALI;EkD1PR;EACA;EACA;EACA;E9ChBE;;A8CoBF;EACE;EACA;EACA;;AAEA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAMJ;EACE;;AAEA;EAEE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;;AAKN;AAEE;EACE;EACA;EACA;;AAEA;EAEE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;;AAKN;AAGE;EACE;;AAEA;EAEE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIJ;AAEE;EACE;EACA;EACA;;AAEA;EAEE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;;AAKN;AAkBA;EACE;EACA;ElD2GI,WALI;EkDpGR;EACA;EACA;E9C5JE;EACA;;A8C8JF;EACE;;;AAIJ;EACE;EACA;;;ACrLF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;ACtBA;EACE;EACA;EACA;;;ADuBJ;EACE;EACA;EACA;EACA;EACA;EACA;EhClBI,YgCmBJ;;AhCfI;EgCQN;IhCPQ;;;;AgCiBR;AAAA;AAAA;EAGE;;;AAGF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AASA;EACE;EACA;EACA;;AAGF;AAAA;AAAA;EAGE;EACA;;AAGF;AAAA;EAEE;EACA;EhC5DE,YgC6DF;;AhCzDE;EgCqDJ;AAAA;IhCpDM;;;;AgCiER;AAAA;EAEE;EACA;EACA;EACA;EAEA;EACA;EACA;EACA,OhDghDmC;EgD/gDnC;EACA,OhD1FS;EgD2FT;EACA;EACA;EACA,ShD2gDmC;EgBjmD/B,YgCuFJ;;AhCnFI;EgCkEN;AAAA;IhCjEQ;;;AgCqFN;AAAA;AAAA;EAEE,OhDpGO;EgDqGP;EACA;EACA,ShDmgDiC;;;AgDhgDrC;EACE;;;AAGF;EACE;;;AAKF;AAAA;EAEE;EACA,OhDogDmC;EgDngDnC,QhDmgDmC;EgDlgDnC;EACA;EACA;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA;EACE;;;AAEF;EACE;;;AAQF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,chD48CmC;EgD38CnC;EACA,ahD08CmC;;AgDx8CnC;EACE;EACA;EACA,OhD08CiC;EgDz8CjC,QhD08CiC;EgDz8CjC;EACA,chD08CiC;EgDz8CjC,ahDy8CiC;EgDx8CjC;EACA;EACA,kBhD1KO;EgD2KP;EACA;EAEA;EACA;EACA,ShDi8CiC;EgBzmD/B,YgCyKF;;AhCrKE;EgCoJJ;IhCnJM;;;AgCuKN;EACE,ShD87CiC;;;AgDr7CrC;EACE;EACA;EACA,QhDw7CmC;EgDv7CnC;EACA,ahDq7CmC;EgDp7CnC,gBhDo7CmC;EgDn7CnC,OhDrMS;EgDsMT;;;AAMA;AAAA;EAEE,QhDy7CiC;;AgDt7CnC;EACE,kBhDxMO;;AgD2MT;EACE,OhD5MO;;;AgDkMT;AAAA;AAAA;EAEE,QhDy7CiC;;AgDt7CnC;EACE,kBhDxMO;;AgD2MT;EACE,OhD5MO;;;AkDdX;AAAA;EAEE;EACA;EACA;EACA;EAEA;EACA;;;AAIF;EACE;IAAK;;;AAIP;EAEE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;;;AAGF;EAEE;EACA;EACA;;;AASF;EACE;IACE;;EAEF;IACE;IACA;;;AAKJ;EAEE;EACA;EACA;EACA;EACA;EAGA;EACA;;;AAGF;EACE;EACA;;;AAIA;EACE;AAAA;IAEE;;;AC/EN;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;A3C6DE;E2C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;InC5BA,YmC8BA;;;AnC1BA;EmCYJ;InCXM;;;ARuDJ;E2C5BE;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;;EAGF;IAEE;;EAGF;IAGE;;;A3C5BJ;E2C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A3CnCN;E2C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;InC5BA,YmC8BA;;;AnC1BA;EmCYJ;InCXM;;;ARuDJ;E2C5BE;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;;EAGF;IAEE;;EAGF;IAGE;;;A3C5BJ;E2C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A3CnCN;E2C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;InC5BA,YmC8BA;;;AnC1BA;EmCYJ;InCXM;;;ARuDJ;E2C5BE;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;;EAGF;IAEE;;EAGF;IAGE;;;A3C5BJ;E2C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A3CnCN;E2C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;InC5BA,YmC8BA;;;AnC1BA;EmCYJ;InCXM;;;ARuDJ;E2C5BE;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;;EAGF;IAEE;;EAGF;IAGE;;;A3C5BJ;E2C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;A3CnCN;E2C5CF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;InC5BA,YmC8BA;;;AnC1BA;EmCYJ;InCXM;;;ARuDJ;E2C5BE;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAGF;IACE;IACA;IACA;IACA;IACA;IACA;;EAGF;IAEE;;EAGF;IAGE;;;A3C5BJ;E2C/BF;IAiEM;IACA;IACA;;EAEA;IACE;;EAGF;IACE;IACA;IACA;IACA;IAEA;;;;AA/ER;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EnC5BA,YmC8BA;;AnC1BA;EmCYJ;InCXM;;;AmC2BF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EAEE;;AAGF;EAGE;;;AA2BR;EPpHE;EACA;EACA;EACA,S5C0mCkC;E4CzmClC;EACA;EACA,kB5CUS;;A4CPT;EAAS;;AACT;EAAS,S5Ci+CyB;;;AmDn3CpC;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AChJF;EACE;EACA;EACA;EACA;EACA;EACA,SpDgzCkC;;AoD9yClC;EACE;EACA;;;AAKJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAKA;EACE;;;AAIJ;EACE;IACE,SpDmxCgC;;;AoD/wCpC;EACE;EACA;EACA;;;AAGF;EACE;IACE;;;AH9CF;EACE;EACA;EACA;;;AIHF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;ACFF;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AATN;EACE;EACA;;AAGE;EAGE;EACA;;;AAOR;EACE;EACA;;AAGE;EAEE;EACA;;;AC1BN;EACE;EAEA;;;ACHF;EACE;EACA,KxD6c4B;EwD5c5B;EACA;EACA,uBxD2c4B;EwD1c5B;;AAEA;EACE;EACA,OxDuc0B;EwDtc1B,QxDsc0B;EwDrc1B;ExCIE,YwCHF;;AxCOE;EwCZJ;IxCaM;;;;AwCDJ;EACE;;;ACnBN;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAKF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;ACrBJ;EACE;EACA;EACA;EACA;EACA,S1DumCkC;;;A0DpmCpC;EACE;EACA;EACA;EACA;EACA,S1D+lCkC;;;A0DvlChC;EACE;EACA;EACA,S1DmlC8B;;;A0DhlChC;EACE;EACA;EACA,S1D6kC8B;;;AQ9iChC;EkDxCA;IACE;IACA;IACA,S1DmlC8B;;E0DhlChC;IACE;IACA;IACA,S1D6kC8B;;;AQ9iChC;EkDxCA;IACE;IACA;IACA,S1DmlC8B;;E0DhlChC;IACE;IACA;IACA,S1D6kC8B;;;AQ9iChC;EkDxCA;IACE;IACA;IACA,S1DmlC8B;;E0DhlChC;IACE;IACA;IACA,S1D6kC8B;;;AQ9iChC;EkDxCA;IACE;IACA;IACA,S1DmlC8B;;E0DhlChC;IACE;IACA;IACA,S1D6kC8B;;;AQ9iChC;EkDxCA;IACE;IACA;IACA,S1DmlC8B;;E0DhlChC;IACE;IACA;IACA,S1D6kC8B;;;A2D5mCpC;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;ACRF;AAAA;ECIE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;AAAA;EACE;;;ACdF;EACE;EACA;EACA;EACA;EACA;EACA,S9DgcsC;E8D/btC;;;ACRJ;ECAE;EACA;EACA;;;ACNF;EACE;EACA;EACA,OjEisB4B;EiEhsB5B;EACA;EACA,SjE2rB4B;;;AkE/nBtB;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAjBJ;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASF;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAjBJ;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASF;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AArBJ;AAcA;EAOI;EAAA;;;AAmBJ;AA1BA;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAjBJ;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASF;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAjBJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AAIJ;EAOI;;;AAKF;EAOI;;;AAnBN;EAOI;;;AAKF;EAOI;;;AAnBN;EAOI;;;AAKF;EAOI;;;AAnBN;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAjBJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AANJ;EACE;;;AAIA;EACE;;;AAIJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAPJ;EAIQ;EAGJ;;;AAjBJ;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASF;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;EAAA;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;AAPJ;EAOI;;;A1DVR;E0DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A1DVR;E0DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A1DVR;E0DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A1DVR;E0DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;A1DVR;E0DGI;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;IAAA;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;ACtDZ;ED+CQ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;ACnCZ;ED4BQ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;EAPJ;IAOI;;;AE5CZ;EACC,OAda;;;AAgBd;EACC,kBAjBa;;;AChBd;EACC;;;AAGD;EACC;EACA;EACA;EACA,aDJW;ECKX;;;AAGD;EACC,aDVa;ECWb;;;AACD;EACC;EACA;;;AAED;EACC;EACA;;;AACD;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAGD;EACC;EACA;;;AAED;EACC,ODxBa;;;AC0Bd;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACC,kBDjDa;ECkDb,OD9Ca;EC+Cb;EACA;EACA;EACA;;AACA;EACC;;AAGA;EACC;;AAEF;EACC;EACA;;AAED;EACC;EACA;EACA;EACA;EACA;EACA,ODpEY;;;AEpBd;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aFNa;EEOb;;A9DqDG;E8D/DJ;IAYE;;;AACD;EACC,kBFEY;EEDZ,cFCY;EEAZ;;AACA;EACC;EACA;;AACF;EACC,kBFFgB;EEGhB,cFHgB;EEIhB;EACA;;AACA;EACC;EACA;;AAGF;EACC;;AACD;EACC;EACA;EACA;EACA;;;ACpCF;EACC;;AAEA;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA,OHKW;;AGJZ;EACC;EACA;;;ACdH;EACC;EACA;EACA;EAGA;EACA;;AAIA;EACC;EACA;EACA;;AhEiDE;EgEpDH;IAME;IACA;IACA;IACA;;;AACD;EACC;EACA;EACA;EACA;EACA;;AhEqCC;EgE1CF;IAOE;;;AhEmCA;EgE1CF;IAWE;IACA;IACA;;;AAED;EACC;EACA;EACA,OJpBc;EIqBd;EACA;EACA;;AACA;EACC,OJ5BS;;A5D+CV;EgE3BD;IAUE;;;AhEiBD;EgE3BD;IAYE;;EACA;IACC;;;AhEaF;EgE3BD;IAiBE;IACA;;EACA;IACC;;;AACH;EACC;EACA;EACA;;AACA;EACC;;AhECD;EgEND;IAQE;;;AhEFD;EgEND;IAUE;IACA;;;AACF;EACC,OJtDU;EIuDV;EACA;EACA,aJvES;EIwET;EACA;;AACF;EACC;EACA;EACA;;AhEhBC;EgEaF;IAME;IACA;;;AACD;EACC;EACA;;AhEvBA;EgEqBD;IAIE;IACA;IACA;IACA;;;;AAGL;EACC;;AACA;EACC;EACA;;AhEnCE;EgEiCH;IAIE;;;AACD;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA,OJ1Fc;EI2Fd;EACA;EACA;;AACA;EACC,OJlGS;;A5D+CV;EgE2CD;IAUE;;;AhErDD;EgE2CD;IAYE;;EACA;IACC;;;AhEzDF;EgE2CD;IAiBE;IACA;;EACA;IACC;;;AACH;EACC;EACA;EACA;;AACA;EACC;;AhErED;EgEgED;IAQE;;;AhExED;EgEgED;IAUE;IACA;;;AACF;EACC,OJ5HU;EI6HV;EACA;EACA,aJ7IS;EI8IT;EACA;;AACH;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AhE3FE;EgEmFH;IAUE;IACA;;;AhE9FC;EgEmFH;IAaE;IACA;;;AhEjGC;EgEmFH;IAgBE;;;AACD;EACC;EACA;;AhEtGC;EgEoGF;IAIE;IACA;IACA;IACA;;;AhE3GA;EgEoGF;IASE;;;AhE7GA;EgEoGF;IAWE;;;;AC9KJ;EACC;;AACA;EACC;;AACA;EACC;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA,kBLGU;EKFV;EACA;EACA;EACA;EACA;;AAEH;EACC;;AjE0CE;EiE3CH;IAGE;;;AjEwCC;EiE3CH;IAKE;IACA;;;AAEF;EACC;;AAEA;EACC,OLhBW;;;AMhBd;EACC,kBNkBiB;EMjBjB;;AlE6DG;EkE/DJ;IAIE;;;AlE2DE;EkE/DJ;IAME;;;AAED;EACC,ONWY;EMVZ;EACA;EACA;EACA;EACA;;AlEiDE;EkEvDH;IAcE;IACA;;;AAEF;EACC;EACA,ONPY;EMQZ;;AAGD;EACC;;AAEA;EACC;;AACD;EACC;;AAED;EACC;EACA,ONzBW;;;AM4Bd;EACC,kBN1BiB;EM2BjB;EACA,ON3Ba;;AM6Bb;EACC;EACA;;AACD;EACC;EACA;;;ACpDF;EACC;EACA;EACA;;AAEA;EACC;EACA;;AAGA;EAEC;EACA;;AnEgDC;EmEnDF;IAKE;IACA;IACA;IACA;;;AACD;EACC;EACA,WAvBU;EAwBV;;AnEuCA;EmE1CD;IAKE;;;AACD;EACC;EACA;;AAEF;EACC;EACA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;;AAGH;EACC;EAaA;;AAZA;EACC,kBP9BU;EO+BV,OP3BU;;AO4BV;EACC,kBP7BS;EO8BT,OPlCS;;AOmCX;EACC,kBP3BW;EO4BX,OPjCU;;AOkCX;EACC,kBPhCc;;A5DwCd;EmEpBF;IAgBE;IACA;IACA;IACA;;;AACD;EACC;EACA,WAjEU;EAkEV;;AnEHA;EmEAD;IAKE;;;AnELD;EmEOA;IAEE;;;AnETF;EmEOA;IAIE;IACA;;;AACF;EACC;;AnEdD;EmEaA;IAGE;;;AnEhBF;EmExDH;IA2EE;;EACA;IACC;;EAEA;IACC;;EACD;IACC;;;;AAIL;EACC;;;AC9FD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;;ApEqDE;EoE3DH;IAQE;IACA;IACA;;;;AAEH;EAEC;EACA;;AACA;EACC;EACA;EACA;EACA;;ApEuCE;EoE3CH;IAME;;;AACD;EACC,ORZW;EQaX;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;ApEyBC;EoEpCF;IAaE;IACA;IACA;IACA;IACA;;;AACF;EACC;EACA,aR1CU;EQ2CV;EACA;EACA;EACA;;ApEYC;EoElBF;IAQE;IACA;IACA;;;AACH;EACC;EACA;;ApEKE;EoEPH;IAIE;;;AACD;EACC;EACA;EACA;EACA;EACA;EACA;;ApEJC;EoEFF;IASE;IACA;IACA;IACA;;;AACD;EACC;EACA;EACA;EACA;EACA;;ApEhBA;EoEWD;IAOE;IACA;;;AAGF;EACC;EACA;;AACD;EACC;EACA;EACA;;AACA;EACC;;AC7FL;EACC,kBTea;ESdb;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;ArEkDE;EqE1DH;IAUE;IACA;;;AAaD;EACC,OTVW;ESWX;EACA;;AACF;EACC;EACA;EACA;;AACA;EACC;;AACA;EACC;EACA;EACA;;ArEqBA;EqExBD;IAKE;;;AACD;EACC;;AACD;EACC;;AACD;EACC,kBTlCS;ESmCT;EACA,aTjDS;ESkDT;EACA;EACA;;AAGF;EACC;;;AAIJ;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAED;EACC,kBT/DW;ESgEX;EACA;EACA;;AACA;EACC;;;ACpFJ;EACC,kBVUW;;AURX;EACC,OVgBY;EUfZ;;AACA;EACC;;;ACPH;EACC;EACA,kBXca;EWbb,OXiBa;;AWhBb;EACC;EACA;EACA;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;EACA;EACA;;AACF;EACC;EACA;EACA;EACA;EACA,aXhCS;EWiCT,OXfU;EWgBV;;AAEF;EACC;;;ACvCH;EACC;;AxE8DG;EwE1DD;IAEE;;;AxEwDD;EwEtDH;IAEE;IACA;;;;ACVD;EACC,ObiBW;EahBX,kBbWiB;EaVjB,kBbWW;EaVX;EACA;EACA;EACA;EACA;;AAEF;EACC;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA,kBbLW;EaMX,ObFW;EaGX;EACA;EACA;EACA;EACA;;AAED;EACC,kBbhBgB;EaiBhB;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;;AzEqBD;EyE7BD;IAUE;IACA;IACA;;;AAGD;EACC;EACA;;AzEYD;EyEdA;IAIE;;;AzEUF;EyETC;IAEE;;;AzEOH;EyETC;IAIE;;;AACH;EACC;;AAIH;EAEC;EACA;;AAEA;EACC;;AAED;EACC;EACA;;AAEF;EACC;EACA;;AzEfC;EyEaF;IAIE;IACA;;;;AC1EJ;EACC;EACA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAED;EACC,kBdPY;EcQZ,OdJY;;;AcOd;EACC;EAGA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;A1EwBE;E0E/BH;IAUE;;;AAEF;EACC;EACA;EACA;EACA;;A1EeE;E0EnBH;IAQE;IACA;IACA;;;AAID;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;A1EHC;E0ELF;IAUE;;;AACD;EACC;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;EACA;EACA;;AACF;EACC;EACA;EACA;EACA;EACA,adnFS;EcoFT;;AAGF;EACC;EACA;EACA;EACA;EACA;EACA,ad5FW;Ec6FX;EACA;EACA;;A1EnCC;E0E0BF;IAWE;;;AAED;EACC;EACA;;A1EzCA;E0EuCD;IAIE;;;A1E3CD;E0EuCD;IAME;;;AACD;EACC;;AAED;EACC,Od9Fa;;AciGb;EACC,OdrGQ;;AcyGV;EACC;;A1E3DD;E0E0DA;IAGE;;;AACF;EACC;EACA;;AACA;EACC;;AAEJ;EACC;EACA;EACA,adnIW;EcoIX;EAEA;;A1E1EC;E0EoEF;IAQE","file":"home.css"} \ No newline at end of file

Track and record data impact assessment results