@jant/core 0.6.13 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-HnzmsaqU.js → app-C-oi_t8W.js} +7419 -6350
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-3_OaxhTs.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-Dfbq9aot.js → export-CsFx6F_M.js} +1273 -131
- package/dist/{github-sync--PSoE-Ne.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-BGTLx8Mf.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +373 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +102 -71
- package/src/client/components/jant-compose-dialog.ts +86 -22
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +872 -2
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +91 -13
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/footnotes.ts +156 -0
- package/src/client/tiptap/markdown-clipboard.ts +614 -12
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +36 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +202 -10
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +952 -64
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -6
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +23 -9
- package/src/ui/layouts/SiteLayout.tsx +19 -13
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-CWJFPjuR.js +0 -6
- package/dist/client/_assets/client-DCrcHVJl.css +0 -2
- package/dist/client/_assets/client-auth-TsYq90xm.js +0 -4733
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -3,6 +3,7 @@ import { strToU8, zipSync } from "fflate";
|
|
|
3
3
|
import { Extension, Node } from "@tiptap/core";
|
|
4
4
|
import { MarkdownManager } from "@tiptap/markdown";
|
|
5
5
|
import CodeBlock from "@tiptap/extension-code-block";
|
|
6
|
+
import Link from "@tiptap/extension-link";
|
|
6
7
|
import StarterKit from "@tiptap/starter-kit";
|
|
7
8
|
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
|
|
8
9
|
//#region src/lib/favicon.ts
|
|
@@ -979,6 +980,7 @@ var JANT_LOGO_POSITIVE_512_PNG_BASE64 = [
|
|
|
979
980
|
].join("");
|
|
980
981
|
//#endregion
|
|
981
982
|
//#region src/lib/jant-branding.ts
|
|
983
|
+
var JANT_HOME_URL = "https://jant.me";
|
|
982
984
|
var JANT_REPO_URL = "https://github.com/jant-me/jant";
|
|
983
985
|
var HOME_BRANDING_PREFIX = "Build with";
|
|
984
986
|
var HOME_BRANDING_LINK_LABEL = "Jant";
|
|
@@ -1411,6 +1413,682 @@ function buildJantBrandPackReadme() {
|
|
|
1411
1413
|
return toPublicPath(`/${storageKey}`, sitePathPrefix);
|
|
1412
1414
|
}
|
|
1413
1415
|
//#endregion
|
|
1416
|
+
//#region src/types/constants.ts
|
|
1417
|
+
/**
|
|
1418
|
+
* Content Type Constants
|
|
1419
|
+
*/ var FORMATS = [
|
|
1420
|
+
"note",
|
|
1421
|
+
"link",
|
|
1422
|
+
"quote"
|
|
1423
|
+
];
|
|
1424
|
+
var STATUSES = ["draft", "published"];
|
|
1425
|
+
var VISIBILITIES = [
|
|
1426
|
+
"public",
|
|
1427
|
+
"latest_hidden",
|
|
1428
|
+
"private"
|
|
1429
|
+
];
|
|
1430
|
+
var SORT_ORDERS = [
|
|
1431
|
+
"newest",
|
|
1432
|
+
"oldest",
|
|
1433
|
+
"rating_desc",
|
|
1434
|
+
"rating_asc"
|
|
1435
|
+
];
|
|
1436
|
+
var COLLECTION_SORT_ORDERS = [
|
|
1437
|
+
"newest",
|
|
1438
|
+
"oldest",
|
|
1439
|
+
"rating_desc"
|
|
1440
|
+
];
|
|
1441
|
+
var NAV_ITEM_TYPES = [
|
|
1442
|
+
"link",
|
|
1443
|
+
"system",
|
|
1444
|
+
"collection",
|
|
1445
|
+
"page"
|
|
1446
|
+
];
|
|
1447
|
+
var SYSTEM_NAV_KEY_VALUES = [
|
|
1448
|
+
"latest",
|
|
1449
|
+
"featured",
|
|
1450
|
+
"collections",
|
|
1451
|
+
"archive",
|
|
1452
|
+
"rss",
|
|
1453
|
+
"settings"
|
|
1454
|
+
];
|
|
1455
|
+
var SYSTEM_NAV_KEYS = {
|
|
1456
|
+
latest: {
|
|
1457
|
+
defaultLabel: "Latest",
|
|
1458
|
+
url: "/latest",
|
|
1459
|
+
defaultPlacement: "header"
|
|
1460
|
+
},
|
|
1461
|
+
featured: {
|
|
1462
|
+
defaultLabel: "Featured",
|
|
1463
|
+
url: "/featured",
|
|
1464
|
+
defaultPlacement: "header"
|
|
1465
|
+
},
|
|
1466
|
+
collections: {
|
|
1467
|
+
defaultLabel: "Collections",
|
|
1468
|
+
url: "/collections",
|
|
1469
|
+
defaultPlacement: "header"
|
|
1470
|
+
},
|
|
1471
|
+
archive: {
|
|
1472
|
+
defaultLabel: "Archive",
|
|
1473
|
+
url: "/archive",
|
|
1474
|
+
defaultPlacement: "header"
|
|
1475
|
+
},
|
|
1476
|
+
rss: {
|
|
1477
|
+
defaultLabel: "Feed",
|
|
1478
|
+
url: "/feed",
|
|
1479
|
+
defaultPlacement: "more"
|
|
1480
|
+
},
|
|
1481
|
+
settings: {
|
|
1482
|
+
defaultLabel: "Settings",
|
|
1483
|
+
url: "/settings",
|
|
1484
|
+
defaultPlacement: "more"
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
var DEFAULT_NAVIGATION_PROFILE = { 1: {
|
|
1488
|
+
version: 1,
|
|
1489
|
+
systemKeys: [
|
|
1490
|
+
"featured",
|
|
1491
|
+
"collections",
|
|
1492
|
+
"archive",
|
|
1493
|
+
"rss",
|
|
1494
|
+
"settings"
|
|
1495
|
+
]
|
|
1496
|
+
} }[1];
|
|
1497
|
+
var MAX_MEDIA_ATTACHMENTS = 20;
|
|
1498
|
+
var MAX_PINNED_POSTS = 3;
|
|
1499
|
+
var MAX_COLLECTION_DESCRIPTION_LENGTH = 2e3;
|
|
1500
|
+
var MAX_SITE_DESCRIPTION_LENGTH = 1e3;
|
|
1501
|
+
var MAX_SITE_FOOTER_LENGTH = 5e3;
|
|
1502
|
+
var TEXT_ATTACHMENT_CONTENT_FORMATS = ["markdown"];
|
|
1503
|
+
var MEDIA_KINDS = [
|
|
1504
|
+
"image",
|
|
1505
|
+
"video",
|
|
1506
|
+
"audio",
|
|
1507
|
+
"text",
|
|
1508
|
+
"document"
|
|
1509
|
+
];
|
|
1510
|
+
var STORAGE_DRIVERS = [
|
|
1511
|
+
"r2",
|
|
1512
|
+
"s3",
|
|
1513
|
+
"local"
|
|
1514
|
+
];
|
|
1515
|
+
/** How long a collection nav item stays "fresh" after new content is added */ var COLLECTION_FRESHNESS_WINDOW_SECONDS = 2880 * 60;
|
|
1516
|
+
//#endregion
|
|
1517
|
+
//#region src/types/config.ts
|
|
1518
|
+
/**
|
|
1519
|
+
* Configuration System
|
|
1520
|
+
*
|
|
1521
|
+
* Single Source of Truth for all configuration fields.
|
|
1522
|
+
*/ var CONFIG_FIELDS = {
|
|
1523
|
+
SITE_NAME: {
|
|
1524
|
+
defaultValue: "Jant",
|
|
1525
|
+
envOnly: false,
|
|
1526
|
+
envKeys: ["SITE_NAME"],
|
|
1527
|
+
editor: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
maxLength: 120
|
|
1530
|
+
},
|
|
1531
|
+
configEditorLink: {
|
|
1532
|
+
type: "string",
|
|
1533
|
+
settingsPath: "/settings/general",
|
|
1534
|
+
display: "value",
|
|
1535
|
+
resettable: true
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
SITE_DESCRIPTION: {
|
|
1539
|
+
defaultValue: "",
|
|
1540
|
+
envOnly: false,
|
|
1541
|
+
envKeys: ["SITE_DESCRIPTION"],
|
|
1542
|
+
editor: {
|
|
1543
|
+
type: "string",
|
|
1544
|
+
maxLength: MAX_SITE_DESCRIPTION_LENGTH
|
|
1545
|
+
},
|
|
1546
|
+
configEditorLink: {
|
|
1547
|
+
type: "string",
|
|
1548
|
+
settingsPath: "/settings/general",
|
|
1549
|
+
display: "configured",
|
|
1550
|
+
resettable: true
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
SITE_LANGUAGE: {
|
|
1554
|
+
defaultValue: "en",
|
|
1555
|
+
envOnly: false,
|
|
1556
|
+
envKeys: ["SITE_LANGUAGE"],
|
|
1557
|
+
editor: {
|
|
1558
|
+
type: "enum",
|
|
1559
|
+
optionsSource: "contentLanguage"
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
DASHBOARD_LANGUAGE: {
|
|
1563
|
+
defaultValue: "",
|
|
1564
|
+
envOnly: false,
|
|
1565
|
+
envKeys: ["DASHBOARD_LANGUAGE"],
|
|
1566
|
+
editor: {
|
|
1567
|
+
type: "enum",
|
|
1568
|
+
options: [
|
|
1569
|
+
"",
|
|
1570
|
+
"en",
|
|
1571
|
+
"zh-Hans",
|
|
1572
|
+
"zh-Hant"
|
|
1573
|
+
]
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
CJK_SERIF_FONT: {
|
|
1577
|
+
defaultValue: "off",
|
|
1578
|
+
envOnly: false,
|
|
1579
|
+
envKeys: ["CJK_SERIF_FONT"],
|
|
1580
|
+
editor: {
|
|
1581
|
+
type: "enum",
|
|
1582
|
+
options: [
|
|
1583
|
+
"off",
|
|
1584
|
+
"zh-Hans",
|
|
1585
|
+
"zh-Hant",
|
|
1586
|
+
"ja",
|
|
1587
|
+
"ko"
|
|
1588
|
+
]
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
MAIN_RSS_FEED: {
|
|
1592
|
+
defaultValue: "featured",
|
|
1593
|
+
envOnly: false,
|
|
1594
|
+
envKeys: ["MAIN_RSS_FEED"],
|
|
1595
|
+
editor: {
|
|
1596
|
+
type: "enum",
|
|
1597
|
+
options: ["featured", "latest"]
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
DEFAULT_THEME: {
|
|
1601
|
+
defaultValue: "tufte",
|
|
1602
|
+
envOnly: true,
|
|
1603
|
+
envKeys: ["DEFAULT_THEME"]
|
|
1604
|
+
},
|
|
1605
|
+
DEFAULT_FONT_THEME: {
|
|
1606
|
+
defaultValue: "classic",
|
|
1607
|
+
envOnly: true,
|
|
1608
|
+
envKeys: ["DEFAULT_FONT_THEME"]
|
|
1609
|
+
},
|
|
1610
|
+
SITE_ORIGIN: {
|
|
1611
|
+
defaultValue: "",
|
|
1612
|
+
envOnly: true,
|
|
1613
|
+
envKeys: ["SITE_ORIGIN"]
|
|
1614
|
+
},
|
|
1615
|
+
SITE_PATH_PREFIX: {
|
|
1616
|
+
defaultValue: "",
|
|
1617
|
+
envOnly: true,
|
|
1618
|
+
envKeys: ["SITE_PATH_PREFIX"]
|
|
1619
|
+
},
|
|
1620
|
+
AUTH_SECRET: {
|
|
1621
|
+
defaultValue: "",
|
|
1622
|
+
envOnly: true,
|
|
1623
|
+
envKeys: ["AUTH_SECRET"]
|
|
1624
|
+
},
|
|
1625
|
+
R2_PUBLIC_URL: {
|
|
1626
|
+
defaultValue: "",
|
|
1627
|
+
envOnly: true,
|
|
1628
|
+
envKeys: ["R2_PUBLIC_URL"]
|
|
1629
|
+
},
|
|
1630
|
+
IMAGE_TRANSFORM_URL: {
|
|
1631
|
+
defaultValue: "",
|
|
1632
|
+
envOnly: true,
|
|
1633
|
+
envKeys: ["IMAGE_TRANSFORM_URL"]
|
|
1634
|
+
},
|
|
1635
|
+
DEMO_EMAIL: {
|
|
1636
|
+
defaultValue: "",
|
|
1637
|
+
envOnly: true,
|
|
1638
|
+
envKeys: ["DEMO_EMAIL"]
|
|
1639
|
+
},
|
|
1640
|
+
DEMO_PASSWORD: {
|
|
1641
|
+
defaultValue: "",
|
|
1642
|
+
envOnly: true,
|
|
1643
|
+
envKeys: ["DEMO_PASSWORD"]
|
|
1644
|
+
},
|
|
1645
|
+
DEMO_MODE: {
|
|
1646
|
+
defaultValue: "false",
|
|
1647
|
+
envOnly: true,
|
|
1648
|
+
envKeys: ["DEMO_MODE"]
|
|
1649
|
+
},
|
|
1650
|
+
PAGE_SIZE: {
|
|
1651
|
+
defaultValue: "50",
|
|
1652
|
+
envOnly: false,
|
|
1653
|
+
envKeys: ["PAGE_SIZE"],
|
|
1654
|
+
editor: {
|
|
1655
|
+
type: "number",
|
|
1656
|
+
min: 1,
|
|
1657
|
+
max: 100,
|
|
1658
|
+
step: 1
|
|
1659
|
+
}
|
|
1660
|
+
},
|
|
1661
|
+
SEARCH_PAGE_SIZE: {
|
|
1662
|
+
defaultValue: "",
|
|
1663
|
+
envOnly: false,
|
|
1664
|
+
fallbackKey: "PAGE_SIZE",
|
|
1665
|
+
envKeys: ["SEARCH_PAGE_SIZE"],
|
|
1666
|
+
editor: {
|
|
1667
|
+
type: "number",
|
|
1668
|
+
min: 1,
|
|
1669
|
+
max: 100,
|
|
1670
|
+
step: 1
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
ARCHIVE_PAGE_SIZE: {
|
|
1674
|
+
defaultValue: "",
|
|
1675
|
+
envOnly: false,
|
|
1676
|
+
fallbackKey: "PAGE_SIZE",
|
|
1677
|
+
envKeys: ["ARCHIVE_PAGE_SIZE"],
|
|
1678
|
+
editor: {
|
|
1679
|
+
type: "number",
|
|
1680
|
+
min: 1,
|
|
1681
|
+
max: 100,
|
|
1682
|
+
step: 1
|
|
1683
|
+
}
|
|
1684
|
+
},
|
|
1685
|
+
STORAGE_DRIVER: {
|
|
1686
|
+
defaultValue: "r2",
|
|
1687
|
+
envOnly: true,
|
|
1688
|
+
envKeys: ["STORAGE_DRIVER"]
|
|
1689
|
+
},
|
|
1690
|
+
S3_ENDPOINT: {
|
|
1691
|
+
defaultValue: "",
|
|
1692
|
+
envOnly: true,
|
|
1693
|
+
envKeys: ["S3_ENDPOINT"]
|
|
1694
|
+
},
|
|
1695
|
+
S3_BUCKET: {
|
|
1696
|
+
defaultValue: "",
|
|
1697
|
+
envOnly: true,
|
|
1698
|
+
envKeys: ["S3_BUCKET"]
|
|
1699
|
+
},
|
|
1700
|
+
S3_ACCESS_KEY_ID: {
|
|
1701
|
+
defaultValue: "",
|
|
1702
|
+
envOnly: true,
|
|
1703
|
+
envKeys: ["S3_ACCESS_KEY_ID"]
|
|
1704
|
+
},
|
|
1705
|
+
S3_SECRET_ACCESS_KEY: {
|
|
1706
|
+
defaultValue: "",
|
|
1707
|
+
envOnly: true,
|
|
1708
|
+
envKeys: ["S3_SECRET_ACCESS_KEY"]
|
|
1709
|
+
},
|
|
1710
|
+
S3_REGION: {
|
|
1711
|
+
defaultValue: "auto",
|
|
1712
|
+
envOnly: true,
|
|
1713
|
+
envKeys: ["S3_REGION"]
|
|
1714
|
+
},
|
|
1715
|
+
S3_PUBLIC_URL: {
|
|
1716
|
+
defaultValue: "",
|
|
1717
|
+
envOnly: true,
|
|
1718
|
+
envKeys: ["S3_PUBLIC_URL"]
|
|
1719
|
+
},
|
|
1720
|
+
ASSET_BASE_URL: {
|
|
1721
|
+
defaultValue: "",
|
|
1722
|
+
envOnly: true,
|
|
1723
|
+
envKeys: ["ASSET_BASE_URL"]
|
|
1724
|
+
},
|
|
1725
|
+
UPLOAD_MAX_FILE_SIZE_MB: {
|
|
1726
|
+
defaultValue: "1024",
|
|
1727
|
+
envOnly: true,
|
|
1728
|
+
envKeys: ["UPLOAD_MAX_FILE_SIZE_MB"]
|
|
1729
|
+
},
|
|
1730
|
+
SUMMARY_MAX_PARAGRAPHS: {
|
|
1731
|
+
defaultValue: "5",
|
|
1732
|
+
envOnly: false,
|
|
1733
|
+
envKeys: ["SUMMARY_MAX_PARAGRAPHS"],
|
|
1734
|
+
editor: {
|
|
1735
|
+
type: "number",
|
|
1736
|
+
min: 1,
|
|
1737
|
+
max: 50,
|
|
1738
|
+
step: 1
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
SUMMARY_MAX_CHARS: {
|
|
1742
|
+
defaultValue: "500",
|
|
1743
|
+
envOnly: false,
|
|
1744
|
+
envKeys: ["SUMMARY_MAX_CHARS"],
|
|
1745
|
+
editor: {
|
|
1746
|
+
type: "number",
|
|
1747
|
+
min: 1,
|
|
1748
|
+
max: 1500,
|
|
1749
|
+
step: 1
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
SLUG_ID_LENGTH: {
|
|
1753
|
+
defaultValue: "5",
|
|
1754
|
+
envOnly: true,
|
|
1755
|
+
envKeys: ["SLUG_ID_LENGTH"]
|
|
1756
|
+
},
|
|
1757
|
+
RSS_FEED_LIMIT: {
|
|
1758
|
+
defaultValue: "50",
|
|
1759
|
+
envOnly: false,
|
|
1760
|
+
envKeys: ["RSS_FEED_LIMIT"],
|
|
1761
|
+
editor: {
|
|
1762
|
+
type: "number",
|
|
1763
|
+
min: 1,
|
|
1764
|
+
max: 200,
|
|
1765
|
+
step: 1
|
|
1766
|
+
}
|
|
1767
|
+
},
|
|
1768
|
+
RSS_PUBLISH_DELAY_SECONDS: {
|
|
1769
|
+
defaultValue: "300",
|
|
1770
|
+
envOnly: false,
|
|
1771
|
+
envKeys: ["RSS_PUBLISH_DELAY_SECONDS"],
|
|
1772
|
+
editor: {
|
|
1773
|
+
type: "number",
|
|
1774
|
+
min: 0,
|
|
1775
|
+
max: 7200,
|
|
1776
|
+
step: 1
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
THEME: {
|
|
1780
|
+
defaultValue: "",
|
|
1781
|
+
envOnly: false,
|
|
1782
|
+
internal: true,
|
|
1783
|
+
configEditorLink: {
|
|
1784
|
+
type: "string",
|
|
1785
|
+
settingsPath: "/settings/color-theme",
|
|
1786
|
+
display: "value",
|
|
1787
|
+
fallbackKey: "DEFAULT_THEME",
|
|
1788
|
+
resettable: true
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
CUSTOM_CSS: {
|
|
1792
|
+
defaultValue: "",
|
|
1793
|
+
envOnly: false,
|
|
1794
|
+
internal: true,
|
|
1795
|
+
configEditorLink: {
|
|
1796
|
+
type: "string",
|
|
1797
|
+
settingsPath: "/settings/custom-css",
|
|
1798
|
+
display: "configured"
|
|
1799
|
+
}
|
|
1800
|
+
},
|
|
1801
|
+
CUSTOM_HEAD_HTML: {
|
|
1802
|
+
defaultValue: "",
|
|
1803
|
+
envOnly: false,
|
|
1804
|
+
internal: true,
|
|
1805
|
+
configEditorLink: {
|
|
1806
|
+
type: "string",
|
|
1807
|
+
settingsPath: "/settings/code-injection",
|
|
1808
|
+
display: "configured"
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
CUSTOM_BODY_END_HTML: {
|
|
1812
|
+
defaultValue: "",
|
|
1813
|
+
envOnly: false,
|
|
1814
|
+
internal: true,
|
|
1815
|
+
configEditorLink: {
|
|
1816
|
+
type: "string",
|
|
1817
|
+
settingsPath: "/settings/code-injection",
|
|
1818
|
+
display: "configured"
|
|
1819
|
+
}
|
|
1820
|
+
},
|
|
1821
|
+
SITE_AVATAR: {
|
|
1822
|
+
defaultValue: "",
|
|
1823
|
+
envOnly: false,
|
|
1824
|
+
internal: true,
|
|
1825
|
+
configEditorLink: {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
settingsPath: "/settings/avatar",
|
|
1828
|
+
display: "configured"
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
SHOW_HEADER_AVATAR: {
|
|
1832
|
+
defaultValue: "",
|
|
1833
|
+
envOnly: false,
|
|
1834
|
+
internal: true,
|
|
1835
|
+
configEditorLink: {
|
|
1836
|
+
type: "boolean",
|
|
1837
|
+
settingsPath: "/settings/avatar",
|
|
1838
|
+
display: "value",
|
|
1839
|
+
fallbackValue: "false",
|
|
1840
|
+
resettable: true
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
SITE_FAVICON_ICO: {
|
|
1844
|
+
defaultValue: "",
|
|
1845
|
+
envOnly: false,
|
|
1846
|
+
internal: true
|
|
1847
|
+
},
|
|
1848
|
+
SITE_FAVICON_APPLE_TOUCH: {
|
|
1849
|
+
defaultValue: "",
|
|
1850
|
+
envOnly: false,
|
|
1851
|
+
internal: true
|
|
1852
|
+
},
|
|
1853
|
+
SITE_FAVICON_VERSION: {
|
|
1854
|
+
defaultValue: "",
|
|
1855
|
+
envOnly: false,
|
|
1856
|
+
internal: true
|
|
1857
|
+
},
|
|
1858
|
+
FONT_THEME: {
|
|
1859
|
+
defaultValue: "",
|
|
1860
|
+
envOnly: false,
|
|
1861
|
+
internal: true,
|
|
1862
|
+
configEditorLink: {
|
|
1863
|
+
type: "string",
|
|
1864
|
+
settingsPath: "/settings/font-theme",
|
|
1865
|
+
display: "value",
|
|
1866
|
+
fallbackKey: "DEFAULT_FONT_THEME",
|
|
1867
|
+
resettable: true
|
|
1868
|
+
}
|
|
1869
|
+
},
|
|
1870
|
+
THEME_MODE: {
|
|
1871
|
+
defaultValue: "",
|
|
1872
|
+
envOnly: false,
|
|
1873
|
+
internal: true,
|
|
1874
|
+
configEditorLink: {
|
|
1875
|
+
type: "string",
|
|
1876
|
+
settingsPath: "/settings/color-theme",
|
|
1877
|
+
display: "value",
|
|
1878
|
+
fallbackValue: "auto",
|
|
1879
|
+
resettable: true
|
|
1880
|
+
}
|
|
1881
|
+
},
|
|
1882
|
+
TIME_ZONE: {
|
|
1883
|
+
defaultValue: "UTC",
|
|
1884
|
+
envOnly: false,
|
|
1885
|
+
envKeys: ["TIME_ZONE"],
|
|
1886
|
+
editor: {
|
|
1887
|
+
type: "enum",
|
|
1888
|
+
optionsSource: "timeZone"
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
SITE_FOOTER: {
|
|
1892
|
+
defaultValue: "",
|
|
1893
|
+
envOnly: false,
|
|
1894
|
+
envKeys: ["SITE_FOOTER"],
|
|
1895
|
+
editor: {
|
|
1896
|
+
type: "string",
|
|
1897
|
+
maxLength: MAX_SITE_FOOTER_LENGTH
|
|
1898
|
+
},
|
|
1899
|
+
configEditorLink: {
|
|
1900
|
+
type: "string",
|
|
1901
|
+
settingsPath: "/settings/general",
|
|
1902
|
+
display: "configured",
|
|
1903
|
+
resettable: true
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
SHOW_JANT_BRANDING_ON_HOME: {
|
|
1907
|
+
defaultValue: "false",
|
|
1908
|
+
envOnly: false,
|
|
1909
|
+
envKeys: ["SHOW_JANT_BRANDING_ON_HOME"],
|
|
1910
|
+
editor: { type: "boolean" }
|
|
1911
|
+
},
|
|
1912
|
+
NOINDEX: {
|
|
1913
|
+
defaultValue: "false",
|
|
1914
|
+
envOnly: false,
|
|
1915
|
+
envKeys: ["NOINDEX"],
|
|
1916
|
+
editor: { type: "boolean" }
|
|
1917
|
+
},
|
|
1918
|
+
PUBLIC_API_ENABLED: {
|
|
1919
|
+
defaultValue: "true",
|
|
1920
|
+
envOnly: false,
|
|
1921
|
+
envKeys: ["PUBLIC_API_ENABLED"],
|
|
1922
|
+
editor: { type: "boolean" }
|
|
1923
|
+
},
|
|
1924
|
+
RSS_FEEDS_ENABLED: {
|
|
1925
|
+
defaultValue: "true",
|
|
1926
|
+
envOnly: false,
|
|
1927
|
+
envKeys: ["RSS_FEEDS_ENABLED"],
|
|
1928
|
+
editor: { type: "boolean" }
|
|
1929
|
+
},
|
|
1930
|
+
DISCOVERY_COMPOSE_OPEN_SHORTCUT_AT: {
|
|
1931
|
+
defaultValue: "",
|
|
1932
|
+
envOnly: false,
|
|
1933
|
+
internal: true
|
|
1934
|
+
},
|
|
1935
|
+
DISCOVERY_SLASH_COMMAND_AT: {
|
|
1936
|
+
defaultValue: "",
|
|
1937
|
+
envOnly: false,
|
|
1938
|
+
internal: true
|
|
1939
|
+
},
|
|
1940
|
+
ONBOARDING_STATUS: {
|
|
1941
|
+
defaultValue: "pending",
|
|
1942
|
+
envOnly: false,
|
|
1943
|
+
internal: true
|
|
1944
|
+
},
|
|
1945
|
+
PASSWORD_RESET_TOKEN: {
|
|
1946
|
+
defaultValue: "",
|
|
1947
|
+
envOnly: false,
|
|
1948
|
+
internal: true
|
|
1949
|
+
},
|
|
1950
|
+
DELETE_CSRF_TOKEN: {
|
|
1951
|
+
defaultValue: "",
|
|
1952
|
+
envOnly: false,
|
|
1953
|
+
internal: true
|
|
1954
|
+
},
|
|
1955
|
+
GITHUB_SYNC_ENABLED: {
|
|
1956
|
+
defaultValue: "false",
|
|
1957
|
+
envOnly: false,
|
|
1958
|
+
internal: true,
|
|
1959
|
+
configEditorLink: {
|
|
1960
|
+
type: "boolean",
|
|
1961
|
+
settingsPath: "/settings/github-sync",
|
|
1962
|
+
display: "value"
|
|
1963
|
+
}
|
|
1964
|
+
},
|
|
1965
|
+
GITHUB_SYNC_REPO: {
|
|
1966
|
+
defaultValue: "",
|
|
1967
|
+
envOnly: false,
|
|
1968
|
+
internal: true,
|
|
1969
|
+
configEditorLink: {
|
|
1970
|
+
type: "string",
|
|
1971
|
+
settingsPath: "/settings/github-sync",
|
|
1972
|
+
display: "configured"
|
|
1973
|
+
}
|
|
1974
|
+
},
|
|
1975
|
+
GITHUB_SYNC_TOKEN: {
|
|
1976
|
+
defaultValue: "",
|
|
1977
|
+
envOnly: false,
|
|
1978
|
+
internal: true
|
|
1979
|
+
},
|
|
1980
|
+
GITHUB_SYNC_WEBHOOK_SECRET: {
|
|
1981
|
+
defaultValue: "",
|
|
1982
|
+
envOnly: false,
|
|
1983
|
+
internal: true
|
|
1984
|
+
},
|
|
1985
|
+
GITHUB_SYNC_WEBHOOK_ID: {
|
|
1986
|
+
defaultValue: "",
|
|
1987
|
+
envOnly: false,
|
|
1988
|
+
internal: true
|
|
1989
|
+
},
|
|
1990
|
+
GITHUB_SYNC_LAST_PUSH_SHA: {
|
|
1991
|
+
defaultValue: "",
|
|
1992
|
+
envOnly: false,
|
|
1993
|
+
internal: true
|
|
1994
|
+
},
|
|
1995
|
+
GITHUB_SYNC_LAST_PUSH_AT: {
|
|
1996
|
+
defaultValue: "",
|
|
1997
|
+
envOnly: false,
|
|
1998
|
+
internal: true
|
|
1999
|
+
},
|
|
2000
|
+
GITHUB_SYNC_PENDING: {
|
|
2001
|
+
defaultValue: "",
|
|
2002
|
+
envOnly: false,
|
|
2003
|
+
internal: true
|
|
2004
|
+
},
|
|
2005
|
+
GITHUB_SYNC_PENDING_AT: {
|
|
2006
|
+
defaultValue: "",
|
|
2007
|
+
envOnly: false,
|
|
2008
|
+
internal: true
|
|
2009
|
+
},
|
|
2010
|
+
GITHUB_SYNC_DIRTY: {
|
|
2011
|
+
defaultValue: "",
|
|
2012
|
+
envOnly: false,
|
|
2013
|
+
internal: true
|
|
2014
|
+
},
|
|
2015
|
+
GITHUB_SYNC_LAST_ERROR: {
|
|
2016
|
+
defaultValue: "",
|
|
2017
|
+
envOnly: false,
|
|
2018
|
+
internal: true
|
|
2019
|
+
},
|
|
2020
|
+
GITHUB_SYNC_AUTH_MODE: {
|
|
2021
|
+
defaultValue: "pat",
|
|
2022
|
+
envOnly: false,
|
|
2023
|
+
internal: true
|
|
2024
|
+
},
|
|
2025
|
+
GITHUB_SYNC_APP_INSTALLATION_ID: {
|
|
2026
|
+
defaultValue: "",
|
|
2027
|
+
envOnly: false,
|
|
2028
|
+
internal: true
|
|
2029
|
+
},
|
|
2030
|
+
GITHUB_APP_ID: {
|
|
2031
|
+
defaultValue: "",
|
|
2032
|
+
envOnly: true,
|
|
2033
|
+
envKeys: ["GITHUB_APP_ID"]
|
|
2034
|
+
},
|
|
2035
|
+
GITHUB_APP_PRIVATE_KEY: {
|
|
2036
|
+
defaultValue: "",
|
|
2037
|
+
envOnly: true,
|
|
2038
|
+
envKeys: ["GITHUB_APP_PRIVATE_KEY"]
|
|
2039
|
+
},
|
|
2040
|
+
GITHUB_APP_SLUG: {
|
|
2041
|
+
defaultValue: "",
|
|
2042
|
+
envOnly: true,
|
|
2043
|
+
envKeys: ["GITHUB_APP_SLUG"]
|
|
2044
|
+
},
|
|
2045
|
+
GITHUB_APP_WEBHOOK_SECRET: {
|
|
2046
|
+
defaultValue: "",
|
|
2047
|
+
envOnly: true,
|
|
2048
|
+
envKeys: ["GITHUB_APP_WEBHOOK_SECRET"]
|
|
2049
|
+
},
|
|
2050
|
+
TELEGRAM_BOT_TOKENS: {
|
|
2051
|
+
defaultValue: "",
|
|
2052
|
+
envOnly: true,
|
|
2053
|
+
envKeys: ["TELEGRAM_BOT_TOKENS"]
|
|
2054
|
+
},
|
|
2055
|
+
TELEGRAM_WEBHOOK_SECRET: {
|
|
2056
|
+
defaultValue: "",
|
|
2057
|
+
envOnly: true,
|
|
2058
|
+
envKeys: ["TELEGRAM_WEBHOOK_SECRET"]
|
|
2059
|
+
},
|
|
2060
|
+
TELEGRAM_BOT_TOKEN: {
|
|
2061
|
+
defaultValue: "",
|
|
2062
|
+
envOnly: false,
|
|
2063
|
+
internal: true
|
|
2064
|
+
},
|
|
2065
|
+
TELEGRAM_BOT_ID: {
|
|
2066
|
+
defaultValue: "",
|
|
2067
|
+
envOnly: false,
|
|
2068
|
+
internal: true
|
|
2069
|
+
},
|
|
2070
|
+
TELEGRAM_BOT_USERNAME: {
|
|
2071
|
+
defaultValue: "",
|
|
2072
|
+
envOnly: false,
|
|
2073
|
+
internal: true,
|
|
2074
|
+
configEditorLink: {
|
|
2075
|
+
type: "string",
|
|
2076
|
+
settingsPath: "/settings/telegram",
|
|
2077
|
+
display: "configured"
|
|
2078
|
+
}
|
|
2079
|
+
},
|
|
2080
|
+
TELEGRAM_BOT_WEBHOOK_SECRET: {
|
|
2081
|
+
defaultValue: "",
|
|
2082
|
+
envOnly: false,
|
|
2083
|
+
internal: true
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
var THEME_MODES = [
|
|
2087
|
+
"auto",
|
|
2088
|
+
"light",
|
|
2089
|
+
"dark"
|
|
2090
|
+
];
|
|
2091
|
+
//#endregion
|
|
1414
2092
|
//#region src/lib/time.ts
|
|
1415
2093
|
var time_exports = /* @__PURE__ */ __exportAll({
|
|
1416
2094
|
formatDate: () => formatDate,
|
|
@@ -1659,6 +2337,189 @@ function isEmptyParagraphNode(node) {
|
|
|
1659
2337
|
content
|
|
1660
2338
|
};
|
|
1661
2339
|
}
|
|
2340
|
+
function getLinkHref(node) {
|
|
2341
|
+
const linkMarks = (node.marks ?? []).filter((mark) => mark.type === "link");
|
|
2342
|
+
if (linkMarks.length !== 1) return "";
|
|
2343
|
+
const href = linkMarks[0]?.attrs?.href;
|
|
2344
|
+
return typeof href === "string" ? href.trim() : "";
|
|
2345
|
+
}
|
|
2346
|
+
function decodeHashTarget(href) {
|
|
2347
|
+
if (!href.startsWith("#")) return "";
|
|
2348
|
+
try {
|
|
2349
|
+
return decodeURIComponent(href.slice(1));
|
|
2350
|
+
} catch {
|
|
2351
|
+
return href.slice(1);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
function normalizeLegacyTargetId(value) {
|
|
2355
|
+
return value.replace(/^user-content-/i, "").toLowerCase();
|
|
2356
|
+
}
|
|
2357
|
+
function getLegacyReferenceLabel(node) {
|
|
2358
|
+
if (node.type !== "text" || typeof node.text !== "string") return "";
|
|
2359
|
+
const text = node.text.trim();
|
|
2360
|
+
const label = normalizeFootnoteLabel(text.match(/^\[\^?([^\]\n]+)\]$/)?.[1] ?? text);
|
|
2361
|
+
if (!label) return "";
|
|
2362
|
+
return normalizeLegacyTargetId(decodeHashTarget(getLinkHref(node))) === `fn-${getFootnoteDomId(label)}`.toLowerCase() ? label : "";
|
|
2363
|
+
}
|
|
2364
|
+
function collectLegacyReferences(node, references) {
|
|
2365
|
+
const label = getLegacyReferenceLabel(node);
|
|
2366
|
+
if (label) {
|
|
2367
|
+
references.push({
|
|
2368
|
+
label,
|
|
2369
|
+
node
|
|
2370
|
+
});
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
for (const child of node.content ?? []) collectLegacyReferences(child, references);
|
|
2374
|
+
}
|
|
2375
|
+
function isBacklinkText(value) {
|
|
2376
|
+
return /^(?:↩(?:︎)?|↵)(?:\s*\d+)?$/.test(value?.trim() ?? "");
|
|
2377
|
+
}
|
|
2378
|
+
function getLegacyBacklinkLabel(node, labels) {
|
|
2379
|
+
if (node.type !== "text" || !isBacklinkText(node.text)) return "";
|
|
2380
|
+
const targetId = normalizeLegacyTargetId(decodeHashTarget(getLinkHref(node)));
|
|
2381
|
+
if (!targetId.startsWith("fnref-")) return "";
|
|
2382
|
+
for (const label of labels) {
|
|
2383
|
+
const base = `fnref-${getFootnoteDomId(label)}`.toLowerCase();
|
|
2384
|
+
const occurrence = targetId.startsWith(`${base}-`) ? targetId.slice(base.length + 1) : "";
|
|
2385
|
+
if (targetId === base || /^\d+$/.test(occurrence)) return label;
|
|
2386
|
+
}
|
|
2387
|
+
return "";
|
|
2388
|
+
}
|
|
2389
|
+
function collectLegacyBacklinkLabels(node, labels, found) {
|
|
2390
|
+
const label = getLegacyBacklinkLabel(node, labels);
|
|
2391
|
+
if (label) {
|
|
2392
|
+
found.add(getFootnoteLabelKey(label));
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
for (const child of node.content ?? []) collectLegacyBacklinkLabels(child, labels, found);
|
|
2396
|
+
}
|
|
2397
|
+
function removeLegacyBacklinks(node, label) {
|
|
2398
|
+
if (getFootnoteLabelKey(getLegacyBacklinkLabel(node, [label])) === getFootnoteLabelKey(label)) return null;
|
|
2399
|
+
if (!node.content) return node;
|
|
2400
|
+
const content = node.content.flatMap((child) => {
|
|
2401
|
+
const normalized = removeLegacyBacklinks(child, label);
|
|
2402
|
+
return normalized ? [normalized] : [];
|
|
2403
|
+
});
|
|
2404
|
+
if (content.length === node.content.length) {
|
|
2405
|
+
if (content.every((child, index) => child === node.content?.[index])) return node;
|
|
2406
|
+
}
|
|
2407
|
+
return content.length > 0 ? {
|
|
2408
|
+
...node,
|
|
2409
|
+
content
|
|
2410
|
+
} : {
|
|
2411
|
+
...node,
|
|
2412
|
+
content: []
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function replaceLegacyReferences(node, labelsByNode) {
|
|
2416
|
+
const label = labelsByNode.get(node);
|
|
2417
|
+
if (label) return {
|
|
2418
|
+
type: "footnoteReference",
|
|
2419
|
+
attrs: { label }
|
|
2420
|
+
};
|
|
2421
|
+
if (!node.content) return node;
|
|
2422
|
+
const content = node.content.map((child) => replaceLegacyReferences(child, labelsByNode));
|
|
2423
|
+
return content.every((child, index) => child === node.content?.[index]) ? node : {
|
|
2424
|
+
...node,
|
|
2425
|
+
content
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
function hasCanonicalFootnoteNode(node) {
|
|
2429
|
+
if (node.type === "footnoteReference" || node.type === "footnoteDefinition") return true;
|
|
2430
|
+
return (node.content ?? []).some(hasCanonicalFootnoteNode);
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Upgrades the historical footnote shape that old rich-text pastes stored as
|
|
2434
|
+
* ordinary fragment links plus a trailing ordered list.
|
|
2435
|
+
*
|
|
2436
|
+
* Conversion is deliberately all-or-nothing. The document must end in an
|
|
2437
|
+
* `hr` and ordered list, every definition item must contain a matching
|
|
2438
|
+
* `#fnref-*` backlink, and every `#fn-*` reference must have exactly one
|
|
2439
|
+
* definition label. This keeps normal fragment links and numbered lists
|
|
2440
|
+
* untouched.
|
|
2441
|
+
*
|
|
2442
|
+
* @param doc - Parsed TipTap document
|
|
2443
|
+
* @returns The canonical document and whether a legacy footnote set changed
|
|
2444
|
+
* @example
|
|
2445
|
+
* ```ts
|
|
2446
|
+
* const result = upgradeLegacyFootnotes({
|
|
2447
|
+
* type: "doc",
|
|
2448
|
+
* content: [],
|
|
2449
|
+
* });
|
|
2450
|
+
* // { doc: { type: "doc", content: [] }, upgraded: false }
|
|
2451
|
+
* ```
|
|
2452
|
+
*/ function upgradeLegacyFootnotes(doc) {
|
|
2453
|
+
if (doc.type !== "doc" || !Array.isArray(doc.content) || doc.content.length < 3 || hasCanonicalFootnoteNode(doc)) return {
|
|
2454
|
+
doc,
|
|
2455
|
+
upgraded: false
|
|
2456
|
+
};
|
|
2457
|
+
const definitionList = doc.content.at(-1);
|
|
2458
|
+
const separator = doc.content.at(-2);
|
|
2459
|
+
if (definitionList?.type !== "orderedList" || separator?.type !== "horizontalRule" || !Array.isArray(definitionList.content) || definitionList.content.length === 0 || definitionList.content.some((item) => item.type !== "listItem")) return {
|
|
2460
|
+
doc,
|
|
2461
|
+
upgraded: false
|
|
2462
|
+
};
|
|
2463
|
+
const bodyNodes = doc.content.slice(0, -2);
|
|
2464
|
+
const references = [];
|
|
2465
|
+
for (const node of bodyNodes) collectLegacyReferences(node, references);
|
|
2466
|
+
if (references.length === 0) return {
|
|
2467
|
+
doc,
|
|
2468
|
+
upgraded: false
|
|
2469
|
+
};
|
|
2470
|
+
const labelsByKey = /* @__PURE__ */ new Map();
|
|
2471
|
+
for (const reference of references) {
|
|
2472
|
+
const key = getFootnoteLabelKey(reference.label);
|
|
2473
|
+
if (!labelsByKey.has(key)) labelsByKey.set(key, reference.label);
|
|
2474
|
+
}
|
|
2475
|
+
const labels = [...labelsByKey.values()].sort((left, right) => getFootnoteDomId(right).length - getFootnoteDomId(left).length);
|
|
2476
|
+
if (labels.length !== definitionList.content.length) return {
|
|
2477
|
+
doc,
|
|
2478
|
+
upgraded: false
|
|
2479
|
+
};
|
|
2480
|
+
const definitions = [];
|
|
2481
|
+
const definedLabels = /* @__PURE__ */ new Set();
|
|
2482
|
+
for (const item of definitionList.content) {
|
|
2483
|
+
const backlinkLabels = /* @__PURE__ */ new Set();
|
|
2484
|
+
collectLegacyBacklinkLabels(item, labels, backlinkLabels);
|
|
2485
|
+
if (backlinkLabels.size !== 1) return {
|
|
2486
|
+
doc,
|
|
2487
|
+
upgraded: false
|
|
2488
|
+
};
|
|
2489
|
+
const key = [...backlinkLabels][0];
|
|
2490
|
+
if (!key) return {
|
|
2491
|
+
doc,
|
|
2492
|
+
upgraded: false
|
|
2493
|
+
};
|
|
2494
|
+
const label = labelsByKey.get(key);
|
|
2495
|
+
if (!label || definedLabels.has(key)) return {
|
|
2496
|
+
doc,
|
|
2497
|
+
upgraded: false
|
|
2498
|
+
};
|
|
2499
|
+
definedLabels.add(key);
|
|
2500
|
+
const content = (item.content ?? []).flatMap((child) => {
|
|
2501
|
+
const normalized = removeLegacyBacklinks(child, label);
|
|
2502
|
+
return normalized ? [normalized] : [];
|
|
2503
|
+
});
|
|
2504
|
+
definitions.push({
|
|
2505
|
+
type: "footnoteDefinition",
|
|
2506
|
+
attrs: { label },
|
|
2507
|
+
content: content.length > 0 ? content : [{ type: "paragraph" }]
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
if (definedLabels.size !== labelsByKey.size) return {
|
|
2511
|
+
doc,
|
|
2512
|
+
upgraded: false
|
|
2513
|
+
};
|
|
2514
|
+
const labelsByNode = new Map(references.map((reference) => [reference.node, reference.label]));
|
|
2515
|
+
return {
|
|
2516
|
+
doc: {
|
|
2517
|
+
...doc,
|
|
2518
|
+
content: [...bodyNodes.map((node) => replaceLegacyReferences(node, labelsByNode)), ...definitions]
|
|
2519
|
+
},
|
|
2520
|
+
upgraded: true
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
1662
2523
|
/**
|
|
1663
2524
|
* Parses one Markdown footnote definition from the start of `src`.
|
|
1664
2525
|
*
|
|
@@ -2066,24 +2927,113 @@ function renderTableCell(tagName, node, context) {
|
|
|
2066
2927
|
const rowspan = getNumberAttr(node.attrs, "rowspan");
|
|
2067
2928
|
return `<${tagName}${colspan !== null && colspan !== 1 ? ` colspan="${colspan}"` : ""}${rowspan !== null && rowspan !== 1 ? ` rowspan="${rowspan}"` : ""}>${context.renderChildren(node.content)}</${tagName}>`;
|
|
2068
2929
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
const
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
2930
|
+
var FOOTNOTE_NAMESPACE_HASH_OFFSET = 14695981039346656037n;
|
|
2931
|
+
var FOOTNOTE_NAMESPACE_HASH_PRIME = 1099511628211n;
|
|
2932
|
+
var FOOTNOTE_NAMESPACE_HASH_MASK = 18446744073709551615n;
|
|
2933
|
+
function createFootnoteIdScope(namespace) {
|
|
2934
|
+
const normalized = (namespace ?? "").trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2935
|
+
if (!normalized) return "";
|
|
2936
|
+
let hash = FOOTNOTE_NAMESPACE_HASH_OFFSET;
|
|
2937
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
2938
|
+
hash ^= BigInt(normalized.charCodeAt(index));
|
|
2939
|
+
hash = hash * FOOTNOTE_NAMESPACE_HASH_PRIME & FOOTNOTE_NAMESPACE_HASH_MASK;
|
|
2940
|
+
}
|
|
2941
|
+
return hash.toString(36).padStart(13, "0");
|
|
2076
2942
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2943
|
+
function collectFootnoteReferences(node, visit) {
|
|
2944
|
+
if (node.type === "footnoteReference") {
|
|
2945
|
+
visit(node);
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
for (const child of node.content ?? []) collectFootnoteReferences(child, visit);
|
|
2949
|
+
}
|
|
2950
|
+
function createFootnotePlan(doc, options) {
|
|
2951
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2952
|
+
const bodyNodes = (doc.content ?? []).filter((node) => node.type !== "footnoteDefinition");
|
|
2953
|
+
for (const node of doc.content ?? []) {
|
|
2954
|
+
if (node.type !== "footnoteDefinition") continue;
|
|
2955
|
+
const key = getFootnoteLabelKey(getStringAttr(node.attrs, "label"));
|
|
2956
|
+
if (key && !definitions.has(key)) definitions.set(key, node);
|
|
2957
|
+
}
|
|
2958
|
+
const namespace = createFootnoteIdScope(options.namespace);
|
|
2959
|
+
const idScope = namespace ? `${namespace}-` : "";
|
|
2960
|
+
const groupsByKey = /* @__PURE__ */ new Map();
|
|
2961
|
+
const groups = [];
|
|
2962
|
+
const referencesByNode = /* @__PURE__ */ new WeakMap();
|
|
2963
|
+
for (const owner of bodyNodes) collectFootnoteReferences(owner, (reference) => {
|
|
2964
|
+
const label = normalizeFootnoteLabel(getStringAttr(reference.attrs, "label")) || "footnote";
|
|
2965
|
+
const key = getFootnoteLabelKey(label);
|
|
2966
|
+
let group = groupsByKey.get(key);
|
|
2967
|
+
if (!group) {
|
|
2968
|
+
const number = groupsByKey.size + 1;
|
|
2969
|
+
group = {
|
|
2970
|
+
definitionId: `fn-${idScope}${number}`,
|
|
2971
|
+
number,
|
|
2972
|
+
label,
|
|
2973
|
+
definition: definitions.get(key) ?? null,
|
|
2974
|
+
references: []
|
|
2975
|
+
};
|
|
2976
|
+
groupsByKey.set(key, group);
|
|
2977
|
+
groups.push(group);
|
|
2978
|
+
}
|
|
2979
|
+
const occurrence = group.references.length + 1;
|
|
2980
|
+
const referencePlan = {
|
|
2981
|
+
definitionId: group.definitionId,
|
|
2982
|
+
referenceId: `fnref-${idScope}${group.number}-${occurrence}`,
|
|
2983
|
+
number: group.number,
|
|
2984
|
+
occurrence,
|
|
2985
|
+
label: group.label,
|
|
2986
|
+
hasDefinition: group.definition !== null
|
|
2987
|
+
};
|
|
2988
|
+
group.references.push(referencePlan);
|
|
2989
|
+
referencesByNode.set(reference, referencePlan);
|
|
2990
|
+
});
|
|
2991
|
+
return {
|
|
2992
|
+
groups,
|
|
2993
|
+
referencesByNode
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
function renderFootnoteReference(node, context) {
|
|
2997
|
+
const plan = context.footnotes.referencesByNode.get(node);
|
|
2998
|
+
const fallbackLabel = normalizeFootnoteLabel(getStringAttr(node.attrs, "label")) || "footnote";
|
|
2999
|
+
const label = plan?.label ?? fallbackLabel;
|
|
3000
|
+
const number = plan?.number ?? label;
|
|
3001
|
+
if (!plan?.hasDefinition) return `<sup class="footnote-ref">${escapeHtml(String(number))}</sup>`;
|
|
3002
|
+
return `<sup class="footnote-ref"><a id="${escapeHtml(plan.referenceId)}" href="#${escapeHtml(plan.definitionId)}" role="doc-noteref">${plan.number}</a></sup>`;
|
|
3003
|
+
}
|
|
3004
|
+
function renderFootnoteGroup(group, context, includeValue) {
|
|
3005
|
+
if (!group.definition) return "";
|
|
3006
|
+
const definitionContent = group.definition.content ?? [];
|
|
3007
|
+
const backlinks = group.references.map((reference) => {
|
|
3008
|
+
const occurrence = group.references.length > 1 ? String(reference.occurrence) : "";
|
|
3009
|
+
return `<a href="#${escapeHtml(reference.referenceId)}" class="footnote-backref" role="doc-backlink">↩︎${occurrence}</a>`;
|
|
3010
|
+
}).join(" ");
|
|
3011
|
+
const backlinksHtml = `<span class="footnote-backlinks">${backlinks}</span>`;
|
|
3012
|
+
const lastBlock = definitionContent.at(-1);
|
|
3013
|
+
let bodyHtml;
|
|
3014
|
+
if (lastBlock?.type === "paragraph") {
|
|
3015
|
+
const precedingBlocks = definitionContent.slice(0, -1);
|
|
3016
|
+
const lastParagraphHtml = context.renderChildren(lastBlock.content);
|
|
3017
|
+
const separator = lastParagraphHtml ? " " : "";
|
|
3018
|
+
bodyHtml = context.renderChildren(precedingBlocks) + `<p>${lastParagraphHtml}${separator}${backlinksHtml}</p>`;
|
|
3019
|
+
} else bodyHtml = context.renderChildren(definitionContent) + `<p class="footnote-backlinks">${backlinks}</p>`;
|
|
3020
|
+
const valueAttr = includeValue ? ` value="${group.number}"` : "";
|
|
3021
|
+
return `<li id="${escapeHtml(group.definitionId)}" class="footnote"${valueAttr}>` + bodyHtml + `</li>`;
|
|
3022
|
+
}
|
|
3023
|
+
function renderFootnoteEndnotes(context) {
|
|
3024
|
+
const groups = context.footnotes.groups.filter((group) => group.definition !== null);
|
|
3025
|
+
if (groups.length === 0) return "";
|
|
3026
|
+
const firstNumber = groups[0]?.number ?? 1;
|
|
3027
|
+
let expectedNumber = firstNumber;
|
|
3028
|
+
const items = groups.map((group) => {
|
|
3029
|
+
const includeValue = group.number !== expectedNumber;
|
|
3030
|
+
expectedNumber = group.number + 1;
|
|
3031
|
+
return renderFootnoteGroup(group, context, includeValue);
|
|
3032
|
+
}).join("");
|
|
3033
|
+
return `<section class="footnote-endnotes" role="doc-endnotes"><ol class="footnote-list"${firstNumber === 1 ? "" : ` start="${firstNumber}"`}>${items}</ol></section>`;
|
|
3034
|
+
}
|
|
3035
|
+
function renderDocumentContent(node, context) {
|
|
3036
|
+
return (node.content ?? []).filter((child) => child.type !== "footnoteDefinition").map((bodyNode) => context.renderNode(bodyNode)).join("") + renderFootnoteEndnotes(context);
|
|
2087
3037
|
}
|
|
2088
3038
|
var MARK_RENDERERS = {
|
|
2089
3039
|
bold: (html) => `<strong>${html}</strong>`,
|
|
@@ -2097,21 +3047,7 @@ var MARK_RENDERERS = {
|
|
|
2097
3047
|
}
|
|
2098
3048
|
};
|
|
2099
3049
|
var NODE_RENDERERS = {
|
|
2100
|
-
doc: (node, context) =>
|
|
2101
|
-
const content = node.content ?? [];
|
|
2102
|
-
const definitionMap = /* @__PURE__ */ new Map();
|
|
2103
|
-
for (const child of content) if (child.type === "footnoteDefinition") {
|
|
2104
|
-
const label = normalizeFootnoteLabel(getStringAttr(child.attrs, "label"));
|
|
2105
|
-
if (label) definitionMap.set(label, child);
|
|
2106
|
-
}
|
|
2107
|
-
activeDefinitionMap = definitionMap.size > 0 ? definitionMap : null;
|
|
2108
|
-
try {
|
|
2109
|
-
const bodyNodes = content.filter((child) => child.type !== "footnoteDefinition");
|
|
2110
|
-
return context.renderChildren(bodyNodes);
|
|
2111
|
-
} finally {
|
|
2112
|
-
activeDefinitionMap = null;
|
|
2113
|
-
}
|
|
2114
|
-
},
|
|
3050
|
+
doc: (node, context) => renderDocumentContent(node, context),
|
|
2115
3051
|
paragraph: (node, context) => `<p>${context.renderChildren(node.content)}</p>`,
|
|
2116
3052
|
heading: (node, context) => {
|
|
2117
3053
|
const level = Math.min(Math.max(getNumberAttr(node.attrs, "level") ?? 1, 1), 6);
|
|
@@ -2143,7 +3079,7 @@ var NODE_RENDERERS = {
|
|
|
2143
3079
|
return `<div class="tiptap-html-block">${html}</div>`;
|
|
2144
3080
|
},
|
|
2145
3081
|
moreBreak: () => "<!--more-->",
|
|
2146
|
-
footnoteReference: (node, context) =>
|
|
3082
|
+
footnoteReference: (node, context) => renderFootnoteReference(node, context),
|
|
2147
3083
|
footnoteDefinition: () => ""
|
|
2148
3084
|
};
|
|
2149
3085
|
function renderText(text, marks = []) {
|
|
@@ -2154,33 +3090,105 @@ function renderText(text, marks = []) {
|
|
|
2154
3090
|
}
|
|
2155
3091
|
return html;
|
|
2156
3092
|
}
|
|
2157
|
-
function renderChildren(content = []) {
|
|
2158
|
-
return content.map(renderNode).join("");
|
|
2159
|
-
}
|
|
2160
3093
|
function renderUnknownNode(node, context) {
|
|
2161
3094
|
return node.content ? context.renderChildren(node.content) : "";
|
|
2162
3095
|
}
|
|
2163
|
-
function
|
|
2164
|
-
|
|
3096
|
+
function createRenderContext(footnotes) {
|
|
3097
|
+
const context = {
|
|
3098
|
+
footnotes,
|
|
3099
|
+
renderChildren(content = []) {
|
|
3100
|
+
return content.map((node) => context.renderNode(node)).join("");
|
|
3101
|
+
},
|
|
3102
|
+
renderNode(node) {
|
|
3103
|
+
return (NODE_RENDERERS[node.type] ?? renderUnknownNode)(node, context);
|
|
3104
|
+
},
|
|
3105
|
+
renderText
|
|
3106
|
+
};
|
|
3107
|
+
return context;
|
|
2165
3108
|
}
|
|
2166
|
-
var RENDER_CONTEXT = {
|
|
2167
|
-
renderChildren,
|
|
2168
|
-
renderNode,
|
|
2169
|
-
renderText
|
|
2170
|
-
};
|
|
2171
3109
|
/**
|
|
2172
3110
|
* Renders a parsed TipTap document to HTML.
|
|
2173
3111
|
*
|
|
2174
3112
|
* @param doc - Parsed TipTap document
|
|
3113
|
+
* @param options - Rendering options, including a stable footnote ID namespace
|
|
2175
3114
|
* @returns HTML string
|
|
2176
|
-
*/ function renderTiptapDocument(doc) {
|
|
3115
|
+
*/ function renderTiptapDocument(doc, options = {}) {
|
|
2177
3116
|
if (doc.type !== "doc") return "";
|
|
2178
|
-
|
|
3117
|
+
const normalized = normalizeFootnoteArtifacts(doc);
|
|
3118
|
+
return createRenderContext(createFootnotePlan(normalized, options)).renderNode(normalized);
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Render a document as two byte-compatible segments around a top-level source
|
|
3122
|
+
* boundary.
|
|
3123
|
+
*
|
|
3124
|
+
* The full document is planned before either segment is rendered, so a
|
|
3125
|
+
* footnote before the boundary still includes backlinks to repeated references
|
|
3126
|
+
* after it. Joining the two strings is therefore identical to a normal render.
|
|
3127
|
+
*
|
|
3128
|
+
* @param doc - Parsed TipTap document
|
|
3129
|
+
* @param boundaryIndex - Index in the original `doc.content` array
|
|
3130
|
+
* @param options - Rendering options, including a stable footnote ID namespace
|
|
3131
|
+
* @returns The rendered segments, or null for an invalid root or boundary
|
|
3132
|
+
* @example
|
|
3133
|
+
* ```ts
|
|
3134
|
+
* renderTiptapDocumentAroundBoundary(
|
|
3135
|
+
* { type: "doc", content: [{ type: "paragraph" }] },
|
|
3136
|
+
* 1,
|
|
3137
|
+
* );
|
|
3138
|
+
* // { beforeHtml: "<p></p>", afterHtml: "" }
|
|
3139
|
+
* ```
|
|
3140
|
+
*/ function renderTiptapDocumentAroundBoundary(doc, boundaryIndex, options = {}) {
|
|
3141
|
+
const originalContent = doc.content ?? [];
|
|
3142
|
+
if (doc.type !== "doc" || !Array.isArray(doc.content) || !Number.isInteger(boundaryIndex) || boundaryIndex < 0 || boundaryIndex > originalContent.length) return null;
|
|
3143
|
+
const normalized = normalizeFootnoteArtifacts(doc);
|
|
3144
|
+
const context = createRenderContext(createFootnotePlan(normalized, options));
|
|
3145
|
+
const normalizedBoundaryIndex = Math.min(boundaryIndex, normalized.content?.length ?? 0);
|
|
3146
|
+
let beforeBodyHtml = "";
|
|
3147
|
+
let afterBodyHtml = "";
|
|
3148
|
+
for (const [index, node] of (normalized.content ?? []).entries()) {
|
|
3149
|
+
if (node.type === "footnoteDefinition") continue;
|
|
3150
|
+
const html = context.renderNode(node);
|
|
3151
|
+
if (index < normalizedBoundaryIndex) beforeBodyHtml += html;
|
|
3152
|
+
else afterBodyHtml += html;
|
|
3153
|
+
}
|
|
3154
|
+
return {
|
|
3155
|
+
beforeHtml: beforeBodyHtml,
|
|
3156
|
+
afterHtml: afterBodyHtml + renderFootnoteEndnotes(context)
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Strictly parses and renders a TipTap JSON document.
|
|
3161
|
+
*
|
|
3162
|
+
* Unlike `renderTiptapJson`, this distinguishes invalid canonical JSON from a
|
|
3163
|
+
* valid empty document. Projection rebuilds use it so malformed historical
|
|
3164
|
+
* content is reported and never marked as current.
|
|
3165
|
+
*
|
|
3166
|
+
* @param json - TipTap JSON string
|
|
3167
|
+
* @param options - Rendering options
|
|
3168
|
+
* @returns A discriminated render result
|
|
3169
|
+
*/ function tryRenderTiptapJson(json, options = {}) {
|
|
3170
|
+
try {
|
|
3171
|
+
const doc = JSON.parse(json);
|
|
3172
|
+
if (doc.type !== "doc") return {
|
|
3173
|
+
ok: false,
|
|
3174
|
+
error: "TipTap body root must be a doc node."
|
|
3175
|
+
};
|
|
3176
|
+
return {
|
|
3177
|
+
ok: true,
|
|
3178
|
+
html: renderTiptapDocument(doc, options)
|
|
3179
|
+
};
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
return {
|
|
3182
|
+
ok: false,
|
|
3183
|
+
error: error instanceof Error ? error.message : "Invalid TipTap JSON."
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
2179
3186
|
}
|
|
2180
3187
|
/**
|
|
2181
3188
|
* Renders a Tiptap JSON document to an HTML string.
|
|
2182
3189
|
*
|
|
2183
3190
|
* @param json - Tiptap JSON string or parsed document object
|
|
3191
|
+
* @param options - Rendering options, including a stable footnote ID namespace
|
|
2184
3192
|
* @returns HTML string
|
|
2185
3193
|
*
|
|
2186
3194
|
* @example
|
|
@@ -2188,12 +3196,9 @@ var RENDER_CONTEXT = {
|
|
|
2188
3196
|
* const html = renderTiptapJson('{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}]}');
|
|
2189
3197
|
* // "<p>Hello</p>"
|
|
2190
3198
|
* ```
|
|
2191
|
-
*/ function renderTiptapJson(json) {
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
} catch {
|
|
2195
|
-
return "";
|
|
2196
|
-
}
|
|
3199
|
+
*/ function renderTiptapJson(json, options = {}) {
|
|
3200
|
+
const result = tryRenderTiptapJson(json, options);
|
|
3201
|
+
return result.ok ? result.html : "";
|
|
2197
3202
|
}
|
|
2198
3203
|
/**
|
|
2199
3204
|
* Returns true if a TipTap node is an empty block — a paragraph (or heading)
|
|
@@ -2408,6 +3413,7 @@ function extractSummary(bodyJson, maxBlocks, maxChars) {
|
|
|
2408
3413
|
* included and `hasMore` is `false`. Avoids a "read more" that reveals only a
|
|
2409
3414
|
* sliver of text. Explicit `moreBreak` markers reflect author intent and are
|
|
2410
3415
|
* never subject to this tolerance.
|
|
3416
|
+
* @param renderOptions - Renderer options such as the post footnote namespace
|
|
2411
3417
|
* @returns HTML summary, whether content was truncated, and the index in
|
|
2412
3418
|
* `doc.content` where the content after the summary boundary begins, or null.
|
|
2413
3419
|
* `breakAtIndex` lets callers align the summary with the full-body rendering
|
|
@@ -2418,7 +3424,7 @@ function extractSummary(bodyJson, maxBlocks, maxChars) {
|
|
|
2418
3424
|
* const result = extractSummaryHtml(body, 5, 500);
|
|
2419
3425
|
* // { html: "<ul><li><p>Item</p></li></ul>", hasMore: true, breakAtIndex: 1 }
|
|
2420
3426
|
* ```
|
|
2421
|
-
*/ function extractSummaryHtml(bodyJson, maxBlocks = 5, maxChars = 500, minHiddenChars = 0) {
|
|
3427
|
+
*/ function extractSummaryHtml(bodyJson, maxBlocks = 5, maxChars = 500, minHiddenChars = 0, renderOptions = {}) {
|
|
2422
3428
|
let doc;
|
|
2423
3429
|
try {
|
|
2424
3430
|
doc = JSON.parse(bodyJson);
|
|
@@ -2435,8 +3441,8 @@ function extractSummary(bodyJson, maxBlocks, maxChars) {
|
|
|
2435
3441
|
return {
|
|
2436
3442
|
html: renderTiptapDocument({
|
|
2437
3443
|
type: "doc",
|
|
2438
|
-
content: selected
|
|
2439
|
-
}),
|
|
3444
|
+
content: [...selected, ...nodes.filter((node) => node.type === "footnoteDefinition")]
|
|
3445
|
+
}, renderOptions),
|
|
2440
3446
|
hasMore: true,
|
|
2441
3447
|
breakAtIndex: moreBreakIdx
|
|
2442
3448
|
};
|
|
@@ -2476,8 +3482,8 @@ function extractSummary(bodyJson, maxBlocks, maxChars) {
|
|
|
2476
3482
|
return {
|
|
2477
3483
|
html: renderTiptapDocument({
|
|
2478
3484
|
type: "doc",
|
|
2479
|
-
content: selected
|
|
2480
|
-
}),
|
|
3485
|
+
content: [...selected, ...nodes.filter((node) => node.type === "footnoteDefinition")]
|
|
3486
|
+
}, renderOptions),
|
|
2481
3487
|
hasMore,
|
|
2482
3488
|
breakAtIndex: lastSelectedIdx + 1
|
|
2483
3489
|
};
|
|
@@ -2625,6 +3631,7 @@ var MarkdownCodeBlock = CodeBlock.extend({ renderMarkdown(node, helpers) {
|
|
|
2625
3631
|
const fence = chooseCodeFence(content);
|
|
2626
3632
|
return `${fence}${language}\n${content}\n${fence}`;
|
|
2627
3633
|
} });
|
|
3634
|
+
var SemanticLink = Link.extend({ clearable: false });
|
|
2628
3635
|
var MarkdownFigureImageSupport = Extension.create({
|
|
2629
3636
|
name: "markdownFigureImageSupport",
|
|
2630
3637
|
markdownTokenName: "imageFigure",
|
|
@@ -2886,6 +3893,68 @@ function createFootnoteReferenceMarkdownToken() {
|
|
|
2886
3893
|
}
|
|
2887
3894
|
};
|
|
2888
3895
|
}
|
|
3896
|
+
var INLINE_FOOTNOTE_CONTENT_ATTR = "__jantInlineFootnoteContent";
|
|
3897
|
+
function parseInlineFootnoteSource(src) {
|
|
3898
|
+
if (!src.startsWith("^[")) return null;
|
|
3899
|
+
let bracketDepth = 1;
|
|
3900
|
+
let codeDelimiterLength = 0;
|
|
3901
|
+
for (let index = 2; index < src.length; index += 1) {
|
|
3902
|
+
const character = src[index];
|
|
3903
|
+
if (character === "\n" || character === "\r") return null;
|
|
3904
|
+
if (character === "\\" && codeDelimiterLength === 0) {
|
|
3905
|
+
index += 1;
|
|
3906
|
+
continue;
|
|
3907
|
+
}
|
|
3908
|
+
if (character === "`") {
|
|
3909
|
+
let delimiterLength = 1;
|
|
3910
|
+
while (src[index + delimiterLength] === "`") delimiterLength += 1;
|
|
3911
|
+
if (codeDelimiterLength === 0) codeDelimiterLength = delimiterLength;
|
|
3912
|
+
else if (codeDelimiterLength === delimiterLength) codeDelimiterLength = 0;
|
|
3913
|
+
index += delimiterLength - 1;
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
if (codeDelimiterLength > 0) continue;
|
|
3917
|
+
if (character === "[") {
|
|
3918
|
+
bracketDepth += 1;
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
if (character !== "]") continue;
|
|
3922
|
+
bracketDepth -= 1;
|
|
3923
|
+
if (bracketDepth !== 0) continue;
|
|
3924
|
+
const content = src.slice(2, index);
|
|
3925
|
+
if (!content.trim()) return null;
|
|
3926
|
+
return {
|
|
3927
|
+
content,
|
|
3928
|
+
raw: src.slice(0, index + 1)
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
return null;
|
|
3932
|
+
}
|
|
3933
|
+
var MarkdownInlineFootnote = Extension.create({
|
|
3934
|
+
name: "inlineFootnote",
|
|
3935
|
+
parseMarkdown: (token, helpers) => helpers.createNode("footnoteReference", { [INLINE_FOOTNOTE_CONTENT_ATTR]: Array.isArray(token.tokens) ? helpers.parseInline(token.tokens) : [] }),
|
|
3936
|
+
markdownTokenizer: {
|
|
3937
|
+
name: "inlineFootnote",
|
|
3938
|
+
level: "inline",
|
|
3939
|
+
start(src) {
|
|
3940
|
+
return src.indexOf("^[");
|
|
3941
|
+
},
|
|
3942
|
+
tokenize(src, _tokens, helpers) {
|
|
3943
|
+
const inlineFootnote = parseInlineFootnoteSource(src);
|
|
3944
|
+
if (!inlineFootnote) return void 0;
|
|
3945
|
+
return {
|
|
3946
|
+
type: "inlineFootnote",
|
|
3947
|
+
raw: inlineFootnote.raw,
|
|
3948
|
+
tokens: helpers.inlineTokens(inlineFootnote.content)
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
function normalizeFootnoteDataLabel(label) {
|
|
3954
|
+
const normalized = normalizeFootnoteLabel(label);
|
|
3955
|
+
const legacyDisplayLabel = normalized.match(/^\[\^([^\]]+)\]:?$/)?.[1];
|
|
3956
|
+
return normalizeFootnoteLabel(legacyDisplayLabel ?? normalized);
|
|
3957
|
+
}
|
|
2889
3958
|
var MarkdownFootnoteReference = Node.create({
|
|
2890
3959
|
name: "footnoteReference",
|
|
2891
3960
|
group: "inline",
|
|
@@ -2900,7 +3969,7 @@ var MarkdownFootnoteReference = Node.create({
|
|
|
2900
3969
|
return [{
|
|
2901
3970
|
tag: "sup[data-footnote-reference]",
|
|
2902
3971
|
getAttrs(dom) {
|
|
2903
|
-
return { label:
|
|
3972
|
+
return { label: normalizeFootnoteDataLabel(dom.getAttribute("data-footnote-label")) };
|
|
2904
3973
|
}
|
|
2905
3974
|
}];
|
|
2906
3975
|
},
|
|
@@ -2935,7 +4004,7 @@ var MarkdownFootnoteDefinition = Node.create({
|
|
|
2935
4004
|
return [{
|
|
2936
4005
|
tag: "div[data-footnote-definition]",
|
|
2937
4006
|
getAttrs(dom) {
|
|
2938
|
-
return { label:
|
|
4007
|
+
return { label: normalizeFootnoteDataLabel(dom.getAttribute("data-footnote-label")) };
|
|
2939
4008
|
}
|
|
2940
4009
|
}];
|
|
2941
4010
|
},
|
|
@@ -2945,7 +4014,7 @@ var MarkdownFootnoteDefinition = Node.create({
|
|
|
2945
4014
|
"div",
|
|
2946
4015
|
{
|
|
2947
4016
|
"data-footnote-definition": "",
|
|
2948
|
-
"data-footnote-label":
|
|
4017
|
+
"data-footnote-label": label,
|
|
2949
4018
|
class: "tiptap-footnote-definition"
|
|
2950
4019
|
},
|
|
2951
4020
|
0
|
|
@@ -2990,11 +4059,13 @@ function createMarkdownContentExtensions(options = {}) {
|
|
|
2990
4059
|
2,
|
|
2991
4060
|
3
|
|
2992
4061
|
] },
|
|
2993
|
-
link:
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
4062
|
+
link: false,
|
|
4063
|
+
codeBlock: false,
|
|
4064
|
+
trailingNode: { notAfter: ["footnoteDefinition"] }
|
|
4065
|
+
}),
|
|
4066
|
+
SemanticLink.configure({
|
|
4067
|
+
openOnClick: false,
|
|
4068
|
+
autolink: false
|
|
2998
4069
|
}),
|
|
2999
4070
|
MarkdownCodeBlock,
|
|
3000
4071
|
Table.configure({
|
|
@@ -3009,13 +4080,79 @@ function createMarkdownContentExtensions(options = {}) {
|
|
|
3009
4080
|
options.moreBreakExtension ?? MarkdownMoreBreak,
|
|
3010
4081
|
options.embedExtension ?? MarkdownEmbedNode,
|
|
3011
4082
|
options.htmlBlockExtension ?? MarkdownHtmlBlockNode,
|
|
4083
|
+
MarkdownInlineFootnote,
|
|
3012
4084
|
MarkdownFootnoteReference,
|
|
3013
4085
|
MarkdownFootnoteDefinition
|
|
3014
4086
|
];
|
|
3015
4087
|
}
|
|
3016
|
-
function
|
|
4088
|
+
function isJsonContent(value) {
|
|
4089
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
4090
|
+
}
|
|
4091
|
+
function collectUsedFootnoteLabels(node, usedLabels) {
|
|
4092
|
+
if (node.type === "footnoteReference" || node.type === "footnoteDefinition") {
|
|
4093
|
+
const label = normalizeFootnoteLabel(node.attrs?.label);
|
|
4094
|
+
if (label) usedLabels.add(getFootnoteLabelKey(label));
|
|
4095
|
+
}
|
|
4096
|
+
for (const child of node.content ?? []) collectUsedFootnoteLabels(child, usedLabels);
|
|
4097
|
+
}
|
|
4098
|
+
function expandInlineFootnotes(doc) {
|
|
4099
|
+
if (doc.type !== "doc") return doc;
|
|
4100
|
+
const usedLabels = /* @__PURE__ */ new Set();
|
|
4101
|
+
collectUsedFootnoteLabels(doc, usedLabels);
|
|
4102
|
+
const definitions = [];
|
|
4103
|
+
let nextNumericLabel = 1;
|
|
4104
|
+
const allocateLabel = () => {
|
|
4105
|
+
while (usedLabels.has(getFootnoteLabelKey(String(nextNumericLabel)))) nextNumericLabel += 1;
|
|
4106
|
+
const label = String(nextNumericLabel);
|
|
4107
|
+
usedLabels.add(getFootnoteLabelKey(label));
|
|
4108
|
+
nextNumericLabel += 1;
|
|
4109
|
+
return label;
|
|
4110
|
+
};
|
|
4111
|
+
const expandNode = (node) => {
|
|
4112
|
+
const inlineContent = node.attrs?.[INLINE_FOOTNOTE_CONTENT_ATTR];
|
|
4113
|
+
if (node.type === "footnoteReference" && Array.isArray(inlineContent)) {
|
|
4114
|
+
const label = allocateLabel();
|
|
4115
|
+
const attrs = {
|
|
4116
|
+
...node.attrs ?? {},
|
|
4117
|
+
label
|
|
4118
|
+
};
|
|
4119
|
+
delete attrs[INLINE_FOOTNOTE_CONTENT_ATTR];
|
|
4120
|
+
const content = inlineContent.filter(isJsonContent).map((child) => expandNode(normalizeMarkdownDocument(child)));
|
|
4121
|
+
definitions.push({
|
|
4122
|
+
type: "footnoteDefinition",
|
|
4123
|
+
attrs: { label },
|
|
4124
|
+
content: [content.length > 0 ? {
|
|
4125
|
+
type: "paragraph",
|
|
4126
|
+
content
|
|
4127
|
+
} : { type: "paragraph" }]
|
|
4128
|
+
});
|
|
4129
|
+
return {
|
|
4130
|
+
...node,
|
|
4131
|
+
attrs
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
return node.content ? {
|
|
4135
|
+
...node,
|
|
4136
|
+
content: node.content.map(expandNode)
|
|
4137
|
+
} : node;
|
|
4138
|
+
};
|
|
4139
|
+
const expanded = expandNode(doc);
|
|
4140
|
+
if (definitions.length === 0) return expanded;
|
|
4141
|
+
return {
|
|
4142
|
+
...expanded,
|
|
4143
|
+
content: [...expanded.content ?? [], ...definitions]
|
|
4144
|
+
};
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* Normalizes Markdown parser output before it enters the editor schema.
|
|
4148
|
+
*
|
|
4149
|
+
* @param node - Parsed Tiptap document or descendant node
|
|
4150
|
+
* @returns A normalized copy safe to load into an editor
|
|
4151
|
+
* @example
|
|
4152
|
+
* const normalized = normalizeMarkdownDocument(markdownManager.parse(source));
|
|
4153
|
+
*/ function normalizeMarkdownDocument(node) {
|
|
3017
4154
|
const normalized = { ...node };
|
|
3018
|
-
if (normalized.content) normalized.content = normalized.content.map(
|
|
4155
|
+
if (normalized.content) normalized.content = normalized.content.map(normalizeMarkdownDocument);
|
|
3019
4156
|
if (normalized.marks) normalized.marks = normalized.marks.map((mark) => {
|
|
3020
4157
|
if (!mark || typeof mark !== "object") return mark;
|
|
3021
4158
|
const nextMark = {
|
|
@@ -3040,8 +4177,8 @@ function normalizeMarkdownDoc(node) {
|
|
|
3040
4177
|
for (let index = 0; index < normalized.content.length; index += 1) {
|
|
3041
4178
|
const child = normalized.content[index];
|
|
3042
4179
|
const nextChild = normalized.content[index + 1];
|
|
3043
|
-
if (child?.type === "text" && typeof child.text === "string" && nextChild?.type === "footnoteReference" && child.text
|
|
3044
|
-
const trimmedText = child.text.replace(/\n
|
|
4180
|
+
if (child?.type === "text" && typeof child.text === "string" && nextChild?.type === "footnoteReference" && /\n[ \t]*$/.test(child.text)) {
|
|
4181
|
+
const trimmedText = child.text.replace(/\n[ \t]*$/, "");
|
|
3045
4182
|
if (trimmedText) nextContent.push({
|
|
3046
4183
|
...child,
|
|
3047
4184
|
text: trimmedText
|
|
@@ -3052,7 +4189,7 @@ function normalizeMarkdownDoc(node) {
|
|
|
3052
4189
|
}
|
|
3053
4190
|
normalized.content = nextContent;
|
|
3054
4191
|
}
|
|
3055
|
-
return normalized;
|
|
4192
|
+
return normalized.type === "doc" ? expandInlineFootnotes(normalized) : normalized;
|
|
3056
4193
|
}
|
|
3057
4194
|
function expandCodeBlockFences(markdown) {
|
|
3058
4195
|
return markdown;
|
|
@@ -3069,7 +4206,7 @@ function getMarkdownManager() {
|
|
|
3069
4206
|
return sharedMarkdownManager;
|
|
3070
4207
|
}
|
|
3071
4208
|
function parseMarkdownDocument(markdown) {
|
|
3072
|
-
return
|
|
4209
|
+
return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
|
|
3073
4210
|
}
|
|
3074
4211
|
function serializeMarkdownDocument(doc) {
|
|
3075
4212
|
return expandCodeBlockFences(getMarkdownManager().serialize(normalizeFootnoteArtifacts(doc)));
|
|
@@ -3091,6 +4228,7 @@ function serializeMarkdownDocument(doc) {
|
|
|
3091
4228
|
* Renders Markdown content to HTML using Jant's shared Markdown pipeline.
|
|
3092
4229
|
*
|
|
3093
4230
|
* @param markdown - The Markdown string to convert to HTML
|
|
4231
|
+
* @param options - Rendering options, including a stable footnote namespace
|
|
3094
4232
|
* @returns The rendered HTML string
|
|
3095
4233
|
*
|
|
3096
4234
|
* @example
|
|
@@ -3098,9 +4236,9 @@ function serializeMarkdownDocument(doc) {
|
|
|
3098
4236
|
* const html = render("# Hello\n\nThis is **bold** text.");
|
|
3099
4237
|
* // "<h1>Hello</h1><p>This is <strong>bold</strong> text.</p>"
|
|
3100
4238
|
* ```
|
|
3101
|
-
*/ function render(markdown) {
|
|
4239
|
+
*/ function render(markdown, options = {}) {
|
|
3102
4240
|
if (!markdown.trim()) return "";
|
|
3103
|
-
return renderTiptapDocument(parseMarkdownDocument(markdown));
|
|
4241
|
+
return renderTiptapDocument(parseMarkdownDocument(markdown), options);
|
|
3104
4242
|
}
|
|
3105
4243
|
/**
|
|
3106
4244
|
* Converts Markdown to plain text by stripping all formatting syntax.
|
|
@@ -3190,8 +4328,9 @@ function serializeMarkdownDocument(doc) {
|
|
|
3190
4328
|
* GitHub sync.
|
|
3191
4329
|
*/
|
|
3192
4330
|
/**
|
|
3193
|
-
* Per-collection membership metadata.
|
|
3194
|
-
*
|
|
4331
|
+
* Per-Thread collection membership metadata. Current exports write this
|
|
4332
|
+
* only on the Thread root bundle. Import also accepts legacy reply-level
|
|
4333
|
+
* entries and folds them into the root without losing metadata.
|
|
3195
4334
|
*/
|
|
3196
4335
|
/**
|
|
3197
4336
|
* Parse front matter from a Markdown file.
|
|
@@ -3255,6 +4394,8 @@ function serializeMarkdownDocument(doc) {
|
|
|
3255
4394
|
"quote_text",
|
|
3256
4395
|
"rating",
|
|
3257
4396
|
"featured_at",
|
|
4397
|
+
"featured_post_ids",
|
|
4398
|
+
"featured_sort_at",
|
|
3258
4399
|
"pinned_at",
|
|
3259
4400
|
"root_aliases",
|
|
3260
4401
|
"collections",
|
|
@@ -3295,19 +4436,19 @@ function serializeMarkdownDocument(doc) {
|
|
|
3295
4436
|
}
|
|
3296
4437
|
//#endregion
|
|
3297
4438
|
//#region src/styles/tokens.css?raw
|
|
3298
|
-
var tokens_default = "/**\n * Design Tokens\n *\n * CSS custom properties for all visual aspects of the UI.\n * These are the stable customization API — override in custom CSS\n * to change typography, layout, surfaces, and element sizing.\n */\n\n:root {\n /* Typography — Font families */\n --font-cjk-serif-fallback:\n \"Songti SC\", STSong, SimSun, \"Songti TC\", PMingLiU, MingLiU,\n \"Noto Serif SC\", \"Noto Serif CJK SC\", \"Noto Serif TC\", \"Noto Serif CJK TC\";\n --font-body:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, \"PingFang TC\", \"PingFang SC\",\n \"Hiragino Sans CNS\", \"Hiragino Sans GB\", \"Microsoft JhengHei\",\n \"Microsoft YaHei\", \"Noto Sans CJK TC\", \"Noto Sans CJK SC\", sans-serif;\n --font-heading:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-site-title:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-serif:\n var(--font-cjk-serif-fallback), ui-serif, \"New York Small\", \"New York\",\n \"Iowan Old Style\", Charter, Georgia, \"Times New Roman\", Times, serif;\n --font-ui:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, \"PingFang TC\", \"PingFang SC\",\n \"Hiragino Sans CNS\", \"Hiragino Sans GB\", \"Microsoft JhengHei\",\n \"Microsoft YaHei\", \"Noto Sans CJK TC\", \"Noto Sans CJK SC\", sans-serif;\n --font-mono:\n ui-monospace, Menlo, Monaco, Consolas, \"Cascadia Code\", \"Courier New\",\n monospace;\n /*\n * Blockquote font family.\n *\n * Defaults to `inherit` so blockquotes follow the body font in sans-only\n * themes (Clean, Friendly, Bold) and serif-only themes (Tufte, Bookish).\n * Mixed themes that want a distinct blockquote voice — e.g. Classic, which\n * pairs a sans body with serif headings — override this token in their\n * `cssVariables` to point at a serif stack. This also restores a type-level\n * distinction for CJK, where italic is disabled.\n */\n --font-blockquote: inherit;\n\n /* Typography — Font weights */\n --fw-light: 300;\n --fw-regular: 400;\n --fw-medium: 500;\n --fw-semibold: 600;\n --fw-bold: 700;\n --fw-extrabold: 800;\n /*\n * Unified type scale.\n *\n * Every font-size in the system — reading content AND UI controls —\n * must reference one of these tokens. No raw rem/px values elsewhere.\n *\n * --type-display 2.7rem (40.5px) — page titles, h1\n * --type-title 2.1rem (31.5px) — h2, feed card titles\n * --type-subtitle 1.8rem (27px) — h3\n * --type-body 1.4rem (21px) — running text, form inputs\n * --type-secondary 1.1rem (16.5px) — meta, nav, sidenotes, UI labels\n * --type-base 1.0rem (15px) — UI buttons, controls\n * --type-sm 0.9rem (13.5px) — small UI text\n * --type-xs 0.8rem (12px) — captions, descriptions, chips\n * --type-2xs 0.65rem (9.75px) — tiny labels, file sizes, badges\n */\n --type-display: 2.7rem;\n --type-title: 2.1rem;\n --type-subtitle: 1.8rem;\n --type-body: 1.4rem;\n --type-secondary: 1.1rem;\n --type-base: 1rem;\n --type-sm: 0.9rem;\n --type-xs: 0.8rem;\n --type-2xs: 0.65rem;\n\n /* Content reading tokens — desktop maps to core scale,\n mobile overrides below cap sizes for small screens.\n --type-content-scale uniformly scales all content sizes\n without affecting UI controls. */\n --type-content-scale: 0.8;\n --type-content-display: calc(var(--type-display) * var(--type-content-scale));\n --type-content-title: calc(var(--type-title) * var(--type-content-scale));\n --type-content-subtitle: calc(\n var(--type-subtitle) * var(--type-content-scale)\n );\n --type-content-body: calc(var(--type-body) * var(--type-content-scale));\n --type-content-quote: calc(var(--type-content-body) * 1.16);\n --type-content-quote-leading: 1.4;\n\n /* Semantic aliases */\n --type-body-size: var(--type-content-body);\n --type-code: calc(var(--type-body-size) * 0.94);\n --type-code-block: calc(var(--type-body-size) * 0.9);\n --type-body-tracking: 0;\n --type-ui-title: var(--type-secondary);\n --type-ui-control: var(--type-base);\n --type-ui-meta: var(--type-base);\n --type-ui-hint: var(--type-sm);\n --type-ui-caption: var(--type-xs);\n --type-ui-micro: var(--type-2xs);\n --type-ui-input: var(--type-secondary);\n --type-thread-context: var(--type-base);\n --type-thread-context-title: var(--type-secondary);\n --type-thread-context-meta: var(--type-sm);\n --feed-note-title-size: calc(var(--type-content-body) * 1.36);\n --feed-note-title-leading: var(--type-heading-leading);\n --type-body-leading: 1.7;\n --type-display-leading: 1.15;\n --type-heading-leading: 1.15;\n --type-heading-weight: var(--fw-medium);\n --type-heading-tracking: 0;\n --type-display-weight: var(--fw-medium);\n --type-display-tracking: 0;\n --type-label-weight: var(--fw-medium);\n --type-label-tracking: 0.08em;\n\n /* Layout — Tufte proportional model */\n --content-max-width: 42rem;\n --form-max-width: 42rem;\n /* compose dialogs + feed content cap */\n --layout-body-max-width: 1088px;\n --layout-content-width: 55%;\n --layout-sidenote-width: 50%;\n --layout-sidenote-margin: -60%;\n --site-padding: 1.5rem;\n --content-gap: 1rem;\n --space-xl: 2rem;\n --space-2xl: 4rem;\n /*\n * Home timeline vertical rhythm — the single source of truth for the gaps\n * between stacked sections on the home page:\n *\n * site description → separator → first post → divider → post → ...\n *\n * where \"separator\" is the description hairline (logged out) or the compose\n * prompt (logged in). Every one of those gaps derives from this token, and\n * the individual pieces (compose prompt, description hairline, post cards)\n * carry no rhythm margin of their own — the structural wrappers\n * (.site-home-header, hr.feed-divider) own the spacing. Retune the whole\n * feed cadence by changing this one value.\n *\n * Note: per-format card padding (.feed-quote-post, thread previews) is the\n * card's own internal spacing and is intentionally NOT part of this rhythm.\n *\n * Kept as a free-standing value (not pinned to the --space-* scale) so the\n * feed cadence can be tuned independently.\n */\n --site-feed-rhythm: 4.4rem;\n /*\n * The post footer's action row (reply / menu buttons) is a taller tap\n * target than its visible content, leaving a few px of dead space below\n * the last visible element of every post (~5.9px logged out from the\n * footer min-height, ~6.8px logged in from the menu button). hr.feed-divider\n * subtracts this from its top margin so the VISIBLE gap between posts equals\n * --site-feed-rhythm — the same as the header spacing. Re-measure if the\n * footer button size or meta font changes.\n */\n --post-footer-overshoot: 6px;\n /*\n * The site intro (description) block is deliberately tighter than\n * --site-feed-rhythm so it reads as a compact header unit rather than a\n * full feed section. -top: gap from the header nav down to the intro.\n * -bottom: gap from the intro down to its separator — the hairline (logged\n * out) or the compose prompt text (logged in). Same values in both states.\n */\n --site-intro-gap-top: 24px;\n --site-intro-gap-bottom: 36px;\n\n /* Sidebar layout (admin + site sidebar pages) */\n --sidebar-width: 12rem;\n --sidebar-gap: 2rem;\n\n /* Surfaces */\n --card-bg: var(--card);\n --card-radius: 0;\n --card-padding: 1rem;\n --card-border-width: 0;\n --card-shadow: none;\n\n /* Elements */\n --avatar-size: 28px;\n --avatar-radius: 50%;\n --media-radius: 0.5rem;\n\n /* Icons */\n --icon-stroke: 2;\n --icon-stroke-fine: 1.5;\n\n /* Derived color tokens (from BaseCoat variables) */\n --site-accent: var(--primary);\n --site-accent-text: var(--primary-foreground);\n --site-column-outline: var(--border);\n --site-border-light: color-mix(\n in srgb,\n var(--site-column-outline) 52%,\n transparent\n );\n --site-threadline: var(--border);\n --site-page-bg: var(--background);\n --site-elevated-bg: var(--background);\n --site-nav-hover-bg: var(--accent);\n --site-text-primary: var(--foreground);\n --site-text-secondary: var(--muted-foreground);\n --site-reading-title: color-mix(\n in oklch,\n var(--site-text-primary) 81%,\n black\n );\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 86%,\n black\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 90%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 95%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 72%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 88%,\n var(--site-text-primary)\n );\n --site-content-link: inherit;\n --site-content-link-hover: var(--site-text-primary);\n --site-content-link-underline: color-mix(\n in srgb,\n var(--site-text-secondary) 58%,\n transparent\n );\n --site-reading-link: var(--site-reading-body);\n --site-reading-link-hover: var(--site-reading-heading);\n --site-reading-link-underline: color-mix(\n in srgb,\n var(--site-reading-meta) 58%,\n transparent\n );\n --site-text-placeholder: oklch(from var(--muted-foreground) l c h / 0.5);\n --site-media-outline: var(--border);\n --site-divider: var(--border);\n --site-feed-card-bg: color-mix(\n in srgb,\n var(--site-elevated-bg) 88%,\n var(--site-nav-hover-bg)\n );\n --site-feed-card-border: color-mix(\n in srgb,\n var(--site-divider) 78%,\n transparent\n );\n --site-feed-card-shadow: color-mix(\n in srgb,\n var(--site-text-primary) 12%,\n transparent\n );\n --site-code-text: color-mix(\n in srgb,\n var(--site-reading-heading) 86%,\n var(--site-reading-body)\n );\n --site-code-bg: color-mix(in srgb, var(--site-reading-meta) 12%, transparent);\n --site-code-block-bg: color-mix(\n in srgb,\n var(--site-elevated-bg) 94%,\n var(--site-nav-hover-bg)\n );\n --site-code-block-border: color-mix(\n in srgb,\n var(--site-divider) 62%,\n transparent\n );\n --site-feed-divider-color: color-mix(\n in srgb,\n var(--site-text-secondary) 30%,\n transparent\n );\n --site-feed-link-tint: color-mix(in srgb, var(--site-accent) 7%, transparent);\n --site-feed-quote-tint: color-mix(\n in srgb,\n var(--site-accent) 10%,\n transparent\n );\n --site-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 22%,\n var(--site-divider)\n );\n --site-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 62%,\n var(--site-page-bg)\n );\n --site-blockquote-text: color-mix(\n in oklch,\n var(--site-text-primary) 92%,\n black\n );\n --site-summary-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 42%,\n var(--site-page-bg)\n );\n --site-reading-blockquote-rail: color-mix(\n in srgb,\n var(--site-reading-link) 18%,\n var(--site-reading-meta)\n );\n --site-reading-blockquote-bg: color-mix(\n in srgb,\n var(--site-accent) 6%,\n var(--site-page-bg)\n );\n --site-thread-context-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 58%,\n transparent\n );\n --site-thread-context-border: color-mix(\n in srgb,\n var(--site-divider) 74%,\n transparent\n );\n --site-thread-gap-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 42%,\n transparent\n );\n --site-thread-item-spacing: 32px;\n --site-thread-context-max-height: 160px;\n --site-thread-dot-ring: color-mix(\n in srgb,\n var(--site-accent) 16%,\n transparent\n );\n --compose-paper-bg: var(--site-page-bg);\n --compose-control-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 72%,\n var(--compose-paper-bg)\n );\n --compose-control-bg-strong: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 88%,\n var(--compose-paper-bg)\n );\n --compose-control-border: color-mix(\n in srgb,\n var(--site-column-outline) 68%,\n transparent\n );\n --compose-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 24%,\n var(--compose-control-border)\n );\n --compose-blockquote-bg: color-mix(\n in srgb,\n var(--compose-control-bg) 56%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-bg-focus: color-mix(\n in srgb,\n var(--compose-control-bg-strong) 70%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-text: color-mix(\n in srgb,\n var(--site-text-primary) 88%,\n var(--site-text-secondary)\n );\n --compose-floating-bg: color-mix(\n in srgb,\n var(--compose-paper-bg) 94%,\n var(--site-nav-hover-bg) 6%\n );\n\n /* Search highlight */\n --search-mark-bg: oklch(0.92 0.14 90 / 0.55);\n --search-mark-color: oklch(0.35 0.09 70);\n\n /* Admin */\n --dash-bg: oklch(0.97 0.005 80);\n --dash-card-radius: 10px;\n}\n\n@media (max-width: 760px) {\n :root {\n --site-padding: 1.875rem;\n }\n}\n\n/* Tufte two-column → single-column collapse.\n *\n * Below this width the post column is already narrowed to `min(100%, 35rem)`\n * (preset.css) and there is no room for a 45% sidenote gutter, so all content\n * goes full-width. Single images (MediaGallery getSingleVisualWidth) and link\n * previews (.link-preview) read this token directly, so they collapse here too.\n *\n * Keep this breakpoint in sync with the post-column collapse (preset.css /\n * ui.css feed-divider, both `max-width: 1024px`) and the sidenote float→inline\n * collapse (ui.css). They are one layout switch; do not let them drift apart. */\n@media (max-width: 1024px) {\n :root {\n --layout-content-width: 100%;\n }\n}\n\n/*\n * Dark-mode reading color overrides.\n *\n * These rules must beat the active color theme's light-mode block, which\n * uses `:root:root { ... }` (specificity 0,0,2,0) with no media query and\n * therefore applies in both light and dark modes. Most themes do not\n * redefine `--site-reading-*` in their dark block, so without higher\n * specificity here the light reading-body color would leak into dark mode\n * (resulting in near-invisible body text on a dark background).\n *\n * We repeat `:root:root` to reach specificity 0,0,3,0, which outranks the\n * theme's light `:root:root` (0,0,2,0). The theme's dark blocks use\n * `:root:root[data-theme-mode=\"dark\"]` or `:root:root:not([data-theme-mode=\"light\"])`\n * (also 0,0,3,0), so a theme that explicitly defines dark reading colors\n * still wins via source order.\n */\n@media (prefers-color-scheme: dark) {\n :root:root:not([data-theme-mode=\"light\"]) {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(\n in oklch,\n var(--site-text-primary) 96%,\n black\n );\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n }\n}\n\n:root:root[data-theme-mode=\"dark\"] {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 96%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n}\n\n@media (max-width: 760px), (hover: none) and (pointer: coarse) {\n :root {\n /* Content layer — tighter scale for mobile reading.\n Ratio ≈ 1.88 : 1.42 : 1.15 : 1 (display:title:subtitle:body).\n At 15px root × 0.8 content-scale: 29.4 / 22.2 / 18 / 15.6px.\n Body is floored to a 16px minimum below (max()) for readability;\n the floor still yields to calc() when the user zooms the root up. */\n --type-content-display: calc(2.45rem * var(--type-content-scale));\n --type-content-title: calc(1.85rem * var(--type-content-scale));\n --type-content-subtitle: calc(1.5rem * var(--type-content-scale));\n --type-content-body: max(16px, calc(1.3rem * var(--type-content-scale)));\n\n /* UI layer — pixel floors for touch targets */\n --type-ui-title: max(16px, var(--type-secondary));\n --type-ui-control: max(15px, var(--type-base));\n --type-ui-meta: max(15px, var(--type-secondary));\n --type-ui-hint: max(13px, var(--type-sm));\n --type-ui-caption: max(13px, var(--type-xs));\n --type-ui-micro: max(12px, var(--type-2xs));\n --type-ui-input: max(16px, var(--type-secondary));\n --type-thread-context: max(15px, var(--type-base));\n --type-thread-context-title: max(16px, var(--type-secondary));\n --type-thread-context-meta: max(14px, var(--type-sm));\n }\n}\n";
|
|
4439
|
+
var tokens_default = "/**\n * Design Tokens\n *\n * CSS custom properties for all visual aspects of the UI.\n * These are the stable customization API — override in custom CSS\n * to change typography, layout, surfaces, and element sizing.\n */\n\n:root {\n /* Typography — Font families */\n --font-cjk-serif-fallback: \"Jant Language Fallback\";\n --font-cjk-sans-fallback: \"Jant Language Fallback\";\n --font-body:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, var(--font-cjk-sans-fallback),\n sans-serif;\n --font-heading:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-site-title:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-serif:\n var(--font-cjk-serif-fallback), ui-serif, \"New York Small\", \"New York\",\n \"Iowan Old Style\", Charter, Georgia, \"Times New Roman\", Times, serif;\n --font-ui:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, var(--font-cjk-sans-fallback),\n sans-serif;\n --font-mono:\n ui-monospace, Menlo, Monaco, Consolas, \"Cascadia Code\", \"Courier New\",\n monospace;\n /*\n * Blockquote font family.\n *\n * Defaults to `inherit` so blockquotes follow the body font in sans-only\n * themes (Clean, Friendly, Bold) and serif-only themes (Tufte, Bookish).\n * Mixed themes that want a distinct blockquote voice — e.g. Classic, which\n * pairs a sans body with serif headings — override this token in their\n * `cssVariables` to point at a serif stack. This also restores a type-level\n * distinction for CJK, where italic is disabled.\n */\n --font-blockquote: inherit;\n\n /* Typography — Font weights */\n --fw-light: 300;\n --fw-regular: 400;\n --fw-medium: 500;\n --fw-semibold: 600;\n --fw-bold: 700;\n --fw-extrabold: 800;\n /*\n * Unified type scale.\n *\n * Every font-size in the system — reading content AND UI controls —\n * must reference one of these tokens. No raw rem/px values elsewhere.\n *\n * --type-display 2.7rem (40.5px) — page titles, h1\n * --type-title 2.1rem (31.5px) — h2, feed card titles\n * --type-subtitle 1.8rem (27px) — h3\n * --type-body 1.4rem (21px) — running text, form inputs\n * --type-secondary 1.1rem (16.5px) — meta, nav, sidenotes, UI labels\n * --type-base 1.0rem (15px) — UI buttons, controls\n * --type-sm 0.9rem (13.5px) — small UI text\n * --type-xs 0.8rem (12px) — captions, descriptions, chips\n * --type-2xs 0.65rem (9.75px) — tiny labels, file sizes, badges\n */\n --type-display: 2.7rem;\n --type-title: 2.1rem;\n --type-subtitle: 1.8rem;\n --type-body: 1.4rem;\n --type-secondary: 1.1rem;\n --type-base: 1rem;\n --type-sm: 0.9rem;\n --type-xs: 0.8rem;\n --type-2xs: 0.65rem;\n\n /* Content reading tokens — desktop maps to core scale,\n mobile overrides below cap sizes for small screens.\n --type-content-scale uniformly scales all content sizes\n without affecting UI controls. */\n --type-content-scale: 0.8;\n --type-content-display: calc(var(--type-display) * var(--type-content-scale));\n --type-content-title: calc(var(--type-title) * var(--type-content-scale));\n --type-content-subtitle: calc(\n var(--type-subtitle) * var(--type-content-scale)\n );\n --type-content-body: calc(var(--type-body) * var(--type-content-scale));\n --type-content-quote: calc(var(--type-content-body) * 1.16);\n --type-content-quote-leading: 1.4;\n\n /* Semantic aliases */\n --type-body-size: var(--type-content-body);\n --type-footnote-ref: calc(var(--type-body-size) * 0.75);\n --type-code: calc(var(--type-body-size) * 0.94);\n --type-code-block: calc(var(--type-body-size) * 0.9);\n --type-body-tracking: 0;\n --type-ui-title: var(--type-secondary);\n --type-ui-control: var(--type-base);\n --type-ui-meta: var(--type-base);\n --type-ui-hint: var(--type-sm);\n --type-ui-caption: var(--type-xs);\n --type-ui-micro: var(--type-2xs);\n --type-ui-input: var(--type-secondary);\n --type-thread-context: var(--type-base);\n --type-thread-context-title: var(--type-secondary);\n --type-thread-context-meta: var(--type-sm);\n --feed-note-title-size: calc(var(--type-content-body) * 1.36);\n --feed-note-title-leading: var(--type-heading-leading);\n --type-body-leading: 1.7;\n --type-display-leading: 1.15;\n --type-heading-leading: 1.15;\n --type-heading-weight: var(--fw-medium);\n --type-heading-tracking: 0;\n --type-display-weight: var(--fw-medium);\n --type-display-tracking: 0;\n --type-label-weight: var(--fw-medium);\n --type-label-tracking: 0.08em;\n\n /* Layout — Tufte proportional model */\n --content-max-width: 42rem;\n --form-max-width: 42rem;\n /* compose dialogs + feed content cap */\n --layout-body-max-width: 1088px;\n --layout-content-width: 55%;\n --layout-sidenote-width: 50%;\n --layout-sidenote-gap: 10%;\n --layout-sidenote-margin: -60%;\n --site-padding: 1.5rem;\n --content-gap: 1rem;\n --space-xl: 2rem;\n --space-2xl: 4rem;\n /*\n * Home timeline vertical rhythm — the single source of truth for the gaps\n * between stacked sections on the home page:\n *\n * site description → separator → first post → divider → post → ...\n *\n * where \"separator\" is the description hairline (logged out) or the compose\n * prompt (logged in). Every one of those gaps derives from this token, and\n * the individual pieces (compose prompt, description hairline, post cards)\n * carry no rhythm margin of their own — the structural wrappers\n * (.site-home-header, hr.feed-divider) own the spacing. Retune the whole\n * feed cadence by changing this one value.\n *\n * Note: per-format card padding (.feed-quote-post, thread previews) is the\n * card's own internal spacing and is intentionally NOT part of this rhythm.\n *\n * Kept as a free-standing value (not pinned to the --space-* scale) so the\n * feed cadence can be tuned independently.\n */\n --site-feed-rhythm: 4.4rem;\n /*\n * The post footer's action row (reply / menu buttons) is a taller tap\n * target than its visible content, leaving a few px of dead space below\n * the last visible element of every post (~5.9px logged out from the\n * footer min-height, ~6.8px logged in from the menu button). hr.feed-divider\n * subtracts this from its top margin so the VISIBLE gap between posts equals\n * --site-feed-rhythm — the same as the header spacing. Re-measure if the\n * footer button size or meta font changes.\n */\n --post-footer-overshoot: 6px;\n /*\n * The site intro (description) block is deliberately tighter than\n * --site-feed-rhythm so it reads as a compact header unit rather than a\n * full feed section. -top: gap from the header nav down to the intro.\n * -bottom: gap from the intro down to its separator — the hairline (logged\n * out) or the compose prompt text (logged in). Same values in both states.\n */\n --site-intro-gap-top: 24px;\n --site-intro-gap-bottom: 36px;\n\n /* Sidebar layout (admin + site sidebar pages) */\n --sidebar-width: 12rem;\n --sidebar-gap: 2rem;\n\n /* Surfaces */\n --card-bg: var(--card);\n --card-radius: 0;\n --card-padding: 1rem;\n --card-border-width: 0;\n --card-shadow: none;\n\n /* Elements */\n --avatar-size: 28px;\n --avatar-radius: 50%;\n --media-radius: 0.5rem;\n\n /* Icons */\n --icon-stroke: 2;\n --icon-stroke-fine: 1.5;\n\n /* Derived color tokens (from BaseCoat variables) */\n --site-accent: var(--primary);\n --site-accent-text: var(--primary-foreground);\n --site-column-outline: var(--border);\n --site-border-light: color-mix(\n in srgb,\n var(--site-column-outline) 52%,\n transparent\n );\n --site-threadline: var(--border);\n --site-page-bg: var(--background);\n --site-elevated-bg: var(--background);\n --site-nav-hover-bg: var(--accent);\n --site-text-primary: var(--foreground);\n --site-text-secondary: var(--muted-foreground);\n --site-reading-title: color-mix(\n in oklch,\n var(--site-text-primary) 81%,\n black\n );\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 86%,\n black\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 90%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 95%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 72%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 88%,\n var(--site-text-primary)\n );\n --site-footnote-text: var(--site-reading-caption);\n --site-footnote-marker: color-mix(\n in oklch,\n var(--site-text-secondary) 88%,\n var(--site-page-bg)\n );\n --site-content-link: inherit;\n --site-content-link-hover: var(--site-text-primary);\n --site-content-link-underline: color-mix(\n in srgb,\n var(--site-text-secondary) 58%,\n transparent\n );\n --site-reading-link: var(--site-reading-body);\n --site-reading-link-hover: var(--site-reading-heading);\n --site-reading-link-underline: color-mix(\n in srgb,\n var(--site-reading-meta) 58%,\n transparent\n );\n --site-text-placeholder: oklch(from var(--muted-foreground) l c h / 0.5);\n --site-media-outline: var(--border);\n --site-divider: var(--border);\n --site-feed-card-bg: color-mix(\n in srgb,\n var(--site-elevated-bg) 88%,\n var(--site-nav-hover-bg)\n );\n --site-feed-card-border: color-mix(\n in srgb,\n var(--site-divider) 78%,\n transparent\n );\n --site-feed-card-shadow: color-mix(\n in srgb,\n var(--site-text-primary) 12%,\n transparent\n );\n --site-code-text: color-mix(\n in srgb,\n var(--site-reading-heading) 86%,\n var(--site-reading-body)\n );\n --site-code-bg: color-mix(in srgb, var(--site-reading-meta) 12%, transparent);\n --site-code-block-bg: color-mix(\n in srgb,\n var(--site-elevated-bg) 94%,\n var(--site-nav-hover-bg)\n );\n --site-code-block-border: color-mix(\n in srgb,\n var(--site-divider) 62%,\n transparent\n );\n --site-feed-divider-color: color-mix(\n in srgb,\n var(--site-text-secondary) 30%,\n transparent\n );\n --site-feed-link-tint: color-mix(in srgb, var(--site-accent) 7%, transparent);\n --site-feed-quote-tint: color-mix(\n in srgb,\n var(--site-accent) 10%,\n transparent\n );\n --site-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 22%,\n var(--site-divider)\n );\n --site-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 62%,\n var(--site-page-bg)\n );\n --site-blockquote-text: color-mix(\n in oklch,\n var(--site-text-primary) 92%,\n black\n );\n --site-summary-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 42%,\n var(--site-page-bg)\n );\n --site-reading-blockquote-rail: color-mix(\n in srgb,\n var(--site-reading-link) 18%,\n var(--site-reading-meta)\n );\n --site-reading-blockquote-bg: color-mix(\n in srgb,\n var(--site-accent) 6%,\n var(--site-page-bg)\n );\n --site-thread-context-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 58%,\n transparent\n );\n --site-thread-context-border: color-mix(\n in srgb,\n var(--site-divider) 74%,\n transparent\n );\n --site-thread-gap-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 42%,\n transparent\n );\n --site-thread-item-spacing: 32px;\n --site-thread-context-max-height: 160px;\n --site-thread-dot-ring: color-mix(\n in srgb,\n var(--site-accent) 16%,\n transparent\n );\n --compose-paper-bg: var(--site-page-bg);\n --compose-control-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 72%,\n var(--compose-paper-bg)\n );\n --compose-control-bg-strong: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 88%,\n var(--compose-paper-bg)\n );\n --compose-control-border: color-mix(\n in srgb,\n var(--site-column-outline) 68%,\n transparent\n );\n --compose-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 24%,\n var(--compose-control-border)\n );\n --compose-blockquote-bg: color-mix(\n in srgb,\n var(--compose-control-bg) 56%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-bg-focus: color-mix(\n in srgb,\n var(--compose-control-bg-strong) 70%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-text: color-mix(\n in srgb,\n var(--site-text-primary) 88%,\n var(--site-text-secondary)\n );\n --compose-floating-bg: color-mix(\n in srgb,\n var(--compose-paper-bg) 94%,\n var(--site-nav-hover-bg) 6%\n );\n\n /* Search highlight */\n --search-mark-bg: oklch(0.92 0.14 90 / 0.55);\n --search-mark-color: oklch(0.35 0.09 70);\n\n /* Admin */\n --dash-bg: oklch(0.97 0.005 80);\n --dash-card-radius: 10px;\n}\n\n@media (max-width: 760px) {\n :root {\n --site-padding: 1.875rem;\n }\n}\n\n/* Tufte two-column → single-column collapse.\n *\n * Below this width the post column is already narrowed to `min(100%, 35rem)`\n * (preset.css) and there is no room for a 45% sidenote gutter, so all content\n * goes full-width. Single images (MediaGallery getSingleVisualWidth) and link\n * previews (.link-preview) read this token directly, so they collapse here too.\n *\n * Keep this breakpoint in sync with the post-column collapse (preset.css /\n * ui.css feed-divider, both `max-width: 1024px`) and the sidenote float→inline\n * collapse (ui.css). They are one layout switch; do not let them drift apart. */\n@media (max-width: 1024px) {\n :root {\n --layout-content-width: 100%;\n }\n}\n\n/*\n * Dark-mode reading color overrides.\n *\n * These rules must beat the active color theme's light-mode block, which\n * uses `:root:root { ... }` (specificity 0,0,2,0) with no media query and\n * therefore applies in both light and dark modes. Most themes do not\n * redefine `--site-reading-*` in their dark block, so without higher\n * specificity here the light reading-body color would leak into dark mode\n * (resulting in near-invisible body text on a dark background).\n *\n * We repeat `:root:root` to reach specificity 0,0,3,0, which outranks the\n * theme's light `:root:root` (0,0,2,0). The theme's dark blocks use\n * `:root:root[data-theme-mode=\"dark\"]` or `:root:root:not([data-theme-mode=\"light\"])`\n * (also 0,0,3,0), so a theme that explicitly defines dark reading colors\n * still wins via source order.\n */\n@media (prefers-color-scheme: dark) {\n :root:root:not([data-theme-mode=\"light\"]) {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(\n in oklch,\n var(--site-text-primary) 96%,\n black\n );\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n }\n}\n\n:root:root[data-theme-mode=\"dark\"] {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 96%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n}\n\n@media (max-width: 760px), (hover: none) and (pointer: coarse) {\n :root {\n /* Content layer — tighter scale for mobile reading.\n Ratio ≈ 1.88 : 1.42 : 1.15 : 1 (display:title:subtitle:body).\n At 15px root × 0.8 content-scale: 29.4 / 22.2 / 18 / 15.6px.\n Body is floored to a 16px minimum below (max()) for readability;\n the floor still yields to calc() when the user zooms the root up. */\n --type-content-display: calc(2.45rem * var(--type-content-scale));\n --type-content-title: calc(1.85rem * var(--type-content-scale));\n --type-content-subtitle: calc(1.5rem * var(--type-content-scale));\n --type-content-body: max(16px, calc(1.3rem * var(--type-content-scale)));\n\n /* UI layer — pixel floors for touch targets */\n --type-ui-title: max(16px, var(--type-secondary));\n --type-ui-control: max(15px, var(--type-base));\n --type-ui-meta: max(15px, var(--type-secondary));\n --type-ui-hint: max(13px, var(--type-sm));\n --type-ui-caption: max(13px, var(--type-xs));\n --type-ui-micro: max(12px, var(--type-2xs));\n --type-ui-input: max(16px, var(--type-secondary));\n --type-thread-context: max(15px, var(--type-base));\n --type-thread-context-title: max(16px, var(--type-secondary));\n --type-thread-context-meta: max(14px, var(--type-sm));\n }\n}\n";
|
|
3299
4440
|
//#endregion
|
|
3300
4441
|
//#region src/services/export-theme/theme.toml?raw
|
|
3301
4442
|
var theme_default = "name = \"jant\"\nlicense = \"MIT\"\nlicenselink = \"https://github.com/jant-me/jant/blob/main/LICENSE\"\ndescription = \"Default theme packaged with Jant exports.\"\nhomepage = \"https://jant.so\"\ntags = [\"blog\", \"microblog\", \"minimal\"]\nfeatures = [\"pagination\", \"aliases\"]\nmin_version = \"0.160.1\"\n\n[author]\n name = \"Jant\"\n homepage = \"https://jant.so\"\n";
|
|
3302
4443
|
//#endregion
|
|
3303
4444
|
//#region src/services/export-theme/styles/main.css?raw
|
|
3304
|
-
var main_default = "/*\n * Jant Hugo Export — main.css\n *\n * Fresh minimal text-first design. All colors, spacing, and type sizing\n * come from tokens.css (loaded first in the <head>). Color theme values\n * are supplied by theme.css; customizations live in custom.css.\n *\n * Load order in <head>: tokens.css → main.css → theme.css → custom.css\n *\n * This file intentionally avoids any hardcoded hex, rgb, or px color\n * values. Everything token-driven so the design evolves with the theme.\n */\n\n/* -------------------------------------------------------------------------\n * Reset + box model\n * ------------------------------------------------------------------------- */\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 15px;\n -webkit-text-size-adjust: 100%;\n text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n font-family: var(--font-body);\n font-size: var(--type-body-size, var(--type-content-body));\n line-height: var(--type-body-leading);\n color: var(--site-reading-body);\n background-color: var(--site-page-bg);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nimg,\nvideo,\naudio,\niframe {\n max-width: 100%;\n height: auto;\n}\n\nfigure {\n margin: 0;\n}\n\nhr {\n border: 0;\n border-top: 1px solid var(--site-divider);\n margin: var(--space-xl) 0;\n}\n\n/* -------------------------------------------------------------------------\n * Typography\n * ------------------------------------------------------------------------- */\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-family: var(--font-heading);\n font-weight: var(--type-heading-weight);\n line-height: var(--type-heading-leading);\n letter-spacing: var(--type-heading-tracking);\n color: var(--site-reading-heading);\n margin: 1.6em 0 0.6em;\n}\n\nh1 {\n font-size: var(--type-content-display);\n line-height: var(--type-display-leading);\n font-weight: var(--type-display-weight);\n letter-spacing: var(--type-display-tracking);\n margin: 4rem 0 1.5rem;\n}\n\nh2 {\n font-size: var(--type-content-title);\n margin: 2.1rem 0 1.4rem;\n}\n\nh3 {\n font-size: var(--type-content-subtitle);\n margin: 2rem 0 1.4rem;\n}\n\nh4 {\n font-size: var(--type-content-body);\n font-weight: var(--fw-medium);\n}\n\nh5,\nh6 {\n font-size: var(--type-content-body);\n font-weight: var(--fw-medium);\n color: var(--site-reading-meta);\n}\n\np {\n margin: 1.4rem 0;\n}\n\nsmall {\n font-size: var(--type-sm);\n}\n\ncode,\nkbd,\nsamp {\n font-family: var(--font-mono);\n font-size: var(--type-code);\n line-height: 1.42;\n}\n\ncode {\n color: var(--site-code-text);\n overflow-wrap: break-word;\n}\n\npre {\n font-family: var(--font-mono);\n font-size: var(--type-code-block);\n line-height: 1.5;\n padding: 1rem;\n overflow-x: auto;\n color: var(--site-code-text);\n background-color: var(--site-code-block-bg);\n border: 1px solid var(--site-code-block-border);\n border-radius: 6px;\n}\n\npre code {\n padding: 0;\n background: transparent;\n border: 0;\n color: inherit;\n font-size: inherit;\n}\n\n:not(pre) > code {\n padding: 0.08em 0.28em;\n background-color: var(--site-code-bg);\n border-radius: 0.22em;\n box-decoration-break: clone;\n -webkit-box-decoration-break: clone;\n}\n\n/* Blockquotes — tinted background card with quote icon (matches main site). */\nblockquote {\n position: relative;\n margin: 1.4rem 0;\n padding: 1.4rem 1rem 0.75rem;\n border: none;\n background: var(--site-blockquote-bg);\n border-radius: 6px;\n color: var(--site-blockquote-text);\n font-family: var(--font-blockquote);\n font-style: italic;\n font-weight: inherit;\n quotes: none;\n text-wrap: pretty;\n}\n\nblockquote::before {\n content: \"\";\n display: block;\n width: 1.3rem;\n height: 1.3rem;\n margin-bottom: 0.35rem;\n background: var(--site-blockquote-rail);\n opacity: 0.6;\n mask-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z'/%3E%3Cpath d='M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z'/%3E%3C/svg%3E\");\n mask-size: contain;\n mask-repeat: no-repeat;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z'/%3E%3Cpath d='M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n}\n\nblockquote :where(p:first-of-type)::before,\nblockquote :where(p:last-of-type)::after {\n content: none;\n}\n\nblockquote > :first-child {\n margin-top: 0;\n}\n\nblockquote > :last-child {\n margin-bottom: 0;\n}\n\nblockquote p {\n margin: 0;\n}\n\nblockquote p + p,\nblockquote ul,\nblockquote ol,\nblockquote pre {\n margin-top: 0.6em;\n}\n\nblockquote cite {\n font-style: normal;\n color: var(--site-reading-meta);\n font-size: var(--type-sm);\n}\n\n/* CJK: no italic for blockquotes (no true italic glyph) */\n:lang(zh) blockquote,\n:lang(ja) blockquote,\n:lang(ko) blockquote {\n font-style: normal;\n}\n\nul,\nol {\n padding-left: 1.4em;\n margin: 1.25em 0;\n}\n\nli {\n margin-top: 0.65em;\n margin-bottom: 0.65em;\n}\n\n/* Normalize li > p so list spacing is controlled by li alone,\n regardless of whether the markdown renderer wraps li contents in <p>. */\nli > p:first-child {\n margin-top: 0;\n}\n\nli > p:last-child {\n margin-bottom: 0;\n}\n\na {\n color: var(--site-reading-link);\n text-decoration: underline;\n text-decoration-color: var(--site-reading-link-underline);\n text-underline-offset: 0.15em;\n transition:\n color 0.2s ease,\n text-decoration-color 0.2s ease;\n}\n\na:hover,\na:focus {\n color: var(--site-reading-link-hover);\n text-decoration-color: currentColor;\n}\n\na:focus-visible {\n outline: 2px solid var(--site-accent);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\ntime {\n color: var(--site-reading-meta);\n font-size: var(--type-sm);\n font-variant-numeric: tabular-nums;\n}\n\n/* -------------------------------------------------------------------------\n * Page layout — Tufte horizontal frame\n *\n * Mirrors the main site's `.site-page > header/main/footer` rule. Every\n * top-level section gets the same asymmetric padding so the reading column\n * aligns with the rest of the site.\n *\n * 12.5% left + 4% right = 16.5% padding → content = 83.5% of the box.\n * max-width = body-max-width / 0.835 so the inner content area exactly\n * equals `--layout-body-max-width` on wide viewports. min() caps keep\n * padding from growing beyond 210px / 67px. Mobile widens to 5% / 5%\n * and the 55% content column collapses to 100% (via tokens.css).\n * ------------------------------------------------------------------------- */\n\n.site-page {\n min-height: 100vh;\n min-height: 100dvh;\n background-color: var(--site-page-bg);\n}\n\n.site-page > header,\n.site-page > main,\n.site-page > footer,\n.site-page > .home-branding-credit {\n width: 100%;\n max-width: calc(var(--layout-body-max-width) / 0.835);\n padding-left: min(12.5%, 210px);\n padding-right: min(4%, 67px);\n margin-left: auto;\n margin-right: auto;\n}\n\n@media (max-width: 760px) {\n .site-page > header,\n .site-page > main,\n .site-page > footer,\n .site-page > .home-branding-credit {\n padding-left: max(5%, 28px);\n padding-right: 5%;\n }\n}\n\n.site-main {\n padding-top: var(--space-xl);\n padding-bottom: var(--space-xl);\n}\n\n/* -------------------------------------------------------------------------\n * Header\n * ------------------------------------------------------------------------- */\n\n.site-header {\n padding-top: 24px;\n background-color: var(--site-page-bg);\n}\n\n@media (min-width: 700px) {\n .site-header {\n padding-top: 30px;\n }\n}\n\n.site-header-inner {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 0;\n}\n\n.site-header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: clamp(0.6rem, 2.2vw, 1rem);\n flex-wrap: nowrap;\n min-height: 2.75rem;\n width: 100%;\n}\n\n.site-header-top-bordered {\n padding-bottom: 15px;\n}\n\n@media (min-width: 700px) {\n .site-header-top-bordered {\n padding-bottom: 18px;\n }\n}\n\n.site-logo {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: 10px;\n min-width: 0;\n padding: 0.15rem 0;\n font-family: var(--font-site-title);\n font-size: var(--type-subtitle);\n font-weight: var(--fw-regular);\n letter-spacing: -0.02em;\n line-height: 1.15;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--site-text-primary);\n text-decoration: none;\n}\n\n.site-logo:hover,\n.site-logo:focus {\n color: var(--site-text-primary);\n text-decoration: none;\n}\n\n.site-logo-avatar {\n box-sizing: border-box;\n width: calc(var(--avatar-size) + 4px);\n height: calc(var(--avatar-size) + 4px);\n border-radius: var(--avatar-radius);\n object-fit: cover;\n border: 1px solid color-mix(in srgb, var(--site-divider) 82%, transparent);\n flex: none;\n}\n\n.site-logo-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.site-header-nav {\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n justify-content: flex-end;\n gap: clamp(0.6rem, 2.2vw, 1rem);\n margin-left: auto;\n min-width: 0;\n font-family: var(--font-ui);\n}\n\n.site-header-link {\n display: inline-flex;\n flex: none;\n align-items: center;\n position: relative;\n min-height: 2rem;\n padding: 0.15rem 0;\n font-size: var(--type-ui-meta);\n font-weight: var(--fw-medium);\n letter-spacing: 0.01em;\n line-height: 1;\n white-space: nowrap;\n color: color-mix(in srgb, var(--site-text-secondary) 62%, transparent);\n text-decoration: none;\n transition: color 0.15s;\n}\n\n.site-header-link:hover,\n.site-header-link:focus {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n text-decoration: none;\n}\n\n.site-header-link-active {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n}\n\n/* --- \"More\" dropdown ---------------------------------------------------- */\n\n.site-header-more {\n position: relative;\n display: inline-flex;\n align-items: center;\n}\n\n.site-header-more-responsive-only {\n display: none;\n}\n\n.site-header-more-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.45rem;\n min-height: 2rem;\n padding: 0.15rem 0;\n border: none;\n background: transparent;\n cursor: pointer;\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n font-weight: var(--fw-medium);\n letter-spacing: 0.01em;\n line-height: 1;\n color: color-mix(in srgb, var(--site-text-secondary) 62%, transparent);\n transition: color 0.15s;\n}\n\n.site-header-more-btn svg {\n width: 0.82rem;\n height: 0.82rem;\n transition: transform 0.18s ease;\n}\n\n.site-header-more-btn:hover,\n.site-header-more-btn[aria-expanded=\"true\"] {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n}\n\n.site-header-more-btn[aria-expanded=\"true\"] svg {\n transform: rotate(180deg);\n}\n\n.site-header-more-popover {\n display: block;\n position: absolute;\n top: 100%;\n right: 0;\n margin-top: 0.6rem;\n min-width: 12.25rem;\n padding: 0.3rem 0;\n background: var(--site-page-bg);\n border: 0.5px solid color-mix(in srgb, var(--site-divider) 80%, transparent);\n border-radius: 0.4rem;\n box-shadow:\n 0 4px 20px -8px rgba(0, 0, 0, 0.12),\n 0 2px 6px -2px rgba(0, 0, 0, 0.06);\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transform: translateY(-6px);\n transform-origin: top right;\n transition:\n opacity 0.18s ease,\n transform 0.18s ease,\n visibility 0s linear 0.18s;\n z-index: 50;\n}\n\n.site-header-more-popover[aria-hidden=\"false\"] {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n transform: translateY(0);\n transition-delay: 0s;\n}\n\n.site-header-more-link {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.35rem;\n padding: 0.45rem 1rem;\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n color: var(--site-text-secondary);\n text-decoration: none;\n transition:\n color 0.15s,\n background-color 0.15s;\n}\n\n.site-header-more-link:hover,\n.site-header-more-link:focus {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n background: color-mix(in srgb, var(--site-nav-hover-bg) 58%, transparent);\n text-decoration: none;\n}\n\n.site-header-more-link-active {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n}\n\n.site-header-more-link-responsive,\n.site-header-more-divider-responsive {\n display: none;\n}\n\n.site-header-more-divider {\n height: 0;\n margin: 0.35rem 0.75rem;\n border-top: 0.5px solid\n color-mix(in srgb, var(--site-divider) 60%, transparent);\n}\n\n/* --- Tiered responsive collapse ---------------------------------------- *\n *\n * ≤960px — 5th+ inline link collapses into More (tier-lg)\n * ≤780px — also 4th collapses (tier-md)\n * ≤580px — also 3rd collapses (tier-sm)\n * The first two links always stay inline. No hamburger on static export —\n * at very narrow widths 2 inline links + More button is the floor.\n */\n\n@media (max-width: 960px) {\n .site-header-link-collapse-lg {\n display: none;\n }\n\n .site-header-more-responsive-only.site-header-more-tier-lg {\n display: inline-flex;\n }\n\n .site-header-more-link-show-lg {\n display: flex;\n }\n\n .site-header-more-divider-responsive {\n display: block;\n }\n}\n\n@media (max-width: 780px) {\n .site-header-link-collapse-md {\n display: none;\n }\n\n .site-header-more-responsive-only.site-header-more-tier-md {\n display: inline-flex;\n }\n\n .site-header-more-link-show-md {\n display: flex;\n }\n}\n\n@media (max-width: 580px) {\n .site-header-link-collapse-sm {\n display: none;\n }\n\n .site-header-more-responsive-only.site-header-more-tier-sm {\n display: inline-flex;\n }\n\n .site-header-more-link-show-sm {\n display: flex;\n }\n}\n\n/* -------------------------------------------------------------------------\n * Footer\n * ------------------------------------------------------------------------- */\n\n.site-footer {\n margin-top: var(--space-xl);\n padding-bottom: var(--space-xl);\n color: var(--site-text-secondary);\n font-size: var(--type-xs);\n background-color: var(--site-page-bg);\n}\n\n.site-footer-inner {\n border-top: 0.5px solid var(--site-divider);\n padding-top: var(--space-xl);\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.site-footer-content {\n color: var(--site-text-secondary);\n}\n\n.site-footer-content p {\n margin: 0 0 0.5em;\n}\n\n.site-footer-nav-list {\n display: flex;\n flex-wrap: wrap;\n gap: 1rem;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.home-branding-credit {\n margin-top: var(--space-xl);\n padding-bottom: var(--space-xl);\n text-align: center;\n color: var(--site-text-secondary);\n font-size: var(--type-base);\n}\n\n.home-branding-credit a {\n display: inline-flex;\n align-items: center;\n gap: 0.38rem;\n color: inherit;\n text-decoration: none;\n border-bottom: 0.5px solid\n color-mix(in srgb, var(--site-text-secondary) 45%, transparent);\n transition:\n color 160ms ease,\n border-color 160ms ease;\n}\n\n.home-branding-credit a:hover,\n.home-branding-credit a:focus-visible {\n color: var(--site-text-primary);\n border-color: currentColor;\n}\n\n/* -------------------------------------------------------------------------\n * Tufte content-width constraint\n *\n * Mirrors the main site's 55% rule: reading text occupies a narrow\n * column inside the Tufte frame, leaving a wide right margin that\n * would host sidenotes on the main site. Media (images, video, audio)\n * is NOT included — galleries intentionally span the full frame so\n * they can breathe.\n *\n * Mobile (<=760px): `--layout-content-width` collapses to 100% via\n * tokens.css. Tablet: cap at 35rem for readability.\n * ------------------------------------------------------------------------- */\n\n.section-header,\n.section-body,\n.post-card-title,\n.post-card-summary,\n.post-card-link-domain,\n.post-card-quote-content,\n.post-card-quote-attribution,\n.post-card-quote-commentary,\n.post-card-footer,\n.reply-title,\n.reply-body,\n.reply-link-domain,\n.reply-footer,\n.thread-title,\n.thread-body,\n.thread-link-domain,\n.thread-footer,\n.collection-directory,\n.pagination,\n.empty-state,\n.page-summary {\n width: var(--layout-content-width);\n max-width: 100%;\n}\n\n@media (min-width: 761px) and (max-width: 1024px) {\n .section-header,\n .section-body,\n .post-card-title,\n .post-card-summary,\n .post-card-link-domain,\n .post-card-quote-content,\n .post-card-quote-attribution,\n .post-card-quote-commentary,\n .post-card-footer,\n .reply-title,\n .reply-body,\n .reply-link-domain,\n .reply-footer,\n .thread-title,\n .thread-body,\n .thread-link-domain,\n .thread-footer,\n .collection-directory,\n .pagination,\n .empty-state,\n .page-summary {\n width: min(100%, 35rem);\n }\n}\n\n/* -------------------------------------------------------------------------\n * Section headers\n * ------------------------------------------------------------------------- */\n\n.section-header {\n margin-bottom: var(--space-xl);\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--site-border-light);\n}\n\n.section-title {\n margin: 0 0 0.25em;\n}\n\n.section-summary {\n margin: 0;\n color: var(--site-reading-meta);\n font-size: var(--type-secondary);\n}\n\n.section-meta {\n margin: 0.5em 0 0;\n color: var(--site-reading-meta);\n font-size: var(--type-sm);\n}\n\n/* -------------------------------------------------------------------------\n * Post list + post cards\n * ------------------------------------------------------------------------- */\n\n.post-list {\n display: flex;\n flex-direction: column;\n gap: calc(var(--space-xl) * 1.25);\n}\n\n.post-list-pinned {\n margin-bottom: calc(var(--space-xl) * 1.25);\n padding-bottom: calc(var(--space-xl) * 1.25);\n border-bottom: 1px solid var(--site-border-light);\n}\n\n/* Decorative divider between posts in a timeline feed. Mirrors the main\n site's `hr.feed-divider`: a trio of small chevron marks masked from the\n current text color, so it picks up the theme automatically.\n `margin-left` centers the divider within the 55% reading column so it\n visually sits at the middle of the post-card text stack. */\nhr.feed-divider {\n border: none;\n width: 30px;\n height: 9px;\n margin: 0;\n margin-left: calc(var(--layout-content-width) / 2 - 15px);\n color: var(--site-feed-divider-color);\n background-color: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 13'%3E%3Cpath fill='black' transform='translate(0,0) rotate(90 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(16,0) rotate(100 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(32,0) rotate(80 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 13'%3E%3Cpath fill='black' transform='translate(0,0) rotate(90 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(16,0) rotate(100 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(32,0) rotate(80 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3C/svg%3E\");\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n -webkit-mask-position: center;\n mask-position: center;\n -webkit-mask-size: contain;\n mask-size: contain;\n}\n\n.post-card {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.post-card-title {\n font-family: var(--font-heading);\n font-size: var(--feed-note-title-size);\n font-weight: var(--type-heading-weight);\n line-height: var(--feed-note-title-leading);\n margin: 0;\n color: var(--site-reading-title);\n}\n\n.post-card-title a {\n color: inherit;\n text-decoration: none;\n}\n\n.post-card-title a:hover,\n.post-card-title a:focus {\n text-decoration: underline;\n text-underline-offset: 0.18em;\n}\n\n.post-card-summary {\n margin: 0;\n color: var(--site-reading-body);\n}\n\n.post-card > .post-card-summary :is(h1, h2),\n.post-card > .post-card-quote-commentary :is(h1, h2) {\n font-size: calc(var(--type-content-body) * 1.12);\n font-weight: var(--fw-medium);\n margin-top: 1.45rem;\n margin-bottom: 0.55rem;\n}\n\n.post-card > .post-card-summary :is(h3, h4),\n.post-card > .post-card-quote-commentary :is(h3, h4) {\n font-size: var(--type-content-body);\n font-weight: var(--fw-medium);\n margin-top: 1.2rem;\n margin-bottom: 0.45rem;\n}\n\n.post-card > .post-card-summary :is(h5, h6),\n.post-card > .post-card-quote-commentary :is(h5, h6) {\n font-size: var(--type-secondary);\n font-weight: var(--fw-medium);\n color: var(--site-text-secondary);\n margin-top: 1rem;\n margin-bottom: 0.35rem;\n}\n\n/* Link card domain row — shown ABOVE the title (matches main site's\n `.feed-link-domain` pattern). Inline flex with icon + host text. */\n.post-card-link-domain,\n.thread-link-domain,\n.reply-link-domain {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n max-width: 100%;\n margin: 0 0 0.4rem 0;\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n font-weight: var(--fw-regular);\n line-height: 1.3;\n color: var(--site-text-secondary);\n text-decoration: none;\n word-break: break-all;\n transition: color 0.18s ease;\n}\n\n.post-card-link-domain:hover,\n.post-card-link-domain:focus,\n.thread-link-domain:hover,\n.thread-link-domain:focus,\n.reply-link-domain:hover,\n.reply-link-domain:focus {\n color: var(--site-text-primary);\n}\n\n.post-card-link-domain-icon {\n width: 0.72rem;\n height: 0.72rem;\n flex-shrink: 0;\n}\n\n/* Slight extra breathing room below link-card titles to match main site. */\n.post-card-link-title,\n.thread-link-title,\n.reply-link-title {\n text-wrap: pretty;\n}\n\n.post-card-link-title a,\n.thread-link-title a,\n.reply-link-title a {\n text-decoration: none;\n}\n\n.post-card-link-title a:hover,\n.post-card-link-title a:focus,\n.thread-link-title a:hover,\n.thread-link-title a:focus,\n.reply-link-title a:hover,\n.reply-link-title a:focus {\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n\n.post-card-media,\n.reply-media,\n.thread-media {\n display: grid;\n grid-template-columns: 1fr;\n gap: 0.75rem;\n}\n\n.post-card-figure img,\n.reply-figure img,\n.thread-figure img {\n display: block;\n width: 100%;\n border-radius: var(--media-radius);\n border: 1px solid var(--site-media-outline);\n}\n\n.post-card-figure video,\n.reply-figure video,\n.thread-figure video {\n display: block;\n width: 100%;\n height: auto;\n border-radius: var(--media-radius);\n border: 1px solid var(--site-media-outline);\n background: #000;\n}\n\n.post-card-figure audio,\n.reply-figure audio,\n.thread-figure audio {\n display: block;\n width: 100%;\n}\n\n.post-card-figure-video a {\n position: relative;\n display: block;\n}\n\n.post-card-video-badge {\n position: absolute;\n left: 0.5rem;\n bottom: 0.5rem;\n padding: 0.125rem 0.5rem;\n font-size: var(--type-xs);\n color: #fff;\n background: rgba(0, 0, 0, 0.65);\n border-radius: 999px;\n pointer-events: none;\n}\n\n.thread-file a,\n.reply-file a {\n color: var(--site-link);\n text-decoration: underline;\n}\n\n/* -------------------------------------------------------------------------\n * Quote format — decorative mark, serif body, attribution line\n * ------------------------------------------------------------------------- */\n\n.post-card-quote {\n position: static;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--site-reading-quote);\n font-family: inherit;\n font-style: normal;\n}\n\n/* Quote-format posts have their own decorative `.post-card-quote-mark`\n SVG inside the blockquote — suppress the global blockquote icon. */\n.post-card-quote::before {\n content: none;\n}\n\n.post-card-quote-mark {\n display: block;\n position: relative;\n width: 1.7rem;\n margin-bottom: -0.1rem;\n margin-left: -0.04rem;\n line-height: 0;\n pointer-events: none;\n color: color-mix(in srgb, var(--site-accent) 14%, var(--site-divider));\n opacity: 0.66;\n}\n\n.post-card-quote-mark svg {\n display: block;\n width: 100%;\n height: auto;\n}\n\n.post-card-quote-content {\n font-family: var(--font-serif);\n color: var(--site-text-primary);\n font-size: var(--type-content-quote);\n line-height: var(--type-content-quote-leading);\n white-space: pre-line;\n text-wrap: pretty;\n margin: 0;\n}\n\n.post-card-quote-attribution {\n display: flex;\n align-items: center;\n gap: 0.45rem;\n flex-wrap: wrap;\n margin-top: 0.95rem;\n color: var(--site-text-secondary);\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n font-style: normal;\n line-height: 1.3;\n}\n\n.post-card-quote-attribution::before {\n content: \"\";\n width: 0.9rem;\n height: 1px;\n background: color-mix(\n in srgb,\n var(--site-text-secondary) 38%,\n var(--site-divider)\n );\n}\n\n.post-card-quote-source {\n color: inherit;\n text-decoration: underline;\n text-decoration-color: color-mix(\n in srgb,\n var(--site-text-secondary) 55%,\n transparent\n );\n text-underline-offset: 3px;\n}\n\n.post-card-quote-source:hover,\n.post-card-quote-source:focus {\n color: var(--site-text-primary);\n text-decoration-color: currentColor;\n}\n\n.post-card-quote-commentary {\n position: relative;\n margin-top: 1.1rem;\n padding-top: 0.95rem;\n color: color-mix(\n in srgb,\n var(--site-text-secondary) 84%,\n var(--site-text-primary)\n );\n text-wrap: pretty;\n}\n\n.post-card-quote-commentary::before {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n height: 1px;\n background: linear-gradient(\n 90deg,\n transparent 0%,\n color-mix(in srgb, var(--site-divider) 48%, transparent) 16%,\n color-mix(in srgb, var(--site-divider) 78%, transparent) 50%,\n color-mix(in srgb, var(--site-divider) 48%, transparent) 84%,\n transparent 100%\n );\n}\n\n.post-card-quote-commentary.prose > :first-child {\n margin-top: 0;\n}\n\n.post-card-quote-commentary.prose > :last-child {\n margin-bottom: 0;\n}\n\n.post-card-quote-commentary p {\n margin: 0;\n}\n\n.post-card-quote-commentary p + p,\n.post-card-quote-commentary ul,\n.post-card-quote-commentary ol,\n.post-card-quote-commentary blockquote,\n.post-card-quote-commentary pre {\n margin-top: 0.55rem;\n}\n\n/* Fade the post meta on quote cards so the quote body stays visually primary. */\n.post-card-quote ~ .post-card-footer,\n.post-card-quote-commentary + .post-card-footer {\n opacity: 0.72;\n}\n\n/* -------------------------------------------------------------------------\n * Post footer — meta (featured, time, external link, collections, pinned)\n * ------------------------------------------------------------------------- */\n\n.post-card-footer,\n.reply-footer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 10px;\n min-height: 2rem;\n font-size: var(--type-ui-hint);\n}\n\n.post-footer-detail {\n margin-top: 24px;\n /* Match the feed/reply footer size so the root post's footer doesn't\n visually dominate. Main site uses a larger size because every post\n on the detail page is rendered at detail size; here only the root\n post gets this class, which otherwise creates a mismatch with the\n replies below. */\n font-size: var(--type-ui-hint);\n color: var(--site-text-secondary);\n}\n\n.post-footer-meta {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n min-width: 0;\n font-family: var(--font-ui);\n line-height: 1.35;\n color: var(--site-text-secondary);\n}\n\n.post-footer-meta time {\n font-size: inherit;\n color: inherit;\n}\n\n.post-footer-featured {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: color-mix(\n in srgb,\n var(--search-mark-color) 72%,\n var(--site-text-secondary)\n );\n flex-shrink: 0;\n}\n\n.post-footer-featured svg {\n width: 1rem;\n height: 1rem;\n opacity: 0.9;\n}\n\n.post-footer-link {\n color: var(--site-text-secondary);\n text-decoration: none;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.post-footer-link:hover,\n.post-footer-link:focus {\n color: var(--site-text-primary);\n text-decoration: underline;\n}\n\n.post-footer-external-link {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.6rem;\n height: 1.6rem;\n border-radius: 0.6rem;\n color: var(--site-text-secondary);\n text-decoration: none;\n transition:\n color 0.18s ease,\n background-color 0.16s ease;\n flex-shrink: 0;\n}\n\n.post-footer-external-link:hover,\n.post-footer-external-link:focus {\n color: var(--site-text-primary);\n}\n\n.post-footer-external-link svg {\n width: 1rem;\n height: 1rem;\n}\n\n.post-collection-tags {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n min-width: 0;\n max-width: 100%;\n color: var(--site-text-secondary);\n}\n\n.post-collection-tag {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n color: inherit;\n text-decoration: none;\n min-width: 0;\n max-width: min(100%, 22ch);\n}\n\n.post-collection-tag:hover,\n.post-collection-tag:focus {\n color: var(--site-text-primary);\n text-decoration: underline;\n text-underline-offset: 0.18em;\n}\n\n.post-collection-tag-text {\n display: block;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.post-card-pin {\n font-size: var(--type-2xs);\n text-transform: uppercase;\n letter-spacing: var(--type-label-tracking);\n color: var(--site-accent);\n border: 1px solid var(--site-accent);\n padding: 0.1em 0.5em;\n border-radius: 999px;\n flex-shrink: 0;\n}\n\n/* -------------------------------------------------------------------------\n * Thread preview (list pages: root context + hero latest reply)\n *\n * Mirrors the main site's `.thread-group.thread-group-preview` layout:\n * content sits flush-left inside the preview, and the vertical rail +\n * dot markers are positioned OUTSIDE the content (overflowing into the\n * container's left gutter) via a negative `left` on the rail/dots.\n * ------------------------------------------------------------------------- */\n\n.thread-preview {\n /* Rail position: negative = overflow outside content. Mirrors\n `--site-thread-rail-line-left` on the main site. */\n --thread-rail-left: -27px;\n --thread-rail-width: 1px;\n --thread-rail-indent: 0px;\n --thread-dot-size: 10px;\n --thread-dot-ring-width: 2px;\n --thread-dot-border-width: 2px;\n --thread-hero-dot-size: 14px;\n --thread-hero-dot-border-width: 3px;\n --thread-item-spacing: 0.35rem;\n\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 0.6rem;\n padding-left: var(--thread-rail-indent);\n}\n\n@media (max-width: 760px) {\n .thread-preview {\n --thread-rail-left: -11px;\n --thread-rail-indent: 8px;\n }\n}\n\n/* Continuous vertical rail — subtle gradient so the line fades into\n whitespace at the top and tail. Sits outside the content via negative\n `left`. */\n.thread-preview::before {\n content: \"\";\n position: absolute;\n left: var(--thread-rail-left);\n top: 0;\n bottom: 0;\n width: var(--thread-rail-width);\n background: linear-gradient(\n 180deg,\n transparent 0,\n color-mix(in srgb, var(--site-threadline) 85%, transparent) 8%,\n color-mix(in srgb, var(--site-threadline) 55%, transparent) 100%\n );\n pointer-events: none;\n}\n\n.thread-preview-context {\n position: relative;\n display: flex;\n flex-direction: column;\n /* Match runtime `.thread-group-preview .thread-item` which uses\n `padding: var(--site-thread-item-spacing) 0` — adjacent items have\n 2x the token between them, so the flex `gap` here is 2x to match. */\n gap: calc(var(--site-thread-item-spacing) * 2);\n margin: 0;\n padding: 0;\n border-left: 0;\n}\n\n/* Individual entry in a thread preview — wraps a full `.post-card` so\n the root, second, and penultimate replies render with their own\n title/body/footer. Dot marker sits on the rail at the card's\n vertical midpoint. */\n.thread-preview .thread-item {\n position: relative;\n min-width: 0;\n max-width: 100%;\n}\n\n.thread-preview .thread-item::before {\n content: \"\";\n position: absolute;\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-dot-size) / 2 - var(--thread-rail-indent)\n );\n top: 1.4rem;\n width: var(--thread-dot-size);\n height: var(--thread-dot-size);\n border-radius: 50%;\n background-color: var(--site-threadline);\n border: var(--thread-dot-border-width) solid var(--site-page-bg);\n box-shadow: 0 0 0 var(--thread-dot-ring-width) var(--site-thread-dot-ring);\n z-index: 1;\n}\n\n/* Gap \"N more posts\" row has no card so we place the marker at its\n vertical midpoint instead of the card's top area. Swap the single dot\n for a short column of small beads (a vertical \"⋮\") so the rail visibly\n \"skips\" a run of posts — mirrors the main site's `.thread-item-gap`. */\n.thread-preview .thread-item-gap {\n display: flex;\n align-items: center;\n padding: 0.15rem 0 0.35rem;\n}\n\n.thread-preview .thread-item-gap::before {\n top: 50%;\n transform: translateY(-50%);\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 - 2px -\n var(--thread-rail-indent)\n );\n width: 4px;\n height: 24px;\n border: 0;\n border-radius: 0;\n background: radial-gradient(\n circle,\n var(--site-threadline) 1.75px,\n transparent 2px\n );\n background-size: 4px 8px;\n background-repeat: repeat-y;\n background-position: center top;\n box-shadow: none;\n}\n\n/* Hidden-posts count — a calm route into the thread, matching the main\n site's `.thread-gap-link`: styled like a sibling of the Show more toggle\n with a small right-pointing chevron rather than an underline, firming up\n to the primary text colour on hover. */\n.thread-preview-gap {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n align-self: flex-start;\n margin: 0.15rem 0;\n padding: 0.25rem 0;\n color: var(--site-text-secondary);\n font-size: var(--type-thread-context-meta);\n font-weight: 500;\n line-height: 1.2;\n text-decoration: none;\n transition: color 0.18s ease;\n}\n\n.thread-preview-gap::after {\n content: \"\";\n width: 0.36em;\n height: 0.36em;\n border: 1.5px solid currentColor;\n border-left: 0;\n border-bottom: 0;\n opacity: 0.65;\n transform: rotate(45deg);\n transition:\n transform 0.18s ease,\n opacity 0.18s ease;\n}\n\n.thread-preview-gap:hover,\n.thread-preview-gap:focus {\n color: var(--site-text-primary);\n}\n\n.thread-preview-gap:hover::after,\n.thread-preview-gap:focus::after {\n opacity: 1;\n transform: translateX(2px) rotate(45deg);\n}\n\n/* Hero (latest reply) row: spacing above and below matches the main\n site's `.thread-item-hero`, and the dot is larger + accent-colored. */\n.thread-preview-hero {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding-top: calc(var(--space-xl) * 1.1);\n margin-top: 0;\n}\n\n.thread-preview .thread-item-hero::before {\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-hero-dot-size) / 2 - var(--thread-rail-indent)\n );\n top: calc(var(--space-xl) * 1.1 + 1.4rem);\n width: var(--thread-hero-dot-size);\n height: var(--thread-hero-dot-size);\n background-color: var(--site-accent);\n border-width: var(--thread-hero-dot-border-width);\n}\n\n.thread-preview-thread-link {\n align-self: flex-start;\n font-size: var(--type-sm);\n color: var(--site-reading-meta);\n text-decoration: none;\n}\n\n.thread-preview-thread-link:hover,\n.thread-preview-thread-link:focus {\n color: var(--site-text-primary);\n text-decoration: underline;\n}\n\n/* -------------------------------------------------------------------------\n * Thread (single post page with inline replies)\n * ------------------------------------------------------------------------- */\n\n.thread {\n /* Rail variables match `.thread-preview` so the detail-page rail\n shares the same visual position. */\n --thread-rail-left: -27px;\n --thread-rail-width: 1px;\n --thread-rail-indent: 0px;\n --thread-dot-size: 10px;\n --thread-dot-ring-width: 2px;\n --thread-dot-border-width: 2px;\n --thread-hero-dot-size: 14px;\n --thread-hero-dot-border-width: 3px;\n\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n padding-left: var(--thread-rail-indent);\n}\n\n@media (max-width: 760px) {\n .thread {\n --thread-rail-left: -11px;\n --thread-rail-indent: 8px;\n }\n}\n\n/* Continuous vertical rail spanning the entire thread (root post + replies).\n Only shown when the root post actually has replies — a lone post should\n not have a rail or dot. Matches `.thread-preview`. */\n.thread-has-replies::before {\n content: \"\";\n position: absolute;\n left: var(--thread-rail-left);\n top: 0;\n bottom: 0;\n width: var(--thread-rail-width);\n background: linear-gradient(\n 180deg,\n transparent 0,\n color-mix(in srgb, var(--site-threadline) 85%, transparent) 8%,\n color-mix(in srgb, var(--site-threadline) 55%, transparent) 100%\n );\n pointer-events: none;\n}\n\n/* Dot marker for each post in the thread (root + replies). Shown only when\n there are replies so a solo post doesn't get an orphaned dot. */\n.thread .thread-item {\n position: relative;\n min-width: 0;\n max-width: 100%;\n}\n\n.thread-has-replies .thread-item::before {\n content: \"\";\n position: absolute;\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-dot-size) / 2 - var(--thread-rail-indent)\n );\n top: 1.4rem;\n width: var(--thread-dot-size);\n height: var(--thread-dot-size);\n border-radius: 50%;\n background-color: var(--site-threadline);\n border: var(--thread-dot-border-width) solid var(--site-page-bg);\n box-shadow: 0 0 0 var(--thread-dot-ring-width) var(--site-thread-dot-ring);\n z-index: 1;\n}\n\n.thread-item-root {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.thread-header {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.thread-title {\n font-family: var(--font-heading);\n font-size: var(--type-content-display);\n font-weight: var(--type-display-weight);\n line-height: var(--type-display-leading);\n margin: 0;\n color: var(--site-reading-title);\n}\n\n.thread-title a {\n color: inherit;\n text-decoration: none;\n}\n\n.thread-title a:hover,\n.thread-title a:focus {\n text-decoration: underline;\n text-underline-offset: 0.18em;\n}\n\n.thread-body {\n font-size: var(--type-content-body);\n line-height: var(--type-body-leading);\n color: var(--site-reading-body);\n}\n\n.thread-body > :first-child {\n margin-top: 0;\n}\n\n.thread-body > :last-child {\n margin-bottom: 0;\n}\n\n.thread-replies {\n /* Flex container for replies. The rail and dots are provided by\n `.thread::before` and `.thread-item::before` so replies stay visually\n connected to the root post. */\n display: flex;\n flex-direction: column;\n gap: var(--space-xl);\n}\n\n/* -------------------------------------------------------------------------\n * Replies\n * ------------------------------------------------------------------------- */\n\n.reply {\n scroll-margin-top: 1.5rem;\n display: flex;\n flex-direction: column;\n gap: 0.6rem;\n}\n\n.reply:target {\n background-color: var(--search-mark-bg);\n border-radius: 0.25rem;\n padding: 0.75rem 1rem;\n margin-left: -1rem;\n margin-right: -1rem;\n}\n\n.reply:target::before {\n background-color: var(--site-accent);\n}\n\n.reply-title {\n font-family: var(--font-heading);\n font-size: var(--type-content-subtitle);\n font-weight: var(--type-heading-weight);\n line-height: var(--type-heading-leading);\n margin: 0;\n color: var(--site-reading-heading);\n}\n\n.reply-body {\n color: var(--site-reading-body);\n}\n\n.reply-body > :first-child {\n margin-top: 0;\n}\n\n.reply-body > :last-child {\n margin-bottom: 0;\n}\n\n/* -------------------------------------------------------------------------\n * Collections page\n *\n * Mirrors the main site's authenticated-less collections view\n * (`ui/pages/CollectionsPage.tsx` + `ui/shared/CollectionDirectory.tsx`):\n * a page-intro block with count, then a two-column grid per row where a\n * monospace sequence label sits beside the collection title, description,\n * and entry/activity meta.\n * ------------------------------------------------------------------------- */\n\n.collections-page-shell {\n position: relative;\n display: flex;\n flex-direction: column;\n width: var(--layout-content-width);\n max-width: 100%;\n gap: clamp(1.25rem, 3vw, 1.75rem);\n}\n\n@media (min-width: 761px) and (max-width: 1024px) {\n .collections-page-shell {\n width: min(100%, 35rem);\n }\n}\n\n@media (max-width: 760px) {\n .collections-page-shell {\n width: 100%;\n }\n}\n\n.collections-page-header {\n position: relative;\n display: flex;\n align-items: flex-start;\n padding-bottom: 0.2rem;\n}\n\n.collections-page-heading {\n min-width: 0;\n flex: 1 1 18rem;\n}\n\n.page-intro {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding-bottom: 0.1rem;\n}\n\n.page-intro-title-row {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0.7rem;\n min-width: 0;\n}\n\n.page-intro-title {\n margin: 0;\n font-family: var(--font-heading);\n font-size: var(--type-title, 2rem);\n font-weight: var(--type-heading-weight);\n line-height: 1.15;\n letter-spacing: -0.01em;\n color: var(--site-text-primary);\n}\n\n.page-intro-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem 1rem;\n}\n\n.page-intro-meta,\n.page-intro-description {\n margin: 0;\n color: var(--site-text-secondary);\n font-family: var(--font-ui);\n font-size: var(--type-secondary);\n line-height: 1.3;\n}\n\n.collection-directory {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.collection-directory-item {\n position: relative;\n display: flex;\n align-items: flex-start;\n gap: 0.75rem;\n padding: 0.95rem 0;\n background: transparent;\n text-decoration: none;\n}\n\n.collection-directory-main {\n --collection-directory-sequence-width: 3.5ch;\n --collection-directory-title-line-height: 1.18;\n min-width: 0;\n flex: 1;\n display: grid;\n grid-template-columns: var(--collection-directory-sequence-width) minmax(\n 0,\n 1fr\n );\n align-items: start;\n column-gap: 0.8rem;\n row-gap: 0.25rem;\n}\n\n.collection-directory-sequence {\n grid-column: 1;\n grid-row: 1;\n display: block;\n width: var(--collection-directory-sequence-width);\n padding-top: 0.2rem;\n font-family: var(--font-mono);\n font-size: var(--type-xs);\n font-variant-numeric: tabular-nums;\n line-height: var(--collection-directory-title-line-height);\n letter-spacing: 0.14em;\n color: var(--site-text-secondary);\n}\n\n.collection-directory-title-row {\n grid-column: 2;\n grid-row: 1;\n min-width: 0;\n display: flex;\n align-items: flex-start;\n}\n\n.collection-directory-title-link {\n color: inherit;\n text-decoration: none;\n transition: color 0.15s ease;\n}\n\n.collection-directory-title-link:hover,\n.collection-directory-title-link:focus-visible {\n color: var(--site-text-primary);\n}\n\n.collection-directory-title-link:hover .collection-directory-title,\n.collection-directory-title-link:focus-visible .collection-directory-title {\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n\n.collection-directory-title {\n min-width: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.45rem;\n font-family: var(--font-heading);\n font-size: var(--type-content-body);\n font-weight: var(--type-heading-weight);\n line-height: var(--collection-directory-title-line-height);\n letter-spacing: -0.02em;\n text-wrap: pretty;\n}\n\n.collection-directory-title-marker {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 0.95rem;\n min-width: 0.95rem;\n height: 0.95rem;\n color: var(--site-text-secondary);\n transition: color 0.15s ease;\n}\n\n.collection-directory-title-link:hover .collection-directory-title-marker,\n.collection-directory-title-link:focus-visible\n .collection-directory-title-marker {\n color: var(--site-text-primary);\n}\n\n.collection-directory-description {\n grid-column: 2;\n grid-row: 2;\n margin: 0;\n color: color-mix(in srgb, var(--site-text-secondary) 80%, transparent);\n font-family: var(--font-body);\n font-size: var(--type-sm);\n line-height: 1.45;\n}\n\n.collection-directory-description :where(p) {\n margin-top: 0.25em;\n margin-bottom: 0.25em;\n}\n\n/* Links inside the description inherit the description color so they sit in\n the same gray tone as the copy around them (matches the main site's\n `.prose { --tw-prose-links: var(--site-content-link); }` which resolves\n to `inherit`). The underline stays, drawn in currentColor. */\n.collection-directory-description :where(a) {\n color: inherit;\n text-decoration: underline;\n text-decoration-color: currentColor;\n text-underline-offset: 0.15em;\n}\n\n.collection-directory-description :where(a:hover),\n.collection-directory-description :where(a:focus-visible) {\n color: var(--site-text-primary);\n}\n\n.collection-directory-description :where(p:first-child) {\n margin-top: 0;\n}\n\n.collection-directory-description :where(p:last-child) {\n margin-bottom: 0;\n}\n\n.collection-directory-description + .collection-directory-summary {\n grid-row: 3;\n}\n\n.collection-directory-summary {\n grid-column: 2;\n grid-row: 2;\n display: flex;\n min-width: 0;\n overflow: hidden;\n align-items: center;\n gap: 0.2rem 0.5rem;\n margin: 0;\n color: var(--site-reading-meta);\n font-family: var(--font-ui);\n font-size: var(--type-sm);\n line-height: 1.3;\n white-space: nowrap;\n}\n\n.collection-directory-meta {\n flex: 0 0 auto;\n color: inherit;\n}\n\n.collection-directory-meta-separator {\n flex: 0 0 auto;\n color: color-mix(in srgb, var(--site-divider) 88%, transparent);\n}\n\n.collection-directory-updated {\n flex: 0 0 auto;\n color: inherit;\n white-space: nowrap;\n}\n\n.collection-directory-divider {\n padding: 1.5rem 0 0.85rem;\n}\n\n.collection-directory-divider-row {\n display: flex;\n align-items: center;\n gap: 0.95rem;\n}\n\n.collection-directory-divider-text {\n font-family: var(--font-heading);\n font-size: var(--type-secondary);\n letter-spacing: 0;\n font-style: normal;\n white-space: nowrap;\n color: var(--site-text-secondary);\n}\n\n.collection-directory-divider-line {\n flex: 1;\n height: 1px;\n border: none;\n margin: 0;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--site-divider) 100%, transparent),\n color-mix(in srgb, var(--site-divider) 54%, transparent) 34%,\n transparent 86%\n );\n}\n\n/* -------------------------------------------------------------------------\n * Pagination\n * ------------------------------------------------------------------------- */\n\n.pagination {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-wrap: wrap;\n gap: 1rem;\n padding: 1.5rem 0;\n font-size: var(--type-sm);\n font-variant-numeric: tabular-nums;\n}\n\n.pagination-link {\n color: var(--site-text-secondary);\n text-decoration: underline;\n text-underline-offset: 3px;\n transition: color 0.15s ease;\n}\n\n.pagination-link:hover,\n.pagination-link:focus {\n color: var(--site-text-primary);\n}\n\n.pagination-link.is-disabled {\n color: color-mix(in srgb, var(--site-text-secondary) 50%, transparent);\n cursor: default;\n text-decoration: none;\n}\n\n.pagination-current {\n color: var(--site-text-primary);\n font-weight: var(--fw-medium);\n}\n\n.pagination-ellipsis {\n color: var(--site-text-secondary);\n}\n\n/* -------------------------------------------------------------------------\n * Empty states + utility\n * ------------------------------------------------------------------------- */\n\n.empty-state {\n color: var(--site-reading-meta);\n font-style: italic;\n text-align: center;\n padding: var(--space-xl) 0;\n}\n\n.page,\n.section {\n display: block;\n}\n\n.page-summary,\n.section-summary {\n font-size: var(--type-secondary);\n color: var(--site-reading-meta);\n}\n\n/* -------------------------------------------------------------------------\n * Wider viewport refinements\n * ------------------------------------------------------------------------- */\n\n@media (min-width: 768px) {\n .site-main {\n padding-top: calc(var(--space-xl) * 1.5);\n padding-bottom: calc(var(--space-xl) * 1.5);\n }\n\n .post-card-media,\n .thread-media,\n .reply-media {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .post-card-media:has(> :only-child),\n .thread-media:has(> :only-child),\n .reply-media:has(> :only-child) {\n grid-template-columns: 1fr;\n }\n}\n";
|
|
4445
|
+
var main_default = "/*\n * Jant Hugo Export — main.css\n *\n * Fresh minimal text-first design. All colors, spacing, and type sizing\n * come from tokens.css (loaded first in the <head>). Color theme values\n * are supplied by theme.css; customizations live in custom.css.\n *\n * Load order in <head>: tokens.css → main.css → theme.css → custom.css\n *\n * This file intentionally avoids any hardcoded hex, rgb, or px color\n * values. Everything token-driven so the design evolves with the theme.\n */\n\n/* -------------------------------------------------------------------------\n * Reset + box model\n * ------------------------------------------------------------------------- */\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 15px;\n -webkit-text-size-adjust: 100%;\n text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n font-family: var(--font-body);\n font-size: var(--type-body-size, var(--type-content-body));\n line-height: var(--type-body-leading);\n color: var(--site-reading-body);\n background-color: var(--site-page-bg);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nimg,\nvideo,\naudio,\niframe {\n max-width: 100%;\n height: auto;\n}\n\nfigure {\n margin: 0;\n}\n\nhr {\n border: 0;\n border-top: 1px solid var(--site-divider);\n margin: var(--space-xl) 0;\n}\n\n/* -------------------------------------------------------------------------\n * Typography\n * ------------------------------------------------------------------------- */\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-family: var(--font-heading);\n font-weight: var(--type-heading-weight);\n line-height: var(--type-heading-leading);\n letter-spacing: var(--type-heading-tracking);\n color: var(--site-reading-heading);\n margin: 1.6em 0 0.6em;\n}\n\nh1 {\n font-size: var(--type-content-display);\n line-height: var(--type-display-leading);\n font-weight: var(--type-display-weight);\n letter-spacing: var(--type-display-tracking);\n margin: 4rem 0 1.5rem;\n}\n\nh2 {\n font-size: var(--type-content-title);\n margin: 2.1rem 0 1.4rem;\n}\n\nh3 {\n font-size: var(--type-content-subtitle);\n margin: 2rem 0 1.4rem;\n}\n\nh4 {\n font-size: var(--type-content-body);\n font-weight: var(--fw-medium);\n}\n\nh5,\nh6 {\n font-size: var(--type-content-body);\n font-weight: var(--fw-medium);\n color: var(--site-reading-meta);\n}\n\np {\n margin: 1.4rem 0;\n}\n\nsmall {\n font-size: var(--type-sm);\n}\n\ncode,\nkbd,\nsamp {\n font-family: var(--font-mono);\n font-size: var(--type-code);\n line-height: 1.42;\n}\n\ncode {\n color: var(--site-code-text);\n overflow-wrap: break-word;\n}\n\npre {\n font-family: var(--font-mono);\n font-size: var(--type-code-block);\n line-height: 1.5;\n padding: 1rem;\n overflow-x: auto;\n color: var(--site-code-text);\n background-color: var(--site-code-block-bg);\n border: 1px solid var(--site-code-block-border);\n border-radius: 6px;\n}\n\npre code {\n padding: 0;\n background: transparent;\n border: 0;\n color: inherit;\n font-size: inherit;\n}\n\n:not(pre) > code {\n padding: 0.08em 0.28em;\n background-color: var(--site-code-bg);\n border-radius: 0.22em;\n box-decoration-break: clone;\n -webkit-box-decoration-break: clone;\n}\n\n/* Blockquotes — tinted background card with quote icon (matches main site). */\nblockquote {\n position: relative;\n margin: 1.4rem 0;\n padding: 1.4rem 1rem 0.75rem;\n border: none;\n background: var(--site-blockquote-bg);\n border-radius: 6px;\n color: var(--site-blockquote-text);\n font-family: var(--font-blockquote);\n font-style: italic;\n font-weight: inherit;\n quotes: none;\n text-wrap: pretty;\n}\n\nblockquote::before {\n content: \"\";\n display: block;\n width: 1.3rem;\n height: 1.3rem;\n margin-bottom: 0.35rem;\n background: var(--site-blockquote-rail);\n opacity: 0.6;\n mask-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z'/%3E%3Cpath d='M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z'/%3E%3C/svg%3E\");\n mask-size: contain;\n mask-repeat: no-repeat;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z'/%3E%3Cpath d='M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n}\n\nblockquote :where(p:first-of-type)::before,\nblockquote :where(p:last-of-type)::after {\n content: none;\n}\n\nblockquote > :first-child {\n margin-top: 0;\n}\n\nblockquote > :last-child {\n margin-bottom: 0;\n}\n\nblockquote p {\n margin: 0;\n}\n\nblockquote p + p,\nblockquote ul,\nblockquote ol,\nblockquote pre {\n margin-top: 0.6em;\n}\n\nblockquote cite {\n font-style: normal;\n color: var(--site-reading-meta);\n font-size: var(--type-sm);\n}\n\n/* CJK: no italic for blockquotes (no true italic glyph) */\n:lang(zh) blockquote,\n:lang(ja) blockquote,\n:lang(ko) blockquote {\n font-style: normal;\n}\n\nul,\nol {\n padding-left: 1.4em;\n margin: 1.25em 0;\n}\n\nol ol {\n list-style-type: lower-alpha;\n margin-top: 0.4em;\n margin-bottom: 0.4em;\n}\n\nol ol ol {\n list-style-type: lower-roman;\n}\n\nli {\n margin-top: 0.5em;\n margin-bottom: 0.5em;\n}\n\n/* Normalize li > p so list spacing is controlled by li alone,\n regardless of whether the markdown renderer wraps li contents in <p>. */\nli > p:first-child {\n margin-top: 0;\n}\n\nli > p:last-child {\n margin-bottom: 0;\n}\n\nli > p:has(+ ol) {\n margin-bottom: 0;\n}\n\na {\n color: var(--site-reading-link);\n text-decoration: underline;\n text-decoration-color: var(--site-reading-link-underline);\n text-underline-offset: 0.15em;\n transition:\n color 0.2s ease,\n text-decoration-color 0.2s ease;\n}\n\na:hover,\na:focus {\n color: var(--site-reading-link-hover);\n text-decoration-color: currentColor;\n}\n\na:focus-visible {\n outline: 2px solid var(--site-accent);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\ntime {\n color: var(--site-reading-meta);\n font-size: var(--type-sm);\n font-variant-numeric: tabular-nums;\n}\n\n/* -------------------------------------------------------------------------\n * Page layout — Tufte horizontal frame\n *\n * Mirrors the main site's `.site-page > header/main/footer` rule. Every\n * top-level section gets the same asymmetric padding so the reading column\n * aligns with the rest of the site.\n *\n * 12.5% left + 4% right = 16.5% padding → content = 83.5% of the box.\n * max-width = body-max-width / 0.835 so the inner content area exactly\n * equals `--layout-body-max-width` on wide viewports. min() caps keep\n * padding from growing beyond 210px / 67px. Mobile widens to 5% / 5%\n * and the 55% content column collapses to 100% (via tokens.css).\n * ------------------------------------------------------------------------- */\n\n.site-page {\n min-height: 100vh;\n min-height: 100dvh;\n background-color: var(--site-page-bg);\n}\n\n.site-page > header,\n.site-page > main,\n.site-page > footer,\n.site-page > .home-branding-credit {\n width: 100%;\n max-width: calc(var(--layout-body-max-width) / 0.835);\n padding-left: min(12.5%, 210px);\n padding-right: min(4%, 67px);\n margin-left: auto;\n margin-right: auto;\n}\n\n@media (max-width: 760px) {\n .site-page > header,\n .site-page > main,\n .site-page > footer,\n .site-page > .home-branding-credit {\n padding-left: max(5%, 28px);\n padding-right: 5%;\n }\n}\n\n.site-main {\n padding-top: var(--space-xl);\n padding-bottom: var(--space-xl);\n}\n\n/* -------------------------------------------------------------------------\n * Header\n * ------------------------------------------------------------------------- */\n\n.site-header {\n padding-top: 24px;\n background-color: var(--site-page-bg);\n}\n\n@media (min-width: 700px) {\n .site-header {\n padding-top: 30px;\n }\n}\n\n.site-header-inner {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 0;\n}\n\n.site-header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: clamp(0.6rem, 2.2vw, 1rem);\n flex-wrap: nowrap;\n min-height: 2.75rem;\n width: 100%;\n}\n\n.site-header-top-bordered {\n padding-bottom: 15px;\n}\n\n@media (min-width: 700px) {\n .site-header-top-bordered {\n padding-bottom: 18px;\n }\n}\n\n.site-logo {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: 10px;\n min-width: 0;\n padding: 0.15rem 0;\n font-family: var(--font-site-title);\n font-size: var(--type-subtitle);\n font-weight: var(--fw-regular);\n letter-spacing: -0.02em;\n line-height: 1.15;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--site-text-primary);\n text-decoration: none;\n}\n\n.site-logo:hover,\n.site-logo:focus {\n color: var(--site-text-primary);\n text-decoration: none;\n}\n\n.site-logo-avatar {\n box-sizing: border-box;\n width: calc(var(--avatar-size) + 4px);\n height: calc(var(--avatar-size) + 4px);\n border-radius: var(--avatar-radius);\n object-fit: cover;\n border: 1px solid color-mix(in srgb, var(--site-divider) 82%, transparent);\n flex: none;\n}\n\n.site-logo-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.site-header-nav {\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n justify-content: flex-end;\n gap: clamp(0.6rem, 2.2vw, 1rem);\n margin-left: auto;\n min-width: 0;\n font-family: var(--font-ui);\n}\n\n.site-header-link {\n display: inline-flex;\n flex: none;\n align-items: center;\n position: relative;\n min-height: 2rem;\n padding: 0.15rem 0;\n font-size: var(--type-ui-meta);\n font-weight: var(--fw-medium);\n letter-spacing: 0.01em;\n line-height: 1;\n white-space: nowrap;\n color: color-mix(in srgb, var(--site-text-secondary) 62%, transparent);\n text-decoration: none;\n transition: color 0.15s;\n}\n\n.site-header-link:hover,\n.site-header-link:focus {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n text-decoration: none;\n}\n\n.site-header-link-active {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n}\n\n/* --- \"More\" dropdown ---------------------------------------------------- */\n\n.site-header-more {\n position: relative;\n display: inline-flex;\n align-items: center;\n}\n\n.site-header-more-responsive-only {\n display: none;\n}\n\n.site-header-more-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.45rem;\n min-height: 2rem;\n padding: 0.15rem 0;\n border: none;\n background: transparent;\n cursor: pointer;\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n font-weight: var(--fw-medium);\n letter-spacing: 0.01em;\n line-height: 1;\n color: color-mix(in srgb, var(--site-text-secondary) 62%, transparent);\n transition: color 0.15s;\n}\n\n.site-header-more-btn svg {\n width: 0.82rem;\n height: 0.82rem;\n transition: transform 0.18s ease;\n}\n\n.site-header-more-btn:hover,\n.site-header-more-btn[aria-expanded=\"true\"] {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n}\n\n.site-header-more-btn[aria-expanded=\"true\"] svg {\n transform: rotate(180deg);\n}\n\n.site-header-more-popover {\n display: block;\n position: absolute;\n top: 100%;\n right: 0;\n margin-top: 0.6rem;\n min-width: 12.25rem;\n padding: 0.3rem 0;\n background: var(--site-page-bg);\n border: 0.5px solid color-mix(in srgb, var(--site-divider) 80%, transparent);\n border-radius: 0.4rem;\n box-shadow:\n 0 4px 20px -8px rgba(0, 0, 0, 0.12),\n 0 2px 6px -2px rgba(0, 0, 0, 0.06);\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transform: translateY(-6px);\n transform-origin: top right;\n transition:\n opacity 0.18s ease,\n transform 0.18s ease,\n visibility 0s linear 0.18s;\n z-index: 50;\n}\n\n.site-header-more-popover[aria-hidden=\"false\"] {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n transform: translateY(0);\n transition-delay: 0s;\n}\n\n.site-header-more-link {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.35rem;\n padding: 0.45rem 1rem;\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n color: var(--site-text-secondary);\n text-decoration: none;\n transition:\n color 0.15s,\n background-color 0.15s;\n}\n\n.site-header-more-link:hover,\n.site-header-more-link:focus {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n background: color-mix(in srgb, var(--site-nav-hover-bg) 58%, transparent);\n text-decoration: none;\n}\n\n.site-header-more-link-active {\n color: color-mix(\n in srgb,\n var(--site-text-primary) 84%,\n var(--site-text-secondary)\n );\n}\n\n.site-header-more-link-responsive,\n.site-header-more-divider-responsive {\n display: none;\n}\n\n.site-header-more-divider {\n height: 0;\n margin: 0.35rem 0.75rem;\n border-top: 0.5px solid\n color-mix(in srgb, var(--site-divider) 60%, transparent);\n}\n\n/* --- Tiered responsive collapse ---------------------------------------- *\n *\n * ≤960px — 5th+ inline link collapses into More (tier-lg)\n * ≤780px — also 4th collapses (tier-md)\n * ≤580px — also 3rd collapses (tier-sm)\n * The first two links always stay inline. No hamburger on static export —\n * at very narrow widths 2 inline links + More button is the floor.\n */\n\n@media (max-width: 960px) {\n .site-header-link-collapse-lg {\n display: none;\n }\n\n .site-header-more-responsive-only.site-header-more-tier-lg {\n display: inline-flex;\n }\n\n .site-header-more-link-show-lg {\n display: flex;\n }\n\n .site-header-more-divider-responsive {\n display: block;\n }\n}\n\n@media (max-width: 780px) {\n .site-header-link-collapse-md {\n display: none;\n }\n\n .site-header-more-responsive-only.site-header-more-tier-md {\n display: inline-flex;\n }\n\n .site-header-more-link-show-md {\n display: flex;\n }\n}\n\n@media (max-width: 580px) {\n .site-header-link-collapse-sm {\n display: none;\n }\n\n .site-header-more-responsive-only.site-header-more-tier-sm {\n display: inline-flex;\n }\n\n .site-header-more-link-show-sm {\n display: flex;\n }\n}\n\n/* -------------------------------------------------------------------------\n * Footer\n * ------------------------------------------------------------------------- */\n\n.site-footer {\n margin-top: var(--space-xl);\n padding-bottom: var(--space-xl);\n color: var(--site-text-secondary);\n font-size: var(--type-xs);\n background-color: var(--site-page-bg);\n}\n\n.site-footer-inner {\n border-top: 0.5px solid var(--site-divider);\n padding-top: var(--space-xl);\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.site-footer-content {\n color: var(--site-text-secondary);\n}\n\n.site-footer-content p {\n margin: 0 0 0.5em;\n}\n\n.site-footer-nav-list {\n display: flex;\n flex-wrap: wrap;\n gap: 1rem;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.home-branding-credit {\n margin-top: var(--space-xl);\n padding-bottom: var(--space-xl);\n text-align: center;\n color: var(--site-text-secondary);\n font-size: var(--type-base);\n}\n\n.home-branding-credit a {\n display: inline-flex;\n align-items: center;\n gap: 0.38rem;\n color: inherit;\n text-decoration: none;\n border-bottom: 0.5px solid\n color-mix(in srgb, var(--site-text-secondary) 45%, transparent);\n transition:\n color 160ms ease,\n border-color 160ms ease;\n}\n\n.home-branding-credit a:hover,\n.home-branding-credit a:focus-visible {\n color: var(--site-text-primary);\n border-color: currentColor;\n}\n\n/* -------------------------------------------------------------------------\n * Tufte content-width constraint\n *\n * Mirrors the main site's 55% rule: reading text occupies a narrow\n * column inside the Tufte frame, leaving a wide right margin that\n * would host sidenotes on the main site. Media (images, video, audio)\n * is NOT included — galleries intentionally span the full frame so\n * they can breathe.\n *\n * Mobile (<=760px): `--layout-content-width` collapses to 100% via\n * tokens.css. Tablet: cap at 35rem for readability.\n * ------------------------------------------------------------------------- */\n\n.section-header,\n.section-body,\n.post-card-title,\n.post-card-summary,\n.post-card-link-domain,\n.post-card-quote-content,\n.post-card-quote-attribution,\n.post-card-quote-commentary,\n.post-card-footer,\n.reply-title,\n.reply-body,\n.reply-link-domain,\n.reply-footer,\n.thread-title,\n.thread-body,\n.thread-link-domain,\n.thread-footer,\n.collection-directory,\n.pagination,\n.empty-state,\n.page-summary {\n width: var(--layout-content-width);\n max-width: 100%;\n}\n\n@media (min-width: 761px) and (max-width: 1024px) {\n .section-header,\n .section-body,\n .post-card-title,\n .post-card-summary,\n .post-card-link-domain,\n .post-card-quote-content,\n .post-card-quote-attribution,\n .post-card-quote-commentary,\n .post-card-footer,\n .reply-title,\n .reply-body,\n .reply-link-domain,\n .reply-footer,\n .thread-title,\n .thread-body,\n .thread-link-domain,\n .thread-footer,\n .collection-directory,\n .pagination,\n .empty-state,\n .page-summary {\n width: min(100%, 35rem);\n }\n}\n\n/* -------------------------------------------------------------------------\n * Section headers\n * ------------------------------------------------------------------------- */\n\n.section-header {\n margin-bottom: var(--space-xl);\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--site-border-light);\n}\n\n.section-title {\n margin: 0 0 0.25em;\n}\n\n.section-summary {\n margin: 0;\n color: var(--site-reading-meta);\n font-size: var(--type-secondary);\n}\n\n.section-meta {\n margin: 0.5em 0 0;\n color: var(--site-reading-meta);\n font-size: var(--type-sm);\n}\n\n/* -------------------------------------------------------------------------\n * Post list + post cards\n * ------------------------------------------------------------------------- */\n\n.post-list {\n display: flex;\n flex-direction: column;\n gap: calc(var(--space-xl) * 1.25);\n}\n\n.post-list-pinned {\n margin-bottom: calc(var(--space-xl) * 1.25);\n padding-bottom: calc(var(--space-xl) * 1.25);\n border-bottom: 1px solid var(--site-border-light);\n}\n\n/* Decorative divider between posts in a timeline feed. Mirrors the main\n site's `hr.feed-divider`: a trio of small chevron marks masked from the\n current text color, so it picks up the theme automatically.\n `margin-left` centers the divider within the 55% reading column so it\n visually sits at the middle of the post-card text stack. */\nhr.feed-divider {\n border: none;\n width: 30px;\n height: 9px;\n margin: 0;\n margin-left: calc(var(--layout-content-width) / 2 - 15px);\n color: var(--site-feed-divider-color);\n background-color: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 13'%3E%3Cpath fill='black' transform='translate(0,0) rotate(90 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(16,0) rotate(100 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(32,0) rotate(80 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 13'%3E%3Cpath fill='black' transform='translate(0,0) rotate(90 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(16,0) rotate(100 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(32,0) rotate(80 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3C/svg%3E\");\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n -webkit-mask-position: center;\n mask-position: center;\n -webkit-mask-size: contain;\n mask-size: contain;\n}\n\n.post-card {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.post-card-title {\n font-family: var(--font-heading);\n font-size: var(--feed-note-title-size);\n font-weight: var(--type-heading-weight);\n line-height: var(--feed-note-title-leading);\n margin: 0;\n color: var(--site-reading-title);\n}\n\n.post-card-title a {\n color: inherit;\n text-decoration: none;\n}\n\n.post-card-title a:hover,\n.post-card-title a:focus {\n text-decoration: underline;\n text-underline-offset: 0.18em;\n}\n\n.post-card-summary {\n margin: 0;\n color: var(--site-reading-body);\n}\n\n.post-card > .post-card-summary :is(h1, h2),\n.post-card > .post-card-quote-commentary :is(h1, h2) {\n font-size: calc(var(--type-content-body) * 1.12);\n font-weight: var(--fw-medium);\n margin-top: 1.45rem;\n margin-bottom: 0.55rem;\n}\n\n.post-card > .post-card-summary :is(h3, h4),\n.post-card > .post-card-quote-commentary :is(h3, h4) {\n font-size: var(--type-content-body);\n font-weight: var(--fw-medium);\n margin-top: 1.2rem;\n margin-bottom: 0.45rem;\n}\n\n.post-card > .post-card-summary :is(h5, h6),\n.post-card > .post-card-quote-commentary :is(h5, h6) {\n font-size: var(--type-secondary);\n font-weight: var(--fw-medium);\n color: var(--site-text-secondary);\n margin-top: 1rem;\n margin-bottom: 0.35rem;\n}\n\n/* Link card domain row — shown ABOVE the title (matches main site's\n `.feed-link-domain` pattern). Inline flex with icon + host text. */\n.post-card-link-domain,\n.thread-link-domain,\n.reply-link-domain {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n max-width: 100%;\n margin: 0 0 0.4rem 0;\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n font-weight: var(--fw-regular);\n line-height: 1.3;\n color: var(--site-text-secondary);\n text-decoration: none;\n word-break: break-all;\n transition: color 0.18s ease;\n}\n\n.post-card-link-domain:hover,\n.post-card-link-domain:focus,\n.thread-link-domain:hover,\n.thread-link-domain:focus,\n.reply-link-domain:hover,\n.reply-link-domain:focus {\n color: var(--site-text-primary);\n}\n\n.post-card-link-domain-icon {\n width: 0.72rem;\n height: 0.72rem;\n flex-shrink: 0;\n}\n\n/* Slight extra breathing room below link-card titles to match main site. */\n.post-card-link-title,\n.thread-link-title,\n.reply-link-title {\n text-wrap: pretty;\n}\n\n.post-card-link-title a,\n.thread-link-title a,\n.reply-link-title a {\n text-decoration: none;\n}\n\n.post-card-link-title a:hover,\n.post-card-link-title a:focus,\n.thread-link-title a:hover,\n.thread-link-title a:focus,\n.reply-link-title a:hover,\n.reply-link-title a:focus {\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n\n.post-card-media,\n.reply-media,\n.thread-media {\n display: grid;\n grid-template-columns: 1fr;\n gap: 0.75rem;\n}\n\n.post-card-figure img,\n.reply-figure img,\n.thread-figure img {\n display: block;\n width: 100%;\n border-radius: var(--media-radius);\n border: 1px solid var(--site-media-outline);\n}\n\n.post-card-figure video,\n.reply-figure video,\n.thread-figure video {\n display: block;\n width: 100%;\n height: auto;\n border-radius: var(--media-radius);\n border: 1px solid var(--site-media-outline);\n background: #000;\n}\n\n.post-card-figure audio,\n.reply-figure audio,\n.thread-figure audio {\n display: block;\n width: 100%;\n}\n\n.post-card-figure-video a {\n position: relative;\n display: block;\n}\n\n.post-card-video-badge {\n position: absolute;\n left: 0.5rem;\n bottom: 0.5rem;\n padding: 0.125rem 0.5rem;\n font-size: var(--type-xs);\n color: #fff;\n background: rgba(0, 0, 0, 0.65);\n border-radius: 999px;\n pointer-events: none;\n}\n\n.thread-file a,\n.reply-file a {\n color: var(--site-link);\n text-decoration: underline;\n}\n\n/* -------------------------------------------------------------------------\n * Quote format — decorative mark, serif body, attribution line\n * ------------------------------------------------------------------------- */\n\n.post-card-quote {\n position: static;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--site-reading-quote);\n font-family: inherit;\n font-style: normal;\n}\n\n/* Quote-format posts have their own decorative `.post-card-quote-mark`\n SVG inside the blockquote — suppress the global blockquote icon. */\n.post-card-quote::before {\n content: none;\n}\n\n.post-card-quote-mark {\n display: block;\n position: relative;\n width: 1.7rem;\n margin-bottom: -0.1rem;\n margin-left: -0.04rem;\n line-height: 0;\n pointer-events: none;\n color: color-mix(in srgb, var(--site-accent) 14%, var(--site-divider));\n opacity: 0.66;\n}\n\n.post-card-quote-mark svg {\n display: block;\n width: 100%;\n height: auto;\n}\n\n.post-card-quote-content {\n font-family: var(--font-serif);\n color: var(--site-text-primary);\n font-size: var(--type-content-quote);\n line-height: var(--type-content-quote-leading);\n white-space: pre-line;\n text-wrap: pretty;\n margin: 0;\n}\n\n.post-card-quote-attribution {\n display: flex;\n align-items: center;\n gap: 0.45rem;\n flex-wrap: wrap;\n margin-top: 0.95rem;\n color: var(--site-text-secondary);\n font-family: var(--font-ui);\n font-size: var(--type-ui-meta);\n font-style: normal;\n line-height: 1.3;\n}\n\n.post-card-quote-attribution::before {\n content: \"\";\n width: 0.9rem;\n height: 1px;\n background: color-mix(\n in srgb,\n var(--site-text-secondary) 38%,\n var(--site-divider)\n );\n}\n\n.post-card-quote-source {\n color: inherit;\n text-decoration: underline;\n text-decoration-color: color-mix(\n in srgb,\n var(--site-text-secondary) 55%,\n transparent\n );\n text-underline-offset: 3px;\n}\n\n.post-card-quote-source:hover,\n.post-card-quote-source:focus {\n color: var(--site-text-primary);\n text-decoration-color: currentColor;\n}\n\n.post-card-quote-commentary {\n position: relative;\n margin-top: 1.1rem;\n padding-top: 0.95rem;\n color: color-mix(\n in srgb,\n var(--site-text-secondary) 84%,\n var(--site-text-primary)\n );\n text-wrap: pretty;\n}\n\n.post-card-quote-commentary::before {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n height: 1px;\n background: linear-gradient(\n 90deg,\n transparent 0%,\n color-mix(in srgb, var(--site-divider) 48%, transparent) 16%,\n color-mix(in srgb, var(--site-divider) 78%, transparent) 50%,\n color-mix(in srgb, var(--site-divider) 48%, transparent) 84%,\n transparent 100%\n );\n}\n\n.post-card-quote-commentary.prose > :first-child {\n margin-top: 0;\n}\n\n.post-card-quote-commentary.prose > :last-child {\n margin-bottom: 0;\n}\n\n.post-card-quote-commentary p {\n margin: 0;\n}\n\n.post-card-quote-commentary p + p,\n.post-card-quote-commentary ul,\n.post-card-quote-commentary ol,\n.post-card-quote-commentary blockquote,\n.post-card-quote-commentary pre {\n margin-top: 0.55rem;\n}\n\n/* Fade the post meta on quote cards so the quote body stays visually primary. */\n.post-card-quote ~ .post-card-footer,\n.post-card-quote-commentary + .post-card-footer {\n opacity: 0.72;\n}\n\n/* -------------------------------------------------------------------------\n * Post footer — meta (featured, time, external link, collections, pinned)\n * ------------------------------------------------------------------------- */\n\n.post-card-footer,\n.reply-footer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 10px;\n min-height: 2rem;\n font-size: var(--type-ui-hint);\n}\n\n.post-footer-detail {\n margin-top: 24px;\n /* Match the feed/reply footer size so the root post's footer doesn't\n visually dominate. Main site uses a larger size because every post\n on the detail page is rendered at detail size; here only the root\n post gets this class, which otherwise creates a mismatch with the\n replies below. */\n font-size: var(--type-ui-hint);\n color: var(--site-text-secondary);\n}\n\n.post-footer-meta {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n min-width: 0;\n font-family: var(--font-ui);\n line-height: 1.35;\n color: var(--site-text-secondary);\n}\n\n.post-footer-meta time {\n font-size: inherit;\n color: inherit;\n}\n\n.post-footer-featured {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: color-mix(\n in srgb,\n var(--search-mark-color) 72%,\n var(--site-text-secondary)\n );\n flex-shrink: 0;\n}\n\n.post-footer-featured svg {\n width: 1rem;\n height: 1rem;\n opacity: 0.9;\n}\n\n.post-footer-link {\n color: var(--site-text-secondary);\n text-decoration: none;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.post-footer-link:hover,\n.post-footer-link:focus {\n color: var(--site-text-primary);\n text-decoration: underline;\n}\n\n.post-footer-external-link {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.6rem;\n height: 1.6rem;\n border-radius: 0.6rem;\n color: var(--site-text-secondary);\n text-decoration: none;\n transition:\n color 0.18s ease,\n background-color 0.16s ease;\n flex-shrink: 0;\n}\n\n.post-footer-external-link:hover,\n.post-footer-external-link:focus {\n color: var(--site-text-primary);\n}\n\n.post-footer-external-link svg {\n width: 1rem;\n height: 1rem;\n}\n\n.post-collection-tags {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n min-width: 0;\n max-width: 100%;\n color: var(--site-text-secondary);\n}\n\n.post-collection-tag {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n color: inherit;\n text-decoration: none;\n min-width: 0;\n max-width: min(100%, 22ch);\n}\n\n.post-collection-tag:hover,\n.post-collection-tag:focus {\n color: var(--site-text-primary);\n text-decoration: underline;\n text-underline-offset: 0.18em;\n}\n\n.post-collection-tag-text {\n display: block;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.post-card-pin {\n font-size: var(--type-2xs);\n text-transform: uppercase;\n letter-spacing: var(--type-label-tracking);\n color: var(--site-accent);\n border: 1px solid var(--site-accent);\n padding: 0.1em 0.5em;\n border-radius: 999px;\n flex-shrink: 0;\n}\n\n/* -------------------------------------------------------------------------\n * Thread preview (list pages: root context + hero latest reply)\n *\n * Mirrors the main site's `.thread-group.thread-group-preview` layout:\n * content sits flush-left inside the preview, and the vertical rail +\n * dot markers are positioned OUTSIDE the content (overflowing into the\n * container's left gutter) via a negative `left` on the rail/dots.\n * ------------------------------------------------------------------------- */\n\n.thread-preview {\n /* Rail position: negative = overflow outside content. Mirrors\n `--site-thread-rail-line-left` on the main site. */\n --thread-rail-left: -27px;\n --thread-rail-width: 1px;\n --thread-rail-indent: 0px;\n --thread-dot-size: 10px;\n --thread-dot-ring-width: 2px;\n --thread-dot-border-width: 2px;\n --thread-hero-dot-size: 14px;\n --thread-hero-dot-border-width: 3px;\n --thread-item-spacing: 0.35rem;\n\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 0.6rem;\n padding-left: var(--thread-rail-indent);\n}\n\n@media (max-width: 760px) {\n .thread-preview {\n --thread-rail-left: -11px;\n --thread-rail-indent: 8px;\n }\n}\n\n/* Continuous vertical rail — subtle gradient so the line fades into\n whitespace at the top and tail. Sits outside the content via negative\n `left`. */\n.thread-preview::before {\n content: \"\";\n position: absolute;\n left: var(--thread-rail-left);\n top: 0;\n bottom: 0;\n width: var(--thread-rail-width);\n background: linear-gradient(\n 180deg,\n transparent 0,\n color-mix(in srgb, var(--site-threadline) 85%, transparent) 8%,\n color-mix(in srgb, var(--site-threadline) 55%, transparent) 100%\n );\n pointer-events: none;\n}\n\n.thread-preview-context {\n position: relative;\n display: flex;\n flex-direction: column;\n /* Match runtime `.thread-group-preview .thread-item` which uses\n `padding: var(--site-thread-item-spacing) 0` — adjacent items have\n 2x the token between them, so the flex `gap` here is 2x to match. */\n gap: calc(var(--site-thread-item-spacing) * 2);\n margin: 0;\n padding: 0;\n border-left: 0;\n}\n\n/* Individual entry in a thread preview — wraps a full `.post-card` so\n the root, second, and penultimate replies render with their own\n title/body/footer. Dot marker sits on the rail at the card's\n vertical midpoint. */\n.thread-preview .thread-item {\n position: relative;\n min-width: 0;\n max-width: 100%;\n}\n\n.thread-preview .thread-item::before {\n content: \"\";\n position: absolute;\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-dot-size) / 2 - var(--thread-rail-indent)\n );\n top: 1.4rem;\n width: var(--thread-dot-size);\n height: var(--thread-dot-size);\n border-radius: 50%;\n background-color: var(--site-threadline);\n border: var(--thread-dot-border-width) solid var(--site-page-bg);\n box-shadow: 0 0 0 var(--thread-dot-ring-width) var(--site-thread-dot-ring);\n z-index: 1;\n}\n\n/* Gap \"N more posts\" row has no card so we place the marker at its\n vertical midpoint instead of the card's top area. Swap the single dot\n for a short column of small beads (a vertical \"⋮\") so the rail visibly\n \"skips\" a run of posts — mirrors the main site's `.thread-item-gap`. */\n.thread-preview .thread-item-gap {\n display: flex;\n align-items: center;\n padding: 0.15rem 0 0.35rem;\n}\n\n.thread-preview .thread-item-gap::before {\n top: 50%;\n transform: translateY(-50%);\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 - 2px -\n var(--thread-rail-indent)\n );\n width: 4px;\n height: 24px;\n border: 0;\n border-radius: 0;\n background: radial-gradient(\n circle,\n var(--site-threadline) 1.75px,\n transparent 2px\n );\n background-size: 4px 8px;\n background-repeat: repeat-y;\n background-position: center top;\n box-shadow: none;\n}\n\n/* Hidden-posts count — a calm route into the thread, matching the main\n site's `.thread-gap-link`: styled like a sibling of the Show more toggle\n with a small right-pointing chevron rather than an underline, firming up\n to the primary text colour on hover. */\n.thread-preview-gap {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n align-self: flex-start;\n margin: 0.15rem 0;\n padding: 0.25rem 0;\n color: var(--site-text-secondary);\n font-size: var(--type-thread-context-meta);\n font-weight: 500;\n line-height: 1.2;\n text-decoration: none;\n transition: color 0.18s ease;\n}\n\n.thread-preview-gap::after {\n content: \"\";\n width: 0.36em;\n height: 0.36em;\n border: 1.5px solid currentColor;\n border-left: 0;\n border-bottom: 0;\n opacity: 0.65;\n transform: rotate(45deg);\n transition:\n transform 0.18s ease,\n opacity 0.18s ease;\n}\n\n.thread-preview-gap:hover,\n.thread-preview-gap:focus {\n color: var(--site-text-primary);\n}\n\n.thread-preview-gap:hover::after,\n.thread-preview-gap:focus::after {\n opacity: 1;\n transform: translateX(2px) rotate(45deg);\n}\n\n/* Hero (latest reply) row: spacing above and below matches the main\n site's `.thread-item-hero`, and the dot is larger + accent-colored. */\n.thread-preview-hero {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding-top: calc(var(--space-xl) * 1.1);\n margin-top: 0;\n}\n\n/* Featured timelines keep Post-level selection while grouping by Thread.\n Selected Root/Child Posts use the accent marker; context Posts retain the\n neutral rail marker. */\n.thread-preview .thread-item-featured::before {\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-hero-dot-size) / 2 - var(--thread-rail-indent)\n );\n width: var(--thread-hero-dot-size);\n height: var(--thread-hero-dot-size);\n background-color: var(--site-accent);\n border-width: var(--thread-hero-dot-border-width);\n}\n\n.thread-preview .thread-item-hero::before {\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-hero-dot-size) / 2 - var(--thread-rail-indent)\n );\n top: calc(var(--space-xl) * 1.1 + 1.4rem);\n width: var(--thread-hero-dot-size);\n height: var(--thread-hero-dot-size);\n background-color: var(--site-accent);\n border-width: var(--thread-hero-dot-border-width);\n}\n\n.thread-preview-thread-link {\n align-self: flex-start;\n font-size: var(--type-sm);\n color: var(--site-reading-meta);\n text-decoration: none;\n}\n\n.thread-preview-thread-link:hover,\n.thread-preview-thread-link:focus {\n color: var(--site-text-primary);\n text-decoration: underline;\n}\n\n/* -------------------------------------------------------------------------\n * Thread (single post page with inline replies)\n * ------------------------------------------------------------------------- */\n\n.thread {\n /* Rail variables match `.thread-preview` so the detail-page rail\n shares the same visual position. */\n --thread-rail-left: -27px;\n --thread-rail-width: 1px;\n --thread-rail-indent: 0px;\n --thread-dot-size: 10px;\n --thread-dot-ring-width: 2px;\n --thread-dot-border-width: 2px;\n --thread-hero-dot-size: 14px;\n --thread-hero-dot-border-width: 3px;\n\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n padding-left: var(--thread-rail-indent);\n}\n\n@media (max-width: 760px) {\n .thread {\n --thread-rail-left: -11px;\n --thread-rail-indent: 8px;\n }\n}\n\n/* Continuous vertical rail spanning the entire thread (root post + replies).\n Only shown when the root post actually has replies — a lone post should\n not have a rail or dot. Matches `.thread-preview`. */\n.thread-has-replies::before {\n content: \"\";\n position: absolute;\n left: var(--thread-rail-left);\n top: 0;\n bottom: 0;\n width: var(--thread-rail-width);\n background: linear-gradient(\n 180deg,\n transparent 0,\n color-mix(in srgb, var(--site-threadline) 85%, transparent) 8%,\n color-mix(in srgb, var(--site-threadline) 55%, transparent) 100%\n );\n pointer-events: none;\n}\n\n/* Dot marker for each post in the thread (root + replies). Shown only when\n there are replies so a solo post doesn't get an orphaned dot. */\n.thread .thread-item {\n position: relative;\n min-width: 0;\n max-width: 100%;\n}\n\n.thread-has-replies .thread-item::before {\n content: \"\";\n position: absolute;\n left: calc(\n var(--thread-rail-left) + var(--thread-rail-width) / 2 -\n var(--thread-dot-size) / 2 - var(--thread-rail-indent)\n );\n top: 1.4rem;\n width: var(--thread-dot-size);\n height: var(--thread-dot-size);\n border-radius: 50%;\n background-color: var(--site-threadline);\n border: var(--thread-dot-border-width) solid var(--site-page-bg);\n box-shadow: 0 0 0 var(--thread-dot-ring-width) var(--site-thread-dot-ring);\n z-index: 1;\n}\n\n.thread-item-root {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.thread-header {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.thread-title {\n font-family: var(--font-heading);\n font-size: var(--type-content-display);\n font-weight: var(--type-display-weight);\n line-height: var(--type-display-leading);\n margin: 0;\n color: var(--site-reading-title);\n}\n\n.thread-title a {\n color: inherit;\n text-decoration: none;\n}\n\n.thread-title a:hover,\n.thread-title a:focus {\n text-decoration: underline;\n text-underline-offset: 0.18em;\n}\n\n.thread-body {\n font-size: var(--type-content-body);\n line-height: var(--type-body-leading);\n color: var(--site-reading-body);\n}\n\n.thread-body > :first-child {\n margin-top: 0;\n}\n\n.thread-body > :last-child {\n margin-bottom: 0;\n}\n\n.thread-replies {\n /* Flex container for replies. The rail and dots are provided by\n `.thread::before` and `.thread-item::before` so replies stay visually\n connected to the root post. */\n display: flex;\n flex-direction: column;\n gap: var(--space-xl);\n}\n\n/* -------------------------------------------------------------------------\n * Replies\n * ------------------------------------------------------------------------- */\n\n.reply {\n scroll-margin-top: 1.5rem;\n display: flex;\n flex-direction: column;\n gap: 0.6rem;\n}\n\n.reply:target {\n background-color: var(--search-mark-bg);\n border-radius: 0.25rem;\n padding: 0.75rem 1rem;\n margin-left: -1rem;\n margin-right: -1rem;\n}\n\n.reply:target::before {\n background-color: var(--site-accent);\n}\n\n.reply-title {\n font-family: var(--font-heading);\n font-size: var(--type-content-subtitle);\n font-weight: var(--type-heading-weight);\n line-height: var(--type-heading-leading);\n margin: 0;\n color: var(--site-reading-heading);\n}\n\n.reply-body {\n color: var(--site-reading-body);\n}\n\n.reply-body > :first-child {\n margin-top: 0;\n}\n\n.reply-body > :last-child {\n margin-bottom: 0;\n}\n\n/* -------------------------------------------------------------------------\n * Collections page\n *\n * Mirrors the main site's authenticated-less collections view\n * (`ui/pages/CollectionsPage.tsx` + `ui/shared/CollectionDirectory.tsx`):\n * a page-intro block with count, then a two-column grid per row where a\n * monospace sequence label sits beside the collection title, description,\n * and entry/activity meta.\n * ------------------------------------------------------------------------- */\n\n.collections-page-shell {\n position: relative;\n display: flex;\n flex-direction: column;\n width: var(--layout-content-width);\n max-width: 100%;\n gap: clamp(1.25rem, 3vw, 1.75rem);\n}\n\n@media (min-width: 761px) and (max-width: 1024px) {\n .collections-page-shell {\n width: min(100%, 35rem);\n }\n}\n\n@media (max-width: 760px) {\n .collections-page-shell {\n width: 100%;\n }\n}\n\n.collections-page-header {\n position: relative;\n display: flex;\n align-items: flex-start;\n padding-bottom: 0.2rem;\n}\n\n.collections-page-heading {\n min-width: 0;\n flex: 1 1 18rem;\n}\n\n.page-intro {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding-bottom: 0.1rem;\n}\n\n.page-intro-title-row {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0.7rem;\n min-width: 0;\n}\n\n.page-intro-title {\n margin: 0;\n font-family: var(--font-heading);\n font-size: var(--type-title, 2rem);\n font-weight: var(--type-heading-weight);\n line-height: 1.15;\n letter-spacing: -0.01em;\n color: var(--site-text-primary);\n}\n\n.page-intro-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem 1rem;\n}\n\n.page-intro-meta,\n.page-intro-description {\n margin: 0;\n color: var(--site-text-secondary);\n font-family: var(--font-ui);\n font-size: var(--type-secondary);\n line-height: 1.3;\n}\n\n.collection-directory {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.collection-directory-item {\n position: relative;\n display: flex;\n align-items: flex-start;\n gap: 0.75rem;\n padding: 0.95rem 0;\n background: transparent;\n text-decoration: none;\n}\n\n.collection-directory-main {\n --collection-directory-sequence-width: 3.5ch;\n --collection-directory-title-line-height: 1.18;\n min-width: 0;\n flex: 1;\n display: grid;\n grid-template-columns: var(--collection-directory-sequence-width) minmax(\n 0,\n 1fr\n );\n align-items: start;\n column-gap: 0.8rem;\n row-gap: 0.25rem;\n}\n\n.collection-directory-sequence {\n grid-column: 1;\n grid-row: 1;\n display: block;\n width: var(--collection-directory-sequence-width);\n padding-top: 0.2rem;\n font-family: var(--font-mono);\n font-size: var(--type-xs);\n font-variant-numeric: tabular-nums;\n line-height: var(--collection-directory-title-line-height);\n letter-spacing: 0.14em;\n color: var(--site-text-secondary);\n}\n\n.collection-directory-title-row {\n grid-column: 2;\n grid-row: 1;\n min-width: 0;\n display: flex;\n align-items: flex-start;\n}\n\n.collection-directory-title-link {\n color: inherit;\n text-decoration: none;\n transition: color 0.15s ease;\n}\n\n.collection-directory-title-link:hover,\n.collection-directory-title-link:focus-visible {\n color: var(--site-text-primary);\n}\n\n.collection-directory-title-link:hover .collection-directory-title,\n.collection-directory-title-link:focus-visible .collection-directory-title {\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n\n.collection-directory-title {\n min-width: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.45rem;\n font-family: var(--font-heading);\n font-size: var(--type-content-body);\n font-weight: var(--type-heading-weight);\n line-height: var(--collection-directory-title-line-height);\n letter-spacing: -0.02em;\n text-wrap: pretty;\n}\n\n.collection-directory-title-marker {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 0.95rem;\n min-width: 0.95rem;\n height: 0.95rem;\n color: var(--site-text-secondary);\n transition: color 0.15s ease;\n}\n\n.collection-directory-title-link:hover .collection-directory-title-marker,\n.collection-directory-title-link:focus-visible\n .collection-directory-title-marker {\n color: var(--site-text-primary);\n}\n\n.collection-directory-description {\n grid-column: 2;\n grid-row: 2;\n margin: 0;\n color: color-mix(in srgb, var(--site-text-secondary) 80%, transparent);\n font-family: var(--font-body);\n font-size: var(--type-sm);\n line-height: 1.45;\n}\n\n.collection-directory-description :where(p) {\n margin-top: 0.25em;\n margin-bottom: 0.25em;\n}\n\n/* Links inside the description inherit the description color so they sit in\n the same gray tone as the copy around them (matches the main site's\n `.prose { --tw-prose-links: var(--site-content-link); }` which resolves\n to `inherit`). The underline stays, drawn in currentColor. */\n.collection-directory-description :where(a) {\n color: inherit;\n text-decoration: underline;\n text-decoration-color: currentColor;\n text-underline-offset: 0.15em;\n}\n\n.collection-directory-description :where(a:hover),\n.collection-directory-description :where(a:focus-visible) {\n color: var(--site-text-primary);\n}\n\n.collection-directory-description :where(p:first-child) {\n margin-top: 0;\n}\n\n.collection-directory-description :where(p:last-child) {\n margin-bottom: 0;\n}\n\n.collection-directory-description + .collection-directory-summary {\n grid-row: 3;\n}\n\n.collection-directory-summary {\n grid-column: 2;\n grid-row: 2;\n display: flex;\n min-width: 0;\n overflow: hidden;\n align-items: center;\n gap: 0.2rem 0.5rem;\n margin: 0;\n color: var(--site-reading-meta);\n font-family: var(--font-ui);\n font-size: var(--type-sm);\n line-height: 1.3;\n white-space: nowrap;\n}\n\n.collection-directory-meta {\n flex: 0 0 auto;\n color: inherit;\n}\n\n.collection-directory-meta-separator {\n flex: 0 0 auto;\n color: color-mix(in srgb, var(--site-divider) 88%, transparent);\n}\n\n.collection-directory-updated {\n flex: 0 0 auto;\n color: inherit;\n white-space: nowrap;\n}\n\n.collection-directory-divider {\n padding: 1.5rem 0 0.85rem;\n}\n\n.collection-directory-divider-row {\n display: flex;\n align-items: center;\n gap: 0.95rem;\n}\n\n.collection-directory-divider-text {\n font-family: var(--font-heading);\n font-size: var(--type-secondary);\n letter-spacing: 0;\n font-style: normal;\n white-space: nowrap;\n color: var(--site-text-secondary);\n}\n\n.collection-directory-divider-line {\n flex: 1;\n height: 1px;\n border: none;\n margin: 0;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--site-divider) 100%, transparent),\n color-mix(in srgb, var(--site-divider) 54%, transparent) 34%,\n transparent 86%\n );\n}\n\n/* -------------------------------------------------------------------------\n * Pagination\n * ------------------------------------------------------------------------- */\n\n.pagination {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-wrap: wrap;\n gap: 1rem;\n padding: 1.5rem 0;\n font-size: var(--type-sm);\n font-variant-numeric: tabular-nums;\n}\n\n.pagination-link {\n color: var(--site-text-secondary);\n text-decoration: underline;\n text-underline-offset: 3px;\n transition: color 0.15s ease;\n}\n\n.pagination-link:hover,\n.pagination-link:focus {\n color: var(--site-text-primary);\n}\n\n.pagination-link.is-disabled {\n color: color-mix(in srgb, var(--site-text-secondary) 50%, transparent);\n cursor: default;\n text-decoration: none;\n}\n\n.pagination-current {\n color: var(--site-text-primary);\n font-weight: var(--fw-medium);\n}\n\n.pagination-ellipsis {\n color: var(--site-text-secondary);\n}\n\n/* -------------------------------------------------------------------------\n * Empty states + utility\n * ------------------------------------------------------------------------- */\n\n.empty-state {\n color: var(--site-reading-meta);\n font-style: italic;\n text-align: center;\n padding: var(--space-xl) 0;\n}\n\n.page,\n.section {\n display: block;\n}\n\n.page-summary,\n.section-summary {\n font-size: var(--type-secondary);\n color: var(--site-reading-meta);\n}\n\n/* -------------------------------------------------------------------------\n * Wider viewport refinements\n * ------------------------------------------------------------------------- */\n\n@media (min-width: 768px) {\n .site-main {\n padding-top: calc(var(--space-xl) * 1.5);\n padding-bottom: calc(var(--space-xl) * 1.5);\n }\n\n .post-card-media,\n .thread-media,\n .reply-media {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .post-card-media:has(> :only-child),\n .thread-media:has(> :only-child),\n .reply-media:has(> :only-child) {\n grid-template-columns: 1fr;\n }\n}\n";
|
|
3305
4446
|
//#endregion
|
|
3306
4447
|
//#region src/services/export-theme/assets/client-site.js?raw
|
|
3307
|
-
var client_site_default$1 = "var e=null,t=0,n=!1,r=null,i=null,a=0;function o(e){return!isFinite(e)||e<0?`0:00`:`${Math.floor(e/60)}:${String(Math.floor(e%60)).padStart(2,`0`)}`}function s(e){return e.querySelector(`audio.media-audio-el`)}function c(e){return e.querySelector(`[data-audio-range]`)}function l(e){return e.querySelector(`[data-audio-waveform]`)}function u(e,t){let n=`${(t*100).toFixed(1)}%`;e.style.background=`linear-gradient(to right, var(--site-text-primary) ${n}, transparent ${n})`}var d=new WeakMap,f=new WeakSet;async function p(e,t){let n=await(await fetch(e)).arrayBuffer(),r=new AudioContext;try{let e=(await r.decodeAudioData(n)).getChannelData(0),i=Math.max(1,Math.floor(e.length/t)),a=Array(t);for(let n=0;n<t;n++){let t=0,r=n*i,o=Math.min(r+i,e.length);for(let n=r;n<o;n++){let r=Math.abs(e[n]);r>t&&(t=r)}a[n]=t}let o=0;for(let e of a)e>o&&(o=e);if(o>0)for(let e=0;e<t;e++)a[e]/=o;return a}finally{await r.close()}}function m(e=document){let t=e.querySelectorAll(`[data-audio-peaks]`),n=[];for(let e of t){let t=e.dataset.audioPeaks;if(!t)continue;let r=e.closest(`.media-audio-card`);if(!(!r||d.has(r)))try{let e=JSON.parse(t);if(!Array.isArray(e))continue;d.set(r,e),r.classList.add(`has-waveform`),n.push(r)}catch{}}n.length>0&&requestAnimationFrame(()=>{for(let e of n)g(e,0)})}async function h(e){if(d.has(e)||f.has(e))return;f.add(e);let t=e.querySelector(`audio.media-audio-el source`),n=l(e);if(!t?.src||!n)return;let r=n.getBoundingClientRect().width,i=Math.max(20,Math.floor(r/3));try{let n=await p(t.src,i);d.set(e,n),e.classList.add(`has-waveform`);let r=s(e),a=r?.duration??0;g(e,r&&isFinite(a)&&a>0?r.currentTime/a:0)}catch{}}function g(e,t){let n=d.get(e),r=l(e);if(!n||!r)return;let i=window.devicePixelRatio||1,a=r.getBoundingClientRect(),o=Math.round(a.width*i),s=Math.round(a.height*i);if(o===0||s===0)return;(r.width!==o||r.height!==s)&&(r.width=o,r.height=s);let c=r.getContext(`2d`);if(!c)return;c.clearRect(0,0,o,s);let u=n.length,f=o/u,p=Math.max(1,Math.round(f*.6)),m=Math.round(2*i),h=s*.85,g=getComputedStyle(r).getPropertyValue(`--site-text-primary`).trim()||`#000`;for(let e=0;e<u;e++){let r=Math.round(e*f+(f-p)/2),a=Math.max(m,Math.round(n[e]*h)),o=Math.round((s-a)/2);c.globalAlpha=(e+.5)/u<=t?.9:.2,c.fillStyle=g;let l=Math.min(p/2,i);c.beginPath(),c.roundRect(r,o,p,a,l),c.fill()}c.globalAlpha=1}function ee(e,t){let{currentTime:r,duration:i}=t,a=isFinite(i)&&i>0,s=a?r/i:0;if(!n){let t=c(e);t&&a&&(t.value=String(Math.round(s*1e3)),u(t,s)),d.has(e)&&g(e,s);let n=e.querySelector(`[data-audio-time]`);n&&(n.textContent=a?`${o(r)} / ${o(i)}`:o(r))}}function _(){if(!e)return;let n=s(e);!n||n.paused||(ee(e,n),t=requestAnimationFrame(_))}function te(){if(e){let t=s(e);t&&!t.paused&&t.pause(),e.classList.remove(`is-playing`),e=null}cancelAnimationFrame(t)}async function ne(n){let r=s(n);if(r)if(e&&e!==n&&te(),r.paused){e=n,n.classList.add(`is-playing`);try{await r.play()}catch{n.classList.remove(`is-playing`),e=null;return}t=requestAnimationFrame(_),h(n)}else r.pause(),n.classList.remove(`is-playing`),cancelAnimationFrame(t),e=null}function re(e){let t=e.closest(`.media-audio-card`);if(!t)return;let n=s(t);if(!n)return;let r=Number(e.value)/1e3,i=n.duration;isFinite(i)&&i>0&&(n.currentTime=r*i)}function ie(e,t){let n=e.getBoundingClientRect();a=Math.max(0,Math.min(1,(t.clientX-n.left)/n.width));let r=e.closest(`.media-audio-card`);if(!r)return;g(r,a);let i=s(r),c=r.querySelector(`[data-audio-time]`);if(i&&c){let e=i.duration;isFinite(e)&&e>0&&(c.textContent=`${o(a*e)} / ${o(e)}`)}}async function ae(n){let r=n.closest(`.media-audio-card`);if(!r)return;let i=s(r);if(i)if(i.paused){e&&e!==r&&te(),e=r,r.classList.add(`is-playing`);try{await i.play()}catch{r.classList.remove(`is-playing`),e=null;return}let n=i.duration;isFinite(n)&&n>0&&(i.currentTime=a*n),t=requestAnimationFrame(_),h(r)}else{let e=i.duration;isFinite(e)&&e>0&&(i.currentTime=a*e)}}document.addEventListener(`pointerdown`,e=>{let t=e.target;t.matches(`[data-audio-range]`)?(n=!0,r=t):t.matches(`[data-audio-waveform]`)&&(n=!0,i=t,ie(t,e))},!0),document.addEventListener(`pointermove`,e=>{n&&i&&ie(i,e)},!0),document.addEventListener(`pointerup`,()=>{n&&(r?(re(r),r=null):i&&=(ae(i),null)),n=!1},!0),document.addEventListener(`pointercancel`,()=>{r=null,i=null,n=!1},!0),document.addEventListener(`click`,e=>{let t=e.target.closest(`[data-audio-play]`);if(!t)return;e.preventDefault();let n=t.closest(`.media-audio-card`);n&&ne(n)}),document.addEventListener(`input`,e=>{let t=e.target;if(!t.matches(`[data-audio-range]`))return;let n=t.closest(`.media-audio-card`);if(!n)return;let r=s(n);if(!r)return;let i=Number(t.value)/1e3;u(t,i);let a=r.duration,c=n.querySelector(`[data-audio-time]`);c&&isFinite(a)&&a>0&&(c.textContent=`${o(i*a)} / ${o(a)}`)},!0),document.addEventListener(`ended`,n=>{let r=n.target;if(!r.closest)return;let i=r.closest(`.media-audio-card`);if(!i)return;i.classList.remove(`is-playing`),cancelAnimationFrame(t),e=null;let a=c(i);a&&(a.value=`0`,u(a,0)),d.has(i)&&g(i,0);let o=i.querySelector(`[data-audio-time]`);o&&(o.textContent=`0:00`)},!0),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>m()):m();var v=`jant:media-lightbox-toggle`,oe=`75% 0px`,se=.6,y=.25,ce=160,b=new Set,le=new WeakMap,ue=new WeakSet,x=null,S=null,C=null,de=!1,w=null,T=null,E=null;function fe(e){return e.length===0?null:[...e].sort((e,t)=>t.visibleArea===e.visibleArea?e.centerDistance===t.centerDistance?t.intersectionRatio-e.intersectionRatio:e.centerDistance-t.centerDistance:t.visibleArea-e.visibleArea)[0]??null}function pe(){return{x:(globalThis.innerWidth||document.documentElement.clientWidth||0)/2,y:(globalThis.innerHeight||document.documentElement.clientHeight||0)/2}}function me(e){return le.get(e)}function he(e){return e.closest(`.media-video-wrap`)?.querySelector(`[data-feed-video-mute-toggle]`)??null}function D(e){let t=he(e);if(!t)return;let n=C!==e||e.muted;t.dataset.muted=n?`true`:`false`,t.setAttribute(`aria-label`,n?`Play with sound`:`Mute video`)}function ge(){for(let e of b)D(e)}function _e(e){if(ue.has(e))return;let t=e.dataset.videoSrc;t&&(e.getAttribute(`src`)!==t&&(e.src=t),e.load(),ue.add(e))}function O(e){e?.pause()}function ve(e){_e(e),e.muted=C!==e,e.playsInline=!0,e.loop=!0,D(e),e.play().catch(()=>{})}function ye(){for(let e of b)e.isConnected||(T?.unobserve(e),E?.unobserve(e),b.delete(e),e===x&&(x=null),e===S&&(S=null),e===C&&(C=null))}function be(){if(w=null,ye(),document.hidden||de){O(x);return}let e=[];for(let t of b){let n=me(t);n&&(n.intersectionRatio<se||e.push({video:t,...n}))}let t=null;if(S?.isConnected){let e=me(S);e&&e.intersectionRatio>y?t=S:(!e||e.intersectionRatio<=y)&&(S=null)}if(t||=fe(e)?.video??null,!t){let e=x?me(x):void 0;if(x&&e&&e.intersectionRatio>y)return;O(x),x=null;return}t!==x&&(O(x),x=t),ve(t);for(let e of b)e!==t&&(O(e),D(e))}function k(){w!==null&&globalThis.clearTimeout(w),w=globalThis.setTimeout(be,ce)}function xe(e){let t=pe();for(let n of e){let e=n.target,r=n.boundingClientRect,i=r.left+r.width/2,a=r.top+r.height/2,o=n.intersectionRect.width*n.intersectionRect.height,s=Math.hypot(i-t.x,a-t.y);le.set(e,{intersectionRatio:n.intersectionRatio,visibleArea:o,centerDistance:s})}k()}function Se(e){for(let t of e)t.isIntersecting&&_e(t.target)}function Ce(){T&&E||globalThis.IntersectionObserver!==void 0&&(T=new globalThis.IntersectionObserver(xe,{threshold:[0,y,se,1]}),E=new globalThis.IntersectionObserver(Se,{rootMargin:oe,threshold:0}))}function we(e){b.has(e)||(Ce(),!(!T||!E)&&(b.add(e),T.observe(e),E.observe(e),he(e)?.addEventListener(`click`,Te),D(e)))}function Te(e){e.preventDefault(),e.stopPropagation();let t=e.currentTarget.closest(`.media-video-wrap`)?.querySelector(`[data-feed-short-video]`);t&&(S=t,C!==t||t.muted?(C&&C!==t&&(C.muted=!0),C=t,x!==t&&(O(x),x=t),ve(t)):(C=null,t.muted=!0,D(t)),ge(),k())}function Ee(e=document){let t=e.querySelectorAll(`[data-feed-short-video]`);for(let e of t)we(e);k()}document.addEventListener(v,e=>{de=e.detail?.open===!0,de&&O(x),k()}),document.addEventListener(`visibilitychange`,()=>{document.hidden&&O(x),k()}),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>Ee(),{once:!0}):queueMicrotask(()=>Ee());var De=4;function Oe(e){return e.querySelector(`[data-post-media]`)}function ke(e){let t=Oe(e);if(!t)return;let{scrollLeft:n,scrollWidth:r,clientWidth:i}=t;e.classList.toggle(`can-scroll-start`,n>De),e.classList.toggle(`can-scroll-end`,n+i<r-De)}function Ae(e){return Math.max(160,Math.round(e.clientWidth*.85))}function A(e,t){e.scrollBy({left:t*Ae(e),behavior:`smooth`})}function j(e){if(e.dataset.scrollHintReady===`1`)return;let t=Oe(e);t&&(e.dataset.scrollHintReady=`1`,ke(e),t.addEventListener(`scroll`,()=>ke(e),{passive:!0}),e.querySelector(`.media-gallery-nav-prev`)?.addEventListener(`click`,()=>A(t,-1)),e.querySelector(`.media-gallery-nav-next`)?.addEventListener(`click`,()=>A(t,1)),t.addEventListener(`keydown`,e=>{if(e.target===t)switch(e.key){case`ArrowRight`:e.preventDefault(),A(t,1);break;case`ArrowLeft`:e.preventDefault(),A(t,-1);break;case`Home`:e.preventDefault(),t.scrollTo({left:0,behavior:`smooth`});break;case`End`:e.preventDefault(),t.scrollTo({left:t.scrollWidth,behavior:`smooth`});break}}))}function je(){document.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(j)}var Me=new globalThis.MutationObserver(e=>{for(let t of e)for(let e of t.addedNodes)e instanceof HTMLElement&&(e.matches(`.media-gallery-scroll-wrap`)&&j(e),e.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(j))});document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>{je(),Me.observe(document.body,{childList:!0,subtree:!0})}):(je(),Me.observe(document.body,{childList:!0,subtree:!0}));var Ne=new WeakMap,Pe=new WeakMap;function Fe(e){let t=e.querySelector(`.site-header-more-btn`),n=e.querySelector(`.site-header-more-popover`);if(!t||!n||t.dataset.moreInitialized===`true`)return;t.dataset.moreInitialized=`true`;let r=new AbortController,i=r.signal;Ne.set(t,r);function a(){n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:t.parentElement}}))}function o(e=!1){n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus()}t.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),t.getAttribute(`aria-expanded`)===`true`?o():a()},{signal:i}),document.addEventListener(`click`,e=>{e.target instanceof Node&&(t.parentElement?.contains(e.target)||o())},{signal:i}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.getAttribute(`aria-hidden`)===`false`&&o(!0)},{signal:i}),document.addEventListener(`basecoat:popover`,e=>{e.detail?.source!==t.parentElement&&o()},{signal:i})}var Ie=`jant:nav-fresh-visits`;function Le(){let e=document.createElement(`span`);return e.className=`site-header-link-fresh`,e.setAttribute(`aria-hidden`,`true`),e.textContent=`*`,e}function Re(e){try{let t=JSON.parse(localStorage.getItem(Ie)||`{}`),n=location.pathname,r=e.querySelectorAll(`[data-fresh-at]`);for(let e of r){let r=new URL(e.href).pathname,i=parseInt(e.dataset.freshAt,10);if(r===n)t[r]=Math.floor(Date.now()/1e3);else{let n=t[r];if(!n||n<i){let t=e.querySelector(`svg`);e.insertBefore(Le(),t)}}}localStorage.setItem(Ie,JSON.stringify(t))}catch{}}function ze(e=document){let t=e.querySelector(`.site-header-hamburger`),n=e.querySelector(`#site-nav-drawer`),r=e.querySelector(`.site-nav-drawer-backdrop`),i=n?.querySelector(`.site-nav-drawer-close`);if(Re(e),Fe(e),!t||!n||!r||t.dataset.drawerInitialized===`true`)return;t.dataset.drawerInitialized=`true`;let a=new AbortController,o=a.signal;Pe.set(t,a);function s(){n.setAttribute(`aria-hidden`,`false`),n.removeAttribute(`inert`),r.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.documentElement.classList.add(`drawer-open`);let e=n.querySelector(`.site-nav-drawer-close`)??n.querySelector(`a[href], button`);e&&e.focus()}function c(e=!0){n.setAttribute(`aria-hidden`,`true`),r.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),document.documentElement.classList.remove(`drawer-open`),n.addEventListener(`transitionend`,()=>{n.getAttribute(`aria-hidden`)===`true`&&n.setAttribute(`inert`,``)},{once:!0,signal:o}),e&&t.focus()}t.addEventListener(`click`,()=>{t.getAttribute(`aria-expanded`)===`true`?c():s()},{signal:o}),i?.addEventListener(`click`,()=>c(),{signal:o}),r.addEventListener(`click`,()=>c(),{signal:o}),n.addEventListener(`click`,e=>{e.target instanceof Element&&e.target.closest(`a[href]`)&&c(!1)},{signal:o}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),c())},{signal:o})}ze();var M=globalThis,N=M.ShadowRoot&&(M.ShadyCSS===void 0||M.ShadyCSS.nativeShadow)&&`adoptedStyleSheets`in Document.prototype&&`replace`in CSSStyleSheet.prototype,Be=Symbol(),Ve=new WeakMap,He=class{constructor(e,t,n){if(this._$cssResult$=!0,n!==Be)throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(N&&e===void 0){let n=t!==void 0&&t.length===1;n&&(e=Ve.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),n&&Ve.set(t,e))}return e}toString(){return this.cssText}},Ue=e=>new He(typeof e==`string`?e:e+``,void 0,Be),We=(e,t)=>{if(N)e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let n of t){let t=document.createElement(`style`),r=M.litNonce;r!==void 0&&t.setAttribute(`nonce`,r),t.textContent=n.cssText,e.appendChild(t)}},Ge=N?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t=``;for(let n of e.cssRules)t+=n.cssText;return Ue(t)})(e):e,{is:Ke,defineProperty:qe,getOwnPropertyDescriptor:Je,getOwnPropertyNames:Ye,getOwnPropertySymbols:Xe,getPrototypeOf:Ze}=Object,P=globalThis,Qe=P.trustedTypes,$e=Qe?Qe.emptyScript:``,et=P.reactiveElementPolyfillSupport,F=(e,t)=>e,I={toAttribute(e,t){switch(t){case Boolean:e=e?$e:null;break;case Object:case Array:e=e==null?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=e!==null;break;case Number:n=e===null?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch{n=null}}return n}},tt=(e,t)=>!Ke(e,t),nt={attribute:!0,type:String,converter:I,reflect:!1,useDefault:!1,hasChanged:tt};Symbol.metadata??=Symbol(`metadata`),P.litPropertyMetadata??=new WeakMap;var L=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=nt){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let n=Symbol(),r=this.getPropertyDescriptor(e,n,t);r!==void 0&&qe(this.prototype,e,r)}}static getPropertyDescriptor(e,t,n){let{get:r,set:i}=Je(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:r,set(t){let a=r?.call(this);i?.call(this,t),this.requestUpdate(e,a,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??nt}static _$Ei(){if(this.hasOwnProperty(F(`elementProperties`)))return;let e=Ze(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(F(`finalized`)))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(F(`properties`))){let e=this.properties,t=[...Ye(e),...Xe(e)];for(let n of t)this.createProperty(n,e[n])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[e,n]of t)this.elementProperties.set(e,n)}this._$Eh=new Map;for(let[e,t]of this.elementProperties){let n=this._$Eu(e,t);n!==void 0&&this._$Eh.set(n,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let n=new Set(e.flat(1/0).reverse());for(let e of n)t.unshift(Ge(e))}else e!==void 0&&t.push(Ge(e));return t}static _$Eu(e,t){let n=t.attribute;return!1===n?void 0:typeof n==`string`?n:typeof e==`string`?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let n of t.keys())this.hasOwnProperty(n)&&(e.set(n,this[n]),delete this[n]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return We(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,n){this._$AK(e,n)}_$ET(e,t){let n=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,n);if(r!==void 0&&!0===n.reflect){let i=(n.converter?.toAttribute===void 0?I:n.converter).toAttribute(t,n.type);this._$Em=e,i==null?this.removeAttribute(r):this.setAttribute(r,i),this._$Em=null}}_$AK(e,t){let n=this.constructor,r=n._$Eh.get(e);if(r!==void 0&&this._$Em!==r){let e=n.getPropertyOptions(r),i=typeof e.converter==`function`?{fromAttribute:e.converter}:e.converter?.fromAttribute===void 0?I:e.converter;this._$Em=r;let a=i.fromAttribute(t,e.type);this[r]=a??this._$Ej?.get(r)??a,this._$Em=null}}requestUpdate(e,t,n,r=!1,i){if(e!==void 0){let a=this.constructor;if(!1===r&&(i=this[e]),n??=a.getPropertyOptions(e),!((n.hasChanged??tt)(i,t)||n.useDefault&&n.reflect&&i===this._$Ej?.get(e)&&!this.hasAttribute(a._$Eu(e,n))))return;this.C(e,t,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:n,reflect:r,wrapped:i},a){n&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,a??t??this[e]),!0!==i||a!==void 0)||(this._$AL.has(e)||(this.hasUpdated||n||(t=void 0),this._$AL.set(e,t)),!0===r&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}let e=this.constructor.elementProperties;if(e.size>0)for(let[t,n]of e){let{wrapped:e}=n,r=this[t];!0!==e||this._$AL.has(t)||r===void 0||this.C(t,void 0,n,r)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};L.elementStyles=[],L.shadowRootOptions={mode:`open`},L[F(`elementProperties`)]=new Map,L[F(`finalized`)]=new Map,et?.({ReactiveElement:L}),(P.reactiveElementVersions??=[]).push(`2.1.2`);var R=globalThis,rt=e=>e,z=R.trustedTypes,it=z?z.createPolicy(`lit-html`,{createHTML:e=>e}):void 0,at=`$lit$`,B=`lit$${Math.random().toFixed(9).slice(2)}$`,ot=`?`+B,st=`<${ot}>`,V=document,H=()=>V.createComment(``),U=e=>e===null||typeof e!=`object`&&typeof e!=`function`,ct=Array.isArray,lt=e=>ct(e)||typeof e?.[Symbol.iterator]==`function`,ut=`[ \n\\f\\r]`,W=/<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g,dt=/-->/g,ft=/>/g,G=RegExp(`>|${ut}(?:([^\\\\s\"'>=/]+)(${ut}*=${ut}*(?:[^ \\t\\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`,`g`),pt=/'/g,mt=/\"/g,ht=/^(?:script|style|textarea|title)$/i,gt=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),K=gt(1),_t=gt(2),q=Symbol.for(`lit-noChange`),J=Symbol.for(`lit-nothing`),vt=new WeakMap,Y=V.createTreeWalker(V,129);function yt(e,t){if(!ct(e)||!e.hasOwnProperty(`raw`))throw Error(`invalid template strings array`);return it===void 0?t:it.createHTML(t)}var bt=(e,t)=>{let n=e.length-1,r=[],i,a=t===2?`<svg>`:t===3?`<math>`:``,o=W;for(let t=0;t<n;t++){let n=e[t],s,c,l=-1,u=0;for(;u<n.length&&(o.lastIndex=u,c=o.exec(n),c!==null);)u=o.lastIndex,o===W?c[1]===`!--`?o=dt:c[1]===void 0?c[2]===void 0?c[3]!==void 0&&(o=G):(ht.test(c[2])&&(i=RegExp(`</`+c[2],`g`)),o=G):o=ft:o===G?c[0]===`>`?(o=i??W,l=-1):c[1]===void 0?l=-2:(l=o.lastIndex-c[2].length,s=c[1],o=c[3]===void 0?G:c[3]===`\"`?mt:pt):o===mt||o===pt?o=G:o===dt||o===ft?o=W:(o=G,i=void 0);let d=o===G&&e[t+1].startsWith(`/>`)?` `:``;a+=o===W?n+st:l>=0?(r.push(s),n.slice(0,l)+at+n.slice(l)+B+d):n+B+(l===-2?t:d)}return[yt(e,a+(e[n]||`<?>`)+(t===2?`</svg>`:t===3?`</math>`:``)),r]},xt=class e{constructor({strings:t,_$litType$:n},r){let i;this.parts=[];let a=0,o=0,s=t.length-1,c=this.parts,[l,u]=bt(t,n);if(this.el=e.createElement(l,r),Y.currentNode=this.el.content,n===2||n===3){let e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;(i=Y.nextNode())!==null&&c.length<s;){if(i.nodeType===1){if(i.hasAttributes())for(let e of i.getAttributeNames())if(e.endsWith(at)){let t=u[o++],n=i.getAttribute(e).split(B),r=/([.?@])?(.*)/.exec(t);c.push({type:1,index:a,name:r[2],strings:n,ctor:r[1]===`.`?wt:r[1]===`?`?Tt:r[1]===`@`?Et:Z}),i.removeAttribute(e)}else e.startsWith(B)&&(c.push({type:6,index:a}),i.removeAttribute(e));if(ht.test(i.tagName)){let e=i.textContent.split(B),t=e.length-1;if(t>0){i.textContent=z?z.emptyScript:``;for(let n=0;n<t;n++)i.append(e[n],H()),Y.nextNode(),c.push({type:2,index:++a});i.append(e[t],H())}}}else if(i.nodeType===8)if(i.data===ot)c.push({type:2,index:a});else{let e=-1;for(;(e=i.data.indexOf(B,e+1))!==-1;)c.push({type:7,index:a}),e+=B.length-1}a++}}static createElement(e,t){let n=V.createElement(`template`);return n.innerHTML=e,n}};function X(e,t,n=e,r){if(t===q)return t;let i=r===void 0?n._$Cl:n._$Co?.[r],a=U(t)?void 0:t._$litDirective$;return i?.constructor!==a&&(i?._$AO?.(!1),a===void 0?i=void 0:(i=new a(e),i._$AT(e,n,r)),r===void 0?n._$Cl=i:(n._$Co??=[])[r]=i),i!==void 0&&(t=X(e,i._$AS(e,t.values),i,r)),t}var St=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:n}=this._$AD,r=(e?.creationScope??V).importNode(t,!0);Y.currentNode=r;let i=Y.nextNode(),a=0,o=0,s=n[0];for(;s!==void 0;){if(a===s.index){let t;s.type===2?t=new Ct(i,i.nextSibling,this,e):s.type===1?t=new s.ctor(i,s.name,s.strings,this,e):s.type===6&&(t=new Dt(i,this,e)),this._$AV.push(t),s=n[++o]}a!==s?.index&&(i=Y.nextNode(),a++)}return Y.currentNode=V,r}p(e){let t=0;for(let n of this._$AV)n!==void 0&&(n.strings===void 0?n._$AI(e[t]):(n._$AI(e,n,t),t+=n.strings.length-2)),t++}},Ct=class e{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,n,r){this.type=2,this._$AH=J,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=n,this.options=r,this._$Cv=r?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=X(this,e,t),U(e)?e===J||e==null||e===``?(this._$AH!==J&&this._$AR(),this._$AH=J):e!==this._$AH&&e!==q&&this._(e):e._$litType$===void 0?e.nodeType===void 0?lt(e)?this.k(e):this._(e):this.T(e):this.$(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==J&&U(this._$AH)?this._$AA.nextSibling.data=e:this.T(V.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:n}=e,r=typeof n==`number`?this._$AC(e):(n.el===void 0&&(n.el=xt.createElement(yt(n.h,n.h[0]),this.options)),n);if(this._$AH?._$AD===r)this._$AH.p(t);else{let e=new St(r,this),n=e.u(this.options);e.p(t),this.T(n),this._$AH=e}}_$AC(e){let t=vt.get(e.strings);return t===void 0&&vt.set(e.strings,t=new xt(e)),t}k(t){ct(this._$AH)||(this._$AH=[],this._$AR());let n=this._$AH,r,i=0;for(let a of t)i===n.length?n.push(r=new e(this.O(H()),this.O(H()),this,this.options)):r=n[i],r._$AI(a),i++;i<n.length&&(this._$AR(r&&r._$AB.nextSibling,i),n.length=i)}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(!1,!0,t);e!==this._$AB;){let t=rt(e).nextSibling;rt(e).remove(),e=t}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e))}},Z=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,n,r,i){this.type=1,this._$AH=J,this._$AN=void 0,this.element=e,this.name=t,this._$AM=r,this.options=i,n.length>2||n[0]!==``||n[1]!==``?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=J}_$AI(e,t=this,n,r){let i=this.strings,a=!1;if(i===void 0)e=X(this,e,t,0),a=!U(e)||e!==this._$AH&&e!==q,a&&(this._$AH=e);else{let r=e,o,s;for(e=i[0],o=0;o<i.length-1;o++)s=X(this,r[n+o],t,o),s===q&&(s=this._$AH[o]),a||=!U(s)||s!==this._$AH[o],s===J?e=J:e!==J&&(e+=(s??``)+i[o+1]),this._$AH[o]=s}a&&!r&&this.j(e)}j(e){e===J?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??``)}},wt=class extends Z{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===J?void 0:e}},Tt=class extends Z{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==J)}},Et=class extends Z{constructor(e,t,n,r,i){super(e,t,n,r,i),this.type=5}_$AI(e,t=this){if((e=X(this,e,t,0)??J)===q)return;let n=this._$AH,r=e===J&&n!==J||e.capture!==n.capture||e.once!==n.once||e.passive!==n.passive,i=e!==J&&(n===J||r);r&&this.element.removeEventListener(this.name,this,n),i&&this.element.addEventListener(this.name,this,e),this._$AH=e}handleEvent(e){typeof this._$AH==`function`?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e)}},Dt=class{constructor(e,t,n){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=n}get _$AU(){return this._$AM._$AU}_$AI(e){X(this,e)}},Ot=R.litHtmlPolyfillSupport;Ot?.(xt,Ct),(R.litHtmlVersions??=[]).push(`3.3.2`);var kt=(e,t,n)=>{let r=n?.renderBefore??t,i=r._$litPart$;if(i===void 0){let e=n?.renderBefore??null;r._$litPart$=i=new Ct(t.insertBefore(H(),e),e,void 0,n??{})}return i._$AI(e),i},At=globalThis,Q=class extends L{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=kt(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return q}};Q._$litElement$=!0,Q.finalized=!0,At.litElementHydrateSupport?.({LitElement:Q});var jt=At.litElementPolyfillSupport;jt?.({LitElement:Q}),(At.litElementVersions??=[]).push(`4.2.2`);function Mt(e){if(!e.mimeType?.startsWith(`video/`)||!Number.isFinite(e.durationSeconds)||!e.durationSeconds||e.durationSeconds<=0||e.durationSeconds>15)return!1;let t=e.size;return!(typeof t==`number`&&t>12582912)}var Nt=640,Pt=8,Ft=72,It=16,Lt=704,Rt=.9,zt=.85;function $(e){if(!(!Number.isFinite(e)||!e||e<=0))return e}function Bt(){return{width:globalThis.innerWidth||document.documentElement.clientWidth||0,height:globalThis.innerHeight||document.documentElement.clientHeight||0}}function Vt(e,t){let n=e<=Nt?Pt:Ft;return{width:Math.max(0,e-n*2),height:Math.max(0,t-It*2)}}function Ht(e,t,n){let r=$(e?.width),i=$(e?.height);if(!r||!i)return null;let a=Vt(t,n);if(a.width<=0||a.height<=0)return null;let o=Math.min(a.width/r,a.height/i);return{width:Math.max(1,Math.round(r*o)),height:Math.max(1,Math.round(i*o))}}function Ut(e,t,n){if(!e||e.mimeType?.startsWith(`video/`)||!$(e.width)||!$(e.height)||t<=0||n<=0)return!1;let r=$(e.width),i=$(e.height);if(!r||!i)return!1;let a=t<=Nt,o=Vt(t,n),s=o.width,c=o.height;if(s<=0||c<=0)return!1;let l=r/i,u=Math.min(s,c*l);return l<Rt&&u<(a?s:Math.min(s,Lt))*zt}var Wt=class extends Q{static properties={_images:{state:!0},_currentIndex:{state:!0},_open:{state:!0},_viewportWidth:{state:!0},_viewportHeight:{state:!0},_videoCurrentTime:{state:!0},_videoDuration:{state:!0},_videoMuted:{state:!0},_imageZoomed:{state:!0}};createRenderRoot(){return this.innerHTML=``,this}constructor(){super();let e=Bt();this._images=[],this._currentIndex=0,this._open=!1,this._viewportWidth=e.width,this._viewportHeight=e.height,this._videoCurrentTime=0,this._videoDuration=0,this._videoMuted=!1,this._imageZoomed=!1}connectedCallback(){super.connectedCallback(),document.addEventListener(`click`,this.#e),window.addEventListener(`resize`,this.#l),this.#u()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(`click`,this.#e),window.removeEventListener(`resize`,this.#l)}open(e,t){this.#u(),this.#d(),this._images=e,this._currentIndex=Math.max(0,Math.min(t,e.length-1)),this.#m(this._images[this._currentIndex]),this._imageZoomed=!1,this._open=!0,document.dispatchEvent(new CustomEvent(v,{detail:{open:!0}})),this.updateComplete.then(()=>{this.querySelector(`.media-lightbox`)?.showModal(),this.#f()})}close(){this.#d(),this.querySelector(`.media-lightbox`)?.close(),this._open=!1,document.dispatchEvent(new CustomEvent(v,{detail:{open:!1}}))}#e=e=>{let t=e.target,n=t.closest(`[data-post-media] a[data-lightbox-index]`);if(n){let t=n.closest(`[data-lightbox-group]`);if(!t)return;e.preventDefault();let r=parseInt(n.dataset.lightboxIndex??`0`,10);try{let e=JSON.parse(t.dataset.lightboxGroup??`[]`);e.length>0&&this.open(e,r)}catch{}return}let r=t.closest(`[data-post-body] img`);if(r){e.preventDefault();let t=r.closest(`[data-post-body]`);if(!t)return;let n=Array.from(t.querySelectorAll(`img`)),i=n.map(e=>({url:e.src,alt:e.alt||``,width:$(e.naturalWidth||Number(e.getAttribute(`width`))),height:$(e.naturalHeight||Number(e.getAttribute(`height`)))})),a=n.indexOf(r);i.length>0&&this.open(i,Math.max(0,a))}};#t(){this._images.length<=1||(this.#d(),this._imageZoomed=!1,this._currentIndex=(this._currentIndex-1+this._images.length)%this._images.length)}#n(){this._images.length<=1||(this.#d(),this._imageZoomed=!1,this._currentIndex=(this._currentIndex+1)%this._images.length)}#r=e=>{let t=this._images[this._currentIndex];Ut(t,this._viewportWidth,this._viewportHeight)&&(e.stopPropagation(),this._imageZoomed=!this._imageZoomed)};#i=e=>{let t=e,n=e.target;if(t.key===`Escape`){e.preventDefault(),this.close();return}if(n instanceof HTMLInputElement||n instanceof HTMLButtonElement||n instanceof HTMLVideoElement)return;if(!this._images[this._currentIndex]?.mimeType?.startsWith(`video/`)){t.key===`ArrowLeft`?(e.preventDefault(),this.#t()):t.key===`ArrowRight`&&(e.preventDefault(),this.#n());return}let r=this.querySelector(`.media-lightbox-video`);r&&this.#a(t,r)};#a(e,t){let n=Number.isFinite(t.duration)&&t.duration>0?t.duration:null,r=e=>{let r=n==null?Math.max(0,e):Math.max(0,Math.min(e,n));t.currentTime=r,this._videoCurrentTime=r},i=e.key,a=i.toLowerCase();if(i===` `||a===`k`)e.preventDefault(),t.paused?t.play().catch(()=>{}):t.pause();else if(i===`ArrowLeft`)e.preventDefault(),r(t.currentTime-2);else if(i===`ArrowRight`)e.preventDefault(),r(t.currentTime+2);else if(i===`Home`)e.preventDefault(),r(0);else if(i===`End`)n!=null&&(e.preventDefault(),r(n));else if(i.length===1&&i>=`0`&&i<=`9`)n!=null&&(e.preventDefault(),r(Number(i)/10*n));else if(i===`ArrowUp`)e.preventDefault(),t.volume=Math.min(1,t.volume+.05);else if(i===`ArrowDown`)e.preventDefault(),t.volume=Math.max(0,t.volume-.05);else if(a===`m`){e.preventDefault();let n=!t.muted;t.muted=n,this._videoMuted=n}else a===`f`&&(e.preventDefault(),this.#o(t))}#o(e){let t=document,n=e;if(document.fullscreenElement??t.webkitFullscreenElement){document.exitFullscreen?document.exitFullscreen().catch(()=>{}):t.webkitExitFullscreen?.();return}e.requestFullscreen?e.requestFullscreen().catch(()=>{}):n.webkitRequestFullscreen?n.webkitRequestFullscreen():n.webkitEnterFullscreen&&n.webkitEnterFullscreen()}#s=e=>{let t=e.target;(t===e.currentTarget||t.classList.contains(`media-lightbox-content`)||t.classList.contains(`media-lightbox-stage`))&&this.close()};#c=()=>{this.#d(),this._open&&document.dispatchEvent(new CustomEvent(v,{detail:{open:!1}})),this._open=!1};#l=()=>{this.#u()};#u(){let e=Bt();e.width===this._viewportWidth&&e.height===this._viewportHeight||(this._viewportWidth=e.width,this._viewportHeight=e.height)}#d(){this.querySelector(`.media-lightbox-video`)?.pause()}#f(){this.querySelector(`.media-lightbox-content`)?.focus()}#p=()=>{this.querySelector(`.media-lightbox-content`)?.focus({preventScroll:!0})};#m(e){this._videoCurrentTime=0,this._videoDuration=e?.durationSeconds&&e.durationSeconds>0?e.durationSeconds:0,this._videoMuted=!1}#h(){let e=this._images[this._currentIndex];if(!Mt(e)){this.#m(e);return}let t=this.querySelector(`.media-lightbox-video`);t&&(t.currentTime=0,t.muted=this._videoMuted,t.play().catch(()=>{}))}#g=e=>{let t=e.currentTarget;Number.isFinite(t.duration)&&t.duration>0&&(this._videoDuration=t.duration),this._videoCurrentTime=t.currentTime,t.muted=this._videoMuted};#_=e=>{let t=e.currentTarget;this._videoCurrentTime=t.currentTime,Number.isFinite(t.duration)&&t.duration>0&&(this._videoDuration=t.duration)};#v=e=>{let t=e.currentTarget,n=this.querySelector(`.media-lightbox-video`),r=Number.parseFloat(t.value);!n||!Number.isFinite(r)||r<0||(n.currentTime=r,this._videoCurrentTime=r)};#y=()=>{this._videoMuted=!this._videoMuted;let e=this.querySelector(`.media-lightbox-video`);e&&(e.muted=this._videoMuted)};updated(e){if(super.updated(e),!this._open||!e.has(`_currentIndex`)&&!e.has(`_open`)&&!e.has(`_imageZoomed`))return;let t=this.querySelector(`.media-lightbox-stage`);t&&(t.scrollTop=0,t.scrollLeft=0,(e.has(`_currentIndex`)||e.has(`_open`))&&(this.#h(),this.#f()))}render(){if(!this._open)return J;let e=this._images[this._currentIndex],t=this._images.length>1,n=e?.mimeType?.startsWith(`video/`),r=Mt(e),i=Ut(e,this._viewportWidth,this._viewportHeight),a=i&&this._imageZoomed,o=r?Ht(e,this._viewportWidth,this._viewportHeight):null,s=r&&!!o&&o.height>o.width,c=o?`width:${o.width}px;height:${o.height}px;`:J,l=this._videoDuration>0?this._videoDuration:e?.durationSeconds??1,u=Math.min(this._videoCurrentTime,l),d=l>0?u/l*100:0;return K`\n <dialog\n class=${`media-lightbox${r?` media-lightbox-short`:``}`}\n @keydown=${this.#i}\n @click=${this.#s}\n @close=${this.#c}\n >\n <div class=\"media-lightbox-content\" tabindex=\"-1\">\n <button\n type=\"button\"\n class=\"media-lightbox-close\"\n @click=${()=>this.close()}\n aria-label=\"Close\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n\n ${t?K`<div class=\"media-lightbox-counter\">\n ${this._currentIndex+1} / ${this._images.length}\n </div>`:J}\n <div\n class=${`media-lightbox-stage${a?` media-lightbox-stage-scroll`:``}`}\n >\n ${n?r?K`<div\n class=${`media-lightbox-short-frame${s?` media-lightbox-short-frame-portrait`:` media-lightbox-short-frame-landscape`}`}\n style=${c}\n >\n <video\n class=\"media-lightbox-video media-lightbox-video-short\"\n src=${e?.url??``}\n poster=${e?.posterUrl??``}\n autoplay\n playsinline\n loop\n ?muted=${this._videoMuted}\n @focus=${this.#p}\n @loadedmetadata=${this.#g}\n @timeupdate=${this.#_}\n ></video>\n <div\n class=${`media-lightbox-short-controls${s?` media-lightbox-short-controls-portrait`:``}`}\n >\n <input\n class=\"media-lightbox-short-progress\"\n type=\"range\"\n min=\"0\"\n max=${l}\n step=\"0.01\"\n .value=${String(u)}\n style=${`--media-progress:${d}%`}\n aria-label=\"Video progress\"\n @input=${this.#v}\n />\n <button\n type=\"button\"\n class=\"media-lightbox-short-mute\"\n @click=${this.#y}\n aria-label=${this._videoMuted?`Unmute video`:`Mute video`}\n >\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 48 48\"\n fill=\"currentColor\"\n color=\"#fff\"\n aria-hidden=\"true\"\n >\n ${this._videoMuted?_t`\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1v-9.8c0-.4-.2-.8-.4-1.1l-22-22c-.3-.3-.7-.4-1.1-.4h-.6zm46.8 31.4-5.5-5.5C44.9 36.6 48 31.4 48 24c0-11.4-7.2-17.4-7.2-17.4-.6-.6-1.6-.6-2.2 0L37.2 8c-.6.6-.6 1.6 0 2.2 0 0 5.7 5 5.7 13.8 0 5.4-2.1 9.3-3.8 11.6L35.5 32c1.1-1.7 2.3-4.4 2.3-8 0-6.8-4.1-10.3-4.1-10.3-.6-.6-1.6-.6-2.2 0l-1.4 1.4c-.6.6-.6 1.6 0 2.2 0 0 2.6 2 2.6 6.7 0 1.8-.4 3.2-.9 4.3L25.5 22V1.4c0-1.3-1.6-1.9-2.5-1L13.5 10 3.3-.3c-.6-.6-1.5-.6-2.1 0L-.2 1.1c-.6.6-.6 1.5 0 2.1L4 7.6l26.8 26.8 13.9 13.9c.6.6 1.5.6 2.1 0l1.4-1.4c.7-.6.7-1.6.1-2.2z\" />\n `:_t`\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1V1.4c0-1.3-1.6-1.9-2.5-1L10.2 13.3H1.5z\" />\n <path d=\"M30.1 15.9c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 4.1 3.5 4.1 11.7s-4.1 11.7-4.1 11.7c-.6.6-1.6.6-2.2 0l-1.4-1.4c-.6-.6-.6-1.6 0-2.2 0 0 2.6-2 2.6-8.1s-2.6-8.1-2.6-8.1z\" />\n <path d=\"M37.2 8c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 5.7 5.1 5.7 19.6s-5.7 19.6-5.7 19.6c-.6.6-1.6.6-2.2 0L37.2 42c-.6-.6-.6-1.6 0-2.2 0 0 4.3-4.4 4.3-15.8S37.2 8 37.2 8z\" />\n `}\n </svg>\n </button>\n </div>\n </div>`:K`<video\n class=\"media-lightbox-video\"\n src=${e?.url??``}\n poster=${e?.posterUrl??``}\n controls\n autoplay\n playsinline\n @focus=${this.#p}\n ></video>`:K`<img\n class=${`media-lightbox-img${i?` media-lightbox-img-zoomable`:``}${a?` media-lightbox-img-scroll`:``}`}\n src=${e?.url??``}\n alt=${e?.alt??``}\n @click=${this.#r}\n />`}\n </div>\n ${t?K`\n <button\n type=\"button\"\n class=\"media-lightbox-nav media-lightbox-nav-prev\"\n @click=${()=>this.#t()}\n aria-label=\"Previous\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"m15 18-6-6 6-6\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"media-lightbox-nav media-lightbox-nav-next\"\n @click=${()=>this.#n()}\n aria-label=\"Next\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"m9 18 6-6-6-6\" />\n </svg>\n </button>\n `:J}\n </div>\n </dialog>\n `}};customElements.define(`jant-media-lightbox`,Wt);";
|
|
4448
|
+
var client_site_default$1 = "var e=null,t=0,n=!1,r=null,i=null,a=0;function o(e){return!isFinite(e)||e<0?`0:00`:`${Math.floor(e/60)}:${String(Math.floor(e%60)).padStart(2,`0`)}`}function s(e){return e.querySelector(`audio.media-audio-el`)}function c(e){return e.querySelector(`[data-audio-range]`)}function l(e){return e.querySelector(`[data-audio-waveform]`)}function u(e,t){let n=`${(t*100).toFixed(1)}%`;e.style.background=`linear-gradient(to right, var(--site-text-primary) ${n}, transparent ${n})`}var d=new WeakMap,f=new WeakSet;async function p(e,t){let n=await(await fetch(e)).arrayBuffer(),r=new AudioContext;try{let e=(await r.decodeAudioData(n)).getChannelData(0),i=Math.max(1,Math.floor(e.length/t)),a=Array(t);for(let n=0;n<t;n++){let t=0,r=n*i,o=Math.min(r+i,e.length);for(let n=r;n<o;n++){let r=Math.abs(e[n]);r>t&&(t=r)}a[n]=t}let o=0;for(let e of a)e>o&&(o=e);if(o>0)for(let e=0;e<t;e++)a[e]/=o;return a}finally{await r.close()}}function ee(e=document){let t=e.querySelectorAll(`[data-audio-peaks]`),n=[];for(let e of t){let t=e.dataset.audioPeaks;if(!t)continue;let r=e.closest(`.media-audio-card`);if(!(!r||d.has(r)))try{let e=JSON.parse(t);if(!Array.isArray(e))continue;d.set(r,e),r.classList.add(`has-waveform`),n.push(r)}catch{}}n.length>0&&requestAnimationFrame(()=>{for(let e of n)m(e,0)})}async function te(e){if(d.has(e)||f.has(e))return;f.add(e);let t=e.querySelector(`audio.media-audio-el source`),n=l(e);if(!t?.src||!n)return;let r=n.getBoundingClientRect().width,i=Math.max(20,Math.floor(r/3));try{let n=await p(t.src,i);d.set(e,n),e.classList.add(`has-waveform`);let r=s(e),a=r?.duration??0;m(e,r&&isFinite(a)&&a>0?r.currentTime/a:0)}catch{}}function m(e,t){let n=d.get(e),r=l(e);if(!n||!r)return;let i=window.devicePixelRatio||1,a=r.getBoundingClientRect(),o=Math.round(a.width*i),s=Math.round(a.height*i);if(o===0||s===0)return;(r.width!==o||r.height!==s)&&(r.width=o,r.height=s);let c=r.getContext(`2d`);if(!c)return;c.clearRect(0,0,o,s);let u=n.length,f=o/u,p=Math.max(1,Math.round(f*.6)),ee=Math.round(2*i),te=s*.85,m=getComputedStyle(r).getPropertyValue(`--site-text-primary`).trim()||`#000`;for(let e=0;e<u;e++){let r=Math.round(e*f+(f-p)/2),a=Math.max(ee,Math.round(n[e]*te)),o=Math.round((s-a)/2);c.globalAlpha=(e+.5)/u<=t?.9:.2,c.fillStyle=m;let l=Math.min(p/2,i);c.beginPath(),c.roundRect(r,o,p,a,l),c.fill()}c.globalAlpha=1}function ne(e,t){let{currentTime:r,duration:i}=t,a=isFinite(i)&&i>0,s=a?r/i:0;if(!n){let t=c(e);t&&a&&(t.value=String(Math.round(s*1e3)),u(t,s)),d.has(e)&&m(e,s);let n=e.querySelector(`[data-audio-time]`);n&&(n.textContent=a?`${o(r)} / ${o(i)}`:o(r))}}function re(){if(!e)return;let n=s(e);!n||n.paused||(ne(e,n),t=requestAnimationFrame(re))}function ie(){if(e){let t=s(e);t&&!t.paused&&t.pause(),e.classList.remove(`is-playing`),e=null}cancelAnimationFrame(t)}async function ae(n){let r=s(n);if(r)if(e&&e!==n&&ie(),r.paused){e=n,n.classList.add(`is-playing`);try{await r.play()}catch{n.classList.remove(`is-playing`),e=null;return}t=requestAnimationFrame(re),te(n)}else r.pause(),n.classList.remove(`is-playing`),cancelAnimationFrame(t),e=null}function oe(e){let t=e.closest(`.media-audio-card`);if(!t)return;let n=s(t);if(!n)return;let r=Number(e.value)/1e3,i=n.duration;isFinite(i)&&i>0&&(n.currentTime=r*i)}function se(e,t){let n=e.getBoundingClientRect();a=Math.max(0,Math.min(1,(t.clientX-n.left)/n.width));let r=e.closest(`.media-audio-card`);if(!r)return;m(r,a);let i=s(r),c=r.querySelector(`[data-audio-time]`);if(i&&c){let e=i.duration;isFinite(e)&&e>0&&(c.textContent=`${o(a*e)} / ${o(e)}`)}}async function ce(n){let r=n.closest(`.media-audio-card`);if(!r)return;let i=s(r);if(i)if(i.paused){e&&e!==r&&ie(),e=r,r.classList.add(`is-playing`);try{await i.play()}catch{r.classList.remove(`is-playing`),e=null;return}let n=i.duration;isFinite(n)&&n>0&&(i.currentTime=a*n),t=requestAnimationFrame(re),te(r)}else{let e=i.duration;isFinite(e)&&e>0&&(i.currentTime=a*e)}}document.addEventListener(`pointerdown`,e=>{let t=e.target;t.matches(`[data-audio-range]`)?(n=!0,r=t):t.matches(`[data-audio-waveform]`)&&(n=!0,i=t,se(t,e))},!0),document.addEventListener(`pointermove`,e=>{n&&i&&se(i,e)},!0),document.addEventListener(`pointerup`,()=>{n&&(r?(oe(r),r=null):i&&=(ce(i),null)),n=!1},!0),document.addEventListener(`pointercancel`,()=>{r=null,i=null,n=!1},!0),document.addEventListener(`click`,e=>{let t=e.target.closest(`[data-audio-play]`);if(!t)return;e.preventDefault();let n=t.closest(`.media-audio-card`);n&&ae(n)}),document.addEventListener(`input`,e=>{let t=e.target;if(!t.matches(`[data-audio-range]`))return;let n=t.closest(`.media-audio-card`);if(!n)return;let r=s(n);if(!r)return;let i=Number(t.value)/1e3;u(t,i);let a=r.duration,c=n.querySelector(`[data-audio-time]`);c&&isFinite(a)&&a>0&&(c.textContent=`${o(i*a)} / ${o(a)}`)},!0),document.addEventListener(`ended`,n=>{let r=n.target;if(!r.closest)return;let i=r.closest(`.media-audio-card`);if(!i)return;i.classList.remove(`is-playing`),cancelAnimationFrame(t),e=null;let a=c(i);a&&(a.value=`0`,u(a,0)),d.has(i)&&m(i,0);let o=i.querySelector(`[data-audio-time]`);o&&(o.textContent=`0:00`)},!0),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>ee()):ee();var h=`jant:media-lightbox-toggle`,le=`jant:media-video-playback-intent`,g=new Set;function _(e){let t=e?.trim();return!!t&&g.has(t)}function ue(e,t){let n=e?.trim();n&&(t?g.add(n):g.delete(n),document.dispatchEvent(new CustomEvent(le,{detail:{mediaId:n,paused:t}})))}var de=`75% 0px`,fe=.6,v=.25,pe=160,y=new Set,me=new WeakMap,he=new WeakSet,b=null,x=null,S=null,ge=!1,C=null,w=null,T=null;function _e(e){return e.length===0?null:[...e].sort((e,t)=>t.visibleArea===e.visibleArea?e.centerDistance===t.centerDistance?t.intersectionRatio-e.intersectionRatio:e.centerDistance-t.centerDistance:t.visibleArea-e.visibleArea)[0]??null}function ve(){return{x:(globalThis.innerWidth||document.documentElement.clientWidth||0)/2,y:(globalThis.innerHeight||document.documentElement.clientHeight||0)/2}}function ye(e){return me.get(e)}function be(e){return _(e)}function E(e){return be(e.dataset.feedVideoId)}function xe(e){return e.closest(`.media-video-wrap`)?.querySelector(`[data-feed-video-mute-toggle]`)??null}function D(e){let t=xe(e);if(!t)return;let n=S!==e||e.muted;t.dataset.muted=n?`true`:`false`,t.setAttribute(`aria-label`,n?`Play with sound`:`Mute video`)}function Se(){for(let e of y)D(e)}function Ce(e){if(he.has(e))return;let t=e.dataset.videoSrc;t&&(e.getAttribute(`src`)!==t&&(e.src=t),e.load(),he.add(e))}function O(e){e?.pause()}function we(e){if(E(e)){O(e);return}Ce(e),e.muted=S!==e,e.playsInline=!0,e.loop=!0,D(e),e.play().catch(()=>{})}function Te(){for(let e of y)e.isConnected||(w?.unobserve(e),T?.unobserve(e),y.delete(e),e===b&&(b=null),e===x&&(x=null),e===S&&(S=null))}function Ee(){if(C=null,Te(),document.hidden||ge){O(b);return}let e=[];for(let t of y){let n=ye(t);n&&(E(t)||n.intersectionRatio<fe||e.push({video:t,...n}))}let t=null;if(x?.isConnected&&!E(x)){let e=ye(x);e&&e.intersectionRatio>v?t=x:(!e||e.intersectionRatio<=v)&&(x=null)}else x&&E(x)&&(x=null);if(t||=_e(e)?.video??null,!t){let e=b?ye(b):void 0;if(b&&!E(b)&&e&&e.intersectionRatio>v)return;O(b),b=null;return}t!==b&&(O(b),b=t),we(t);for(let e of y)e!==t&&(O(e),D(e))}function k(){C!==null&&globalThis.clearTimeout(C),C=globalThis.setTimeout(Ee,pe)}function De(e){let t=ve();for(let n of e){let e=n.target,r=n.boundingClientRect,i=r.left+r.width/2,a=r.top+r.height/2,o=n.intersectionRect.width*n.intersectionRect.height,s=Math.hypot(i-t.x,a-t.y);me.set(e,{intersectionRatio:n.intersectionRatio,visibleArea:o,centerDistance:s})}k()}function Oe(e){for(let t of e)t.isIntersecting&&Ce(t.target)}function ke(){w&&T||globalThis.IntersectionObserver!==void 0&&(w=new globalThis.IntersectionObserver(De,{threshold:[0,v,fe,1]}),T=new globalThis.IntersectionObserver(Oe,{rootMargin:de,threshold:0}))}function Ae(e){y.has(e)||(ke(),!(!w||!T)&&(y.add(e),w.observe(e),T.observe(e),xe(e)?.addEventListener(`click`,je),D(e)))}function je(e){e.preventDefault(),e.stopPropagation();let t=e.currentTarget.closest(`.media-video-wrap`)?.querySelector(`[data-feed-short-video]`);if(!t)return;let n=t.dataset.feedVideoId?.trim();n&&ue(n,!1),x=t,S!==t||t.muted?(S&&S!==t&&(S.muted=!0),S=t,b!==t&&(O(b),b=t),we(t)):(S=null,t.muted=!0,D(t)),Se(),k()}function Me(e=document){let t=e.querySelectorAll(`[data-feed-short-video]`);for(let e of t)Ae(e);k()}document.addEventListener(h,e=>{ge=e.detail?.open===!0,ge&&O(b),k()}),document.addEventListener(le,e=>{let t=e.detail,n=t?.mediaId?.trim();if(!(!n||typeof t.paused!=`boolean`)){for(let e of y)e.dataset.feedVideoId?.trim()===n&&t.paused&&(O(e),b===e&&(b=null),x===e&&(x=null));k()}}),document.addEventListener(`visibilitychange`,()=>{document.hidden&&O(b),k()}),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>Me(),{once:!0}):queueMicrotask(()=>Me());var Ne=4;function Pe(e){return e.querySelector(`[data-post-media]`)}function Fe(e){let t=Pe(e);if(!t)return;let{scrollLeft:n,scrollWidth:r,clientWidth:i}=t;e.classList.toggle(`can-scroll-start`,n>Ne),e.classList.toggle(`can-scroll-end`,n+i<r-Ne)}function Ie(e){return Math.max(160,Math.round(e.clientWidth*.85))}function A(e,t){e.scrollBy({left:t*Ie(e),behavior:`smooth`})}function j(e){if(e.dataset.scrollHintReady===`1`)return;let t=Pe(e);t&&(e.dataset.scrollHintReady=`1`,Fe(e),t.addEventListener(`scroll`,()=>Fe(e),{passive:!0}),e.querySelector(`.media-gallery-nav-prev`)?.addEventListener(`click`,()=>A(t,-1)),e.querySelector(`.media-gallery-nav-next`)?.addEventListener(`click`,()=>A(t,1)),t.addEventListener(`keydown`,e=>{if(e.target===t)switch(e.key){case`ArrowRight`:e.preventDefault(),A(t,1);break;case`ArrowLeft`:e.preventDefault(),A(t,-1);break;case`Home`:e.preventDefault(),t.scrollTo({left:0,behavior:`smooth`});break;case`End`:e.preventDefault(),t.scrollTo({left:t.scrollWidth,behavior:`smooth`});break}}))}function Le(){document.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(j)}var Re=new globalThis.MutationObserver(e=>{for(let t of e)for(let e of t.addedNodes)e instanceof HTMLElement&&(e.matches(`.media-gallery-scroll-wrap`)&&j(e),e.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(j))});document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>{Le(),Re.observe(document.body,{childList:!0,subtree:!0})}):(Le(),Re.observe(document.body,{childList:!0,subtree:!0}));var ze=new WeakMap,Be=new WeakMap;function Ve(e){let t=e.querySelector(`.site-header-more-btn`),n=e.querySelector(`.site-header-more-popover`);if(!t||!n||t.dataset.moreInitialized===`true`)return;t.dataset.moreInitialized=`true`;let r=new AbortController,i=r.signal;ze.set(t,r);function a(){n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:t.parentElement}}))}function o(e=!1){n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus()}t.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),t.getAttribute(`aria-expanded`)===`true`?o():a()},{signal:i}),document.addEventListener(`click`,e=>{e.target instanceof Node&&(t.parentElement?.contains(e.target)||o())},{signal:i}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.getAttribute(`aria-hidden`)===`false`&&o(!0)},{signal:i}),document.addEventListener(`basecoat:popover`,e=>{e.detail?.source!==t.parentElement&&o()},{signal:i})}var He=`jant:nav-fresh-visits`;function Ue(){let e=document.createElement(`span`);return e.className=`site-header-link-fresh`,e.setAttribute(`aria-hidden`,`true`),e.textContent=`*`,e}function We(e){try{let t=JSON.parse(localStorage.getItem(He)||`{}`),n=location.pathname,r=e.querySelectorAll(`[data-fresh-at]`);for(let e of r){let r=new URL(e.href).pathname,i=parseInt(e.dataset.freshAt,10);if(r===n)t[r]=Math.floor(Date.now()/1e3);else{let n=t[r];if(!n||n<i){let t=e.querySelector(`svg`);e.insertBefore(Ue(),t)}}}localStorage.setItem(He,JSON.stringify(t))}catch{}}function Ge(e=document){let t=e.querySelector(`.site-header-hamburger`),n=e.querySelector(`#site-nav-drawer`),r=e.querySelector(`.site-nav-drawer-backdrop`),i=n?.querySelector(`.site-nav-drawer-close`);if(We(e),Ve(e),!t||!n||!r||t.dataset.drawerInitialized===`true`)return;t.dataset.drawerInitialized=`true`;let a=new AbortController,o=a.signal;Be.set(t,a);function s(){n.setAttribute(`aria-hidden`,`false`),n.removeAttribute(`inert`),r.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.documentElement.classList.add(`drawer-open`);let e=n.querySelector(`.site-nav-drawer-close`)??n.querySelector(`a[href], button`);e&&e.focus()}function c(e=!0){n.setAttribute(`aria-hidden`,`true`),r.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),document.documentElement.classList.remove(`drawer-open`),n.addEventListener(`transitionend`,()=>{n.getAttribute(`aria-hidden`)===`true`&&n.setAttribute(`inert`,``)},{once:!0,signal:o}),e&&t.focus()}t.addEventListener(`click`,()=>{t.getAttribute(`aria-expanded`)===`true`?c():s()},{signal:o}),i?.addEventListener(`click`,()=>c(),{signal:o}),r.addEventListener(`click`,()=>c(),{signal:o}),n.addEventListener(`click`,e=>{e.target instanceof Element&&e.target.closest(`a[href]`)&&c(!1)},{signal:o}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),c())},{signal:o})}Ge();var M=globalThis,N=M.ShadowRoot&&(M.ShadyCSS===void 0||M.ShadyCSS.nativeShadow)&&`adoptedStyleSheets`in Document.prototype&&`replace`in CSSStyleSheet.prototype,Ke=Symbol(),qe=new WeakMap,Je=class{constructor(e,t,n){if(this._$cssResult$=!0,n!==Ke)throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(N&&e===void 0){let n=t!==void 0&&t.length===1;n&&(e=qe.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),n&&qe.set(t,e))}return e}toString(){return this.cssText}},Ye=e=>new Je(typeof e==`string`?e:e+``,void 0,Ke),Xe=(e,t)=>{if(N)e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let n of t){let t=document.createElement(`style`),r=M.litNonce;r!==void 0&&t.setAttribute(`nonce`,r),t.textContent=n.cssText,e.appendChild(t)}},Ze=N?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t=``;for(let n of e.cssRules)t+=n.cssText;return Ye(t)})(e):e,{is:Qe,defineProperty:$e,getOwnPropertyDescriptor:et,getOwnPropertyNames:tt,getOwnPropertySymbols:nt,getPrototypeOf:rt}=Object,P=globalThis,it=P.trustedTypes,at=it?it.emptyScript:``,ot=P.reactiveElementPolyfillSupport,F=(e,t)=>e,I={toAttribute(e,t){switch(t){case Boolean:e=e?at:null;break;case Object:case Array:e=e==null?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=e!==null;break;case Number:n=e===null?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch{n=null}}return n}},st=(e,t)=>!Qe(e,t),ct={attribute:!0,type:String,converter:I,reflect:!1,useDefault:!1,hasChanged:st};Symbol.metadata??=Symbol(`metadata`),P.litPropertyMetadata??=new WeakMap;var L=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=ct){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let n=Symbol(),r=this.getPropertyDescriptor(e,n,t);r!==void 0&&$e(this.prototype,e,r)}}static getPropertyDescriptor(e,t,n){let{get:r,set:i}=et(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:r,set(t){let a=r?.call(this);i?.call(this,t),this.requestUpdate(e,a,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??ct}static _$Ei(){if(this.hasOwnProperty(F(`elementProperties`)))return;let e=rt(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(F(`finalized`)))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(F(`properties`))){let e=this.properties,t=[...tt(e),...nt(e)];for(let n of t)this.createProperty(n,e[n])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[e,n]of t)this.elementProperties.set(e,n)}this._$Eh=new Map;for(let[e,t]of this.elementProperties){let n=this._$Eu(e,t);n!==void 0&&this._$Eh.set(n,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let n=new Set(e.flat(1/0).reverse());for(let e of n)t.unshift(Ze(e))}else e!==void 0&&t.push(Ze(e));return t}static _$Eu(e,t){let n=t.attribute;return!1===n?void 0:typeof n==`string`?n:typeof e==`string`?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let n of t.keys())this.hasOwnProperty(n)&&(e.set(n,this[n]),delete this[n]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return Xe(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,n){this._$AK(e,n)}_$ET(e,t){let n=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,n);if(r!==void 0&&!0===n.reflect){let i=(n.converter?.toAttribute===void 0?I:n.converter).toAttribute(t,n.type);this._$Em=e,i==null?this.removeAttribute(r):this.setAttribute(r,i),this._$Em=null}}_$AK(e,t){let n=this.constructor,r=n._$Eh.get(e);if(r!==void 0&&this._$Em!==r){let e=n.getPropertyOptions(r),i=typeof e.converter==`function`?{fromAttribute:e.converter}:e.converter?.fromAttribute===void 0?I:e.converter;this._$Em=r;let a=i.fromAttribute(t,e.type);this[r]=a??this._$Ej?.get(r)??a,this._$Em=null}}requestUpdate(e,t,n,r=!1,i){if(e!==void 0){let a=this.constructor;if(!1===r&&(i=this[e]),n??=a.getPropertyOptions(e),!((n.hasChanged??st)(i,t)||n.useDefault&&n.reflect&&i===this._$Ej?.get(e)&&!this.hasAttribute(a._$Eu(e,n))))return;this.C(e,t,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:n,reflect:r,wrapped:i},a){n&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,a??t??this[e]),!0!==i||a!==void 0)||(this._$AL.has(e)||(this.hasUpdated||n||(t=void 0),this._$AL.set(e,t)),!0===r&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}let e=this.constructor.elementProperties;if(e.size>0)for(let[t,n]of e){let{wrapped:e}=n,r=this[t];!0!==e||this._$AL.has(t)||r===void 0||this.C(t,void 0,n,r)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};L.elementStyles=[],L.shadowRootOptions={mode:`open`},L[F(`elementProperties`)]=new Map,L[F(`finalized`)]=new Map,ot?.({ReactiveElement:L}),(P.reactiveElementVersions??=[]).push(`2.1.2`);var lt=globalThis,ut=e=>e,R=lt.trustedTypes,dt=R?R.createPolicy(`lit-html`,{createHTML:e=>e}):void 0,ft=`$lit$`,z=`lit$${Math.random().toFixed(9).slice(2)}$`,pt=`?`+z,mt=`<${pt}>`,B=document,V=()=>B.createComment(``),H=e=>e===null||typeof e!=`object`&&typeof e!=`function`,ht=Array.isArray,gt=e=>ht(e)||typeof e?.[Symbol.iterator]==`function`,_t=`[ \n\\f\\r]`,U=/<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g,vt=/-->/g,yt=/>/g,W=RegExp(`>|${_t}(?:([^\\\\s\"'>=/]+)(${_t}*=${_t}*(?:[^ \\t\\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`,`g`),bt=/'/g,xt=/\"/g,St=/^(?:script|style|textarea|title)$/i,Ct=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),G=Ct(1),K=Ct(2),q=Symbol.for(`lit-noChange`),J=Symbol.for(`lit-nothing`),wt=new WeakMap,Y=B.createTreeWalker(B,129);function Tt(e,t){if(!ht(e)||!e.hasOwnProperty(`raw`))throw Error(`invalid template strings array`);return dt===void 0?t:dt.createHTML(t)}var Et=(e,t)=>{let n=e.length-1,r=[],i,a=t===2?`<svg>`:t===3?`<math>`:``,o=U;for(let t=0;t<n;t++){let n=e[t],s,c,l=-1,u=0;for(;u<n.length&&(o.lastIndex=u,c=o.exec(n),c!==null);)u=o.lastIndex,o===U?c[1]===`!--`?o=vt:c[1]===void 0?c[2]===void 0?c[3]!==void 0&&(o=W):(St.test(c[2])&&(i=RegExp(`</`+c[2],`g`)),o=W):o=yt:o===W?c[0]===`>`?(o=i??U,l=-1):c[1]===void 0?l=-2:(l=o.lastIndex-c[2].length,s=c[1],o=c[3]===void 0?W:c[3]===`\"`?xt:bt):o===xt||o===bt?o=W:o===vt||o===yt?o=U:(o=W,i=void 0);let d=o===W&&e[t+1].startsWith(`/>`)?` `:``;a+=o===U?n+mt:l>=0?(r.push(s),n.slice(0,l)+ft+n.slice(l)+z+d):n+z+(l===-2?t:d)}return[Tt(e,a+(e[n]||`<?>`)+(t===2?`</svg>`:t===3?`</math>`:``)),r]},Dt=class e{constructor({strings:t,_$litType$:n},r){let i;this.parts=[];let a=0,o=0,s=t.length-1,c=this.parts,[l,u]=Et(t,n);if(this.el=e.createElement(l,r),Y.currentNode=this.el.content,n===2||n===3){let e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;(i=Y.nextNode())!==null&&c.length<s;){if(i.nodeType===1){if(i.hasAttributes())for(let e of i.getAttributeNames())if(e.endsWith(ft)){let t=u[o++],n=i.getAttribute(e).split(z),r=/([.?@])?(.*)/.exec(t);c.push({type:1,index:a,name:r[2],strings:n,ctor:r[1]===`.`?At:r[1]===`?`?jt:r[1]===`@`?Mt:Z}),i.removeAttribute(e)}else e.startsWith(z)&&(c.push({type:6,index:a}),i.removeAttribute(e));if(St.test(i.tagName)){let e=i.textContent.split(z),t=e.length-1;if(t>0){i.textContent=R?R.emptyScript:``;for(let n=0;n<t;n++)i.append(e[n],V()),Y.nextNode(),c.push({type:2,index:++a});i.append(e[t],V())}}}else if(i.nodeType===8)if(i.data===pt)c.push({type:2,index:a});else{let e=-1;for(;(e=i.data.indexOf(z,e+1))!==-1;)c.push({type:7,index:a}),e+=z.length-1}a++}}static createElement(e,t){let n=B.createElement(`template`);return n.innerHTML=e,n}};function X(e,t,n=e,r){if(t===q)return t;let i=r===void 0?n._$Cl:n._$Co?.[r],a=H(t)?void 0:t._$litDirective$;return i?.constructor!==a&&(i?._$AO?.(!1),a===void 0?i=void 0:(i=new a(e),i._$AT(e,n,r)),r===void 0?n._$Cl=i:(n._$Co??=[])[r]=i),i!==void 0&&(t=X(e,i._$AS(e,t.values),i,r)),t}var Ot=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:n}=this._$AD,r=(e?.creationScope??B).importNode(t,!0);Y.currentNode=r;let i=Y.nextNode(),a=0,o=0,s=n[0];for(;s!==void 0;){if(a===s.index){let t;s.type===2?t=new kt(i,i.nextSibling,this,e):s.type===1?t=new s.ctor(i,s.name,s.strings,this,e):s.type===6&&(t=new Nt(i,this,e)),this._$AV.push(t),s=n[++o]}a!==s?.index&&(i=Y.nextNode(),a++)}return Y.currentNode=B,r}p(e){let t=0;for(let n of this._$AV)n!==void 0&&(n.strings===void 0?n._$AI(e[t]):(n._$AI(e,n,t),t+=n.strings.length-2)),t++}},kt=class e{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,n,r){this.type=2,this._$AH=J,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=n,this.options=r,this._$Cv=r?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=X(this,e,t),H(e)?e===J||e==null||e===``?(this._$AH!==J&&this._$AR(),this._$AH=J):e!==this._$AH&&e!==q&&this._(e):e._$litType$===void 0?e.nodeType===void 0?gt(e)?this.k(e):this._(e):this.T(e):this.$(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==J&&H(this._$AH)?this._$AA.nextSibling.data=e:this.T(B.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:n}=e,r=typeof n==`number`?this._$AC(e):(n.el===void 0&&(n.el=Dt.createElement(Tt(n.h,n.h[0]),this.options)),n);if(this._$AH?._$AD===r)this._$AH.p(t);else{let e=new Ot(r,this),n=e.u(this.options);e.p(t),this.T(n),this._$AH=e}}_$AC(e){let t=wt.get(e.strings);return t===void 0&&wt.set(e.strings,t=new Dt(e)),t}k(t){ht(this._$AH)||(this._$AH=[],this._$AR());let n=this._$AH,r,i=0;for(let a of t)i===n.length?n.push(r=new e(this.O(V()),this.O(V()),this,this.options)):r=n[i],r._$AI(a),i++;i<n.length&&(this._$AR(r&&r._$AB.nextSibling,i),n.length=i)}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(!1,!0,t);e!==this._$AB;){let t=ut(e).nextSibling;ut(e).remove(),e=t}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e))}},Z=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,n,r,i){this.type=1,this._$AH=J,this._$AN=void 0,this.element=e,this.name=t,this._$AM=r,this.options=i,n.length>2||n[0]!==``||n[1]!==``?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=J}_$AI(e,t=this,n,r){let i=this.strings,a=!1;if(i===void 0)e=X(this,e,t,0),a=!H(e)||e!==this._$AH&&e!==q,a&&(this._$AH=e);else{let r=e,o,s;for(e=i[0],o=0;o<i.length-1;o++)s=X(this,r[n+o],t,o),s===q&&(s=this._$AH[o]),a||=!H(s)||s!==this._$AH[o],s===J?e=J:e!==J&&(e+=(s??``)+i[o+1]),this._$AH[o]=s}a&&!r&&this.j(e)}j(e){e===J?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??``)}},At=class extends Z{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===J?void 0:e}},jt=class extends Z{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==J)}},Mt=class extends Z{constructor(e,t,n,r,i){super(e,t,n,r,i),this.type=5}_$AI(e,t=this){if((e=X(this,e,t,0)??J)===q)return;let n=this._$AH,r=e===J&&n!==J||e.capture!==n.capture||e.once!==n.once||e.passive!==n.passive,i=e!==J&&(n===J||r);r&&this.element.removeEventListener(this.name,this,n),i&&this.element.addEventListener(this.name,this,e),this._$AH=e}handleEvent(e){typeof this._$AH==`function`?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e)}},Nt=class{constructor(e,t,n){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=n}get _$AU(){return this._$AM._$AU}_$AI(e){X(this,e)}},Pt=lt.litHtmlPolyfillSupport;Pt?.(Dt,kt),(lt.litHtmlVersions??=[]).push(`3.3.2`);var Ft=(e,t,n)=>{let r=n?.renderBefore??t,i=r._$litPart$;if(i===void 0){let e=n?.renderBefore??null;r._$litPart$=i=new kt(t.insertBefore(V(),e),e,void 0,n??{})}return i._$AI(e),i},It=globalThis,Q=class extends L{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=Ft(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return q}};Q._$litElement$=!0,Q.finalized=!0,It.litElementHydrateSupport?.({LitElement:Q});var Lt=It.litElementPolyfillSupport;Lt?.({LitElement:Q}),(It.litElementVersions??=[]).push(`4.2.2`);function Rt(e){if(!e.mimeType?.startsWith(`video/`)||!Number.isFinite(e.durationSeconds)||!e.durationSeconds||e.durationSeconds<=0||e.durationSeconds>15)return!1;let t=e.size;return!(typeof t==`number`&&t>12582912)}var zt=640,Bt=8,Vt=72,Ht=16,Ut=704,Wt=.9,Gt=.85;function $(e){if(!(!Number.isFinite(e)||!e||e<=0))return e}function Kt(){return{width:globalThis.innerWidth||document.documentElement.clientWidth||0,height:globalThis.innerHeight||document.documentElement.clientHeight||0}}function qt(e,t){let n=e<=zt?Bt:Vt;return{width:Math.max(0,e-n*2),height:Math.max(0,t-Ht*2)}}function Jt(e,t,n){let r=$(e?.width),i=$(e?.height);if(!r||!i)return null;let a=qt(t,n);if(a.width<=0||a.height<=0)return null;let o=Math.min(a.width/r,a.height/i);return{width:Math.max(1,Math.round(r*o)),height:Math.max(1,Math.round(i*o))}}function Yt(e,t,n){if(!e||e.mimeType?.startsWith(`video/`)||!$(e.width)||!$(e.height)||t<=0||n<=0)return!1;let r=$(e.width),i=$(e.height);if(!r||!i)return!1;let a=t<=zt,o=qt(t,n),s=o.width,c=o.height;if(s<=0||c<=0)return!1;let l=r/i,u=Math.min(s,c*l);return l<Wt&&u<(a?s:Math.min(s,Ut))*Gt}var Xt=class extends Q{static properties={_images:{state:!0},_currentIndex:{state:!0},_open:{state:!0},_viewportWidth:{state:!0},_viewportHeight:{state:!0},_videoCurrentTime:{state:!0},_videoDuration:{state:!0},_videoMuted:{state:!0},_videoPaused:{state:!0},_imageZoomed:{state:!0}};createRenderRoot(){return this.innerHTML=``,this}constructor(){super();let e=Kt();this._images=[],this._currentIndex=0,this._open=!1,this._viewportWidth=e.width,this._viewportHeight=e.height,this._videoCurrentTime=0,this._videoDuration=0,this._videoMuted=!1,this._videoPaused=!1,this._imageZoomed=!1}connectedCallback(){super.connectedCallback(),document.addEventListener(`click`,this.#e),window.addEventListener(`resize`,this.#l),this.#u()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(`click`,this.#e),window.removeEventListener(`resize`,this.#l)}open(e,t){this.#u(),this.#d(),this._images=e,this._currentIndex=Math.max(0,Math.min(t,e.length-1)),this.#m(this._images[this._currentIndex]),this._imageZoomed=!1,this._open=!0,document.dispatchEvent(new CustomEvent(h,{detail:{open:!0}})),this.updateComplete.then(()=>{this.querySelector(`.media-lightbox`)?.showModal(),this.#f()})}close(){this.#d(),this.querySelector(`.media-lightbox`)?.close(),this._open=!1,document.dispatchEvent(new CustomEvent(h,{detail:{open:!1}}))}#e=e=>{let t=e.target,n=t.closest(`[data-post-media] a[data-lightbox-index]`);if(n){let t=n.closest(`[data-lightbox-group]`);if(!t)return;e.preventDefault();let r=parseInt(n.dataset.lightboxIndex??`0`,10);try{let e=JSON.parse(t.dataset.lightboxGroup??`[]`);e.length>0&&this.open(e,r)}catch{}return}let r=t.closest(`[data-post-body] img`);if(r){e.preventDefault();let t=r.closest(`[data-post-body]`);if(!t)return;let n=Array.from(t.querySelectorAll(`img`)),i=n.map(e=>({url:e.src,alt:e.alt||``,width:$(e.naturalWidth||Number(e.getAttribute(`width`))),height:$(e.naturalHeight||Number(e.getAttribute(`height`)))})),a=n.indexOf(r);i.length>0&&this.open(i,Math.max(0,a))}};#t(){this._images.length<=1||(this.#d(),this._imageZoomed=!1,this._currentIndex=(this._currentIndex-1+this._images.length)%this._images.length)}#n(){this._images.length<=1||(this.#d(),this._imageZoomed=!1,this._currentIndex=(this._currentIndex+1)%this._images.length)}#r=e=>{let t=this._images[this._currentIndex];Yt(t,this._viewportWidth,this._viewportHeight)&&(e.stopPropagation(),this._imageZoomed=!this._imageZoomed)};#i=e=>{let t=e,n=e.target;if(t.key===`Escape`){e.preventDefault(),this.close();return}if(n instanceof HTMLInputElement||n instanceof HTMLButtonElement||n instanceof HTMLVideoElement)return;if(!this._images[this._currentIndex]?.mimeType?.startsWith(`video/`)){t.key===`ArrowLeft`?(e.preventDefault(),this.#t()):t.key===`ArrowRight`&&(e.preventDefault(),this.#n());return}let r=this.querySelector(`.media-lightbox-video`);r&&this.#a(t,r)};#a(e,t){let n=Number.isFinite(t.duration)&&t.duration>0?t.duration:null,r=e=>{let r=n==null?Math.max(0,e):Math.max(0,Math.min(e,n));t.currentTime=r,this._videoCurrentTime=r},i=e.key,a=i.toLowerCase();if(i===` `||a===`k`)e.preventDefault(),this.#x(t);else if(i===`ArrowLeft`)e.preventDefault(),r(t.currentTime-2);else if(i===`ArrowRight`)e.preventDefault(),r(t.currentTime+2);else if(i===`Home`)e.preventDefault(),r(0);else if(i===`End`)n!=null&&(e.preventDefault(),r(n));else if(i.length===1&&i>=`0`&&i<=`9`)n!=null&&(e.preventDefault(),r(Number(i)/10*n));else if(i===`ArrowUp`)e.preventDefault(),t.volume=Math.min(1,t.volume+.05);else if(i===`ArrowDown`)e.preventDefault(),t.volume=Math.max(0,t.volume-.05);else if(a===`m`){e.preventDefault();let n=!t.muted;t.muted=n,this._videoMuted=n}else a===`f`&&(e.preventDefault(),this.#o(t))}#o(e){let t=document,n=e;if(document.fullscreenElement??t.webkitFullscreenElement){document.exitFullscreen?document.exitFullscreen().catch(()=>{}):t.webkitExitFullscreen?.();return}e.requestFullscreen?e.requestFullscreen().catch(()=>{}):n.webkitRequestFullscreen?n.webkitRequestFullscreen():n.webkitEnterFullscreen&&n.webkitEnterFullscreen()}#s=e=>{let t=e.target;(t===e.currentTarget||t.classList.contains(`media-lightbox-content`)||t.classList.contains(`media-lightbox-stage`))&&this.close()};#c=()=>{this.#d(),this._open&&document.dispatchEvent(new CustomEvent(h,{detail:{open:!1}})),this._open=!1};#l=()=>{this.#u()};#u(){let e=Kt();e.width===this._viewportWidth&&e.height===this._viewportHeight||(this._viewportWidth=e.width,this._viewportHeight=e.height)}#d(){this.querySelector(`.media-lightbox-video`)?.pause()}#f(){this.querySelector(`.media-lightbox-content`)?.focus()}#p=()=>{this.querySelector(`.media-lightbox-content`)?.focus({preventScroll:!0})};#m(e){this._videoCurrentTime=0,this._videoDuration=e?.durationSeconds&&e.durationSeconds>0?e.durationSeconds:0,this._videoMuted=!1,this._videoPaused=_(e?.id)}#h(){let e=this._images[this._currentIndex];if(!Rt(e)){this.#m(e);return}let t=this.querySelector(`.media-lightbox-video`);if(t){if(t.currentTime=0,t.muted=this._videoMuted,this._videoPaused=_(e.id),this._videoPaused){t.pause();return}t.play().catch(()=>{this._videoPaused=!0})}}#g=e=>{let t=e.currentTarget;Number.isFinite(t.duration)&&t.duration>0&&(this._videoDuration=t.duration),this._videoCurrentTime=t.currentTime,t.muted=this._videoMuted};#_=e=>{let t=e.currentTarget;this._videoCurrentTime=t.currentTime,Number.isFinite(t.duration)&&t.duration>0&&(this._videoDuration=t.duration)};#v=()=>{this._videoPaused=!1};#y=()=>{this._videoPaused=!0};#b(e){let t=this._images[this._currentIndex],n=t?.id?.trim();!n||!Rt(t)||ue(n,e)}#x(e){if(e.paused){this._videoPaused=!1,this.#b(!1),e.play().catch(()=>{this._videoPaused=!0,this.#b(!0)});return}e.pause(),this._videoPaused=!0,this.#b(!0)}#S=()=>{let e=this.querySelector(`.media-lightbox-video`);e&&this.#x(e)};#C=e=>{let t=e.currentTarget,n=this.querySelector(`.media-lightbox-video`),r=Number.parseFloat(t.value);!n||!Number.isFinite(r)||r<0||(n.currentTime=r,this._videoCurrentTime=r)};#w=()=>{this._videoMuted=!this._videoMuted;let e=this.querySelector(`.media-lightbox-video`);e&&(e.muted=this._videoMuted)};updated(e){if(super.updated(e),!this._open||!e.has(`_currentIndex`)&&!e.has(`_open`)&&!e.has(`_imageZoomed`))return;let t=this.querySelector(`.media-lightbox-stage`);t&&(t.scrollTop=0,t.scrollLeft=0,(e.has(`_currentIndex`)||e.has(`_open`))&&(this.#h(),this.#f()))}render(){if(!this._open)return J;let e=this._images[this._currentIndex],t=this._images.length>1,n=e?.mimeType?.startsWith(`video/`),r=Rt(e),i=Yt(e,this._viewportWidth,this._viewportHeight),a=i&&this._imageZoomed,o=r?Jt(e,this._viewportWidth,this._viewportHeight):null,s=r&&!!o&&o.height>o.width,c=o?`--media-lightbox-short-width:${o.width}px;--media-lightbox-short-height:${o.height}px;`:J,l=this._videoDuration>0?this._videoDuration:e?.durationSeconds??1,u=Math.min(this._videoCurrentTime,l),d=l>0?u/l*100:0;return G`\n <dialog\n class=${`media-lightbox${r?` media-lightbox-short`:``}`}\n @keydown=${this.#i}\n @click=${this.#s}\n @close=${this.#c}\n >\n <div class=\"media-lightbox-content\" tabindex=\"-1\">\n <button\n type=\"button\"\n class=\"media-lightbox-close\"\n @click=${()=>this.close()}\n aria-label=\"Close\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n\n ${t?G`<div class=\"media-lightbox-counter\">\n ${this._currentIndex+1} / ${this._images.length}\n </div>`:J}\n <div\n class=${`media-lightbox-stage${a?` media-lightbox-stage-scroll`:``}`}\n >\n ${n?r?G`<div\n class=${`media-lightbox-short-frame${o?` media-lightbox-short-frame-contained`:``}${s?` media-lightbox-short-frame-portrait`:` media-lightbox-short-frame-landscape`}`}\n style=${c}\n >\n <div class=\"media-lightbox-short-viewport\">\n <video\n class=\"media-lightbox-video media-lightbox-video-short\"\n src=${e?.url??``}\n poster=${e?.posterUrl??``}\n ?autoplay=${!this._videoPaused}\n playsinline\n loop\n ?muted=${this._videoMuted}\n @click=${this.#S}\n @focus=${this.#p}\n @loadedmetadata=${this.#g}\n @timeupdate=${this.#_}\n @play=${this.#v}\n @pause=${this.#y}\n ></video>\n </div>\n <div\n class=${`media-lightbox-short-controls${s?` media-lightbox-short-controls-portrait`:``}`}\n >\n <button\n type=\"button\"\n class=\"media-lightbox-short-playback\"\n @click=${this.#S}\n aria-label=${this._videoPaused?`Play video`:`Pause video`}\n >\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n ${this._videoPaused?K`<path d=\"M8 5v14l11-7z\" />`:K`<path d=\"M6 5h4v14H6zM14 5h4v14h-4z\" />`}\n </svg>\n </button>\n <input\n class=\"media-lightbox-short-progress\"\n type=\"range\"\n min=\"0\"\n max=${l}\n step=\"0.01\"\n .value=${String(u)}\n style=${`--media-progress:${d}%`}\n aria-label=\"Video progress\"\n @input=${this.#C}\n />\n <button\n type=\"button\"\n class=\"media-lightbox-short-mute\"\n @click=${this.#w}\n aria-label=${this._videoMuted?`Unmute video`:`Mute video`}\n >\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 48 48\"\n fill=\"currentColor\"\n color=\"#fff\"\n aria-hidden=\"true\"\n >\n ${this._videoMuted?K`\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1v-9.8c0-.4-.2-.8-.4-1.1l-22-22c-.3-.3-.7-.4-1.1-.4h-.6zm46.8 31.4-5.5-5.5C44.9 36.6 48 31.4 48 24c0-11.4-7.2-17.4-7.2-17.4-.6-.6-1.6-.6-2.2 0L37.2 8c-.6.6-.6 1.6 0 2.2 0 0 5.7 5 5.7 13.8 0 5.4-2.1 9.3-3.8 11.6L35.5 32c1.1-1.7 2.3-4.4 2.3-8 0-6.8-4.1-10.3-4.1-10.3-.6-.6-1.6-.6-2.2 0l-1.4 1.4c-.6.6-.6 1.6 0 2.2 0 0 2.6 2 2.6 6.7 0 1.8-.4 3.2-.9 4.3L25.5 22V1.4c0-1.3-1.6-1.9-2.5-1L13.5 10 3.3-.3c-.6-.6-1.5-.6-2.1 0L-.2 1.1c-.6.6-.6 1.5 0 2.1L4 7.6l26.8 26.8 13.9 13.9c.6.6 1.5.6 2.1 0l1.4-1.4c.7-.6.7-1.6.1-2.2z\" />\n `:K`\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1V1.4c0-1.3-1.6-1.9-2.5-1L10.2 13.3H1.5z\" />\n <path d=\"M30.1 15.9c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 4.1 3.5 4.1 11.7s-4.1 11.7-4.1 11.7c-.6.6-1.6.6-2.2 0l-1.4-1.4c-.6-.6-.6-1.6 0-2.2 0 0 2.6-2 2.6-8.1s-2.6-8.1-2.6-8.1z\" />\n <path d=\"M37.2 8c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 5.7 5.1 5.7 19.6s-5.7 19.6-5.7 19.6c-.6.6-1.6.6-2.2 0L37.2 42c-.6-.6-.6-1.6 0-2.2 0 0 4.3-4.4 4.3-15.8S37.2 8 37.2 8z\" />\n `}\n </svg>\n </button>\n </div>\n </div>`:G`<video\n class=\"media-lightbox-video\"\n src=${e?.url??``}\n poster=${e?.posterUrl??``}\n controls\n autoplay\n playsinline\n @focus=${this.#p}\n ></video>`:G`<img\n class=${`media-lightbox-img${i?` media-lightbox-img-zoomable`:``}${a?` media-lightbox-img-scroll`:``}`}\n src=${e?.url??``}\n alt=${e?.alt??``}\n @click=${this.#r}\n />`}\n </div>\n ${t?G`\n <button\n type=\"button\"\n class=\"media-lightbox-nav media-lightbox-nav-prev\"\n @click=${()=>this.#t()}\n aria-label=\"Previous\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"m15 18-6-6 6-6\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"media-lightbox-nav media-lightbox-nav-next\"\n @click=${()=>this.#n()}\n aria-label=\"Next\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"m9 18 6-6-6-6\" />\n </svg>\n </button>\n `:J}\n </div>\n </dialog>\n `}};customElements.define(`jant-media-lightbox`,Xt);";
|
|
3308
4449
|
//#endregion
|
|
3309
4450
|
//#region src/services/export-theme/assets/client-site.css?raw
|
|
3310
|
-
var client_site_default = "@keyframes lightbox-fade-in{0%{opacity:0}to{opacity:1}}@keyframes lightbox-scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.media-lightbox{background:0 0;border:none;outline:none;width:100%;max-width:100%;height:100%;max-height:100%;padding:0}.media-lightbox[open]{animation:.2s both lightbox-fade-in}.media-lightbox::backdrop{background-color:#000}.media-lightbox-content{outline:none;width:100%;height:100dvh;position:relative}.media-lightbox-stage{box-sizing:border-box;justify-content:center;align-items:center;width:100%;height:100%;padding:64px 96px;display:flex;overflow:hidden}.media-lightbox-stage-scroll{overscroll-behavior:contain;scrollbar-gutter:stable both-edges;-webkit-overflow-scrolling:touch;align-items:flex-start;overflow:hidden auto}.media-lightbox-img{object-fit:contain;border-radius:4px;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in;display:block}.media-lightbox-img-zoomable{cursor:zoom-in}.media-lightbox-img-scroll{width:min(100%,44rem);max-width:none;height:auto;max-height:none;margin:0 auto}.media-lightbox-img-zoomable.media-lightbox-img-scroll{cursor:zoom-out}.media-lightbox-close{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:40px;height:40px;transition:background-color .15s;display:flex;position:fixed;top:16px;left:16px}.media-lightbox-close:hover{background-color:#000000b3}.media-lightbox-nav{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s;display:flex;position:fixed;top:50%;transform:translateY(-50%)}.media-lightbox-nav:hover{background-color:#000000b3}.media-lightbox-nav-prev{left:16px}.media-lightbox-nav-next{right:16px}.media-lightbox-counter{z-index:10;font-size:var(--type-xs);color:#ffffffb3;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;position:fixed;top:20px;left:50%;transform:translate(-50%)}.media-lightbox-short-frame{max-width:100%;max-height:100%;display:block;position:relative}.media-lightbox-short-controls{z-index:2;pointer-events:none;height:86px;position:absolute;bottom:0;left:0;right:0}.media-lightbox-short-progress{--media-progress:0%;z-index:1;appearance:none;pointer-events:auto;cursor:pointer;background:0 0;width:auto;height:34px;margin:0;padding:0;display:block;position:absolute;bottom:0;left:16px;right:16px}.media-lightbox-short-progress::-webkit-slider-runnable-track{background:linear-gradient(to right, #ffffffeb 0, #ffffffeb var(--media-progress), #ffffff2e var(--media-progress), #ffffff2e 100%);border-radius:999px;height:2px;transition:height .15s,background .15s}.media-lightbox-short-progress::-webkit-slider-thumb{appearance:none;opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;margin-top:-4px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-progress::-moz-range-track{background:#ffffff2e;border:none;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-progress{background:#ffffffeb;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-thumb{opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-controls-portrait .media-lightbox-short-progress{width:calc(100% - 48px);right:auto}.media-lightbox-short-progress:hover::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:focus-visible::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-track{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-progress{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-progress{height:4px}.media-lightbox-short-progress:hover::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:hover::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-mute{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#777;border:none;border-radius:999px;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:30px;right:24px}.media-lightbox-short-mute svg{flex-shrink:0;width:16px;height:16px;display:block}.media-lightbox-short-mute:hover{background-color:#686868;transform:scale(1.03)}.media-lightbox-short-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}@media (max-width:640px){.media-lightbox-stage{padding:48px 16px}.media-lightbox-img{border-radius:0}.media-lightbox-img-scroll{width:100%}.media-lightbox-close{width:36px;height:36px;top:12px;left:12px}.media-lightbox-nav{width:36px;height:36px}.media-lightbox-nav-prev{left:8px}.media-lightbox-nav-next{right:8px}.media-lightbox-short-mute{bottom:26px;right:24px}}.media-visual-frame{border-radius:var(--media-radius,.5rem);background-color:var(--color-muted);display:block;overflow:hidden}.media-visual{background-position:50%;background-repeat:no-repeat;display:block}.media-video-wrap{position:relative}.media-video-link{cursor:pointer;display:block}.media-video-wrap video{object-fit:contain;background-color:var(--color-muted);width:100%;max-height:24rem}.media-video-wrap-short video{background-color:#000}.media-feed-video-mute{z-index:1;color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:999px;justify-content:center;align-items:center;width:28px;height:28px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:16px;right:16px}.media-feed-video-mute svg{width:12px;height:12px}.media-feed-video-mute:hover{background-color:#0000009e;transform:scale(1.03)}.media-feed-video-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-feed-video-icon{transition:opacity .15s,transform .15s;position:absolute}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-muted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-unmuted{opacity:1;transform:scale(1)}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-unmuted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-muted{opacity:0;transform:scale(.92)}.media-video-play-overlay{pointer-events:none;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:absolute;inset:0}.media-video-play-overlay svg{filter:drop-shadow(0 2px 6px #0006);opacity:.85;width:48px;height:48px}.media-gallery-card.media-audio-card{flex-direction:column;display:flex}.media-audio-card .media-audio-el{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.media-audio-card .media-audio-artwork{background:linear-gradient(160deg,#8080801f 0%,#80808008 100%);flex:1;justify-content:center;align-items:center;width:100%;min-height:0;display:flex}.media-audio-card .media-audio-artwork svg{width:32px;height:32px;color:var(--site-text-secondary);opacity:.3}.media-audio-card .media-audio-waveform{cursor:pointer;touch-action:none;width:100%;height:24px;display:none}.media-audio-card.has-waveform .media-audio-waveform{display:block}.media-audio-card.has-waveform .media-audio-range{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.media-audio-card .media-audio-controls{flex-direction:column;flex-shrink:0;padding:0 0 6px;display:flex}.media-audio-card .media-audio-range{appearance:none;cursor:pointer;touch-action:none;background:0 0;width:100%;height:20px;margin:0;padding:0}.media-audio-card .media-audio-range::-webkit-slider-runnable-track{background:#80808026;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-track{background:#80808026;border:none;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-progress{background:var(--site-text-primary);border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;margin-top:-3.5px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-webkit-slider-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-webkit-slider-thumb{opacity:1}.media-audio-card .media-audio-range::-moz-range-thumb{background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-moz-range-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-moz-range-thumb{opacity:1}.media-audio-card .media-audio-range:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:2px}.media-audio-card .media-audio-row{align-items:center;gap:6px;min-width:0;padding:6px 8px 0;display:flex}.media-audio-card .media-audio-info{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.media-audio-card .media-audio-title{font-size:var(--type-2xs);font-weight:var(--fw-medium,500);color:var(--site-text-primary);text-overflow:ellipsis;white-space:nowrap;line-height:1.3;overflow:hidden}.media-audio-card .media-audio-time{font-size:var(--type-2xs);color:var(--site-text-secondary);font-variant-numeric:tabular-nums;line-height:1}.media-audio-card .media-audio-play-btn{cursor:pointer;background:var(--site-text-primary);width:28px;height:28px;color:var(--background,#fff);border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;transition:transform .12s;display:flex}.media-audio-card .media-audio-play-btn:hover{transform:scale(1.1)}.media-audio-card .media-audio-play-btn:active{transform:scale(.92)}.media-audio-card .media-audio-play-btn svg{width:14px;height:14px}.media-audio-card .media-audio-icon-play{margin-left:2px}.media-audio-card .media-audio-icon-pause,.media-audio-card.is-playing .media-audio-icon-play{display:none}.media-audio-card.is-playing .media-audio-icon-pause{display:block}.media-gallery-card{color:var(--site-text-primary);border-radius:var(--media-radius,.5rem);background-color:var(--site-nav-hover-bg);border:1px solid var(--site-divider);text-decoration:none;transition:background-color .15s;display:block;overflow:hidden}a.media-gallery-card:hover,button.media-gallery-card:hover{background-color:var(--site-divider)}button.media-gallery-card{cursor:pointer;font:inherit;text-align:inherit}.media-gallery-card-inner{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:100%;padding:16px 12px;display:flex}.media-gallery-card-icon{color:var(--site-text-secondary);opacity:.6}.media-gallery-card-summary{font-size:var(--type-xs);color:var(--site-text-secondary);-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;line-height:1.4;display:-webkit-box;overflow:hidden}.media-gallery-card-meta{font-size:var(--type-xs);color:var(--site-text-secondary)}.media-lightbox-video{background-color:#000;border-radius:4px;outline:none;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in}.media-lightbox-video:focus,.media-lightbox-video:focus-visible{outline:none}.media-lightbox-video-short{object-fit:contain;width:100%;max-width:none;height:100%;max-height:none;display:block}@media (max-width:640px){.media-lightbox-video{border-radius:0}}[data-post-media] img{background:0 0}.media-gallery-scroll-wrap{position:relative}.media-gallery-nav{z-index:2;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer;opacity:0;pointer-events:none;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:34px;height:34px;padding:0;transition:opacity .15s,background-color .15s;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.media-gallery-nav:hover{background-color:#000000b3}.media-gallery-nav svg{width:18px;height:18px;display:block}.media-gallery-nav-prev{left:8px}.media-gallery-nav-next{right:8px}@media (hover:hover) and (pointer:fine){.media-gallery-scroll-wrap.can-scroll-start:hover .media-gallery-nav-prev,.media-gallery-scroll-wrap.can-scroll-end:hover .media-gallery-nav-next{opacity:1;pointer-events:auto}}.media-gallery-scroll-wrap>[data-post-media]:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:4px}\n/*$vite$:1*/";
|
|
4451
|
+
var client_site_default = "@keyframes lightbox-fade-in{0%{opacity:0}to{opacity:1}}@keyframes lightbox-scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.media-lightbox{background:0 0;border:none;outline:none;width:100%;max-width:100%;height:100%;max-height:100%;padding:0}.media-lightbox[open]{animation:.2s both lightbox-fade-in}.media-lightbox::backdrop{background-color:#000}.media-lightbox-content{outline:none;width:100%;height:100dvh;position:relative}.media-lightbox-stage{box-sizing:border-box;justify-content:center;align-items:center;width:100%;height:100%;padding:64px 96px;display:flex;overflow:hidden}.media-lightbox-stage-scroll{overscroll-behavior:contain;scrollbar-gutter:stable both-edges;-webkit-overflow-scrolling:touch;align-items:flex-start;overflow:hidden auto}.media-lightbox-img{object-fit:contain;border-radius:4px;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in;display:block}.media-lightbox-img-zoomable{cursor:zoom-in}.media-lightbox-img-scroll{width:min(100%,44rem);max-width:none;height:auto;max-height:none;margin:0 auto}.media-lightbox-img-zoomable.media-lightbox-img-scroll{cursor:zoom-out}.media-lightbox-close{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:40px;height:40px;transition:background-color .15s;display:flex;position:fixed;top:16px;left:16px}.media-lightbox-close:hover{background-color:#000000b3}.media-lightbox-nav{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s;display:flex;position:fixed;top:50%;transform:translateY(-50%)}.media-lightbox-nav:hover{background-color:#000000b3}.media-lightbox-nav-prev{left:16px}.media-lightbox-nav-next{right:16px}.media-lightbox-counter{z-index:10;font-size:var(--type-xs);color:#ffffffb3;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;position:fixed;top:20px;left:50%;transform:translate(-50%)}.media-lightbox-short-frame{background-color:#000;border-radius:4px;max-width:100%;max-height:100%;display:block;position:relative;overflow:hidden}.media-lightbox-short-frame-contained{width:var(--media-lightbox-short-width);height:var(--media-lightbox-short-height)}.media-lightbox-short-viewport{width:100%;height:100%;min-height:0}.media-lightbox-video-short{object-fit:contain;cursor:pointer;width:100%;max-width:none;height:100%;max-height:none;display:block}.media-lightbox-short-controls{z-index:2;box-sizing:border-box;pointer-events:none;background:linear-gradient(#0000,#000000ad);align-items:center;gap:2px;height:44px;padding:0 6px;transition:opacity .18s,transform .18s;display:flex;position:absolute;bottom:0;left:0;right:0}.media-lightbox-short-progress{--media-progress:0%;z-index:1;appearance:none;pointer-events:auto;cursor:pointer;background:0 0;flex:auto;width:0;min-width:48px;height:44px;margin:0;padding:0;display:block;position:relative}.media-lightbox-short-progress::-webkit-slider-runnable-track{background:linear-gradient(to right, #ffffffeb 0, #ffffffeb var(--media-progress), #ffffff2e var(--media-progress), #ffffff2e 100%);border-radius:999px;height:2px;transition:height .15s,background .15s}.media-lightbox-short-progress::-webkit-slider-thumb{appearance:none;opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;margin-top:-4px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-progress::-moz-range-track{background:#ffffff2e;border:none;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-progress{background:#ffffffeb;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-thumb{opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-playback{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#0000;border:none;border-radius:999px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;padding:0;transition:background-color .15s,transform .15s;display:inline-flex;position:relative}.media-lightbox-short-playback svg{width:16px;height:16px;display:block}.media-lightbox-short-playback:hover{background-color:#ffffff24}.media-lightbox-short-playback:active{transform:scale(.92)}.media-lightbox-short-playback:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-lightbox-short-progress:hover::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:focus-visible::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-track{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-progress{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-progress{height:4px}.media-lightbox-short-progress:hover::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:hover::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-mute{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#0000;border:none;border-radius:999px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;padding:0;transition:background-color .15s,transform .15s;display:inline-flex;position:relative}.media-lightbox-short-mute svg{flex-shrink:0;width:16px;height:16px;display:block}.media-lightbox-short-mute:hover{background-color:#ffffff24}.media-lightbox-short-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}@media (max-width:640px){.media-lightbox-stage{padding:48px 16px}.media-lightbox-img{border-radius:0}.media-lightbox-img-scroll{width:100%}.media-lightbox-close{width:36px;height:36px;top:12px;left:12px}.media-lightbox-nav{width:36px;height:36px}.media-lightbox-nav-prev{left:8px}.media-lightbox-nav-next{right:8px}.media-lightbox-short-frame{border-radius:0}}@media (hover:none),(pointer:coarse){.media-lightbox-short-frame-contained{height:calc(var(--media-lightbox-short-height) + 44px);grid-template-rows:minmax(0,1fr) 44px;display:grid}.media-lightbox-short-controls{background:#000;grid-row:2;position:relative}}@media (hover:hover) and (pointer:fine){.media-lightbox-short-controls{opacity:0;transform:translateY(4px)}.media-lightbox-short-controls>*{pointer-events:none}.media-lightbox-short-frame:hover .media-lightbox-short-controls,.media-lightbox-short-frame:focus-within .media-lightbox-short-controls{opacity:1;transform:translateY(0)}.media-lightbox-short-frame:hover .media-lightbox-short-controls>*,.media-lightbox-short-frame:focus-within .media-lightbox-short-controls>*{pointer-events:auto}}.media-visual-frame{border-radius:var(--media-radius,.5rem);background-color:var(--color-muted);display:block;overflow:hidden}.media-visual{background-position:50%;background-repeat:no-repeat;display:block}.media-video-wrap{position:relative}.media-video-link{cursor:pointer;display:block}.media-video-wrap video{object-fit:contain;background-color:var(--color-muted);width:100%;max-height:24rem}.media-video-wrap-short video{background-color:#000}.media-feed-video-mute{z-index:1;color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:999px;justify-content:center;align-items:center;width:28px;height:28px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:16px;right:16px}.media-feed-video-mute svg{width:12px;height:12px}.media-feed-video-mute:hover{background-color:#0000009e;transform:scale(1.03)}.media-feed-video-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-feed-video-icon{transition:opacity .15s,transform .15s;position:absolute}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-muted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-unmuted{opacity:1;transform:scale(1)}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-unmuted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-muted{opacity:0;transform:scale(.92)}.media-video-play-overlay{pointer-events:none;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:absolute;inset:0}.media-video-play-overlay svg{filter:drop-shadow(0 2px 6px #0006);opacity:.85;width:48px;height:48px}.media-gallery-card.media-audio-card{flex-direction:column;display:flex}.media-audio-card .media-audio-el{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.media-audio-card .media-audio-artwork{background:linear-gradient(160deg,#8080801f 0%,#80808008 100%);flex:1;justify-content:center;align-items:center;width:100%;min-height:0;display:flex}.media-audio-card .media-audio-artwork svg{width:32px;height:32px;color:var(--site-text-secondary);opacity:.3}.media-audio-card .media-audio-waveform{cursor:pointer;touch-action:none;width:100%;height:24px;display:none}.media-audio-card.has-waveform .media-audio-waveform{display:block}.media-audio-card.has-waveform .media-audio-range{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.media-audio-card .media-audio-controls{flex-direction:column;flex-shrink:0;padding:0 0 6px;display:flex}.media-audio-card .media-audio-range{appearance:none;cursor:pointer;touch-action:none;background:0 0;width:100%;height:20px;margin:0;padding:0}.media-audio-card .media-audio-range::-webkit-slider-runnable-track{background:#80808026;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-track{background:#80808026;border:none;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-progress{background:var(--site-text-primary);border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;margin-top:-3.5px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-webkit-slider-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-webkit-slider-thumb{opacity:1}.media-audio-card .media-audio-range::-moz-range-thumb{background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-moz-range-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-moz-range-thumb{opacity:1}.media-audio-card .media-audio-range:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:2px}.media-audio-card .media-audio-row{align-items:center;gap:6px;min-width:0;padding:6px 8px 0;display:flex}.media-audio-card .media-audio-info{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.media-audio-card .media-audio-title{font-size:var(--type-2xs);font-weight:var(--fw-medium,500);color:var(--site-text-primary);text-overflow:ellipsis;white-space:nowrap;line-height:1.3;overflow:hidden}.media-audio-card .media-audio-time{font-size:var(--type-2xs);color:var(--site-text-secondary);font-variant-numeric:tabular-nums;line-height:1}.media-audio-card .media-audio-play-btn{cursor:pointer;background:var(--site-text-primary);width:28px;height:28px;color:var(--background,#fff);border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;transition:transform .12s;display:flex}.media-audio-card .media-audio-play-btn:hover{transform:scale(1.1)}.media-audio-card .media-audio-play-btn:active{transform:scale(.92)}.media-audio-card .media-audio-play-btn svg{width:14px;height:14px}.media-audio-card .media-audio-icon-play{margin-left:2px}.media-audio-card .media-audio-icon-pause,.media-audio-card.is-playing .media-audio-icon-play{display:none}.media-audio-card.is-playing .media-audio-icon-pause{display:block}.media-gallery-card{color:var(--site-text-primary);border-radius:var(--media-radius,.5rem);background-color:var(--site-nav-hover-bg);border:1px solid var(--site-divider);text-decoration:none;transition:background-color .15s;display:block;overflow:hidden}a.media-gallery-card:hover,button.media-gallery-card:hover{background-color:var(--site-divider)}button.media-gallery-card{cursor:pointer;font:inherit;text-align:inherit}.media-gallery-card-inner{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:100%;padding:16px 12px;display:flex}.media-gallery-card-icon{color:var(--site-text-secondary);opacity:.6}.media-gallery-card-summary{font-size:var(--type-xs);color:var(--site-text-secondary);-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;line-height:1.4;display:-webkit-box;overflow:hidden}.media-gallery-card-meta{font-size:var(--type-xs);color:var(--site-text-secondary)}.media-lightbox-video{background-color:#000;border-radius:4px;outline:none;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in}.media-lightbox-video:focus,.media-lightbox-video:focus-visible{outline:none}.media-lightbox-video-short{object-fit:contain;width:100%;max-width:none;height:100%;max-height:none;display:block}@media (max-width:640px){.media-lightbox-video{border-radius:0}}[data-post-media] img{background:0 0}.media-gallery-scroll-wrap{position:relative}.media-gallery-nav{z-index:2;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer;opacity:0;pointer-events:none;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:34px;height:34px;padding:0;transition:opacity .15s,background-color .15s;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.media-gallery-nav:hover{background-color:#000000b3}.media-gallery-nav svg{width:18px;height:18px;display:block}.media-gallery-nav-prev{left:8px}.media-gallery-nav-next{right:8px}@media (hover:hover) and (pointer:fine){.media-gallery-scroll-wrap.can-scroll-start:hover .media-gallery-nav-prev,.media-gallery-scroll-wrap.can-scroll-end:hover .media-gallery-nav-next{opacity:1;pointer-events:auto}}.media-gallery-scroll-wrap>[data-post-media]:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:4px}\n/*$vite$:1*/";
|
|
3311
4452
|
//#endregion
|
|
3312
4453
|
//#region src/services/export-theme/layouts/_default/baseof.html?raw
|
|
3313
4454
|
var baseof_default = "<!doctype html>\n{{- $lang := .Site.LanguageCode | default \"en\" -}}\n{{- $themeMode := .Site.Params.theme_mode | default \"auto\" -}}\n{{- $themeId := .Site.Params.theme_id -}}\n<html lang=\"{{ $lang }}\"{{ with $themeId }} data-theme=\"{{ . }}\"{{ end }}{{ if ne $themeMode \"auto\" }} data-theme-mode=\"{{ $themeMode }}\"{{ end }}>\n <head>\n {{ partial \"head.html\" . }}\n </head>\n <body>\n <div class=\"site-page\">\n {{ partial \"header.html\" . }}\n <main class=\"site-main\" id=\"main\">\n {{ block \"main\" . }}{{ end }}\n </main>\n {{ partial \"footer.html\" . }}\n </div>\n {{/* Mount the shared media lightbox web component once per page.\n The component installs a document-level click listener on connect\n and intercepts clicks on [data-post-media] a[data-lightbox-index]. */}}\n <jant-media-lightbox></jant-media-lightbox>\n </body>\n</html>\n";
|
|
@@ -3316,7 +4457,7 @@ var baseof_default = "<!doctype html>\n{{- $lang := .Site.LanguageCode | default
|
|
|
3316
4457
|
var single_default$1 = "{{ define \"main\" }}\n <article class=\"page\">\n {{- with .Title -}}\n <header class=\"page-header\">\n <h1 class=\"page-title\">{{ . }}</h1>\n </header>\n {{- end -}}\n {{- with .Params.summary_text -}}\n <p class=\"page-summary\">{{ . }}</p>\n {{- end -}}\n <div class=\"page-body\">\n {{ .Content }}\n </div>\n </article>\n{{ end }}\n";
|
|
3317
4458
|
//#endregion
|
|
3318
4459
|
//#region src/services/export-theme/layouts/_default/list.html?raw
|
|
3319
|
-
var list_default$4 = "{{ define \"main\" }}\n {{- /*\n _default/list.html is the section/list template fallback. It branches on\n page type so section pages with `type: collection` (branch bundles under\n content/{collection-slug}/_index.md) render a filtered, per-collection\n timeline. Other section types (archive, featured, etc.) have dedicated\n templates; this block only fires for collection sections and as a final\n fallback for unknown section types.\n */ -}}\n {{- if eq .Type \"collection\" -}}\n {{- $collectionSlug := .Params.slug | default .Slug -}}\n {{- $allPosts := where .Site.Pages \"Type\" \"post\" -}}\n {{- $members := slice -}}\n {{- range $allPosts -}}\n {{- $post := . -}}\n {{- with $post.Params.collections -}}\n {{- range . -}}\n {{- if eq .slug $collectionSlug -}}\n {{- $members = $members | append (dict \"post\" $post \"entry\" .) -}}\n {{- end -}}\n {{- end -}}\n {{- end -}}\n {{- end -}}\n\n {{- /* Split into pinned + unpinned, sort each, then concat. */ -}}\n {{- $pinned := slice -}}\n {{- $unpinned := slice -}}\n {{- range $members -}}\n {{- if .entry.pinned_at -}}\n {{- $pinned = $pinned | append . -}}\n {{- else -}}\n {{- $unpinned = $unpinned | append . -}}\n {{- end -}}\n {{- end -}}\n {{- if $pinned -}}\n {{- $pinned = sort $pinned \"entry.pinned_at\" \"desc\" -}}\n {{- end -}}\n {{- $sortOrder := .Params.sort_order | default \"position\" -}}\n {{- if $unpinned -}}\n {{- if eq $sortOrder \"collected_at_desc\" -}}\n {{- $unpinned = sort $unpinned \"entry.collected_at\" \"desc\" -}}\n {{- else -}}\n {{- $unpinned = sort $unpinned \"entry.position\" \"asc\" -}}\n {{- end -}}\n {{- end -}}\n {{- $ordered := $pinned | append $unpinned -}}\n {{- $posts := slice -}}\n {{- range $ordered -}}{{- $posts = $posts | append .post -}}{{- end -}}\n\n {{- $pageSize := .Site.Params.page_size | default 10 -}}\n {{- $paginator := .Paginate $posts $pageSize -}}\n\n <section class=\"section section-collection\">\n <header class=\"section-header\">\n <h1 class=\"section-title\">{{ .Title }}</h1>\n {{- with .Params.summary_text -}}\n <p class=\"section-summary\">{{ . }}</p>\n {{- end -}}\n {{- with .Params.entry_count -}}\n <p class=\"section-meta\">{{ . }} {{ if eq (int .) 1 }}
|
|
4460
|
+
var list_default$4 = "{{ define \"main\" }}\n {{- /*\n _default/list.html is the section/list template fallback. It branches on\n page type so section pages with `type: collection` (branch bundles under\n content/{collection-slug}/_index.md) render a filtered, per-collection\n timeline. Other section types (archive, featured, etc.) have dedicated\n templates; this block only fires for collection sections and as a final\n fallback for unknown section types.\n */ -}}\n {{- if eq .Type \"collection\" -}}\n {{- $collectionSlug := .Params.slug | default .Slug -}}\n {{- /* Thread collection membership is stored only on root bundles. */ -}}\n {{- $allPosts := where (where .Site.Pages \"Type\" \"post\") \"Kind\" \"section\" -}}\n {{- $members := slice -}}\n {{- range $allPosts -}}\n {{- $post := . -}}\n {{- with $post.Params.collections -}}\n {{- range . -}}\n {{- if eq .slug $collectionSlug -}}\n {{- $members = $members | append (dict \"post\" $post \"entry\" .) -}}\n {{- end -}}\n {{- end -}}\n {{- end -}}\n {{- end -}}\n\n {{- /* Split into pinned + unpinned, sort each, then concat. */ -}}\n {{- $pinned := slice -}}\n {{- $unpinned := slice -}}\n {{- range $members -}}\n {{- if .entry.pinned_at -}}\n {{- $pinned = $pinned | append . -}}\n {{- else -}}\n {{- $unpinned = $unpinned | append . -}}\n {{- end -}}\n {{- end -}}\n {{- if $pinned -}}\n {{- $pinned = sort $pinned \"entry.pinned_at\" \"desc\" -}}\n {{- end -}}\n {{- $sortOrder := .Params.sort_order | default \"position\" -}}\n {{- if $unpinned -}}\n {{- if eq $sortOrder \"collected_at_desc\" -}}\n {{- $unpinned = sort $unpinned \"entry.collected_at\" \"desc\" -}}\n {{- else -}}\n {{- $unpinned = sort $unpinned \"entry.position\" \"asc\" -}}\n {{- end -}}\n {{- end -}}\n {{- $ordered := $pinned | append $unpinned -}}\n {{- $posts := slice -}}\n {{- range $ordered -}}{{- $posts = $posts | append .post -}}{{- end -}}\n\n {{- $pageSize := .Site.Params.page_size | default 10 -}}\n {{- $paginator := .Paginate $posts $pageSize -}}\n\n <section class=\"section section-collection\">\n <header class=\"section-header\">\n <h1 class=\"section-title\">{{ .Title }}</h1>\n {{- with .Params.summary_text -}}\n <p class=\"section-summary\">{{ . }}</p>\n {{- end -}}\n {{- with .Params.entry_count -}}\n <p class=\"section-meta\">{{ . }} {{ if eq (int .) 1 }}thread{{ else }}threads{{ end }}</p>\n {{- end -}}\n {{ .Content }}\n </header>\n {{- if $paginator.Pages -}}\n <div class=\"post-list\">\n {{- range $i, $p := $paginator.Pages -}}\n {{- if gt $i 0 -}}<hr class=\"feed-divider\" aria-hidden=\"true\" />{{- end -}}\n {{- $replies := $p.Pages.ByDate -}}\n {{- $hasReplies := gt (len $replies) 0 -}}\n <div class=\"thread thread-full{{ if $hasReplies }} thread-has-replies{{ end }}\" data-slug=\"{{ $p.Params.slug | default $p.Slug }}\">\n <div class=\"thread-item thread-item-root\">\n {{ partial \"post-card.html\" $p }}\n </div>\n {{- if $hasReplies -}}\n <section class=\"thread-replies\" aria-label=\"Replies\">\n {{- range $replies -}}\n {{ partial \"reply.html\" . }}\n {{- end -}}\n </section>\n {{- end -}}\n </div>\n {{- end -}}\n </div>\n {{ partial \"pagination.html\" $paginator }}\n {{- else -}}\n <p class=\"empty-state\">This collection doesn't contain any threads.</p>\n {{- end -}}\n </section>\n {{- else -}}\n <section class=\"section\">\n <header class=\"section-header\">\n <h1 class=\"section-title\">{{ .Title }}</h1>\n {{- with .Params.summary_text -}}\n <p class=\"section-summary\">{{ . }}</p>\n {{- end -}}\n </header>\n <div class=\"section-body\">\n {{ .Content }}\n </div>\n {{- $posts := where .Site.Pages \"Type\" \"post\" -}}\n {{- $posts = $posts.ByDate.Reverse -}}\n {{- $pageSize := .Site.Params.page_size | default 10 -}}\n {{- $paginator := .Paginate $posts $pageSize -}}\n {{- if $paginator.Pages -}}\n <div class=\"post-list\">\n {{- range $i, $p := $paginator.Pages -}}\n {{- if gt $i 0 -}}<hr class=\"feed-divider\" aria-hidden=\"true\" />{{- end -}}\n {{ partial \"post-card.html\" $p }}\n {{- end -}}\n </div>\n {{ partial \"pagination.html\" $paginator }}\n {{- else -}}\n <p class=\"empty-state\">Nothing here yet.</p>\n {{- end -}}\n </section>\n {{- end -}}\n{{ end }}\n";
|
|
3320
4461
|
//#endregion
|
|
3321
4462
|
//#region src/services/export-theme/layouts/_default/alias.html?raw
|
|
3322
4463
|
var alias_default = "<!doctype html>\n<html lang=\"{{ .Site.LanguageCode | default \"en\" }}\">\n <head>\n <meta charset=\"utf-8\">\n <title>{{ .Site.Title }}</title>\n <meta name=\"robots\" content=\"noindex,follow\">\n <meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\">\n <link rel=\"canonical\" href=\"{{ .Permalink }}\">\n <script>\n (function () {\n var target = {{ .Permalink }};\n var rootAliases = {{ with .Params.root_aliases }}{{ . | jsonify }}{{ else }}[]{{ end }};\n try {\n var path = window.location.pathname || \"\";\n var normalized = \"/\" + path.replace(/^\\/+|\\/+$/g, \"\") + \"/\";\n var isHistoricalRootAlias = false;\n for (var i = 0; i < rootAliases.length; i++) {\n var alias = String(rootAliases[i]);\n var aliasNorm = \"/\" + alias.replace(/^\\/+|\\/+$/g, \"\") + \"/\";\n if (aliasNorm === normalized) {\n isHistoricalRootAlias = true;\n break;\n }\n }\n if (isHistoricalRootAlias) {\n // Historical root-slug alias — redirect to the post itself\n // without injecting an anchor.\n window.location.replace(target);\n return;\n }\n // Thread reply alias — append the reply slug as an anchor so\n // the thread page scrolls to the original reply.\n var parts = normalized.split(\"/\").filter(Boolean);\n var slug = parts.length > 0 ? parts[parts.length - 1] : \"\";\n if (slug) {\n window.location.replace(target + \"#\" + slug);\n } else {\n window.location.replace(target);\n }\n } catch (_err) {\n window.location.replace(target);\n }\n })();\n <\/script>\n </head>\n <body>\n <p>Redirecting to <a href=\"{{ .Permalink }}\">{{ .Permalink }}</a>…</p>\n </body>\n</html>\n";
|
|
@@ -3328,7 +4469,7 @@ var layouts_default = "{{ define \"main\" }} {{- $pageSize := .Site.Params.page_
|
|
|
3328
4469
|
var list_default$3 = "{{ define \"main\" }}\n {{- $root := . -}}\n {{- $format := $root.Params.format | default \"note\" -}}\n {{- $slug := $root.Params.slug | default $root.Slug -}}\n {{- $media := $root.Params.media | default slice -}}\n {{- $collections := $root.Params.collections | default slice -}}\n {{- $linkUrl := $root.Params.link_url -}}\n {{- $replies := $root.Pages.ByDate -}}\n {{- $hasReplies := gt (len $replies) 0 -}}\n <article class=\"thread thread-{{ $format }}{{ if $hasReplies }} thread-has-replies{{ end }}\" id=\"{{ $slug }}\" data-format=\"{{ $format }}\" data-slug=\"{{ $slug }}\"\n {{- if $root.Params.pinned_at }} data-post-pinned{{ end -}}\n {{- if $root.Params.featured_at }} data-post-featured{{ end -}}\n >\n <div class=\"thread-item thread-item-root\">\n <header class=\"thread-header\">\n {{- if eq $format \"link\" -}}\n {{- if $linkUrl -}}\n {{- $domain := \"\" -}}\n {{- with urls.Parse $linkUrl -}}{{- $domain = .Host -}}{{- end -}}\n {{- if $domain -}}\n <a href=\"{{ $linkUrl }}\" class=\"thread-link-domain\" rel=\"noopener noreferrer\" target=\"_blank\">\n <svg class=\"post-card-link-domain-icon\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n <span>{{ $domain }}</span>\n </a>\n {{- end -}}\n {{- end -}}\n {{- if $root.Title -}}\n <h1 class=\"thread-title thread-link-title\">\n {{- if $linkUrl -}}\n <a href=\"{{ $linkUrl }}\" rel=\"noopener noreferrer\" target=\"_blank\">{{ $root.Title }}</a>\n {{- else -}}\n {{ $root.Title }}\n {{- end -}}\n </h1>\n {{- end -}}\n {{- else if eq $format \"quote\" -}}\n <blockquote class=\"thread-quote post-card-quote\">\n {{- with $root.Params.quote_text -}}\n <span class=\"post-card-quote-mark\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 96 96\" role=\"presentation\" focusable=\"false\">\n <path fill=\"currentColor\" d=\"M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z\" />\n <path fill=\"currentColor\" d=\"M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z\" />\n </svg>\n </span>\n <div class=\"post-card-quote-content\">{{ . }}</div>\n {{- end -}}\n {{- if or $root.Params.source_name $root.Params.source_url -}}\n <div class=\"post-card-quote-attribution\">\n {{- if and $root.Params.source_name $root.Params.source_url -}}\n <a href=\"{{ $root.Params.source_url }}\" class=\"post-card-quote-source\" rel=\"noopener noreferrer\" target=\"_blank\">{{ $root.Params.source_name }}</a>\n {{- else if $root.Params.source_name -}}\n <span class=\"post-card-quote-source\">{{ $root.Params.source_name }}</span>\n {{- else -}}\n {{- $sourceDomain := \"\" -}}\n {{- with urls.Parse $root.Params.source_url -}}\n {{- $sourceDomain = .Host | replaceRE \"^(?:www|m|mobile)\\\\.\" \"\" -}}\n {{- end -}}\n <a href=\"{{ $root.Params.source_url }}\" class=\"post-card-quote-source\" rel=\"noopener noreferrer\" target=\"_blank\">{{ or $sourceDomain $root.Params.source_url }}</a>\n {{- end -}}\n </div>\n {{- end -}}\n </blockquote>\n {{- else -}}\n {{- with $root.Title -}}<h1 class=\"thread-title\">{{ . }}</h1>{{- end -}}\n {{- end -}}\n </header>\n\n {{- if eq $format \"quote\" -}}\n {{- with $root.Content -}}\n <div class=\"thread-body post-card-quote-commentary prose\">{{ . }}</div>\n {{- end -}}\n {{- else -}}\n <div class=\"thread-body prose\">\n {{ $root.Content }}\n </div>\n {{- end -}}\n\n {{ partial \"media-gallery.html\" (dict \"media\" $media \"permalink\" $root.RelPermalink) }}\n\n <footer class=\"thread-footer post-menu-footer post-footer-detail\" data-post-meta>\n <div class=\"post-footer-meta\">\n {{- if $root.Params.featured_at -}}\n <span class=\"post-footer-featured\" aria-label=\"Featured\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.35\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12 3 10.1 10.1 3 12l7.1 1.9L12 21l1.9-7.1L21 12l-7.1-1.9Z\" />\n </svg>\n </span>\n {{- end -}}\n <a class=\"post-footer-link\" href=\"{{ $root.RelPermalink }}\">\n <time datetime=\"{{ $root.Date.Format \"2006-01-02T15:04:05Z07:00\" }}\">\n {{ $root.Date.Format \"Jan 2, 2006 · 15:04\" }}\n </time>\n </a>\n {{- if and (eq $format \"link\") $linkUrl -}}\n <a href=\"{{ $linkUrl }}\" class=\"post-footer-external-link\" target=\"_blank\" rel=\"noopener noreferrer\" aria-label=\"Open external link\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M7 17 17 7\" />\n <path d=\"M9 7h8v8\" />\n </svg>\n </a>\n {{- end -}}\n {{- if $collections -}}\n <span class=\"post-collection-tags\">\n {{- range $i, $c := $collections -}}\n {{- if gt $i 0 -}}<span class=\"post-collection-sep\" aria-hidden=\"true\">, </span>{{- end -}}\n <a class=\"post-collection-tag\" href=\"{{ printf \"/%s/\" .slug | relURL }}\">\n <span class=\"post-collection-tag-text\">{{ .title }}</span>\n </a>\n {{- end -}}\n </span>\n {{- end -}}\n {{- if $root.Params.pinned_at -}}\n <span class=\"post-card-pin\" aria-label=\"Pinned\">Pinned</span>\n {{- end -}}\n </div>\n </footer>\n </div>\n\n {{- if $hasReplies -}}\n <section class=\"thread-replies\" aria-label=\"Replies\">\n {{- range $replies -}}\n {{ partial \"reply.html\" . }}\n {{- end -}}\n </section>\n {{- end -}}\n </article>\n{{ end }}\n";
|
|
3329
4470
|
//#endregion
|
|
3330
4471
|
//#region src/services/export-theme/layouts/featured/list.html?raw
|
|
3331
|
-
var list_default$2 = "{{ define \"main\" }}\n {{- /* Root
|
|
4472
|
+
var list_default$2 = "{{ define \"main\" }}\n {{- /* Root bundles carry a derived projection of every Featured Post in the Thread. */ -}}\n {{- $posts := where .Site.Pages \"Type\" \"post\" -}}\n {{- $roots := where $posts \"Kind\" \"section\" -}}\n {{- $featured := where $roots \"Params.featured_sort_at\" \"ne\" nil -}}\n {{- $featured = sort $featured \"Params.featured_sort_at\" \"desc\" -}}\n {{- $pageSize := .Site.Params.page_size | default 10 -}}\n {{- $paginator := .Paginate $featured $pageSize -}}\n\n <section class=\"section section-featured\">\n <header class=\"section-header\">\n <h1 class=\"section-title\">{{ .Title | default \"Featured\" }}</h1>\n {{- with .Params.summary_text -}}\n <p class=\"section-summary\">{{ . }}</p>\n {{- end -}}\n </header>\n {{- if $paginator.Pages -}}\n <div class=\"post-list\">\n {{- range $i, $p := $paginator.Pages -}}\n {{- if gt $i 0 -}}<hr class=\"feed-divider\" aria-hidden=\"true\" />{{- end -}}\n {{ partial \"featured-thread.html\" $p }}\n {{- end -}}\n </div>\n {{ partial \"pagination.html\" $paginator }}\n {{- else -}}\n <p class=\"empty-state\">Nothing featured yet. Star a post to highlight it here.</p>\n {{- end -}}\n </section>\n{{ end }}\n";
|
|
3332
4473
|
//#endregion
|
|
3333
4474
|
//#region src/services/export-theme/layouts/archive/list.html?raw
|
|
3334
4475
|
var list_default$1 = "{{ define \"main\" }}\n {{- /* Root posts are branch bundles (kind=section), so we iterate .Site.Pages.\n Hugo already excludes draft pages, so `private` posts (emitted with\n `draft: true`) are filtered out automatically. This leaves `public`\n and `latest_hidden` posts — both visible in the archive timeline. */ -}}\n {{- $posts := where .Site.Pages \"Type\" \"post\" -}}\n {{- $posts = $posts.ByDate.Reverse -}}\n {{- $pageSize := .Site.Params.archive_page_size | default (.Site.Params.page_size | default 10) -}}\n {{- $paginator := .Paginate $posts $pageSize -}}\n\n <section class=\"section section-archive home\">\n <header class=\"section-header\">\n <h1 class=\"section-title\">{{ .Title | default \"Archive\" }}</h1>\n {{- with .Params.summary_text -}}\n <p class=\"section-summary\">{{ . }}</p>\n {{- end -}}\n </header>\n\n {{- if $paginator.Pages -}}\n <div class=\"post-list post-list-main\">\n {{- range $i, $p := $paginator.Pages -}}\n {{- if gt $i 0 -}}<hr class=\"feed-divider\" aria-hidden=\"true\" />{{- end -}}\n {{ partial \"thread-preview.html\" $p }}\n {{- end -}}\n </div>\n {{ partial \"pagination.html\" $paginator }}\n {{- else -}}\n <p class=\"empty-state\">Nothing published yet. Write your first post to get started.</p>\n {{- end -}}\n </section>\n{{ end }}\n";
|
|
@@ -3346,7 +4487,7 @@ var head_default = "{{- $jant := hugo.Data.jant -}}\n{{- $siteTitle := .Site.Tit
|
|
|
3346
4487
|
var header_default = "{{- $jant := hugo.Data.jant -}}\n{{- $showAvatar := false -}}\n{{- with $jant -}}{{- if eq .show_header_avatar true -}}{{- $showAvatar = true -}}{{- end -}}{{- end -}}\n{{- $avatarUrl := \"\" -}}\n{{- with $jant -}}{{- with .site_avatar_url -}}{{- $avatarUrl = . -}}{{- end -}}{{- end -}}\n\n{{- /* Split nav items by placement: \"header\" (inline) vs \"more\" (supplemental dropdown). */ -}}\n{{- $headerItems := slice -}}\n{{- $moreItems := slice -}}\n{{- with $jant -}}{{- range .nav -}}\n {{- if eq .placement \"more\" -}}\n {{- $moreItems = $moreItems | append . -}}\n {{- else -}}\n {{- $headerItems = $headerItems | append . -}}\n {{- end -}}\n{{- end -}}{{- end -}}\n\n{{- $homeURL := \"/\" | relURL -}}\n{{- $currentURL := .RelPermalink -}}\n{{- $headerCount := len $headerItems -}}\n{{- $hasResponsiveOverflow := gt $headerCount 2 -}}\n{{- $hasSupplementalMore := gt (len $moreItems) 0 -}}\n{{- $showMoreMenu := or $hasResponsiveOverflow $hasSupplementalMore -}}\n\n{{- /* When only responsive overflow triggers the More button (no supplemental\n items), hide it until the narrowest tier where overflow actually kicks in. */ -}}\n{{- $moreWrapClass := \"site-header-more\" -}}\n{{- if and $hasResponsiveOverflow (not $hasSupplementalMore) -}}\n {{- $tier := \"site-header-more-tier-sm\" -}}\n {{- if ge $headerCount 5 -}}{{- $tier = \"site-header-more-tier-lg\" -}}\n {{- else if eq $headerCount 4 -}}{{- $tier = \"site-header-more-tier-md\" -}}{{- end -}}\n {{- $moreWrapClass = printf \"%s site-header-more-responsive-only %s\" $moreWrapClass $tier -}}\n{{- end -}}\n\n<header class=\"site-header\" role=\"banner\">\n <div class=\"site-header-inner\">\n <div class=\"site-header-top site-header-top-bordered\">\n <a class=\"site-logo\" href=\"{{ \"/\" | relURL }}\">\n {{- if and $showAvatar $avatarUrl -}}\n <img class=\"site-logo-avatar\" src=\"{{ $avatarUrl }}\" alt=\"\" width=\"40\" height=\"40\" loading=\"eager\">\n {{- end -}}\n <span class=\"site-logo-text\">{{ .Site.Title }}</span>\n </a>\n {{- if or $headerItems $showMoreMenu -}}\n <nav class=\"site-header-nav\" aria-label=\"Primary\">\n {{- range $i, $item := $headerItems -}}\n {{- $isExternal := or (hasPrefix $item.url \"http://\") (hasPrefix $item.url \"https://\") -}}\n {{- $navURL := $item.url -}}\n {{- if not $isExternal -}}{{- $navURL = $item.url | relURL -}}{{- end -}}\n {{- $isActive := false -}}\n {{- if not $isExternal -}}\n {{- if eq $navURL $homeURL -}}\n {{- if eq $currentURL $homeURL -}}{{- $isActive = true -}}{{- end -}}\n {{- else if or (eq $currentURL $navURL) (hasPrefix $currentURL $navURL) -}}\n {{- $isActive = true -}}\n {{- end -}}\n {{- end -}}\n {{- $tierClass := \"\" -}}\n {{- if eq $i 2 -}}{{- $tierClass = \"site-header-link-overflow site-header-link-collapse-sm\" -}}\n {{- else if eq $i 3 -}}{{- $tierClass = \"site-header-link-overflow site-header-link-collapse-md\" -}}\n {{- else if gt $i 3 -}}{{- $tierClass = \"site-header-link-overflow site-header-link-collapse-lg\" -}}\n {{- else -}}{{- $tierClass = \"site-header-link-primary\" -}}{{- end -}}\n <a class=\"site-header-link {{ $tierClass }}{{ if $isActive }} site-header-link-active{{ end }}\" href=\"{{ $navURL }}\"{{ if $isExternal }} rel=\"noopener noreferrer\" target=\"_blank\"{{ end }}>{{ $item.label }}</a>\n {{- end -}}\n {{- if $showMoreMenu -}}\n <div class=\"{{ $moreWrapClass }}\">\n <button type=\"button\" class=\"site-header-more-btn\" aria-haspopup=\"menu\" aria-expanded=\"false\">\n More\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg>\n </button>\n <div class=\"site-header-more-popover\" aria-hidden=\"true\">\n {{- range $i, $item := $headerItems -}}\n {{- if ge $i 2 -}}\n {{- $isExternal := or (hasPrefix $item.url \"http://\") (hasPrefix $item.url \"https://\") -}}\n {{- $navURL := $item.url -}}\n {{- if not $isExternal -}}{{- $navURL = $item.url | relURL -}}{{- end -}}\n {{- $isActive := false -}}\n {{- if not $isExternal -}}\n {{- if eq $navURL $homeURL -}}\n {{- if eq $currentURL $homeURL -}}{{- $isActive = true -}}{{- end -}}\n {{- else if or (eq $currentURL $navURL) (hasPrefix $currentURL $navURL) -}}\n {{- $isActive = true -}}\n {{- end -}}\n {{- end -}}\n {{- $show := \"site-header-more-link-show-lg\" -}}\n {{- if eq $i 2 -}}{{- $show = \"site-header-more-link-show-sm\" -}}\n {{- else if eq $i 3 -}}{{- $show = \"site-header-more-link-show-md\" -}}{{- end -}}\n <a class=\"site-header-more-link site-header-more-link-responsive {{ $show }}{{ if $isActive }} site-header-more-link-active{{ end }}\" href=\"{{ $navURL }}\"{{ if $isExternal }} rel=\"noopener noreferrer\" target=\"_blank\"{{ end }}>{{ $item.label }}</a>\n {{- end -}}\n {{- end -}}\n {{- if and $hasResponsiveOverflow $hasSupplementalMore -}}\n <div class=\"site-header-more-divider site-header-more-divider-responsive\"></div>\n {{- end -}}\n {{- range $moreItems -}}\n {{- $isExternal := or (hasPrefix .url \"http://\") (hasPrefix .url \"https://\") -}}\n {{- $navURL := .url -}}\n {{- if not $isExternal -}}{{- $navURL = .url | relURL -}}{{- end -}}\n {{- $isActive := false -}}\n {{- if not $isExternal -}}\n {{- if eq $navURL $homeURL -}}\n {{- if eq $currentURL $homeURL -}}{{- $isActive = true -}}{{- end -}}\n {{- else if or (eq $currentURL $navURL) (hasPrefix $currentURL $navURL) -}}\n {{- $isActive = true -}}\n {{- end -}}\n {{- end -}}\n <a class=\"site-header-more-link site-header-more-link-supplemental{{ if $isActive }} site-header-more-link-active{{ end }}\" href=\"{{ $navURL }}\"{{ if $isExternal }} rel=\"noopener noreferrer\" target=\"_blank\"{{ end }}>{{ .label }}</a>\n {{- end -}}\n </div>\n </div>\n {{- end -}}\n </nav>\n {{- end -}}\n </div>\n </div>\n</header>\n";
|
|
3347
4488
|
//#endregion
|
|
3348
4489
|
//#region src/services/export-theme/layouts/partials/footer.html?raw
|
|
3349
|
-
var footer_default = "{{- $jant := hugo.Data.jant -}}\n{{- $footerHtml := \"\" -}}\n{{- with $jant -}}{{- with .site_footer_html -}}{{- $footerHtml = . -}}{{- end -}}{{- end -}}\n{{- $showBranding := false -}}\n{{- with $jant -}}{{- if eq .show_jant_branding_on_home true -}}{{- $showBranding = true -}}{{- end -}}{{- end -}}\n{{- $navFooterItems := slice -}}\n{{- with $jant -}}{{- range .nav -}}{{- if eq .placement \"footer\" -}}{{- $navFooterItems = $navFooterItems | append . -}}{{- end -}}{{- end -}}{{- end -}}\n{{- $hasFooter := or $navFooterItems $footerHtml -}}\n{{- if $hasFooter -}}\n<footer class=\"site-footer\" role=\"contentinfo\">\n <div class=\"site-footer-inner\">\n {{- if $navFooterItems -}}\n <nav class=\"site-footer-nav\" aria-label=\"Footer\">\n <ul class=\"site-footer-nav-list\">\n {{- range $navFooterItems -}}\n {{- $isExternal := or (hasPrefix .url \"http://\") (hasPrefix .url \"https://\") -}}\n <li><a href=\"{{ .url }}\"{{ if $isExternal }} rel=\"noopener noreferrer\" target=\"_blank\"{{ end }}>{{ .label }}</a></li>\n {{- end -}}\n </ul>\n </nav>\n {{- end -}}\n {{- with $footerHtml -}}\n <div class=\"site-footer-content\">{{ . | safeHTML }}</div>\n {{- end -}}\n </div>\n</footer>\n{{- end -}}\n{{- if and $showBranding $.IsHome -}}\n<footer class=\"home-branding-credit\">\n Build with <a href=\"https://
|
|
4490
|
+
var footer_default = "{{- $jant := hugo.Data.jant -}}\n{{- $footerHtml := \"\" -}}\n{{- with $jant -}}{{- with .site_footer_html -}}{{- $footerHtml = . -}}{{- end -}}{{- end -}}\n{{- $showBranding := false -}}\n{{- with $jant -}}{{- if eq .show_jant_branding_on_home true -}}{{- $showBranding = true -}}{{- end -}}{{- end -}}\n{{- $navFooterItems := slice -}}\n{{- with $jant -}}{{- range .nav -}}{{- if eq .placement \"footer\" -}}{{- $navFooterItems = $navFooterItems | append . -}}{{- end -}}{{- end -}}{{- end -}}\n{{- $hasFooter := or $navFooterItems $footerHtml -}}\n{{- if $hasFooter -}}\n<footer class=\"site-footer\" role=\"contentinfo\">\n <div class=\"site-footer-inner\">\n {{- if $navFooterItems -}}\n <nav class=\"site-footer-nav\" aria-label=\"Footer\">\n <ul class=\"site-footer-nav-list\">\n {{- range $navFooterItems -}}\n {{- $isExternal := or (hasPrefix .url \"http://\") (hasPrefix .url \"https://\") -}}\n <li><a href=\"{{ .url }}\"{{ if $isExternal }} rel=\"noopener noreferrer\" target=\"_blank\"{{ end }}>{{ .label }}</a></li>\n {{- end -}}\n </ul>\n </nav>\n {{- end -}}\n {{- with $footerHtml -}}\n <div class=\"site-footer-content\">{{ . | safeHTML }}</div>\n {{- end -}}\n </div>\n</footer>\n{{- end -}}\n{{- if and $showBranding $.IsHome -}}\n<footer class=\"home-branding-credit\">\n Build with <a href=\"https://jant.me\" rel=\"noopener noreferrer\" target=\"_blank\">Jant</a>\n</footer>\n{{- end -}}\n";
|
|
3350
4491
|
//#endregion
|
|
3351
4492
|
//#region src/services/export-theme/layouts/partials/pagination.html?raw
|
|
3352
4493
|
var pagination_default = "{{- $paginator := . -}}\n{{- $total := $paginator.TotalPages -}}\n{{- $current := $paginator.PageNumber -}}\n{{- if gt $total 1 -}}\n {{- /* Mirror main site's getPageNumbers: always include 1, last, current,\n and current±1. Insert 0 as an ellipsis marker for gaps. */ -}}\n {{- $pages := slice -}}\n {{- if le $total 7 -}}\n {{- range seq 1 $total -}}\n {{- $pages = $pages | append . -}}\n {{- end -}}\n {{- else -}}\n {{- $set := slice 1 $total $current -}}\n {{- if gt $current 1 -}}{{- $set = $set | append (sub $current 1) -}}{{- end -}}\n {{- if lt $current $total -}}{{- $set = $set | append (add $current 1) -}}{{- end -}}\n {{- $sorted := sort (uniq $set) -}}\n {{- $prev := 0 -}}\n {{- range $i, $p := $sorted -}}\n {{- if and (gt $i 0) (gt (sub $p $prev) 1) -}}\n {{- $pages = $pages | append 0 -}}\n {{- end -}}\n {{- $pages = $pages | append $p -}}\n {{- $prev = $p -}}\n {{- end -}}\n {{- end -}}\n <nav class=\"pagination\" aria-label=\"Pagination\">\n {{- if $paginator.HasPrev -}}\n <a class=\"pagination-link pagination-prev\" href=\"{{ $paginator.Prev.URL }}\" rel=\"prev\">Previous</a>\n {{- else -}}\n <span class=\"pagination-link pagination-prev is-disabled\" aria-disabled=\"true\">Previous</span>\n {{- end -}}\n {{- range $pages -}}\n {{- if eq . 0 -}}\n <span class=\"pagination-ellipsis\" aria-hidden=\"true\">...</span>\n {{- else if eq . $current -}}\n <span class=\"pagination-current\" aria-current=\"page\">{{ . }}</span>\n {{- else -}}\n {{- $href := \"\" -}}\n {{- if eq . 1 -}}\n {{- $href = ($paginator.First.URL | default \"/\") -}}\n {{- else -}}\n {{- with index $paginator.Pagers (sub . 1) -}}\n {{- $href = .URL -}}\n {{- end -}}\n {{- end -}}\n <a class=\"pagination-link\" href=\"{{ $href }}\">{{ . }}</a>\n {{- end -}}\n {{- end -}}\n {{- if $paginator.HasNext -}}\n <a class=\"pagination-link pagination-next\" href=\"{{ $paginator.Next.URL }}\" rel=\"next\">Next</a>\n {{- else -}}\n <span class=\"pagination-link pagination-next is-disabled\" aria-disabled=\"true\">Next</span>\n {{- end -}}\n </nav>\n{{- end -}}\n";
|
|
@@ -3355,7 +4496,7 @@ var pagination_default = "{{- $paginator := . -}}\n{{- $total := $paginator.Tota
|
|
|
3355
4496
|
var post_card_default = "{{- $post := . -}}\n{{- $format := $post.Params.format | default \"note\" -}}\n{{- $summaryText := $post.Params.summary_text -}}\n{{- $media := $post.Params.media | default slice -}}\n{{- $collections := $post.Params.collections | default slice -}}\n{{- $slug := $post.Params.slug | default $post.Slug -}}\n{{- $linkUrl := $post.Params.link_url -}}\n<article class=\"post-card post-card-{{ $format }}\" data-format=\"{{ $format }}\" data-slug=\"{{ $slug }}\"\n {{- if $post.Params.pinned_at }} data-post-pinned{{ end -}}\n {{- if $post.Params.featured_at }} data-post-featured{{ end -}}\n>\n {{- if eq $format \"link\" -}}\n {{- if $linkUrl -}}\n {{- $domain := \"\" -}}\n {{- with urls.Parse $linkUrl -}}{{- $domain = .Host -}}{{- end -}}\n {{- if $domain -}}\n <a href=\"{{ $linkUrl }}\" class=\"post-card-link-domain\" rel=\"noopener noreferrer\" target=\"_blank\">\n <svg class=\"post-card-link-domain-icon\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n <span>{{ $domain }}</span>\n </a>\n {{- end -}}\n {{- end -}}\n {{- if and $post.Title $linkUrl -}}\n <h2 class=\"post-card-title post-card-link-title\">\n <a href=\"{{ $linkUrl }}\" rel=\"noopener noreferrer\" target=\"_blank\">{{ $post.Title }}</a>\n </h2>\n {{- else if $post.Title -}}\n <h2 class=\"post-card-title post-card-link-title\"><a href=\"{{ $post.RelPermalink }}\">{{ $post.Title }}</a></h2>\n {{- end -}}\n {{- /* Prefer Hugo's rendered HTML body — preserves paragraphs, links,\n emphasis, lists. Falls back to the plain-text `summary_text` stored\n in front matter. */ -}}\n {{- if $post.Summary -}}\n <div class=\"post-card-summary prose\">{{ $post.Summary }}</div>\n {{- else if $summaryText -}}\n <p class=\"post-card-summary\">{{ $summaryText }}</p>\n {{- end -}}\n {{- else if eq $format \"quote\" -}}\n <blockquote class=\"post-card-quote\">\n {{- with $post.Params.quote_text -}}\n <span class=\"post-card-quote-mark\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 96 96\" role=\"presentation\" focusable=\"false\">\n <path fill=\"currentColor\" d=\"M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z\" />\n <path fill=\"currentColor\" d=\"M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z\" />\n </svg>\n </span>\n <div class=\"post-card-quote-content\">{{ . }}</div>\n {{- end -}}\n {{- if or $post.Params.source_name $post.Params.source_url -}}\n <div class=\"post-card-quote-attribution\">\n {{- if and $post.Params.source_name $post.Params.source_url -}}\n <a href=\"{{ $post.Params.source_url }}\" class=\"post-card-quote-source\" rel=\"noopener noreferrer\" target=\"_blank\">{{ $post.Params.source_name }}</a>\n {{- else if $post.Params.source_name -}}\n <span class=\"post-card-quote-source\">{{ $post.Params.source_name }}</span>\n {{- else -}}\n {{- $sourceDomain := \"\" -}}\n {{- with urls.Parse $post.Params.source_url -}}\n {{- $sourceDomain = .Host | replaceRE \"^(?:www|m|mobile)\\\\.\" \"\" -}}\n {{- end -}}\n <a href=\"{{ $post.Params.source_url }}\" class=\"post-card-quote-source\" rel=\"noopener noreferrer\" target=\"_blank\">{{ or $sourceDomain $post.Params.source_url }}</a>\n {{- end -}}\n </div>\n {{- end -}}\n </blockquote>\n {{- /* Quote commentary comes ONLY from the post body (rendered via\n `.Summary`). Do NOT fall back to `summary_text` — for quote posts,\n `summary_text` in front matter is the quote text itself (see\n `getArchiveSummaryText`), which would duplicate the quote body. */ -}}\n {{- with $post.Summary -}}\n <div class=\"post-card-quote-commentary prose\">{{ . }}</div>\n {{- end -}}\n {{- else -}}\n {{- with $post.Title -}}\n <h2 class=\"post-card-title\"><a href=\"{{ $post.RelPermalink }}\">{{ . }}</a></h2>\n {{- end -}}\n {{- /* Prefer Hugo's rendered HTML summary — preserves paragraphs, links,\n emphasis, lists. Falls back to the plain-text `summary_text` stored\n in front matter (used for quote bodies, OG meta, etc.). */ -}}\n {{- if $post.Summary -}}\n <div class=\"post-card-summary prose\">{{ $post.Summary }}</div>\n {{- else if $summaryText -}}\n <p class=\"post-card-summary\">{{ $summaryText }}</p>\n {{- end -}}\n {{- end -}}\n\n {{ partial \"media-gallery.html\" (dict \"media\" $media \"permalink\" $post.RelPermalink) }}\n\n <footer class=\"post-card-footer post-menu-footer\" data-post-meta>\n <div class=\"post-footer-meta\">\n {{- if $post.Params.featured_at -}}\n <span class=\"post-footer-featured\" aria-label=\"Featured\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.35\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12 3 10.1 10.1 3 12l7.1 1.9L12 21l1.9-7.1L21 12l-7.1-1.9Z\" />\n </svg>\n </span>\n {{- end -}}\n <a class=\"post-footer-link post-card-permalink\" href=\"{{ $post.RelPermalink }}\">\n <time datetime=\"{{ $post.Date.Format \"2006-01-02T15:04:05Z07:00\" }}\">\n {{ $post.Date.Format \"Jan 2, 2006\" }}\n </time>\n </a>\n {{- if and (eq $format \"link\") $linkUrl -}}\n <a href=\"{{ $linkUrl }}\" class=\"post-footer-external-link\" target=\"_blank\" rel=\"noopener noreferrer\" aria-label=\"Open external link\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M7 17 17 7\" />\n <path d=\"M9 7h8v8\" />\n </svg>\n </a>\n {{- end -}}\n {{- if $collections -}}\n <span class=\"post-collection-tags\">\n {{- range $i, $c := $collections -}}\n {{- if gt $i 0 -}}<span class=\"post-collection-sep\" aria-hidden=\"true\">, </span>{{- end -}}\n <a class=\"post-collection-tag\" href=\"{{ printf \"/%s/\" .slug | relURL }}\">\n <span class=\"post-collection-tag-text\">{{ .title }}</span>\n </a>\n {{- end -}}\n </span>\n {{- end -}}\n {{- if $post.Params.pinned_at -}}\n <span class=\"post-card-pin\" aria-label=\"Pinned\">Pinned</span>\n {{- end -}}\n </div>\n </footer>\n</article>\n";
|
|
3356
4497
|
//#endregion
|
|
3357
4498
|
//#region src/services/export-theme/layouts/partials/media-gallery.html?raw
|
|
3358
|
-
var media_gallery_default = "{{- /*\n Media gallery — unified horizontal scroll row mirroring main-site MediaGallery.tsx.\n Emits images, videos, audio cards, documents, and text attachments in the\n post's stored `media` order (already sorted by position in the exporter).\n\n Lightbox: images and videos get `[data-lightbox-index]` pointers into a\n JSON blob on `[data-post-media]`. The client-site bundle's\n `<jant-media-lightbox>` delegates clicks on those anchors.\n\n Layout: mirrors MediaGallery.tsx — one horizontal row with a computed\n `rowHeight` (driven by the first visual's aspect ratio) so mixed sizes\n align into a clean strip. Single-visual posts expand to their natural\n aspect ratio (capped at 24rem / 85% width). Non-visual attachments\n (audio/doc/text) use a 3:4 portrait card sized to the row height.\n\n Inline styles (not Tailwind utilities) because the exported theme ships\n no utility CSS — only tokens.css + main.css + theme.css + site-media.css.\n\n Context: expects `.` to be a dict with `media` (the slice) and optional\n `permalink` for future use. Caller should pass\n `(dict \"media\" .Params.media \"permalink\" .RelPermalink)`.\n*/ -}}\n\n{{- $media := .media | default slice -}}\n{{- if $media -}}\n {{- /* Collect items + build lightbox group from images/videos */ -}}\n {{- $items := slice -}}\n {{- $lightbox := slice -}}\n {{- $lbIdx := 0 -}}\n {{- $hasNonVisual := false -}}\n {{- $firstRatio := 0.0 -}}\n {{- range $media -}}\n {{- $kind := .kind | default \"file\" -}}\n {{- $item := . -}}\n {{- if or (eq $kind \"image\") (eq $kind \"video\") -}}\n {{- $lbEntry := dict \"url\" .src \"alt\" (.alt | default \"\") -}}\n {{- with .width }}{{- $lbEntry = merge $lbEntry (dict \"width\" .) -}}{{- end -}}\n {{- with .height }}{{- $lbEntry = merge $lbEntry (dict \"height\" .) -}}{{- end -}}\n {{- if eq $kind \"video\" -}}\n {{- with $item.mime_type }}{{- $lbEntry = merge $lbEntry (dict \"mimeType\" .) -}}{{- end -}}\n {{- with $item.poster }}{{- $lbEntry = merge $lbEntry (dict \"posterUrl\" .) -}}{{- end -}}\n {{- with $item.duration_seconds }}{{- $lbEntry = merge $lbEntry (dict \"durationSeconds\" .) -}}{{- end -}}\n {{- with $item.size }}{{- $lbEntry = merge $lbEntry (dict \"size\" .) -}}{{- end -}}\n {{- end -}}\n {{- $lightbox = $lightbox | append $lbEntry -}}\n {{- $items = $items | append (merge $item (dict \"_kind\" $kind \"_lbIdx\" $lbIdx)) -}}\n {{- $lbIdx = add $lbIdx 1 -}}\n {{- /* Capture first visual's aspect ratio */ -}}\n {{- if and (eq $firstRatio 0.0) $item.width $item.height (gt (float $item.width) 0.0) (gt (float $item.height) 0.0) -}}\n {{- $firstRatio = div (float $item.width) (float $item.height) -}}\n {{- end -}}\n {{- else -}}\n {{- $items = $items | append (merge $item (dict \"_kind\" $kind)) -}}\n {{- $hasNonVisual = true -}}\n {{- end -}}\n {{- end -}}\n\n {{- $count := len $items -}}\n {{- $single := eq $count 1 -}}\n {{- $first := index $items 0 -}}\n {{- $firstKind := $first._kind -}}\n {{- $singleVisual := and $single (or (eq $firstKind \"image\") (eq $firstKind \"video\")) -}}\n\n {{- /* rowHeight — mirrors MediaGallery.tsx. 4/3 fallback when no visuals. */ -}}\n {{- $rowMin := cond $hasNonVisual 220 280 -}}\n {{- $rowMax := cond $hasNonVisual 300 440 -}}\n {{- $rowHeight := cond $hasNonVisual 220 360 -}}\n {{- if and (not $singleVisual) (gt $count 1) (gt $firstRatio 0.0) -}}\n {{- $ratio := cond (lt $firstRatio 0.5) 0.5 $firstRatio -}}\n {{- $computed := math.Round (div 320.0 $ratio) -}}\n {{- $rowHeight = cond (gt $computed $rowMax) $rowMax (cond (lt $computed $rowMin) $rowMin (int $computed)) -}}\n {{- end -}}\n {{- $docCardWidth := int (math.Round (mul (float $rowHeight) 0.75)) -}}\n\n {{- /* Outer wrapper — keeps the media-gallery-scroll-wrap hooks for the\n fade-mask client script in the non-single case. */ -}}\n {{- $wrapClass := cond $singleVisual \"\" \"media-gallery-scroll-wrap\" -}}\n <div class=\"{{ $wrapClass }}\" style=\"margin-top: 0.75rem;\">\n <div data-post-media\n {{- if $lightbox }} data-lightbox-group=\"{{ $lightbox | jsonify }}\"{{- end -}}\n {{- if not $singleVisual }} tabindex=\"0\" role=\"group\" aria-label=\"Media gallery\"{{- end -}}\n style=\"display: flex; gap: 0.5rem;\n {{- if not $singleVisual }} overflow-x: auto; scroll-behavior: smooth; scrollbar-width: none; -ms-overflow-style: none;{{- end }}\">\n {{- range $items -}}\n {{- $k := ._kind -}}\n {{- /* Per-item width for non-single visuals */ -}}\n {{- $itemRatio := 1.3333 -}}\n {{- if and .width .height (gt (float .width) 0.0) (gt (float .height) 0.0) -}}\n {{- $itemRatio = div (float .width) (float .height) -}}\n {{- end -}}\n {{- $itemWidth := int (math.Round (mul (float $rowHeight) $itemRatio)) -}}\n {{- if lt $itemWidth 160 -}}{{- $itemWidth = 160 -}}{{- end -}}\n\n {{- if eq $k \"image\" -}}\n {{- if $singleVisual -}}\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\"\n class=\"media-visual-frame\"\n style=\"display: block; width: min(100%, calc(24rem * {{ printf \"%.4f\" $itemRatio }})); max-width: 100%;\">\n <img src=\"{{ .src }}\" alt=\"{{ .alt | default \"\" }}\"\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n loading=\"lazy\" decoding=\"async\"\n class=\"media-visual\"\n style=\"display: block; width: 100%; height: auto; border-radius: 0.5rem;\n {{- if and .width .height }} aspect-ratio: {{ .width }}/{{ .height }};{{- end }}\">\n </a>\n {{- else -}}\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\"\n class=\"media-visual-frame\"\n style=\"display: block; flex-shrink: 0; width: {{ $itemWidth }}px; max-width: 85%;\">\n <img src=\"{{ .src }}\" alt=\"{{ .alt | default \"\" }}\"\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n loading=\"lazy\" decoding=\"async\"\n class=\"media-visual\"\n style=\"display: block; width: 100%; height: {{ $rowHeight }}px; object-fit: cover;\">\n </a>\n {{- end -}}\n {{- else if eq $k \"video\" -}}\n {{- $isShort := false -}}\n {{- if and .mime_type (hasPrefix .mime_type \"video/\") .duration_seconds -}}\n {{- if and (le (int .duration_seconds) 15) (or (not .size) (le (int .size) 12582912)) -}}\n {{- $isShort = true -}}\n {{- end -}}\n {{- end -}}\n {{- $videoWrapStyle := \"position: relative;\" -}}\n {{- if $singleVisual -}}\n {{- $videoWrapStyle = printf \"%s display: block; width: min(100%%, calc(24rem * %s)); max-width: 100%%;\" $videoWrapStyle (printf \"%.4f\" $itemRatio) -}}\n {{- else -}}\n {{- $videoWrapStyle = printf \"%s display: block; flex-shrink: 0; width: %dpx; max-width: 85%%;\" $videoWrapStyle $itemWidth -}}\n {{- end -}}\n {{- $videoStyle := cond $singleVisual\n (cond (and (gt (int (default 0 .width)) 0) (gt (int (default 0 .height)) 0))\n (printf \"display: block; width: 100%%; height: auto; aspect-ratio: %d/%d;\" (int .width) (int .height))\n \"display: block; width: 100%; height: auto;\")\n (printf \"display: block; width: 100%%; height: %dpx; object-fit: cover;\" $rowHeight) -}}\n {{- if $isShort -}}\n <div class=\"media-video-wrap media-video-wrap-short\" style=\"{{ $videoWrapStyle | safeCSS }}\">\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\" class=\"media-visual-frame media-video-link\" style=\"display: block; cursor: pointer;\">\n <video preload=\"none\" muted playsinline loop\n {{- with .poster }} poster=\"{{ . }}\"{{- end -}}\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n data-feed-short-video=\"\"\n data-video-src=\"{{ .src }}\"\n data-feed-video-id=\"{{ .id }}\"\n class=\"media-visual\"\n style=\"{{ $videoStyle | safeCSS }}\"></video>\n </a>\n <button type=\"button\" class=\"media-feed-video-mute\" data-feed-video-mute-toggle data-muted=\"true\" aria-label=\"Play with sound\">\n <svg class=\"media-feed-video-icon media-feed-video-icon-muted\" width=\"12\" height=\"12\" viewBox=\"0 0 48 48\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1v-9.8c0-.4-.2-.8-.4-1.1l-22-22c-.3-.3-.7-.4-1.1-.4h-.6zm46.8 31.4-5.5-5.5C44.9 36.6 48 31.4 48 24c0-11.4-7.2-17.4-7.2-17.4-.6-.6-1.6-.6-2.2 0L37.2 8c-.6.6-.6 1.6 0 2.2 0 0 5.7 5 5.7 13.8 0 5.4-2.1 9.3-3.8 11.6L35.5 32c1.1-1.7 2.3-4.4 2.3-8 0-6.8-4.1-10.3-4.1-10.3-.6-.6-1.6-.6-2.2 0l-1.4 1.4c-.6.6-.6 1.6 0 2.2 0 0 2.6 2 2.6 6.7 0 1.8-.4 3.2-.9 4.3L25.5 22V1.4c0-1.3-1.6-1.9-2.5-1L13.5 10 3.3-.3c-.6-.6-1.5-.6-2.1 0L-.2 1.1c-.6.6-.6 1.5 0 2.1L4 7.6l26.8 26.8 13.9 13.9c.6.6 1.5.6 2.1 0l1.4-1.4c.7-.6.7-1.6.1-2.2z\"/>\n </svg>\n <svg class=\"media-feed-video-icon media-feed-video-icon-unmuted\" width=\"12\" height=\"12\" viewBox=\"0 0 48 48\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1V1.4c0-1.3-1.6-1.9-2.5-1L10.2 13.3H1.5z\"/>\n <path d=\"M30.1 15.9c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 4.1 3.5 4.1 11.7s-4.1 11.7-4.1 11.7c-.6.6-1.6.6-2.2 0l-1.4-1.4c-.6-.6-.6-1.6 0-2.2 0 0 2.6-2 2.6-8.1s-2.6-8.1-2.6-8.1z\"/>\n <path d=\"M37.2 8c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 5.7 5.1 5.7 19.6s-5.7 19.6-5.7 19.6c-.6.6-1.6.6-2.2 0L37.2 42c-.6-.6-.6-1.6 0-2.2 0 0 4.3-4.4 4.3-15.8S37.2 8 37.2 8z\"/>\n </svg>\n </button>\n </div>\n {{- else -}}\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\"\n class=\"media-video-wrap media-visual-frame\"\n style=\"{{ $videoWrapStyle | safeCSS }}\">\n <video preload=\"none\" muted playsinline\n {{- with .poster }} poster=\"{{ . }}\"{{- end -}}\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n class=\"media-visual\"\n style=\"{{ $videoStyle | safeCSS }}\"></video>\n <div class=\"media-video-play-overlay\">\n <svg viewBox=\"0 0 24 24\" fill=\"white\"><path d=\"M8 5v14l11-7z\"/></svg>\n </div>\n </a>\n {{- end -}}\n {{- else if eq $k \"audio\" -}}\n {{- $audioName := .original_name | default (.alt | default \"Audio\") -}}\n <div class=\"media-gallery-card media-audio-card{{ if .waveform }} has-waveform{{ end }}\"\n style=\"flex-shrink: 0; width: {{ $docCardWidth }}px; height: {{ $rowHeight }}px;\">\n <audio preload=\"none\" class=\"media-audio-el\">\n <source src=\"{{ .src }}\"{{ with .mime_type }} type=\"{{ . }}\"{{ end }}>\n </audio>\n <div class=\"media-audio-artwork\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 18V5l12-2v13\"/>\n <circle cx=\"6\" cy=\"18\" r=\"3\"/>\n <circle cx=\"18\" cy=\"16\" r=\"3\"/>\n </svg>\n </div>\n <div class=\"media-audio-controls\">\n <input type=\"range\" min=\"0\" max=\"1000\" value=\"0\" class=\"media-audio-range\" data-audio-range aria-label=\"Seek\">\n <canvas class=\"media-audio-waveform\" data-audio-waveform{{ with .waveform }} data-audio-peaks=\"{{ . }}\"{{ end }}></canvas>\n <div class=\"media-audio-row\">\n <div class=\"media-audio-info\">\n <div class=\"media-audio-title\" title=\"{{ $audioName }}\">{{ $audioName }}</div>\n <div class=\"media-audio-time\" data-audio-time>0:00</div>\n </div>\n <button type=\"button\" class=\"media-audio-play-btn\" data-audio-play aria-label=\"Play\">\n <svg class=\"media-audio-icon-play\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>\n <svg class=\"media-audio-icon-pause\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M6 4h4v16H6zM14 4h4v16h-4z\"/></svg>\n </button>\n </div>\n </div>\n </div>\n {{- else if eq $k \"text\" -}}\n {{- /* Static site has no text-preview modal — link direct to file. */ -}}\n {{- $label := .summary | default (.original_name | default \"Attached text\") -}}\n <a href=\"{{ .src }}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"media-gallery-card\"\n style=\"flex-shrink: 0; width: {{ $docCardWidth }}px; height: {{ $rowHeight }}px;\">\n <div class=\"media-gallery-card-inner\">\n <div class=\"media-gallery-card-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"24\" height=\"24\" aria-hidden=\"true\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/>\n <polyline points=\"14 2 14 8 20 8\"/>\n <line x1=\"8\" y1=\"13\" x2=\"16\" y2=\"13\"/>\n <line x1=\"8\" y1=\"17\" x2=\"16\" y2=\"17\"/>\n </svg>\n </div>\n <span class=\"media-gallery-card-summary\">{{ $label }}</span>\n {{- with .chars -}}\n <span class=\"media-gallery-card-meta\">{{ . }} chars</span>\n {{- end -}}\n </div>\n </a>\n {{- else -}}\n {{- /* document / file */ -}}\n {{- $label := .original_name | default (path.Base .src) -}}\n <a href=\"{{ .src }}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"media-gallery-card\"\n style=\"flex-shrink: 0; width: {{ $docCardWidth }}px; height: {{ $rowHeight }}px;\">\n <div class=\"media-gallery-card-inner\">\n <div class=\"media-gallery-card-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"24\" height=\"24\" aria-hidden=\"true\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/>\n <polyline points=\"14 2 14 8 20 8\"/>\n </svg>\n </div>\n <span class=\"media-gallery-card-summary\">{{ $label }}</span>\n {{- with .size -}}\n <span class=\"media-gallery-card-meta\">{{ . }} B</span>\n {{- end -}}\n </div>\n </a>\n {{- end -}}\n {{- end -}}\n </div>\n {{- if not $singleVisual -}}\n <button type=\"button\" class=\"media-gallery-nav media-gallery-nav-prev\" tabindex=\"-1\" aria-label=\"Scroll to previous media\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M15 18l-6-6 6-6\"/></svg>\n </button>\n <button type=\"button\" class=\"media-gallery-nav media-gallery-nav-next\" tabindex=\"-1\" aria-label=\"Scroll to next media\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M9 18l6-6-6-6\"/></svg>\n </button>\n {{- end -}}\n </div>\n{{- end -}}\n";
|
|
4499
|
+
var media_gallery_default = "{{- /*\n Media gallery — unified horizontal scroll row mirroring main-site MediaGallery.tsx.\n Emits images, videos, audio cards, documents, and text attachments in the\n post's stored `media` order (already sorted by position in the exporter).\n\n Lightbox: images and videos get `[data-lightbox-index]` pointers into a\n JSON blob on `[data-post-media]`. The client-site bundle's\n `<jant-media-lightbox>` delegates clicks on those anchors.\n\n Layout: mirrors MediaGallery.tsx — one horizontal row with a computed\n `rowHeight` (driven by the first visual's aspect ratio) so mixed sizes\n align into a clean strip. Single-visual posts expand to their natural\n aspect ratio (capped at 24rem / 85% width). Non-visual attachments\n (audio/doc/text) use a 3:4 portrait card sized to the row height.\n\n Inline styles (not Tailwind utilities) because the exported theme ships\n no utility CSS — only tokens.css + main.css + theme.css + site-media.css.\n\n Context: expects `.` to be a dict with `media` (the slice) and optional\n `permalink` for future use. Caller should pass\n `(dict \"media\" .Params.media \"permalink\" .RelPermalink)`.\n*/ -}}\n\n{{- $media := .media | default slice -}}\n{{- if $media -}}\n {{- /* Collect items + build lightbox group from images/videos */ -}}\n {{- $items := slice -}}\n {{- $lightbox := slice -}}\n {{- $lbIdx := 0 -}}\n {{- $hasNonVisual := false -}}\n {{- $firstRatio := 0.0 -}}\n {{- range $media -}}\n {{- $kind := .kind | default \"file\" -}}\n {{- $item := . -}}\n {{- if or (eq $kind \"image\") (eq $kind \"video\") -}}\n {{- $lbEntry := dict \"url\" .src \"alt\" (.alt | default \"\") -}}\n {{- with .id }}{{- $lbEntry = merge $lbEntry (dict \"id\" .) -}}{{- end -}}\n {{- with .width }}{{- $lbEntry = merge $lbEntry (dict \"width\" .) -}}{{- end -}}\n {{- with .height }}{{- $lbEntry = merge $lbEntry (dict \"height\" .) -}}{{- end -}}\n {{- if eq $kind \"video\" -}}\n {{- with $item.mime_type }}{{- $lbEntry = merge $lbEntry (dict \"mimeType\" .) -}}{{- end -}}\n {{- with $item.poster }}{{- $lbEntry = merge $lbEntry (dict \"posterUrl\" .) -}}{{- end -}}\n {{- with $item.duration_seconds }}{{- $lbEntry = merge $lbEntry (dict \"durationSeconds\" .) -}}{{- end -}}\n {{- with $item.size }}{{- $lbEntry = merge $lbEntry (dict \"size\" .) -}}{{- end -}}\n {{- end -}}\n {{- $lightbox = $lightbox | append $lbEntry -}}\n {{- $items = $items | append (merge $item (dict \"_kind\" $kind \"_lbIdx\" $lbIdx)) -}}\n {{- $lbIdx = add $lbIdx 1 -}}\n {{- /* Capture first visual's aspect ratio */ -}}\n {{- if and (eq $firstRatio 0.0) $item.width $item.height (gt (float $item.width) 0.0) (gt (float $item.height) 0.0) -}}\n {{- $firstRatio = div (float $item.width) (float $item.height) -}}\n {{- end -}}\n {{- else -}}\n {{- $items = $items | append (merge $item (dict \"_kind\" $kind)) -}}\n {{- $hasNonVisual = true -}}\n {{- end -}}\n {{- end -}}\n\n {{- $count := len $items -}}\n {{- $single := eq $count 1 -}}\n {{- $first := index $items 0 -}}\n {{- $firstKind := $first._kind -}}\n {{- $singleVisual := and $single (or (eq $firstKind \"image\") (eq $firstKind \"video\")) -}}\n\n {{- /* rowHeight — mirrors MediaGallery.tsx. 4/3 fallback when no visuals. */ -}}\n {{- $rowMin := cond $hasNonVisual 220 280 -}}\n {{- $rowMax := cond $hasNonVisual 300 440 -}}\n {{- $rowHeight := cond $hasNonVisual 220 360 -}}\n {{- if and (not $singleVisual) (gt $count 1) (gt $firstRatio 0.0) -}}\n {{- $ratio := cond (lt $firstRatio 0.5) 0.5 $firstRatio -}}\n {{- $computed := math.Round (div 320.0 $ratio) -}}\n {{- $rowHeight = cond (gt $computed $rowMax) $rowMax (cond (lt $computed $rowMin) $rowMin (int $computed)) -}}\n {{- end -}}\n {{- $docCardWidth := int (math.Round (mul (float $rowHeight) 0.75)) -}}\n\n {{- /* Outer wrapper — keeps the media-gallery-scroll-wrap hooks for the\n fade-mask client script in the non-single case. */ -}}\n {{- $wrapClass := cond $singleVisual \"\" \"media-gallery-scroll-wrap\" -}}\n <div class=\"{{ $wrapClass }}\" style=\"margin-top: 0.75rem;\">\n <div data-post-media\n {{- if $lightbox }} data-lightbox-group=\"{{ $lightbox | jsonify }}\"{{- end -}}\n {{- if not $singleVisual }} tabindex=\"0\" role=\"group\" aria-label=\"Media gallery\"{{- end -}}\n style=\"display: flex; gap: 0.5rem;\n {{- if not $singleVisual }} overflow-x: auto; scroll-behavior: smooth; scrollbar-width: none; -ms-overflow-style: none;{{- end }}\">\n {{- range $items -}}\n {{- $k := ._kind -}}\n {{- /* Per-item width for non-single visuals */ -}}\n {{- $itemRatio := 1.3333 -}}\n {{- if and .width .height (gt (float .width) 0.0) (gt (float .height) 0.0) -}}\n {{- $itemRatio = div (float .width) (float .height) -}}\n {{- end -}}\n {{- $itemWidth := int (math.Round (mul (float $rowHeight) $itemRatio)) -}}\n {{- if lt $itemWidth 160 -}}{{- $itemWidth = 160 -}}{{- end -}}\n\n {{- if eq $k \"image\" -}}\n {{- if $singleVisual -}}\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\"\n class=\"media-visual-frame\"\n style=\"display: block; width: min(100%, calc(24rem * {{ printf \"%.4f\" $itemRatio }})); max-width: 100%;\">\n <img src=\"{{ .src }}\" alt=\"{{ .alt | default \"\" }}\"\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n loading=\"lazy\" decoding=\"async\"\n class=\"media-visual\"\n style=\"display: block; width: 100%; height: auto; border-radius: 0.5rem;\n {{- if and .width .height }} aspect-ratio: {{ .width }}/{{ .height }};{{- end }}\">\n </a>\n {{- else -}}\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\"\n class=\"media-visual-frame\"\n style=\"display: block; flex-shrink: 0; width: {{ $itemWidth }}px; max-width: 85%;\">\n <img src=\"{{ .src }}\" alt=\"{{ .alt | default \"\" }}\"\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n loading=\"lazy\" decoding=\"async\"\n class=\"media-visual\"\n style=\"display: block; width: 100%; height: {{ $rowHeight }}px; object-fit: cover;\">\n </a>\n {{- end -}}\n {{- else if eq $k \"video\" -}}\n {{- $isShort := false -}}\n {{- if and .mime_type (hasPrefix .mime_type \"video/\") .duration_seconds -}}\n {{- if and (le (int .duration_seconds) 15) (or (not .size) (le (int .size) 12582912)) -}}\n {{- $isShort = true -}}\n {{- end -}}\n {{- end -}}\n {{- $videoWrapStyle := \"position: relative;\" -}}\n {{- if $singleVisual -}}\n {{- $videoWrapStyle = printf \"%s display: block; width: min(100%%, calc(24rem * %s)); max-width: 100%%;\" $videoWrapStyle (printf \"%.4f\" $itemRatio) -}}\n {{- else -}}\n {{- $videoWrapStyle = printf \"%s display: block; flex-shrink: 0; width: %dpx; max-width: 85%%;\" $videoWrapStyle $itemWidth -}}\n {{- end -}}\n {{- $videoStyle := cond $singleVisual\n (cond (and (gt (int (default 0 .width)) 0) (gt (int (default 0 .height)) 0))\n (printf \"display: block; width: 100%%; height: auto; aspect-ratio: %d/%d;\" (int .width) (int .height))\n \"display: block; width: 100%; height: auto;\")\n (printf \"display: block; width: 100%%; height: %dpx; object-fit: cover;\" $rowHeight) -}}\n {{- if $isShort -}}\n <div class=\"media-video-wrap media-video-wrap-short\" style=\"{{ $videoWrapStyle | safeCSS }}\">\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\" class=\"media-visual-frame media-video-link\" style=\"display: block; cursor: pointer;\">\n <video preload=\"none\" muted playsinline loop\n {{- with .poster }} poster=\"{{ . }}\"{{- end -}}\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n data-feed-short-video=\"\"\n data-video-src=\"{{ .src }}\"\n data-feed-video-id=\"{{ .id }}\"\n class=\"media-visual\"\n style=\"{{ $videoStyle | safeCSS }}\"></video>\n </a>\n <button type=\"button\" class=\"media-feed-video-mute\" data-feed-video-mute-toggle data-muted=\"true\" aria-label=\"Play with sound\">\n <svg class=\"media-feed-video-icon media-feed-video-icon-muted\" width=\"12\" height=\"12\" viewBox=\"0 0 48 48\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1v-9.8c0-.4-.2-.8-.4-1.1l-22-22c-.3-.3-.7-.4-1.1-.4h-.6zm46.8 31.4-5.5-5.5C44.9 36.6 48 31.4 48 24c0-11.4-7.2-17.4-7.2-17.4-.6-.6-1.6-.6-2.2 0L37.2 8c-.6.6-.6 1.6 0 2.2 0 0 5.7 5 5.7 13.8 0 5.4-2.1 9.3-3.8 11.6L35.5 32c1.1-1.7 2.3-4.4 2.3-8 0-6.8-4.1-10.3-4.1-10.3-.6-.6-1.6-.6-2.2 0l-1.4 1.4c-.6.6-.6 1.6 0 2.2 0 0 2.6 2 2.6 6.7 0 1.8-.4 3.2-.9 4.3L25.5 22V1.4c0-1.3-1.6-1.9-2.5-1L13.5 10 3.3-.3c-.6-.6-1.5-.6-2.1 0L-.2 1.1c-.6.6-.6 1.5 0 2.1L4 7.6l26.8 26.8 13.9 13.9c.6.6 1.5.6 2.1 0l1.4-1.4c.7-.6.7-1.6.1-2.2z\"/>\n </svg>\n <svg class=\"media-feed-video-icon media-feed-video-icon-unmuted\" width=\"12\" height=\"12\" viewBox=\"0 0 48 48\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0 .8.7 1.5 1.5 1.5h8.7l12.9 12.9c.9.9 2.5.3 2.5-1V1.4c0-1.3-1.6-1.9-2.5-1L10.2 13.3H1.5z\"/>\n <path d=\"M30.1 15.9c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 4.1 3.5 4.1 11.7s-4.1 11.7-4.1 11.7c-.6.6-1.6.6-2.2 0l-1.4-1.4c-.6-.6-.6-1.6 0-2.2 0 0 2.6-2 2.6-8.1s-2.6-8.1-2.6-8.1z\"/>\n <path d=\"M37.2 8c-.6-.6-.6-1.6 0-2.2l1.4-1.4c.6-.6 1.6-.6 2.2 0 0 0 5.7 5.1 5.7 19.6s-5.7 19.6-5.7 19.6c-.6.6-1.6.6-2.2 0L37.2 42c-.6-.6-.6-1.6 0-2.2 0 0 4.3-4.4 4.3-15.8S37.2 8 37.2 8z\"/>\n </svg>\n </button>\n </div>\n {{- else -}}\n <a href=\"{{ .src }}\" data-lightbox-index=\"{{ ._lbIdx }}\"\n class=\"media-video-wrap media-visual-frame\"\n style=\"{{ $videoWrapStyle | safeCSS }}\">\n <video preload=\"none\" muted playsinline\n {{- with .poster }} poster=\"{{ . }}\"{{- end -}}\n {{- with .width }} width=\"{{ . }}\"{{- end -}}\n {{- with .height }} height=\"{{ . }}\"{{- end }}\n class=\"media-visual\"\n style=\"{{ $videoStyle | safeCSS }}\"></video>\n <div class=\"media-video-play-overlay\">\n <svg viewBox=\"0 0 24 24\" fill=\"white\"><path d=\"M8 5v14l11-7z\"/></svg>\n </div>\n </a>\n {{- end -}}\n {{- else if eq $k \"audio\" -}}\n {{- $audioName := .original_name | default (.alt | default \"Audio\") -}}\n <div class=\"media-gallery-card media-audio-card{{ if .waveform }} has-waveform{{ end }}\"\n style=\"flex-shrink: 0; width: {{ $docCardWidth }}px; height: {{ $rowHeight }}px;\">\n <audio preload=\"none\" class=\"media-audio-el\">\n <source src=\"{{ .src }}\"{{ with .mime_type }} type=\"{{ . }}\"{{ end }}>\n </audio>\n <div class=\"media-audio-artwork\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 18V5l12-2v13\"/>\n <circle cx=\"6\" cy=\"18\" r=\"3\"/>\n <circle cx=\"18\" cy=\"16\" r=\"3\"/>\n </svg>\n </div>\n <div class=\"media-audio-controls\">\n <input type=\"range\" min=\"0\" max=\"1000\" value=\"0\" class=\"media-audio-range\" data-audio-range aria-label=\"Seek\">\n <canvas class=\"media-audio-waveform\" data-audio-waveform{{ with .waveform }} data-audio-peaks=\"{{ . }}\"{{ end }}></canvas>\n <div class=\"media-audio-row\">\n <div class=\"media-audio-info\">\n <div class=\"media-audio-title\" title=\"{{ $audioName }}\">{{ $audioName }}</div>\n <div class=\"media-audio-time\" data-audio-time>0:00</div>\n </div>\n <button type=\"button\" class=\"media-audio-play-btn\" data-audio-play aria-label=\"Play\">\n <svg class=\"media-audio-icon-play\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>\n <svg class=\"media-audio-icon-pause\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M6 4h4v16H6zM14 4h4v16h-4z\"/></svg>\n </button>\n </div>\n </div>\n </div>\n {{- else if eq $k \"text\" -}}\n {{- /* Static site has no text-preview modal — link direct to file. */ -}}\n {{- $label := .summary | default (.original_name | default \"Attached text\") -}}\n <a href=\"{{ .src }}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"media-gallery-card\"\n style=\"flex-shrink: 0; width: {{ $docCardWidth }}px; height: {{ $rowHeight }}px;\">\n <div class=\"media-gallery-card-inner\">\n <div class=\"media-gallery-card-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"24\" height=\"24\" aria-hidden=\"true\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/>\n <polyline points=\"14 2 14 8 20 8\"/>\n <line x1=\"8\" y1=\"13\" x2=\"16\" y2=\"13\"/>\n <line x1=\"8\" y1=\"17\" x2=\"16\" y2=\"17\"/>\n </svg>\n </div>\n <span class=\"media-gallery-card-summary\">{{ $label }}</span>\n {{- with .chars -}}\n <span class=\"media-gallery-card-meta\">{{ . }} chars</span>\n {{- end -}}\n </div>\n </a>\n {{- else -}}\n {{- /* document / file */ -}}\n {{- $label := .original_name | default (path.Base .src) -}}\n <a href=\"{{ .src }}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"media-gallery-card\"\n style=\"flex-shrink: 0; width: {{ $docCardWidth }}px; height: {{ $rowHeight }}px;\">\n <div class=\"media-gallery-card-inner\">\n <div class=\"media-gallery-card-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"24\" height=\"24\" aria-hidden=\"true\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/>\n <polyline points=\"14 2 14 8 20 8\"/>\n </svg>\n </div>\n <span class=\"media-gallery-card-summary\">{{ $label }}</span>\n {{- with .size -}}\n <span class=\"media-gallery-card-meta\">{{ . }} B</span>\n {{- end -}}\n </div>\n </a>\n {{- end -}}\n {{- end -}}\n </div>\n {{- if not $singleVisual -}}\n <button type=\"button\" class=\"media-gallery-nav media-gallery-nav-prev\" tabindex=\"-1\" aria-label=\"Scroll to previous media\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M15 18l-6-6 6-6\"/></svg>\n </button>\n <button type=\"button\" class=\"media-gallery-nav media-gallery-nav-next\" tabindex=\"-1\" aria-label=\"Scroll to next media\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M9 18l6-6-6-6\"/></svg>\n </button>\n {{- end -}}\n </div>\n{{- end -}}\n";
|
|
3359
4500
|
//#endregion
|
|
3360
4501
|
//#region src/services/export-theme/layouts/partials/reply.html?raw
|
|
3361
4502
|
var reply_default = "{{- $reply := . -}}\n{{- $format := $reply.Params.format | default \"note\" -}}\n{{- $slug := $reply.Params.slug | default $reply.Slug -}}\n{{- $media := $reply.Params.media | default slice -}}\n{{- $linkUrl := $reply.Params.link_url -}}\n<article class=\"reply thread-item reply-{{ $format }}\" id=\"{{ $slug }}\" data-format=\"{{ $format }}\" data-slug=\"{{ $slug }}\">\n {{- if eq $format \"link\" -}}\n {{- if $linkUrl -}}\n {{- $domain := \"\" -}}\n {{- with urls.Parse $linkUrl -}}{{- $domain = .Host -}}{{- end -}}\n {{- if $domain -}}\n <a href=\"{{ $linkUrl }}\" class=\"reply-link-domain\" rel=\"noopener noreferrer\" target=\"_blank\">\n <svg class=\"post-card-link-domain-icon\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n <span>{{ $domain }}</span>\n </a>\n {{- end -}}\n {{- end -}}\n {{- if $reply.Title -}}\n <h3 class=\"reply-title reply-link-title\">\n {{- if $linkUrl -}}\n <a href=\"{{ $linkUrl }}\" rel=\"noopener noreferrer\" target=\"_blank\">{{ $reply.Title }}</a>\n {{- else -}}\n {{ $reply.Title }}\n {{- end -}}\n </h3>\n {{- end -}}\n {{- else if eq $format \"quote\" -}}\n <blockquote class=\"reply-quote post-card-quote\">\n {{- with $reply.Params.quote_text -}}\n <span class=\"post-card-quote-mark\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 96 96\" role=\"presentation\" focusable=\"false\">\n <path fill=\"currentColor\" d=\"M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z\" />\n <path fill=\"currentColor\" d=\"M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z\" />\n </svg>\n </span>\n <div class=\"post-card-quote-content\">{{ . }}</div>\n {{- end -}}\n {{- if or $reply.Params.source_name $reply.Params.source_url -}}\n <div class=\"post-card-quote-attribution\">\n {{- if and $reply.Params.source_name $reply.Params.source_url -}}\n <a href=\"{{ $reply.Params.source_url }}\" class=\"post-card-quote-source\" rel=\"noopener noreferrer\" target=\"_blank\">{{ $reply.Params.source_name }}</a>\n {{- else if $reply.Params.source_name -}}\n <span class=\"post-card-quote-source\">{{ $reply.Params.source_name }}</span>\n {{- else -}}\n {{- $sourceDomain := \"\" -}}\n {{- with urls.Parse $reply.Params.source_url -}}\n {{- $sourceDomain = .Host | replaceRE \"^(?:www|m|mobile)\\\\.\" \"\" -}}\n {{- end -}}\n <a href=\"{{ $reply.Params.source_url }}\" class=\"post-card-quote-source\" rel=\"noopener noreferrer\" target=\"_blank\">{{ or $sourceDomain $reply.Params.source_url }}</a>\n {{- end -}}\n </div>\n {{- end -}}\n </blockquote>\n {{- else -}}\n {{- with $reply.Title -}}\n <h3 class=\"reply-title\">{{ . }}</h3>\n {{- end -}}\n {{- end -}}\n\n {{- if eq $format \"quote\" -}}\n {{- with $reply.Content -}}\n <div class=\"reply-body post-card-quote-commentary prose\">{{ . }}</div>\n {{- end -}}\n {{- else -}}\n <div class=\"reply-body prose\">\n {{ $reply.Content }}\n </div>\n {{- end -}}\n\n {{ partial \"media-gallery.html\" (dict \"media\" $media \"permalink\" $reply.RelPermalink) }}\n\n <footer class=\"reply-footer post-menu-footer\" data-post-meta>\n <div class=\"post-footer-meta\">\n <a class=\"post-footer-link reply-anchor\" href=\"#{{ $slug }}\">\n <time datetime=\"{{ $reply.Date.Format \"2006-01-02T15:04:05Z07:00\" }}\">\n {{ $reply.Date.Format \"Jan 2, 2006 · 15:04\" }}\n </time>\n </a>\n {{- if and (eq $format \"link\") $linkUrl -}}\n <a href=\"{{ $linkUrl }}\" class=\"post-footer-external-link\" target=\"_blank\" rel=\"noopener noreferrer\" aria-label=\"Open external link\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M7 17 17 7\" />\n <path d=\"M9 7h8v8\" />\n </svg>\n </a>\n {{- end -}}\n </div>\n </footer>\n</article>\n";
|
|
@@ -3363,8 +4504,11 @@ var reply_default = "{{- $reply := . -}}\n{{- $format := $reply.Params.format |
|
|
|
3363
4504
|
//#region src/services/export-theme/layouts/partials/thread-preview.html?raw
|
|
3364
4505
|
var thread_preview_default = "{{- /*\n Thread preview partial.\n\n Matches the main site's `ThreadPreview` component layout:\n root post (full card, as \"context\")\n second reply (full card, as \"context\") ← if distinct\n \"N more posts\" gap pill ← if there are hidden middle replies\n penultimate reply (full card, as \"context\") ← if distinct\n latest reply (full card, as \"hero\")\n\n Each entry is wrapped in a `.thread-item` so the rail dot markers\n line up the same way the main site does via `.thread-item::before`.\n\n When the root has no replies, falls through to a plain post-card.\n*/ -}}\n{{- $root := . -}}\n{{- $replies := where $root.Pages \"Params.visibility\" \"public\" -}}\n{{- $replies = $replies.ByDate -}}\n{{- $replyCount := len $replies -}}\n{{- if eq $replyCount 0 -}}\n {{ partial \"post-card.html\" $root }}\n{{- else -}}\n {{- /* First-in / last-in replies, with overlap handled below. */ -}}\n {{- $latest := index $replies (sub $replyCount 1) -}}\n {{- $second := index $replies 0 -}}\n {{- $penultimate := cond (gt $replyCount 2) (index $replies (sub $replyCount 2)) false -}}\n\n {{- $renderSecond := ne $second.File.UniqueID $latest.File.UniqueID -}}\n {{- $renderPenultimate := and $penultimate (ne $penultimate.File.UniqueID $latest.File.UniqueID) -}}\n {{- if $penultimate -}}\n {{- if eq $penultimate.File.UniqueID $second.File.UniqueID -}}\n {{- $renderPenultimate = false -}}\n {{- end -}}\n {{- end -}}\n\n {{- /* Count of context posts actually rendered above the hero (second\n + penultimate when distinct from latest and each other). The\n remainder of replies is the \"hidden middle\" count. Mirrors\n `getThreadPreviewState` in thread-preview-state.ts. */ -}}\n {{- $visibleContextReplies := 0 -}}\n {{- if $renderSecond -}}{{- $visibleContextReplies = add $visibleContextReplies 1 -}}{{- end -}}\n {{- if $renderPenultimate -}}{{- $visibleContextReplies = add $visibleContextReplies 1 -}}{{- end -}}\n {{- /* Total replies rendered = visibleContextReplies + 1 (hero). */ -}}\n {{- $hiddenCount := sub $replyCount (add $visibleContextReplies 1) -}}\n {{- if lt $hiddenCount 0 -}}{{- $hiddenCount = 0 -}}{{- end -}}\n\n {{- $slug := $root.Params.slug | default $root.Slug -}}\n <article class=\"thread-preview\" data-slug=\"{{ $slug }}\" data-reply-count=\"{{ $replyCount }}\">\n <div class=\"thread-preview-context\">\n <div class=\"thread-item thread-item-context thread-item-root\">\n {{ partial \"post-card.html\" $root }}\n </div>\n\n {{- if $renderSecond -}}\n <div class=\"thread-item thread-item-context\">\n {{ partial \"post-card.html\" $second }}\n </div>\n {{- end -}}\n\n {{- if gt $hiddenCount 0 -}}\n <div class=\"thread-item thread-item-gap\">\n <a class=\"thread-preview-gap\" href=\"{{ $root.RelPermalink }}\">\n {{ $hiddenCount }} more {{ if eq $hiddenCount 1 }}post{{ else }}posts{{ end }}\n </a>\n </div>\n {{- end -}}\n\n {{- if $renderPenultimate -}}\n <div class=\"thread-item thread-item-context\">\n {{ partial \"post-card.html\" $penultimate }}\n </div>\n {{- end -}}\n </div>\n\n <div class=\"thread-item thread-item-hero thread-preview-hero\">\n {{ partial \"post-card.html\" $latest }}\n <a class=\"thread-preview-thread-link\" href=\"{{ $root.RelPermalink }}\">\n View full thread →\n </a>\n </div>\n </article>\n{{- end -}}\n";
|
|
3365
4506
|
//#endregion
|
|
4507
|
+
//#region src/services/export-theme/layouts/partials/featured-thread.html?raw
|
|
4508
|
+
var featured_thread_default = "{{- /*\n Curated Featured Thread.\n\n The Root carries a derived `featured_post_ids` projection. Render the Root,\n every selected Featured Post, and the final Post once, preserving the real\n Thread positions so omitted runs can be represented as gap links.\n*/ -}}\n{{- $root := . -}}\n{{- $featuredIds := $root.Params.featured_post_ids | default slice -}}\n{{- $posts := slice $root -}}\n{{- range $root.Pages.ByDate -}}\n {{- $posts = $posts | append . -}}\n{{- end -}}\n{{- $lastIndex := sub (len $posts) 1 -}}\n{{- $visible := slice -}}\n\n{{- range $index, $post := $posts -}}\n {{- $postId := string ($post.Params.id | default \"\") -}}\n {{- $highlighted := in $featuredIds $postId -}}\n {{- if or (eq $index 0) $highlighted (eq $index $lastIndex) -}}\n {{- $visible = $visible | append (dict\n \"post\" $post\n \"position\" $index\n \"highlighted\" $highlighted\n ) -}}\n {{- end -}}\n{{- end -}}\n\n{{- if eq (len $visible) 1 -}}\n {{- partial \"post-card.html\" $root -}}\n{{- else -}}\n {{- $slug := $root.Params.slug | default $root.Slug -}}\n {{- $previousPosition := -1 -}}\n <article class=\"thread-preview thread-preview-curated\" data-slug=\"{{ $slug }}\">\n <div class=\"thread-preview-context\">\n {{- range $entry := $visible -}}\n {{- $position := int $entry.position -}}\n {{- $hiddenCount := sub $position (add $previousPosition 1) -}}\n {{- if gt $hiddenCount 0 -}}\n <div class=\"thread-item thread-item-gap\">\n <a class=\"thread-preview-gap\" href=\"{{ $root.RelPermalink }}\">\n {{ $hiddenCount }} hidden {{ if eq $hiddenCount 1 }}post{{ else }}posts{{ end }}\n </a>\n </div>\n {{- end -}}\n <div class=\"thread-item {{ if $entry.highlighted }}thread-item-featured{{ else }}thread-item-context{{ end }}\">\n {{ partial \"post-card.html\" $entry.post }}\n </div>\n {{- $previousPosition = $position -}}\n {{- end -}}\n </div>\n <a class=\"thread-preview-thread-link\" href=\"{{ $root.RelPermalink }}\">\n View full thread →\n </a>\n </article>\n{{- end -}}\n";
|
|
4509
|
+
//#endregion
|
|
3366
4510
|
//#region src/services/export-theme/layouts/_default/rss.xml?raw
|
|
3367
|
-
var rss_default = "{{- /*\n Atom 2005 feed template — mirrors the main site's `lib/feed.ts`.\n\n One template covers every page that opts into the custom `RSS` output\n format (home, featured, archive, and each collection section). Root-post\n sections do NOT opt in, so no `/{slug}/index.xml` gets emitted.\n\n Feed filter parity with the corresponding HTML list view:\n - home (`.Kind == \"home\"`): visibility == \"public\" (drops latest_hidden)\n - featured (`.Type == \"featured\"`):
|
|
4511
|
+
var rss_default = "{{- /*\n Atom 2005 feed template — mirrors the main site's `lib/feed.ts`.\n\n One template covers every page that opts into the custom `RSS` output\n format (home, featured, archive, and each collection section). Root-post\n sections do NOT opt in, so no `/{slug}/index.xml` gets emitted.\n\n Feed filter parity with the corresponding HTML list view:\n - home (`.Kind == \"home\"`): visibility == \"public\" (drops latest_hidden)\n - featured (`.Type == \"featured\"`): one Root per Thread whose derived\n featured projection is non-empty, ordered by selected Post publication\n - archive (`.Type == \"archive\"`): all published (includes latest_hidden)\n - collection (`.Type == \"collection\"`): Params.collections.slug match,\n ordered per `sort_order`; pinning is ignored for RSS\n\n Per-entry payload mirrors `buildSinglePostContent` + `buildFeedContent`:\n - Title: empty for quote posts, else .Title\n - Link rel=alternate: external URL for `link` format, else permalink;\n link-format posts also get a <link rel=\"related\"> back to the\n permalink\n - Content (CDATA):\n quote → <blockquote cite><p>{quote}</p></blockquote>\n + <p>— <a href=url>{source}</a></p>\n body → Hugo-rendered .Content (stripped of <script>/<style>)\n rating → <p>★★★★☆ 4/5</p>\n link → trailing <p><a href=permalink> ★ </a></p>\n replies → <hr/> + <p><small><time>{dt}</time></small></p>\n + inline title/link metadata + content\n\n Hugo's default RSS media-type override (`[outputFormats.RSS]`) makes this\n template produce application/atom+xml at /{section}/index.xml.\n*/ -}}\n{{- $page := . -}}\n{{- $limit := int (.Site.Params.rss_feed_limit | default 50) -}}\n{{- $allPosts := where .Site.Pages \"Type\" \"post\" -}}\n{{- $rootPosts := where $allPosts \"Kind\" \"section\" -}}\n\n{{- /* ------------------------------------------------------------------\n Post selection per feed kind\n ------------------------------------------------------------------ */ -}}\n{{- $posts := slice -}}\n{{- $isFeatured := eq $page.Type \"featured\" -}}\n\n{{- if eq $page.Kind \"home\" -}}\n {{- $public := where $allPosts \"Params.visibility\" \"public\" -}}\n {{- $posts = $public.ByDate.Reverse -}}\n{{- else if $isFeatured -}}\n {{- $featured := where $rootPosts \"Params.featured_sort_at\" \"ne\" nil -}}\n {{- $posts = sort $featured \"Params.featured_sort_at\" \"desc\" -}}\n{{- else if eq $page.Type \"archive\" -}}\n {{- $posts = $allPosts.ByDate.Reverse -}}\n{{- else if eq $page.Type \"collection\" -}}\n {{- $collectionSlug := $page.Params.slug | default $page.Slug -}}\n {{- $members := slice -}}\n {{- range $rootPosts -}}\n {{- $post := . -}}\n {{- with $post.Params.collections -}}\n {{- range . -}}\n {{- if eq .slug $collectionSlug -}}\n {{- $members = $members | append (dict \"post\" $post \"entry\" .) -}}\n {{- end -}}\n {{- end -}}\n {{- end -}}\n {{- end -}}\n {{- $sortOrder := $page.Params.sort_order | default \"position\" -}}\n {{- $ordered := $members -}}\n {{- if $ordered -}}\n {{- if eq $sortOrder \"collected_at_desc\" -}}\n {{- $ordered = sort $ordered \"entry.collected_at\" \"desc\" -}}\n {{- else -}}\n {{- $ordered = sort $ordered \"entry.position\" \"asc\" -}}\n {{- end -}}\n {{- end -}}\n {{- range $ordered -}}{{- $posts = $posts | append .post -}}{{- end -}}\n{{- end -}}\n\n{{- if gt (len $posts) $limit -}}\n {{- $posts = first $limit $posts -}}\n{{- end -}}\n\n{{- /* ------------------------------------------------------------------\n Feed title (mirrors the main site's Atom feed titles)\n ------------------------------------------------------------------ */ -}}\n{{- $feedTitle := .Site.Title -}}\n{{- if eq $page.Kind \"home\" -}}\n {{- $feedTitle = printf \"%s - Latest posts\" .Site.Title -}}\n{{- else if $isFeatured -}}\n {{- $feedTitle = printf \"%s - Featured posts\" .Site.Title -}}\n{{- else if eq $page.Type \"archive\" -}}\n {{- $feedTitle = printf \"%s - Archive\" .Site.Title -}}\n{{- else if eq $page.Type \"collection\" -}}\n {{- $feedTitle = printf \"%s - %s\" .Site.Title $page.Title -}}\n{{- end -}}\n\n{{- $siteDescription := .Site.Params.description | default \"\" -}}\n{{- $siteBase := .Site.BaseURL -}}\n{{- $selfUrl := .Permalink -}}\n{{- $now := now.UTC.Format \"2006-01-02T15:04:05Z\" -}}\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\">\n <title>{{ $feedTitle | transform.XMLEscape }}</title>\n <subtitle>{{ $siteDescription | transform.XMLEscape }}</subtitle>\n <link href=\"{{ $siteBase | transform.XMLEscape }}\" rel=\"alternate\"/>\n <link href=\"{{ $selfUrl | transform.XMLEscape }}\" rel=\"self\"/>\n <id>{{ $selfUrl | transform.XMLEscape }}</id>\n <updated>{{ $now }}</updated>\n {{- range $posts }}\n {{- $post := . -}}\n {{- $format := .Params.format | default \"note\" -}}\n {{- $permalink := .Permalink -}}\n {{- $isLink := and (eq $format \"link\") .Params.link_url -}}\n {{- $alternate := cond $isLink (string .Params.link_url) $permalink -}}\n {{- /* Atom <title>: empty for quotes (matches main site's getAtomTitle). */ -}}\n {{- $entryTitle := cond (eq $format \"quote\") \"\" (.Title | default \"\") -}}\n {{- /* <published>: from `date` frontmatter (= publishedAt).\n <updated>: prefer `last_activity_at` (thread-bumped), then `updated`\n (root edit time), else `date`. Featured membership does not rewrite\n content time or chronological ordering. */ -}}\n {{- $published := .Date.UTC.Format \"2006-01-02T15:04:05Z\" -}}\n {{- $updatedSrc := .Date -}}\n {{- with .Params.last_activity_at -}}{{- $updatedSrc = time . -}}\n {{- else -}}{{- with .Params.updated -}}{{- $updatedSrc = time . -}}{{- end -}}{{- end -}}\n {{- $updated := $updatedSrc.UTC.Format \"2006-01-02T15:04:05Z\" -}}\n {{- /* Plain-text summary fallback chain (mirrors getFeedSummaryText). */ -}}\n {{- $summaryText := \"\" -}}\n {{- if eq $format \"quote\" -}}\n {{- $summaryText = or (string (.Params.summary_text | default \"\")) (string (.Params.quote_text | default \"\")) (.Title | default \"\") (string (.Params.link_url | default \"\")) (string (.Params.source_url | default \"\")) -}}\n {{- else -}}\n {{- $summaryText = or (string (.Params.summary_text | default \"\")) (.Title | default \"\") (string (.Params.link_url | default \"\")) -}}\n {{- end -}}\n {{- if eq $summaryText \"\" -}}{{- $summaryText = printf \"Post %s\" (string (.Params.id | default \"\")) -}}{{- end -}}\n <entry>\n <title>{{ $entryTitle | transform.XMLEscape }}</title>\n <link href=\"{{ $alternate | transform.XMLEscape }}\" rel=\"alternate\"/>\n {{- if $isLink }}\n <link href=\"{{ $permalink | transform.XMLEscape }}\" rel=\"related\"/>\n {{- end }}\n <id>{{ $permalink | transform.XMLEscape }}</id>\n <published>{{ $published }}</published>\n <updated>{{ $updated }}</updated>\n <summary type=\"text\">{{ $summaryText | transform.XMLEscape }}</summary>\n <content type=\"html\"><![CDATA[\n{{- partial \"feed-post-content.xml\" (dict \"post\" $post \"permalink\" $permalink \"includeReplies\" true) -}}\n]]></content>\n </entry>\n {{- end }}\n</feed>\n";
|
|
3368
4512
|
//#endregion
|
|
3369
4513
|
//#region src/services/export-theme/layouts/partials/feed-post-content.xml?raw
|
|
3370
4514
|
var feed_post_content_default = "{{- /*\n Feed entry content builder — mirrors `buildSinglePostContent` +\n `buildFeedContent` from `packages/core/src/lib/feed.ts`.\n\n Arguments (dict):\n post — the Hugo Page for this post\n permalink — absolute permalink to the blog post (used for the\n link-format \"★\" suffix)\n includeReplies — when true, appends each public reply separated by\n <hr/> + <small><time></time></small>\n inline — when true, renders title/link metadata that top-level\n Atom entries expose through their entry fields\n\n Output is raw HTML suitable for embedding inside Atom\n <content type=\"html\"><![CDATA[...]]></content>. Since CDATA ends at the\n first `]]>`, any such sequence in rendered bodies is split using\n `replace ... \"]]>\" \"]]]]><![CDATA[>\"` — the same trick the main site uses\n in `escapeCdata`.\n*/ -}}\n{{- $post := .post -}}\n{{- $permalink := .permalink -}}\n{{- $includeReplies := .includeReplies -}}\n{{- $inline := false -}}\n{{- with .inline -}}{{- $inline = . -}}{{- end -}}\n{{- $format := $post.Params.format | default \"note\" -}}\n{{- $parts := slice -}}\n\n{{- /* --- Inline title/link metadata for thread replies --- */ -}}\n{{- if and $inline (ne $format \"quote\") -}}\n {{- $title := $post.Title | default \"\" -}}\n {{- $linkUrl := string ($post.Params.link_url | default \"\") -}}\n {{- if eq $format \"link\" -}}\n {{- if ne $linkUrl \"\" -}}\n {{- $domain := $linkUrl -}}\n {{- with urls.Parse $linkUrl -}}{{- if .Host -}}{{- $domain = replaceRE `^(www|m|mobile)\\.` \"\" .Host -}}{{- end -}}{{- end -}}\n {{- $parts = $parts | append (printf `<p><a href=%q>%s</a></p>` $linkUrl (transform.XMLEscape $domain)) -}}\n {{- end -}}\n {{- if ne $title \"\" -}}\n {{- $titleHref := $permalink -}}\n {{- if ne $linkUrl \"\" -}}{{- $titleHref = $linkUrl -}}{{- end -}}\n {{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $titleHref (transform.XMLEscape $title)) -}}\n {{- end -}}\n {{- else if ne $title \"\" -}}\n {{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $permalink (transform.XMLEscape $title)) -}}\n {{- end -}}\n{{- end -}}\n\n{{- /* --- Quote block (format=quote) --- */ -}}\n{{- if eq $format \"quote\" -}}\n {{- $quoteText := string ($post.Params.quote_text | default \"\") -}}\n {{- if ne $quoteText \"\" -}}\n {{- $sourceName := string ($post.Params.source_name | default \"\") -}}\n {{- $sourceUrl := string ($post.Params.source_url | default \"\") -}}\n {{- $cite := \"\" -}}\n {{- if ne $sourceUrl \"\" -}}{{- $cite = printf ` cite=%q` $sourceUrl -}}{{- end -}}\n {{- $block := printf \"<blockquote%s><p>%s</p></blockquote>\" $cite (transform.XMLEscape $quoteText) -}}\n {{- $parts = $parts | append $block -}}\n {{- /* Attribution line — prefer name, fall back to URL host or the\n raw URL itself (mirrors main site's extractDisplayDomain fallback\n chain in `lib/url.ts`). */ -}}\n {{- $attribution := \"\" -}}\n {{- if and (ne $sourceName \"\") (ne $sourceUrl \"\") -}}\n {{- $attribution = printf `<a href=%q>%s</a>` $sourceUrl (transform.XMLEscape $sourceName) -}}\n {{- else if ne $sourceName \"\" -}}\n {{- $attribution = transform.XMLEscape $sourceName -}}\n {{- else if ne $sourceUrl \"\" -}}\n {{- $host := $sourceUrl -}}\n {{- with urls.Parse $sourceUrl -}}{{- if .Host -}}{{- $host = .Host -}}{{- end -}}{{- end -}}\n {{- $attribution = printf `<a href=%q>%s</a>` $sourceUrl (transform.XMLEscape $host) -}}\n {{- end -}}\n {{- if ne $attribution \"\" -}}\n {{- $parts = $parts | append (printf \"<p>— %s</p>\" $attribution) -}}\n {{- end -}}\n {{- end -}}\n{{- end -}}\n\n{{- /* --- Rendered body HTML (with <script>/<style> stripped) --- */ -}}\n{{- $body := $post.Content -}}\n{{- if ne (string $body) \"\" -}}\n {{- $bodyStr := string $body -}}\n {{- $bodyStr = replaceRE `(?is)<script\\b[^>]*>.*?<\/script>` \"\" $bodyStr -}}\n {{- $bodyStr = replaceRE `(?is)<style\\b[^>]*>.*?</style>` \"\" $bodyStr -}}\n {{- /* Split any CDATA-terminator sequences so the content can be safely\n wrapped in <![CDATA[...]]> by the caller. */ -}}\n {{- $bodyStr = replace $bodyStr \"]]>\" \"]]]]><![CDATA[>\" -}}\n {{- if ne (trim $bodyStr \" \\t\\n\\r\") \"\" -}}\n {{- $parts = $parts | append $bodyStr -}}\n {{- end -}}\n{{- end -}}\n\n{{- /* --- Star rating --- */ -}}\n{{- with $post.Params.rating -}}\n {{- $r := int . -}}\n {{- if gt $r 0 -}}\n {{- $filled := strings.Repeat $r \"★\" -}}\n {{- $empty := strings.Repeat (sub 5 $r) \"☆\" -}}\n {{- $parts = $parts | append (printf \"<p>%s%s %d/5</p>\" $filled $empty $r) -}}\n {{- end -}}\n{{- end -}}\n\n{{- /* --- Empty-body fallback (plain-text summary) --- */ -}}\n{{- if eq (len $parts) 0 -}}\n {{- $fallback := string ($post.Params.summary_text | default \"\") -}}\n {{- if eq $fallback \"\" -}}{{- $fallback = $post.Title | default (printf \"Post %s\" (string ($post.Params.id | default \"\"))) -}}{{- end -}}\n {{- $parts = $parts | append (printf \"<p>%s</p>\" (transform.XMLEscape $fallback)) -}}\n{{- end -}}\n\n{{- /* --- link-format permalink suffix (Daring Fireball ★) --- */ -}}\n{{- if and (eq $format \"link\") (ne $permalink \"\") -}}\n {{- $parts = $parts | append (printf `<p><a href=%q title=\"Permalink\"> ★ </a></p>` $permalink) -}}\n{{- end -}}\n\n{{- /* --- Thread replies --- */ -}}\n{{- if $includeReplies -}}\n {{- $replies := where $post.Pages \"Params.visibility\" \"public\" -}}\n {{- $replies = $replies.ByDate -}}\n {{- range $replies -}}\n {{- $replyPermalink := .Permalink -}}\n {{- $replyDatetime := .Date.UTC.Format \"2006-01-02T15:04:05Z\" -}}\n {{- /* Human-readable timestamp; mirrors the main site's\n `publishedAtFormatted` in the feed — there it's locale-formatted\n by `lib/format-timestamp.ts`, but Hugo doesn't have that at\n export time, so use a simple ISO-style fallback. */ -}}\n {{- $replyLabel := .Date.Format \"Jan 2, 2006 15:04\" -}}\n {{- $parts = $parts | append \"<hr/>\" -}}\n {{- $parts = $parts | append (printf `<p><small><time datetime=%q>%s</time></small></p>` $replyDatetime (transform.XMLEscape $replyLabel)) -}}\n {{- $parts = $parts | append (partial \"feed-post-content.xml\" (dict \"post\" . \"permalink\" $replyPermalink \"includeReplies\" false \"inline\" true)) -}}\n {{- end -}}\n{{- end -}}\n\n{{- delimit $parts \"\\n\" | safeHTML -}}\n";
|
|
@@ -3421,9 +4565,9 @@ function createExportService(services, siteConfig, deps = {}) {
|
|
|
3421
4565
|
const roots = allPosts.filter((p) => p.replyToId === null);
|
|
3422
4566
|
const replies = allPosts.filter((p) => p.replyToId !== null);
|
|
3423
4567
|
const rootPostIds = roots.map((p) => p.id);
|
|
3424
|
-
const [
|
|
3425
|
-
services.collections.getCollectionsByPostIds(
|
|
3426
|
-
services.collections.
|
|
4568
|
+
const [collectionsByRoot, collectionEntriesByThread, rawMediaByPost, slugMap, aliasMap, collectionSlugMap] = await Promise.all([
|
|
4569
|
+
services.collections.getCollectionsByPostIds(rootPostIds),
|
|
4570
|
+
services.collections.getCollectionEntriesByThreadIds(rootPostIds),
|
|
3427
4571
|
services.media.getByPostIds(allPostIds),
|
|
3428
4572
|
services.paths.getPostSlugMap(allPostIds),
|
|
3429
4573
|
services.paths.getPostAliases(rootPostIds),
|
|
@@ -3432,7 +4576,7 @@ function createExportService(services, siteConfig, deps = {}) {
|
|
|
3432
4576
|
const collectionTitleMap = /* @__PURE__ */ new Map();
|
|
3433
4577
|
for (const collection of allCollections) collectionTitleMap.set(collection.id, collection.title);
|
|
3434
4578
|
const iconAssets = await buildSiteIconAssets(siteConfig, deps.storage);
|
|
3435
|
-
const collectionMetrics = buildExportedCollectionMetrics(allCollections, allPosts,
|
|
4579
|
+
const collectionMetrics = buildExportedCollectionMetrics(allCollections, allPosts, collectionsByRoot);
|
|
3436
4580
|
const exportedCollectionDirectoryItems = buildExportedCollectionDirectoryItems(collectionDirectoryData?.items ?? allCollections.map((collection) => ({
|
|
3437
4581
|
id: collection.id,
|
|
3438
4582
|
type: "collection",
|
|
@@ -3449,15 +4593,15 @@ function createExportService(services, siteConfig, deps = {}) {
|
|
|
3449
4593
|
const bundleMedia = deps.bundleMedia ?? true;
|
|
3450
4594
|
for (const root of roots) {
|
|
3451
4595
|
const slug = slugMap.get(root.id) ?? root.slug;
|
|
3452
|
-
const bundleFiles = await buildThreadBundle(root, repliesByThread.get(root.id) ?? [], slug, [...aliasMap.get(root.id) ?? []],
|
|
4596
|
+
const bundleFiles = await buildThreadBundle(root, repliesByThread.get(root.id) ?? [], slug, [...aliasMap.get(root.id) ?? []], buildExportedCollectionEntriesForThread(root.id, collectionEntriesByThread, collectionSlugMap, collectionTitleMap), slugMap, rawMediaByPost, siteConfig, deps.storage ?? null, bundleMedia);
|
|
3453
4597
|
exportFiles.push(...bundleFiles);
|
|
3454
4598
|
}
|
|
3455
4599
|
for (const collection of allCollections) {
|
|
3456
4600
|
const slug = collectionSlugMap.get(collection.id) ?? collection.slug;
|
|
3457
|
-
const entryCount = collectionMetrics.get(collection.id)?.
|
|
4601
|
+
const entryCount = collectionMetrics.get(collection.id)?.threadCount ?? 0;
|
|
3458
4602
|
exportFiles.push({
|
|
3459
4603
|
path: `content/${slug}/_index.md`,
|
|
3460
|
-
content: await buildCollectionSection(collection, slug, entryCount)
|
|
4604
|
+
content: await buildCollectionSection(collection, slug, entryCount, siteConfig.rssFeedsEnabled)
|
|
3461
4605
|
});
|
|
3462
4606
|
}
|
|
3463
4607
|
exportFiles.push({
|
|
@@ -3474,14 +4618,14 @@ function createExportService(services, siteConfig, deps = {}) {
|
|
|
3474
4618
|
});
|
|
3475
4619
|
exportFiles.push({
|
|
3476
4620
|
path: "content/archive/_index.md",
|
|
3477
|
-
content: await buildArchiveSection()
|
|
4621
|
+
content: await buildArchiveSection(siteConfig.rssFeedsEnabled)
|
|
3478
4622
|
});
|
|
3479
4623
|
const usedSlugs = /* @__PURE__ */ new Set();
|
|
3480
4624
|
for (const s of slugMap.values()) usedSlugs.add(s);
|
|
3481
4625
|
for (const s of collectionSlugMap.values()) usedSlugs.add(s);
|
|
3482
4626
|
if (!usedSlugs.has("featured")) exportFiles.push({
|
|
3483
4627
|
path: "content/featured/_index.md",
|
|
3484
|
-
content: await buildFeaturedSection()
|
|
4628
|
+
content: await buildFeaturedSection(siteConfig.rssFeedsEnabled)
|
|
3485
4629
|
});
|
|
3486
4630
|
exportFiles.push({
|
|
3487
4631
|
path: "data/jant.toml",
|
|
@@ -3563,6 +4707,10 @@ function createExportService(services, siteConfig, deps = {}) {
|
|
|
3563
4707
|
path: "themes/jant/layouts/partials/thread-preview.html",
|
|
3564
4708
|
content: thread_preview_default
|
|
3565
4709
|
});
|
|
4710
|
+
exportFiles.push({
|
|
4711
|
+
path: "themes/jant/layouts/partials/featured-thread.html",
|
|
4712
|
+
content: featured_thread_default
|
|
4713
|
+
});
|
|
3566
4714
|
exportFiles.push({
|
|
3567
4715
|
path: "themes/jant/layouts/_default/rss.xml",
|
|
3568
4716
|
content: rss_default
|
|
@@ -3663,8 +4811,8 @@ async function buildSiteIconAssets(config, storage) {
|
|
|
3663
4811
|
appleTouchMode: "custom"
|
|
3664
4812
|
};
|
|
3665
4813
|
}
|
|
3666
|
-
function
|
|
3667
|
-
const entries =
|
|
4814
|
+
function buildExportedCollectionEntriesForThread(threadId, collectionEntriesByThread, collectionSlugMap, collectionTitleMap) {
|
|
4815
|
+
const entries = collectionEntriesByThread.get(threadId) ?? [];
|
|
3668
4816
|
const resolved = [];
|
|
3669
4817
|
for (const entry of entries) {
|
|
3670
4818
|
const slug = collectionSlugMap.get(entry.collectionId);
|
|
@@ -3769,8 +4917,11 @@ function extOfStorageKey(key) {
|
|
|
3769
4917
|
* Build a complete set of ExportFile entries for a single thread bundle:
|
|
3770
4918
|
* the root `_index.md`, one `index.md` per reply, and resource blobs for
|
|
3771
4919
|
* attached media when the storage driver can fetch them.
|
|
3772
|
-
*/ async function buildThreadBundle(root, threadReplies, rootSlug, rootAliases, rootCollectionEntries, slugMap,
|
|
4920
|
+
*/ async function buildThreadBundle(root, threadReplies, rootSlug, rootAliases, rootCollectionEntries, slugMap, mediaByPost, siteConfig, storage, bundleMedia) {
|
|
3773
4921
|
const files = [];
|
|
4922
|
+
const featuredPosts = [root, ...threadReplies].filter((post) => post.status === "published" && post.featuredAt !== null);
|
|
4923
|
+
const featuredPostIds = featuredPosts.map((post) => post.id);
|
|
4924
|
+
const featuredSortAt = featuredPosts.reduce((latest, post) => Math.max(latest ?? -1, post.publishedAt ?? post.createdAt), null);
|
|
3774
4925
|
const aliases = [...rootAliases];
|
|
3775
4926
|
for (const reply of threadReplies) {
|
|
3776
4927
|
const replySlug = slugMap.get(reply.id) ?? reply.slug;
|
|
@@ -3799,6 +4950,8 @@ function extOfStorageKey(key) {
|
|
|
3799
4950
|
quote_text: root.quoteText ?? void 0,
|
|
3800
4951
|
rating: root.rating ?? void 0,
|
|
3801
4952
|
featured_at: root.featuredAt !== null ? toISOString(root.featuredAt) : void 0,
|
|
4953
|
+
featured_post_ids: featuredPostIds.length > 0 ? featuredPostIds : void 0,
|
|
4954
|
+
featured_sort_at: featuredSortAt !== null ? toISOString(featuredSortAt) : void 0,
|
|
3802
4955
|
pinned_at: root.pinnedAt !== null ? toISOString(root.pinnedAt) : void 0,
|
|
3803
4956
|
root_aliases: rootAliases.length > 0 ? rootAliases : void 0,
|
|
3804
4957
|
collections: rootCollectionEntries.length > 0 ? collectionEntriesToRefs(rootCollectionEntries) : void 0,
|
|
@@ -3827,7 +4980,6 @@ function extOfStorageKey(key) {
|
|
|
3827
4980
|
const replyMedia = mediaByPost.get(reply.id) ?? [];
|
|
3828
4981
|
const replyEmissions = replyMedia.map((m) => buildMediaEmission(m, siteConfig, bundleMedia));
|
|
3829
4982
|
const replyMediaList = replyEmissions.map((e) => e.entry);
|
|
3830
|
-
const replyCollectionEntries = buildExportedCollectionEntriesForPost(reply.id, collectionEntriesByPost, collectionSlugMap, collectionTitleMap);
|
|
3831
4983
|
const replyFrontMatter = {
|
|
3832
4984
|
id: reply.id,
|
|
3833
4985
|
title: reply.format !== "quote" ? reply.title ?? void 0 : void 0,
|
|
@@ -3851,7 +5003,6 @@ function extOfStorageKey(key) {
|
|
|
3851
5003
|
rating: reply.rating ?? void 0,
|
|
3852
5004
|
featured_at: reply.featuredAt !== null ? toISOString(reply.featuredAt) : void 0,
|
|
3853
5005
|
pinned_at: reply.pinnedAt !== null ? toISOString(reply.pinnedAt) : void 0,
|
|
3854
|
-
collections: replyCollectionEntries.length > 0 ? collectionEntriesToRefs(replyCollectionEntries) : void 0,
|
|
3855
5006
|
media: replyMediaList.length > 0 ? replyMediaList : void 0
|
|
3856
5007
|
};
|
|
3857
5008
|
const replyBody = reply.body ? tiptapJsonToMarkdown(reply.body) : "";
|
|
@@ -3906,21 +5057,21 @@ async function buildCollectionsSection() {
|
|
|
3906
5057
|
type: "collections"
|
|
3907
5058
|
})}\n`;
|
|
3908
5059
|
}
|
|
3909
|
-
async function buildArchiveSection() {
|
|
5060
|
+
async function buildArchiveSection(rssFeedsEnabled) {
|
|
3910
5061
|
return `${await formatFrontMatter({
|
|
3911
5062
|
title: "Archive",
|
|
3912
5063
|
type: "archive",
|
|
3913
|
-
outputs: ["html", "rss"]
|
|
5064
|
+
outputs: rssFeedsEnabled ? ["html", "rss"] : ["html"]
|
|
3914
5065
|
})}\n`;
|
|
3915
5066
|
}
|
|
3916
|
-
async function buildFeaturedSection() {
|
|
5067
|
+
async function buildFeaturedSection(rssFeedsEnabled) {
|
|
3917
5068
|
return `${await formatFrontMatter({
|
|
3918
5069
|
title: "Featured",
|
|
3919
5070
|
type: "featured",
|
|
3920
|
-
outputs: ["html", "rss"]
|
|
5071
|
+
outputs: rssFeedsEnabled ? ["html", "rss"] : ["html"]
|
|
3921
5072
|
})}\n`;
|
|
3922
5073
|
}
|
|
3923
|
-
async function buildCollectionSection(collection, slug, entryCount) {
|
|
5074
|
+
async function buildCollectionSection(collection, slug, entryCount, rssFeedsEnabled) {
|
|
3924
5075
|
return `${await formatFrontMatter({
|
|
3925
5076
|
title: collection.title,
|
|
3926
5077
|
slug,
|
|
@@ -3928,7 +5079,7 @@ async function buildCollectionSection(collection, slug, entryCount) {
|
|
|
3928
5079
|
summary_text: collection.description ?? void 0,
|
|
3929
5080
|
sort_order: collection.sortOrder,
|
|
3930
5081
|
entry_count: entryCount,
|
|
3931
|
-
outputs: ["html", "rss"]
|
|
5082
|
+
outputs: rssFeedsEnabled ? ["html", "rss"] : ["html"]
|
|
3932
5083
|
})}\n`;
|
|
3933
5084
|
}
|
|
3934
5085
|
function normalizeArchiveText(text) {
|
|
@@ -4016,7 +5167,7 @@ function buildExportedCollectionDirectoryItems(items, collectionSlugMap, collect
|
|
|
4016
5167
|
sequence: sequenceLabels[index] ?? "",
|
|
4017
5168
|
label: item.label,
|
|
4018
5169
|
url: item.url,
|
|
4019
|
-
descriptionHtml: description ? render(description) : null
|
|
5170
|
+
descriptionHtml: description ? render(description, { namespace: `collection-directory-link-${sequenceLabels[index] ?? index}` }) : null
|
|
4020
5171
|
});
|
|
4021
5172
|
return;
|
|
4022
5173
|
}
|
|
@@ -4032,54 +5183,40 @@ function buildExportedCollectionDirectoryItems(items, collectionSlugMap, collect
|
|
|
4032
5183
|
sequence: sequenceLabels[index] ?? "",
|
|
4033
5184
|
slug,
|
|
4034
5185
|
title: collection.title || slug,
|
|
4035
|
-
descriptionHtml: collectionDescription ? render(collectionDescription) : null,
|
|
4036
|
-
entryCount: metrics?.
|
|
5186
|
+
descriptionHtml: collectionDescription ? render(collectionDescription, { namespace: collection.id }) : null,
|
|
5187
|
+
entryCount: metrics?.threadCount ?? (typeof collection.threadCount === "number" ? collection.threadCount : void 0),
|
|
4037
5188
|
recentActivityLabel: formatCollectionActivityLabel(activityTimestamp),
|
|
4038
5189
|
recentActivityIso: formatCollectionActivityIso(activityTimestamp)
|
|
4039
5190
|
});
|
|
4040
5191
|
});
|
|
4041
5192
|
return exportedItems;
|
|
4042
5193
|
}
|
|
4043
|
-
function buildExportedCollectionMetrics(collections, posts,
|
|
5194
|
+
function buildExportedCollectionMetrics(collections, posts, collectionsByThread) {
|
|
4044
5195
|
const metrics = /* @__PURE__ */ new Map();
|
|
4045
5196
|
for (const collection of collections) metrics.set(collection.id, {
|
|
4046
|
-
|
|
5197
|
+
threadCount: 0,
|
|
4047
5198
|
recentActivityAt: collection.updatedAt
|
|
4048
5199
|
});
|
|
4049
5200
|
for (const post of posts) {
|
|
4050
5201
|
if (post.status === "draft" || post.visibility === "private") continue;
|
|
4051
5202
|
if (post.replyToId !== null) continue;
|
|
4052
5203
|
const activityAt = post.lastActivityAt ?? post.publishedAt ?? post.updatedAt ?? post.createdAt;
|
|
4053
|
-
const
|
|
4054
|
-
for (const collection of
|
|
5204
|
+
const threadCollections = collectionsByThread.get(post.id) ?? [];
|
|
5205
|
+
for (const collection of threadCollections) {
|
|
4055
5206
|
const current = metrics.get(collection.id);
|
|
4056
5207
|
if (!current) continue;
|
|
4057
|
-
if (current.
|
|
5208
|
+
if (current.threadCount === 0) current.recentActivityAt = activityAt;
|
|
4058
5209
|
else current.recentActivityAt = Math.max(current.recentActivityAt, activityAt);
|
|
4059
|
-
current.
|
|
5210
|
+
current.threadCount += 1;
|
|
4060
5211
|
}
|
|
4061
5212
|
}
|
|
4062
5213
|
return metrics;
|
|
4063
5214
|
}
|
|
4064
|
-
/**
|
|
4065
|
-
* System nav items on the main site store an empty `label` in the DB and
|
|
4066
|
-
* resolve their display text at render time through i18n
|
|
4067
|
-
* (`getNavItemDisplayLabel`). The Hugo export has no i18n runtime, so fall
|
|
4068
|
-
* back to these English defaults when serializing to `data/jant.toml`.
|
|
4069
|
-
* Users can still override by setting a custom label on the nav item.
|
|
4070
|
-
*/ var SYSTEM_NAV_FALLBACK_LABELS = {
|
|
4071
|
-
latest: "Latest",
|
|
4072
|
-
featured: "Featured",
|
|
4073
|
-
collections: "Collections",
|
|
4074
|
-
archive: "Archive",
|
|
4075
|
-
rss: "RSS",
|
|
4076
|
-
settings: "Settings"
|
|
4077
|
-
};
|
|
4078
5215
|
function resolveNavItemLabel(item) {
|
|
4079
5216
|
if (item.label) return item.label;
|
|
4080
5217
|
if (item.systemKey) {
|
|
4081
|
-
const
|
|
4082
|
-
if (
|
|
5218
|
+
const definition = SYSTEM_NAV_KEYS[item.systemKey];
|
|
5219
|
+
if (definition) return definition.defaultLabel;
|
|
4083
5220
|
}
|
|
4084
5221
|
return item.label;
|
|
4085
5222
|
}
|
|
@@ -4127,7 +5264,7 @@ function buildHugoToml(config) {
|
|
|
4127
5264
|
" unsafe = true",
|
|
4128
5265
|
"",
|
|
4129
5266
|
"[outputs]",
|
|
4130
|
-
|
|
5267
|
+
` home = ["html"${config.rssFeedsEnabled ? ", \"rss\"" : ""}]`,
|
|
4131
5268
|
" section = [\"html\"]",
|
|
4132
5269
|
"",
|
|
4133
5270
|
"[outputFormats]",
|
|
@@ -4144,6 +5281,8 @@ function buildHugoToml(config) {
|
|
|
4144
5281
|
"[params]",
|
|
4145
5282
|
` description = "${escapeTomlString(config.siteDescription)}"`,
|
|
4146
5283
|
` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
|
|
5284
|
+
` public_api_enabled = ${config.publicApiEnabled}`,
|
|
5285
|
+
` rss_feeds_enabled = ${config.rssFeedsEnabled}`,
|
|
4147
5286
|
` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
|
|
4148
5287
|
` show_header_avatar = ${config.showHeaderAvatar}`,
|
|
4149
5288
|
` noindex = ${config.noindex}`,
|
|
@@ -4160,7 +5299,7 @@ function buildHugoToml(config) {
|
|
|
4160
5299
|
return `${parts.join("\n")}\n`;
|
|
4161
5300
|
}
|
|
4162
5301
|
function buildJantDataToml(config, iconAssets, directoryItems) {
|
|
4163
|
-
const footerHtml = config.siteFooter ? render(config.siteFooter) : "";
|
|
5302
|
+
const footerHtml = config.siteFooter ? render(config.siteFooter, { namespace: "site-footer" }) : "";
|
|
4164
5303
|
const parts = [
|
|
4165
5304
|
"format = \"jant-site\"",
|
|
4166
5305
|
"version = 1",
|
|
@@ -4169,6 +5308,8 @@ function buildJantDataToml(config, iconAssets, directoryItems) {
|
|
|
4169
5308
|
`site_description = "${escapeTomlString(config.siteDescription)}"`,
|
|
4170
5309
|
`site_language = "${escapeTomlString(config.siteLanguage)}"`,
|
|
4171
5310
|
`main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
|
|
5311
|
+
`public_api_enabled = ${config.publicApiEnabled}`,
|
|
5312
|
+
`rss_feeds_enabled = ${config.rssFeedsEnabled}`,
|
|
4172
5313
|
`show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
|
|
4173
5314
|
`show_header_avatar = ${config.showHeaderAvatar}`,
|
|
4174
5315
|
`noindex = ${config.noindex}`,
|
|
@@ -4191,6 +5332,7 @@ function buildJantDataToml(config, iconAssets, directoryItems) {
|
|
|
4191
5332
|
if (config.siteFooter) parts.push(`site_footer_markdown = "${escapeTomlString(config.siteFooter)}"`);
|
|
4192
5333
|
for (const item of config.navItems) {
|
|
4193
5334
|
if (item.systemKey === "settings") continue;
|
|
5335
|
+
if (!config.rssFeedsEnabled && item.type === "system" && item.systemKey === "rss") continue;
|
|
4194
5336
|
parts.push("");
|
|
4195
5337
|
parts.push("[[nav]]");
|
|
4196
5338
|
parts.push(`type = "${escapeTomlString(item.type)}"`);
|
|
@@ -4331,4 +5473,4 @@ Safe to re-run; files already on disk are reused. Anything that fails to downloa
|
|
|
4331
5473
|
`;
|
|
4332
5474
|
}
|
|
4333
5475
|
//#endregion
|
|
4334
|
-
export { JANT_POSITIVE_LOGO_PNG_FILENAME as A,
|
|
5476
|
+
export { JANT_POSITIVE_LOGO_PNG_FILENAME as $, DEFAULT_NAVIGATION_PROFILE as A, STATUSES as B, now as C, THEME_MODES as D, CONFIG_FIELDS as E, MAX_SITE_DESCRIPTION_LENGTH as F, VISIBILITIES as G, SYSTEM_NAV_KEYS as H, MAX_SITE_FOOTER_LENGTH as I, getPublicUrlForProvider as J, getImageUrl as K, MEDIA_KINDS as L, MAX_COLLECTION_DESCRIPTION_LENGTH as M, MAX_MEDIA_ATTACHMENTS as N, COLLECTION_FRESHNESS_WINDOW_SECONDS as O, MAX_PINNED_POSTS as P, JANT_HOME_URL as Q, NAV_ITEM_TYPES as R, formatYearMonth as S, toISOString as T, SYSTEM_NAV_KEY_VALUES as U, STORAGE_DRIVERS as V, TEXT_ATTACHMENT_CONTENT_FORMATS as W, HOME_BRANDING_PREFIX as X, HOME_BRANDING_LINK_LABEL as Y, JANT_BRAND_PACK_FILENAME as Z, upgradeLegacyFootnotes as _, markdown_exports as a, getJantIconHref as at, formatRelativeTime as b, parseMarkdownDocument as c, getJantLogoHref as ct, extractSummaryHtml as d, JANT_LOGO_VIEW_BOX as dt, getDefaultJantAppleTouchIconBytes as et, renderTiptapDocument as f, arrayBufferToBase64 as ft, escapeHtml as g, trimTiptapBody as h, tiptapJsonToMarkdown as i, getJantIconFilename as it, FORMATS as j, COLLECTION_SORT_ORDERS as k, extractBodyText as l, getJantPositiveLogoPngHref as lt, renderTiptapJson as m, export_exports as n, getJantBrandPackHref as nt, render as o, getJantLogoFilename as ot, renderTiptapDocumentAroundBoundary as p, base64ToUint8Array as pt, getMediaUrl as q, parseFrontMatter as r, getJantBundledAsset as rt, toPlainText as s, getJantLogoFills as st, createExportService as t, getDefaultJantFaviconIcoBytes as tt, extractSummary as u, JANT_LOGO_PATH_DATA as ut, formatDate as v, time_exports as w, formatTime as x, formatRelativeAge as y, SORT_ORDERS as z };
|