@jant/core 0.6.7 → 0.6.9

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.
Files changed (131) hide show
  1. package/bin/commands/uploads/cleanup.js +2 -0
  2. package/dist/{app-L1UPUArB.js → app-C-jxWmAV.js} +12421 -12033
  3. package/dist/app-DqHzOwL5.js +6 -0
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-CGf2m3qp.css +2 -0
  6. package/dist/client/_assets/{client-B0MvB2r0.js → client-DWy1LEEk.js} +2 -2
  7. package/dist/client/_assets/{client-auth-CwwuucF_.js → client-auth-Blg-a5Ep.js} +365 -345
  8. package/dist/{env-CoSe-1y4.js → env-OHRKGcMj.js} +1 -1
  9. package/dist/{export-DLukCOO3.js → export-C2DIB7mm.js} +34 -9
  10. package/dist/{github-api-UD4u_7fa.js → github-api-BgSiE71w.js} +1 -1
  11. package/dist/{github-app-DeX6Td1O.js → github-app-BbklkFmU.js} +1 -1
  12. package/dist/{github-sync-BeDecPen.js → github-sync-7XQ5ZM6z.js} +3 -3
  13. package/dist/{github-sync-BtHY2AST.js → github-sync-BEFCfLKK.js} +3 -3
  14. package/dist/index.js +5 -5
  15. package/dist/node.js +6 -6
  16. package/dist/{url-XF0GbKGO.js → url-BMYO-Zlt.js} +42 -2
  17. package/package.json +1 -1
  18. package/src/__tests__/bin/uploads-cleanup.test.ts +2 -0
  19. package/src/client/__tests__/compose-bridge.test.ts +105 -0
  20. package/src/client/__tests__/hydrate-partial.test.ts +27 -0
  21. package/src/client/__tests__/note-expand.test.ts +130 -0
  22. package/src/client/archive-nav.js +2 -1
  23. package/src/client/audio-player.ts +7 -3
  24. package/src/client/components/__tests__/compose-format-convert.test.ts +357 -0
  25. package/src/client/components/__tests__/jant-compose-dialog.test.ts +313 -0
  26. package/src/client/components/__tests__/jant-compose-editor.test.ts +2 -0
  27. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +2 -0
  28. package/src/client/components/__tests__/jant-settings-avatar.test.ts +5 -2
  29. package/src/client/components/__tests__/jant-settings-general.test.ts +55 -8
  30. package/src/client/components/compose-format-convert.ts +255 -0
  31. package/src/client/components/compose-types.ts +2 -0
  32. package/src/client/components/jant-compose-dialog.ts +110 -44
  33. package/src/client/components/jant-compose-editor.ts +64 -11
  34. package/src/client/components/jant-settings-general.ts +56 -18
  35. package/src/client/components/settings-types.ts +11 -0
  36. package/src/client/compose-bridge.ts +17 -0
  37. package/src/client/feed-video-player.ts +1 -1
  38. package/src/client/hydrate-partial.ts +25 -0
  39. package/src/client/note-expand.ts +63 -0
  40. package/src/client/settings-bridge.ts +3 -0
  41. package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
  42. package/src/client/tiptap/bubble-menu.ts +37 -4
  43. package/src/client.ts +1 -0
  44. package/src/db/migrations/0026_absent_rhodey.sql +14 -0
  45. package/src/db/migrations/meta/0026_snapshot.json +2511 -0
  46. package/src/db/migrations/meta/_journal.json +7 -0
  47. package/src/db/migrations/pg/0024_high_violations.sql +14 -0
  48. package/src/db/migrations/pg/meta/0024_snapshot.json +3204 -0
  49. package/src/db/migrations/pg/meta/_journal.json +7 -0
  50. package/src/db/pg/schema.ts +36 -0
  51. package/src/db/schema.ts +36 -0
  52. package/src/i18n/__tests__/middleware.test.ts +46 -0
  53. package/src/i18n/locales/public/en.po +41 -0
  54. package/src/i18n/locales/public/en.ts +1 -1
  55. package/src/i18n/locales/public/zh-Hans.po +41 -0
  56. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  57. package/src/i18n/locales/public/zh-Hant.po +41 -0
  58. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  59. package/src/i18n/locales/settings/en.po +37 -22
  60. package/src/i18n/locales/settings/en.ts +1 -1
  61. package/src/i18n/locales/settings/zh-Hans.po +37 -22
  62. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  63. package/src/i18n/locales/settings/zh-Hant.po +37 -22
  64. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  65. package/src/i18n/middleware.ts +17 -8
  66. package/src/i18n/supported-locales.ts +5 -4
  67. package/src/lib/__tests__/markdown-to-tiptap.test.ts +1 -1
  68. package/src/lib/__tests__/markdown.test.ts +1 -1
  69. package/src/lib/__tests__/summary.test.ts +87 -0
  70. package/src/lib/__tests__/timeline.test.ts +48 -1
  71. package/src/lib/__tests__/tiptap-render.test.ts +4 -4
  72. package/src/lib/__tests__/url.test.ts +44 -0
  73. package/src/lib/__tests__/view.test.ts +168 -1
  74. package/src/lib/ids.ts +1 -0
  75. package/src/lib/navigation.ts +1 -0
  76. package/src/lib/resolve-config.ts +3 -2
  77. package/src/lib/summary.ts +42 -3
  78. package/src/lib/tiptap-render.ts +6 -2
  79. package/src/lib/upload.ts +16 -2
  80. package/src/lib/url.ts +41 -0
  81. package/src/lib/view.ts +102 -40
  82. package/src/preset.css +7 -1
  83. package/src/routes/api/__tests__/settings.test.ts +1 -4
  84. package/src/routes/api/__tests__/upload.test.ts +2 -0
  85. package/src/routes/api/internal/__tests__/uploads.test.ts +86 -0
  86. package/src/routes/api/internal/sites.ts +44 -1
  87. package/src/routes/api/public/__tests__/archive.test.ts +66 -0
  88. package/src/routes/api/public/archive.ts +22 -6
  89. package/src/routes/api/settings.ts +2 -1
  90. package/src/routes/api/telegram.ts +2 -1
  91. package/src/routes/auth/__tests__/setup.test.ts +14 -0
  92. package/src/routes/dash/__tests__/settings-avatar.test.ts +35 -17
  93. package/src/routes/dash/custom-urls.tsx +1 -1
  94. package/src/routes/dash/settings.tsx +23 -7
  95. package/src/routes/pages/__tests__/archive-params.test.ts +135 -0
  96. package/src/routes/pages/archive.tsx +116 -20
  97. package/src/routes/pages/collections.tsx +1 -0
  98. package/src/services/__tests__/media.test.ts +274 -30
  99. package/src/services/__tests__/post.test.ts +81 -0
  100. package/src/services/__tests__/settings.test.ts +55 -0
  101. package/src/services/bootstrap.ts +7 -0
  102. package/src/services/export-theme/assets/client-site.js +1 -1
  103. package/src/services/export-theme/layouts/_default/baseof.html +2 -1
  104. package/src/services/export-theme/styles/main.css +49 -15
  105. package/src/services/media.ts +199 -42
  106. package/src/services/post.ts +22 -2
  107. package/src/services/search.ts +4 -4
  108. package/src/services/settings.ts +49 -15
  109. package/src/services/upload-session.ts +28 -0
  110. package/src/styles/tokens.css +7 -5
  111. package/src/styles/ui.css +163 -34
  112. package/src/types/bindings.ts +1 -0
  113. package/src/types/config.ts +14 -1
  114. package/src/types/props.ts +3 -0
  115. package/src/ui/compose/ComposeDialog.tsx +13 -0
  116. package/src/ui/dash/settings/AccountMenuContent.tsx +0 -39
  117. package/src/ui/dash/settings/GeneralContent.tsx +38 -4
  118. package/src/ui/dash/settings/SettingsDirectory.tsx +26 -1
  119. package/src/ui/dash/settings/SettingsRootContent.tsx +46 -1
  120. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +55 -0
  121. package/src/ui/feed/NoteCard.tsx +54 -5
  122. package/src/ui/feed/__tests__/timeline-cards.test.ts +73 -0
  123. package/src/ui/layouts/BaseLayout.tsx +1 -0
  124. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +13 -0
  125. package/src/ui/pages/ArchivePage.tsx +89 -6
  126. package/src/ui/pages/CollectionsPage.tsx +7 -1
  127. package/src/ui/pages/__tests__/ArchivePage.test.tsx +37 -0
  128. package/src/ui/shared/CollectionDirectory.tsx +13 -3
  129. package/src/ui/shared/CollectionsManager.tsx +3 -0
  130. package/dist/app-C1QgMNRY.js +0 -6
  131. package/dist/client/_assets/client-BMPMuwvV.css +0 -2
@@ -1,4 +1,4 @@
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(){let e=document.querySelectorAll(`[data-audio-peaks]`),t=[];for(let n of e){let e=n.dataset.audioPeaks;if(!e)continue;let r=n.closest(`.media-audio-card`);if(!(!r||d.has(r)))try{let n=JSON.parse(e);if(!Array.isArray(n))continue;d.set(r,n),r.classList.add(`has-waveform`),t.push(r)}catch{}}t.length>0&&requestAnimationFrame(()=>{for(let e of t)h(e,0)})}async function ee(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;h(e,r&&isFinite(a)&&a>0?r.currentTime/a:0)}catch{}}function h(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),ee=s*.85,h=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]*ee)),o=Math.round((s-a)/2);c.globalAlpha=(e+.5)/u<=t?.9:.2,c.fillStyle=h;let l=Math.min(p/2,i);c.beginPath(),c.roundRect(r,o,p,a,l),c.fill()}c.globalAlpha=1}function te(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)&&h(e,s);let n=e.querySelector(`[data-audio-time]`);n&&(n.textContent=a?`${o(r)} / ${o(i)}`:o(r))}}function ne(){if(!e)return;let n=s(e);!n||n.paused||(te(e,n),t=requestAnimationFrame(ne))}function re(){if(e){let t=s(e);t&&!t.paused&&t.pause(),e.classList.remove(`is-playing`),e=null}cancelAnimationFrame(t)}async function ie(n){let r=s(n);if(r)if(e&&e!==n&&re(),r.paused){e=n,n.classList.add(`is-playing`);try{await r.play()}catch{n.classList.remove(`is-playing`),e=null;return}t=requestAnimationFrame(ne),ee(n)}else r.pause(),n.classList.remove(`is-playing`),cancelAnimationFrame(t),e=null}function ae(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 oe(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;h(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 se(n){let r=n.closest(`.media-audio-card`);if(!r)return;let i=s(r);if(i)if(i.paused){e&&e!==r&&re(),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(ne),ee(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,oe(t,e))},!0),document.addEventListener(`pointermove`,e=>{n&&i&&oe(i,e)},!0),document.addEventListener(`pointerup`,()=>{n&&(r?(ae(r),r=null):i&&=(se(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&&ie(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)&&h(i,0);let o=i.querySelector(`[data-audio-time]`);o&&(o.textContent=`0:00`)},!0),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,m):m();var g=`jant:media-lightbox-toggle`,ce=`75% 0px`,le=.6,_=.25,ue=160,v=new Set,de=new WeakMap,fe=new WeakSet,y=null,b=null,x=null,pe=!1,S=null,C=null,w=null;function me(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 he(){return{x:(globalThis.innerWidth||document.documentElement.clientWidth||0)/2,y:(globalThis.innerHeight||document.documentElement.clientHeight||0)/2}}function ge(e){return de.get(e)}function _e(e){return e.closest(`.media-video-wrap`)?.querySelector(`[data-feed-video-mute-toggle]`)??null}function T(e){let t=_e(e);if(!t)return;let n=x!==e||e.muted;t.dataset.muted=n?`true`:`false`,t.setAttribute(`aria-label`,n?`Play with sound`:`Mute video`)}function ve(){for(let e of v)T(e)}function ye(e){if(fe.has(e))return;let t=e.dataset.videoSrc;t&&(e.getAttribute(`src`)!==t&&(e.src=t),e.load(),fe.add(e))}function E(e){e?.pause()}function be(e){ye(e),e.muted=x!==e,e.playsInline=!0,e.loop=!0,T(e),e.play().catch(()=>{})}function xe(){for(let e of v)e.isConnected||(C?.unobserve(e),w?.unobserve(e),v.delete(e),e===y&&(y=null),e===b&&(b=null),e===x&&(x=null))}function Se(){if(S=null,xe(),document.hidden||pe){E(y);return}let e=[];for(let t of v){let n=ge(t);n&&(n.intersectionRatio<le||e.push({video:t,...n}))}let t=null;if(b?.isConnected){let e=ge(b);e&&e.intersectionRatio>_?t=b:(!e||e.intersectionRatio<=_)&&(b=null)}if(t||=me(e)?.video??null,!t){let e=y?ge(y):void 0;if(y&&e&&e.intersectionRatio>_)return;E(y),y=null;return}t!==y&&(E(y),y=t),be(t);for(let e of v)e!==t&&(E(e),T(e))}function D(){S!==null&&globalThis.clearTimeout(S),S=globalThis.setTimeout(Se,ue)}function Ce(e){let t=he();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);de.set(e,{intersectionRatio:n.intersectionRatio,visibleArea:o,centerDistance:s})}D()}function we(e){for(let t of e)t.isIntersecting&&ye(t.target)}function Te(){C&&w||globalThis.IntersectionObserver!==void 0&&(C=new globalThis.IntersectionObserver(Ce,{threshold:[0,_,le,1]}),w=new globalThis.IntersectionObserver(we,{rootMargin:ce,threshold:0}))}function Ee(e){v.has(e)||(Te(),!(!C||!w)&&(v.add(e),C.observe(e),w.observe(e),_e(e)?.addEventListener(`click`,De),T(e)))}function De(e){e.preventDefault(),e.stopPropagation();let t=e.currentTarget.closest(`.media-video-wrap`)?.querySelector(`[data-feed-short-video]`);t&&(b=t,x!==t||t.muted?(x&&x!==t&&(x.muted=!0),x=t,y!==t&&(E(y),y=t),be(t)):(x=null,t.muted=!0,T(t)),ve(),D())}function Oe(e=document){let t=e.querySelectorAll(`[data-feed-short-video]`);for(let e of t)Ee(e);D()}document.addEventListener(g,e=>{pe=e.detail?.open===!0,pe&&E(y),D()}),document.addEventListener(`visibilitychange`,()=>{document.hidden&&E(y),D()}),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>Oe(),{once:!0}):queueMicrotask(()=>Oe());var ke=4;function Ae(e){return e.querySelector(`[data-post-media]`)}function je(e){let t=Ae(e);if(!t)return;let{scrollLeft:n,scrollWidth:r,clientWidth:i}=t;e.classList.toggle(`can-scroll-start`,n>ke),e.classList.toggle(`can-scroll-end`,n+i<r-ke)}function Me(e){return Math.max(160,Math.round(e.clientWidth*.85))}function O(e,t){e.scrollBy({left:t*Me(e),behavior:`smooth`})}function k(e){if(e.dataset.scrollHintReady===`1`)return;let t=Ae(e);t&&(e.dataset.scrollHintReady=`1`,je(e),t.addEventListener(`scroll`,()=>je(e),{passive:!0}),e.querySelector(`.media-gallery-nav-prev`)?.addEventListener(`click`,()=>O(t,-1)),e.querySelector(`.media-gallery-nav-next`)?.addEventListener(`click`,()=>O(t,1)),t.addEventListener(`keydown`,e=>{if(e.target===t)switch(e.key){case`ArrowRight`:e.preventDefault(),O(t,1);break;case`ArrowLeft`:e.preventDefault(),O(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 Ne(){document.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(k)}var Pe=new globalThis.MutationObserver(e=>{for(let t of e)for(let e of t.addedNodes)e instanceof HTMLElement&&(e.matches(`.media-gallery-scroll-wrap`)&&k(e),e.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(k))});document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>{Ne(),Pe.observe(document.body,{childList:!0,subtree:!0})}):(Ne(),Pe.observe(document.body,{childList:!0,subtree:!0}));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`;function r(){n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:t.parentElement}}))}function i(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`?i():r()}),document.addEventListener(`click`,e=>{e.target instanceof Node&&(t.parentElement?.contains(e.target)||i())}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.getAttribute(`aria-hidden`)===`false`&&i(!0)}),document.addEventListener(`basecoat:popover`,e=>{e.detail?.source!==t.parentElement&&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`;function a(){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 o(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}),e&&t.focus()}t.addEventListener(`click`,()=>{t.getAttribute(`aria-expanded`)===`true`?o():a()}),i?.addEventListener(`click`,()=>o()),r.addEventListener(`click`,()=>o()),n.addEventListener(`click`,e=>{e.target instanceof Element&&e.target.closest(`a[href]`)&&o(!1)}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),o())})}ze();var A=globalThis,j=A.ShadowRoot&&(A.ShadyCSS===void 0||A.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(j&&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(j)e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let n of t){let t=document.createElement(`style`),r=A.litNonce;r!==void 0&&t.setAttribute(`nonce`,r),t.textContent=n.cssText,e.appendChild(t)}},Ge=j?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,M=globalThis,Qe=M.trustedTypes,$e=Qe?Qe.emptyScript:``,et=M.reactiveElementPolyfillSupport,N=(e,t)=>e,P={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:P,reflect:!1,useDefault:!1,hasChanged:tt};Symbol.metadata??=Symbol(`metadata`),M.litPropertyMetadata??=new WeakMap;var F=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(N(`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(N(`finalized`)))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(N(`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?P: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?P: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){}};F.elementStyles=[],F.shadowRootOptions={mode:`open`},F[N(`elementProperties`)]=new Map,F[N(`finalized`)]=new Map,et?.({ReactiveElement:F}),(M.reactiveElementVersions??=[]).push(`2.1.2`);var I=globalThis,rt=e=>e,L=I.trustedTypes,it=L?L.createPolicy(`lit-html`,{createHTML:e=>e}):void 0,at=`$lit$`,R=`lit$${Math.random().toFixed(9).slice(2)}$`,ot=`?`+R,st=`<${ot}>`,z=document,B=()=>z.createComment(``),V=e=>e===null||typeof e!=`object`&&typeof e!=`function`,ct=Array.isArray,lt=e=>ct(e)||typeof e?.[Symbol.iterator]==`function`,ut=`[
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)h(e,0)})}async function ee(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;h(e,r&&isFinite(a)&&a>0?r.currentTime/a:0)}catch{}}function h(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),ee=s*.85,h=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]*ee)),o=Math.round((s-a)/2);c.globalAlpha=(e+.5)/u<=t?.9:.2,c.fillStyle=h;let l=Math.min(p/2,i);c.beginPath(),c.roundRect(r,o,p,a,l),c.fill()}c.globalAlpha=1}function te(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)&&h(e,s);let n=e.querySelector(`[data-audio-time]`);n&&(n.textContent=a?`${o(r)} / ${o(i)}`:o(r))}}function ne(){if(!e)return;let n=s(e);!n||n.paused||(te(e,n),t=requestAnimationFrame(ne))}function re(){if(e){let t=s(e);t&&!t.paused&&t.pause(),e.classList.remove(`is-playing`),e=null}cancelAnimationFrame(t)}async function ie(n){let r=s(n);if(r)if(e&&e!==n&&re(),r.paused){e=n,n.classList.add(`is-playing`);try{await r.play()}catch{n.classList.remove(`is-playing`),e=null;return}t=requestAnimationFrame(ne),ee(n)}else r.pause(),n.classList.remove(`is-playing`),cancelAnimationFrame(t),e=null}function ae(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 oe(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;h(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 se(n){let r=n.closest(`.media-audio-card`);if(!r)return;let i=s(r);if(i)if(i.paused){e&&e!==r&&re(),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(ne),ee(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,oe(t,e))},!0),document.addEventListener(`pointermove`,e=>{n&&i&&oe(i,e)},!0),document.addEventListener(`pointerup`,()=>{n&&(r?(ae(r),r=null):i&&=(se(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&&ie(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)&&h(i,0);let o=i.querySelector(`[data-audio-time]`);o&&(o.textContent=`0:00`)},!0),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>m()):m();var g=`jant:media-lightbox-toggle`,ce=`75% 0px`,le=.6,_=.25,ue=160,v=new Set,de=new WeakMap,fe=new WeakSet,y=null,b=null,x=null,pe=!1,S=null,C=null,w=null;function me(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 he(){return{x:(globalThis.innerWidth||document.documentElement.clientWidth||0)/2,y:(globalThis.innerHeight||document.documentElement.clientHeight||0)/2}}function ge(e){return de.get(e)}function _e(e){return e.closest(`.media-video-wrap`)?.querySelector(`[data-feed-video-mute-toggle]`)??null}function T(e){let t=_e(e);if(!t)return;let n=x!==e||e.muted;t.dataset.muted=n?`true`:`false`,t.setAttribute(`aria-label`,n?`Play with sound`:`Mute video`)}function ve(){for(let e of v)T(e)}function ye(e){if(fe.has(e))return;let t=e.dataset.videoSrc;t&&(e.getAttribute(`src`)!==t&&(e.src=t),e.load(),fe.add(e))}function E(e){e?.pause()}function be(e){ye(e),e.muted=x!==e,e.playsInline=!0,e.loop=!0,T(e),e.play().catch(()=>{})}function xe(){for(let e of v)e.isConnected||(C?.unobserve(e),w?.unobserve(e),v.delete(e),e===y&&(y=null),e===b&&(b=null),e===x&&(x=null))}function Se(){if(S=null,xe(),document.hidden||pe){E(y);return}let e=[];for(let t of v){let n=ge(t);n&&(n.intersectionRatio<le||e.push({video:t,...n}))}let t=null;if(b?.isConnected){let e=ge(b);e&&e.intersectionRatio>_?t=b:(!e||e.intersectionRatio<=_)&&(b=null)}if(t||=me(e)?.video??null,!t){let e=y?ge(y):void 0;if(y&&e&&e.intersectionRatio>_)return;E(y),y=null;return}t!==y&&(E(y),y=t),be(t);for(let e of v)e!==t&&(E(e),T(e))}function D(){S!==null&&globalThis.clearTimeout(S),S=globalThis.setTimeout(Se,ue)}function Ce(e){let t=he();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);de.set(e,{intersectionRatio:n.intersectionRatio,visibleArea:o,centerDistance:s})}D()}function we(e){for(let t of e)t.isIntersecting&&ye(t.target)}function Te(){C&&w||globalThis.IntersectionObserver!==void 0&&(C=new globalThis.IntersectionObserver(Ce,{threshold:[0,_,le,1]}),w=new globalThis.IntersectionObserver(we,{rootMargin:ce,threshold:0}))}function Ee(e){v.has(e)||(Te(),!(!C||!w)&&(v.add(e),C.observe(e),w.observe(e),_e(e)?.addEventListener(`click`,De),T(e)))}function De(e){e.preventDefault(),e.stopPropagation();let t=e.currentTarget.closest(`.media-video-wrap`)?.querySelector(`[data-feed-short-video]`);t&&(b=t,x!==t||t.muted?(x&&x!==t&&(x.muted=!0),x=t,y!==t&&(E(y),y=t),be(t)):(x=null,t.muted=!0,T(t)),ve(),D())}function Oe(e=document){let t=e.querySelectorAll(`[data-feed-short-video]`);for(let e of t)Ee(e);D()}document.addEventListener(g,e=>{pe=e.detail?.open===!0,pe&&E(y),D()}),document.addEventListener(`visibilitychange`,()=>{document.hidden&&E(y),D()}),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>Oe(),{once:!0}):queueMicrotask(()=>Oe());var ke=4;function Ae(e){return e.querySelector(`[data-post-media]`)}function je(e){let t=Ae(e);if(!t)return;let{scrollLeft:n,scrollWidth:r,clientWidth:i}=t;e.classList.toggle(`can-scroll-start`,n>ke),e.classList.toggle(`can-scroll-end`,n+i<r-ke)}function Me(e){return Math.max(160,Math.round(e.clientWidth*.85))}function O(e,t){e.scrollBy({left:t*Me(e),behavior:`smooth`})}function k(e){if(e.dataset.scrollHintReady===`1`)return;let t=Ae(e);t&&(e.dataset.scrollHintReady=`1`,je(e),t.addEventListener(`scroll`,()=>je(e),{passive:!0}),e.querySelector(`.media-gallery-nav-prev`)?.addEventListener(`click`,()=>O(t,-1)),e.querySelector(`.media-gallery-nav-next`)?.addEventListener(`click`,()=>O(t,1)),t.addEventListener(`keydown`,e=>{if(e.target===t)switch(e.key){case`ArrowRight`:e.preventDefault(),O(t,1);break;case`ArrowLeft`:e.preventDefault(),O(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 Ne(){document.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(k)}var Pe=new globalThis.MutationObserver(e=>{for(let t of e)for(let e of t.addedNodes)e instanceof HTMLElement&&(e.matches(`.media-gallery-scroll-wrap`)&&k(e),e.querySelectorAll(`.media-gallery-scroll-wrap`).forEach(k))});document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>{Ne(),Pe.observe(document.body,{childList:!0,subtree:!0})}):(Ne(),Pe.observe(document.body,{childList:!0,subtree:!0}));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`;function r(){n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:t.parentElement}}))}function i(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`?i():r()}),document.addEventListener(`click`,e=>{e.target instanceof Node&&(t.parentElement?.contains(e.target)||i())}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.getAttribute(`aria-hidden`)===`false`&&i(!0)}),document.addEventListener(`basecoat:popover`,e=>{e.detail?.source!==t.parentElement&&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`;function a(){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 o(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}),e&&t.focus()}t.addEventListener(`click`,()=>{t.getAttribute(`aria-expanded`)===`true`?o():a()}),i?.addEventListener(`click`,()=>o()),r.addEventListener(`click`,()=>o()),n.addEventListener(`click`,e=>{e.target instanceof Element&&e.target.closest(`a[href]`)&&o(!1)}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),o())})}ze();var A=globalThis,j=A.ShadowRoot&&(A.ShadyCSS===void 0||A.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(j&&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(j)e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let n of t){let t=document.createElement(`style`),r=A.litNonce;r!==void 0&&t.setAttribute(`nonce`,r),t.textContent=n.cssText,e.appendChild(t)}},Ge=j?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,M=globalThis,Qe=M.trustedTypes,$e=Qe?Qe.emptyScript:``,et=M.reactiveElementPolyfillSupport,N=(e,t)=>e,P={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:P,reflect:!1,useDefault:!1,hasChanged:tt};Symbol.metadata??=Symbol(`metadata`),M.litPropertyMetadata??=new WeakMap;var F=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(N(`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(N(`finalized`)))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(N(`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?P: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?P: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){}};F.elementStyles=[],F.shadowRootOptions={mode:`open`},F[N(`elementProperties`)]=new Map,F[N(`finalized`)]=new Map,et?.({ReactiveElement:F}),(M.reactiveElementVersions??=[]).push(`2.1.2`);var I=globalThis,rt=e=>e,L=I.trustedTypes,it=L?L.createPolicy(`lit-html`,{createHTML:e=>e}):void 0,at=`$lit$`,R=`lit$${Math.random().toFixed(9).slice(2)}$`,ot=`?`+R,st=`<${ot}>`,z=document,B=()=>z.createComment(``),V=e=>e===null||typeof e!=`object`&&typeof e!=`function`,ct=Array.isArray,lt=e=>ct(e)||typeof e?.[Symbol.iterator]==`function`,ut=`[
2
2
  \f\r]`,H=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,dt=/-->/g,ft=/>/g,U=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}),W=gt(1),_t=gt(2),G=Symbol.for(`lit-noChange`),K=Symbol.for(`lit-nothing`),vt=new WeakMap,q=z.createTreeWalker(z,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=H;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===H?c[1]===`!--`?o=dt:c[1]===void 0?c[2]===void 0?c[3]!==void 0&&(o=U):(ht.test(c[2])&&(i=RegExp(`</`+c[2],`g`)),o=U):o=ft:o===U?c[0]===`>`?(o=i??H,l=-1):c[1]===void 0?l=-2:(l=o.lastIndex-c[2].length,s=c[1],o=c[3]===void 0?U:c[3]===`"`?mt:pt):o===mt||o===pt?o=U:o===dt||o===ft?o=H:(o=U,i=void 0);let d=o===U&&e[t+1].startsWith(`/>`)?` `:``;a+=o===H?n+st:l>=0?(r.push(s),n.slice(0,l)+at+n.slice(l)+R+d):n+R+(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),q.currentNode=this.el.content,n===2||n===3){let e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;(i=q.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(R),r=/([.?@])?(.*)/.exec(t);c.push({type:1,index:a,name:r[2],strings:n,ctor:r[1]===`.`?Ct:r[1]===`?`?wt:r[1]===`@`?Tt:X}),i.removeAttribute(e)}else e.startsWith(R)&&(c.push({type:6,index:a}),i.removeAttribute(e));if(ht.test(i.tagName)){let e=i.textContent.split(R),t=e.length-1;if(t>0){i.textContent=L?L.emptyScript:``;for(let n=0;n<t;n++)i.append(e[n],B()),q.nextNode(),c.push({type:2,index:++a});i.append(e[t],B())}}}else if(i.nodeType===8)if(i.data===ot)c.push({type:2,index:a});else{let e=-1;for(;(e=i.data.indexOf(R,e+1))!==-1;)c.push({type:7,index:a}),e+=R.length-1}a++}}static createElement(e,t){let n=z.createElement(`template`);return n.innerHTML=e,n}};function J(e,t,n=e,r){if(t===G)return t;let i=r===void 0?n._$Cl:n._$Co?.[r],a=V(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=J(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??z).importNode(t,!0);q.currentNode=r;let i=q.nextNode(),a=0,o=0,s=n[0];for(;s!==void 0;){if(a===s.index){let t;s.type===2?t=new Y(i,i.nextSibling,this,e):s.type===1?t=new s.ctor(i,s.name,s.strings,this,e):s.type===6&&(t=new Et(i,this,e)),this._$AV.push(t),s=n[++o]}a!==s?.index&&(i=q.nextNode(),a++)}return q.currentNode=z,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++}},Y=class e{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,n,r){this.type=2,this._$AH=K,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=J(this,e,t),V(e)?e===K||e==null||e===``?(this._$AH!==K&&this._$AR(),this._$AH=K):e!==this._$AH&&e!==G&&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!==K&&V(this._$AH)?this._$AA.nextSibling.data=e:this.T(z.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(B()),this.O(B()),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))}},X=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,n,r,i){this.type=1,this._$AH=K,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=K}_$AI(e,t=this,n,r){let i=this.strings,a=!1;if(i===void 0)e=J(this,e,t,0),a=!V(e)||e!==this._$AH&&e!==G,a&&(this._$AH=e);else{let r=e,o,s;for(e=i[0],o=0;o<i.length-1;o++)s=J(this,r[n+o],t,o),s===G&&(s=this._$AH[o]),a||=!V(s)||s!==this._$AH[o],s===K?e=K:e!==K&&(e+=(s??``)+i[o+1]),this._$AH[o]=s}a&&!r&&this.j(e)}j(e){e===K?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??``)}},Ct=class extends X{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===K?void 0:e}},wt=class extends X{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==K)}},Tt=class extends X{constructor(e,t,n,r,i){super(e,t,n,r,i),this.type=5}_$AI(e,t=this){if((e=J(this,e,t,0)??K)===G)return;let n=this._$AH,r=e===K&&n!==K||e.capture!==n.capture||e.once!==n.once||e.passive!==n.passive,i=e!==K&&(n===K||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)}},Et=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){J(this,e)}},Dt=I.litHtmlPolyfillSupport;Dt?.(xt,Y),(I.litHtmlVersions??=[]).push(`3.3.2`);var Ot=(e,t,n)=>{let r=n?.renderBefore??t,i=r._$litPart$;if(i===void 0){let e=n?.renderBefore??null;r._$litPart$=i=new Y(t.insertBefore(B(),e),e,void 0,n??{})}return i._$AI(e),i},Z=globalThis,Q=class extends F{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=Ot(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return G}};Q._$litElement$=!0,Q.finalized=!0,Z.litElementHydrateSupport?.({LitElement:Q});var kt=Z.litElementPolyfillSupport;kt?.({LitElement:Q}),(Z.litElementVersions??=[]).push(`4.2.2`);function At(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 jt=640,Mt=8,Nt=72,Pt=16,Ft=704,It=.9,Lt=.85;function $(e){if(!(!Number.isFinite(e)||!e||e<=0))return e}function Rt(){return{width:globalThis.innerWidth||document.documentElement.clientWidth||0,height:globalThis.innerHeight||document.documentElement.clientHeight||0}}function zt(e,t){let n=e<=jt?Mt:Nt;return{width:Math.max(0,e-n*2),height:Math.max(0,t-Pt*2)}}function Bt(e,t,n){let r=$(e?.width),i=$(e?.height);if(!r||!i)return null;let a=zt(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 Vt(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<=jt,o=zt(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<It&&u<(a?s:Math.min(s,Ft))*Lt}var Ht=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=Rt();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(g,{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(g,{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];Vt(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(g,{detail:{open:!1}})),this._open=!1};#l=()=>{this.#u()};#u(){let e=Rt();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(!At(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 K;let e=this._images[this._currentIndex],t=this._images.length>1,n=e?.mimeType?.startsWith(`video/`),r=At(e),i=Vt(e,this._viewportWidth,this._viewportHeight),a=i&&this._imageZoomed,o=r?Bt(e,this._viewportWidth,this._viewportHeight):null,s=r&&!!o&&o.height>o.width,c=o?`width:${o.width}px;height:${o.height}px;`:K,l=this._videoDuration>0?this._videoDuration:e?.durationSeconds??1,u=Math.min(this._videoCurrentTime,l),d=l>0?u/l*100:0;return W`
3
3
  <dialog
4
4
  class=${`media-lightbox${r?` media-lightbox-short`:``}`}
@@ -1,7 +1,8 @@
1
1
  <!doctype html>
2
2
  {{- $lang := .Site.LanguageCode | default "en" -}}
3
3
  {{- $themeMode := .Site.Params.theme_mode | default "auto" -}}
4
- <html lang="{{ $lang }}"{{ if ne $themeMode "auto" }} data-theme-mode="{{ $themeMode }}"{{ end }}>
4
+ {{- $themeId := .Site.Params.theme_id -}}
5
+ <html lang="{{ $lang }}"{{ with $themeId }} data-theme="{{ . }}"{{ end }}{{ if ne $themeMode "auto" }} data-theme-mode="{{ $themeMode }}"{{ end }}>
5
6
  <head>
6
7
  {{ partial "head.html" . }}
7
8
  </head>
@@ -1351,8 +1351,10 @@ hr.feed-divider {
1351
1351
  z-index: 1;
1352
1352
  }
1353
1353
 
1354
- /* Gap "N more posts" row has no card so we place the dot at its
1355
- vertical midpoint instead of the card's top area. */
1354
+ /* Gap "N more posts" row has no card so we place the marker at its
1355
+ vertical midpoint instead of the card's top area. Swap the single dot
1356
+ for a short column of small beads (a vertical "⋮") so the rail visibly
1357
+ "skips" a run of posts — mirrors the main site's `.thread-item-gap`. */
1356
1358
  .thread-preview .thread-item-gap {
1357
1359
  display: flex;
1358
1360
  align-items: center;
@@ -1362,35 +1364,67 @@ hr.feed-divider {
1362
1364
  .thread-preview .thread-item-gap::before {
1363
1365
  top: 50%;
1364
1366
  transform: translateY(-50%);
1367
+ left: calc(
1368
+ var(--thread-rail-left) + var(--thread-rail-width) / 2 - 2px -
1369
+ var(--thread-rail-indent)
1370
+ );
1371
+ width: 4px;
1372
+ height: 24px;
1373
+ border: 0;
1374
+ border-radius: 0;
1375
+ background: radial-gradient(
1376
+ circle,
1377
+ var(--site-threadline) 1.75px,
1378
+ transparent 2px
1379
+ );
1380
+ background-size: 4px 8px;
1381
+ background-repeat: repeat-y;
1382
+ background-position: center top;
1383
+ box-shadow: none;
1365
1384
  }
1366
1385
 
1367
- /* Hidden-posts count — rendered as a rounded dashed pill matching
1368
- the main site's `.thread-gap-link`. */
1386
+ /* Hidden-posts count — a calm route into the thread, matching the main
1387
+ site's `.thread-gap-link`: styled like a sibling of the Show more toggle
1388
+ with a small right-pointing chevron rather than an underline, firming up
1389
+ to the primary text colour on hover. */
1369
1390
  .thread-preview-gap {
1370
1391
  display: inline-flex;
1371
1392
  align-items: center;
1372
- gap: 0.35rem;
1393
+ gap: 0.4rem;
1373
1394
  align-self: flex-start;
1374
1395
  margin: 0.15rem 0;
1375
- padding: 0.3rem 0.72rem;
1376
- border: 1px dashed var(--site-thread-context-border);
1377
- border-radius: 999px;
1378
- background: var(--site-thread-gap-bg);
1396
+ padding: 0.25rem 0;
1379
1397
  color: var(--site-text-secondary);
1380
1398
  font-size: var(--type-thread-context-meta);
1381
- line-height: 1.3;
1399
+ font-weight: 500;
1400
+ line-height: 1.2;
1382
1401
  text-decoration: none;
1402
+ transition: color 0.18s ease;
1403
+ }
1404
+
1405
+ .thread-preview-gap::after {
1406
+ content: "";
1407
+ width: 0.36em;
1408
+ height: 0.36em;
1409
+ border: 1.5px solid currentColor;
1410
+ border-left: 0;
1411
+ border-bottom: 0;
1412
+ opacity: 0.65;
1413
+ transform: rotate(45deg);
1383
1414
  transition:
1384
- border-color 0.18s ease,
1385
- color 0.18s ease,
1386
- background-color 0.18s ease;
1415
+ transform 0.18s ease,
1416
+ opacity 0.18s ease;
1387
1417
  }
1388
1418
 
1389
1419
  .thread-preview-gap:hover,
1390
1420
  .thread-preview-gap:focus {
1391
- border-color: color-mix(in srgb, var(--site-accent) 22%, var(--site-divider));
1392
1421
  color: var(--site-text-primary);
1393
- text-decoration: none;
1422
+ }
1423
+
1424
+ .thread-preview-gap:hover::after,
1425
+ .thread-preview-gap:focus::after {
1426
+ opacity: 1;
1427
+ transform: translateX(2px) rotate(45deg);
1394
1428
  }
1395
1429
 
1396
1430
  /* Hero (latest reply) row: spacing above and below matches the main
@@ -4,7 +4,18 @@
4
4
  * Handles media upload and management with pluggable storage backends.
5
5
  */
6
6
 
7
- import { eq, desc, inArray, asc, sql, and, or } from "drizzle-orm";
7
+ import {
8
+ eq,
9
+ desc,
10
+ inArray,
11
+ asc,
12
+ sql,
13
+ and,
14
+ or,
15
+ isNull,
16
+ lt,
17
+ lte,
18
+ } from "drizzle-orm";
8
19
  import { generateKeyBetween } from "fractional-indexing";
9
20
  import { type Database, supportsDrizzleTransaction } from "../db/index.js";
10
21
  import type { DatabaseDialect } from "../db/dialect.js";
@@ -16,11 +27,12 @@ import { createEntityId } from "../lib/ids.js";
16
27
  import { markdownToTiptapJson } from "../lib/markdown-to-tiptap.js";
17
28
  import { extractBodyText } from "../lib/summary.js";
18
29
  import { now } from "../lib/time.js";
19
- import type { StorageDriver } from "../lib/storage.js";
30
+ import { supportsCopy, type StorageDriver } from "../lib/storage.js";
20
31
  import { renderTiptapJson } from "../lib/tiptap-render.js";
21
32
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
22
33
  import {
23
34
  generateStorageKey,
35
+ SITE_ASSET_STORAGE_KEY_LIKE_PATTERN,
24
36
  toMediaKind,
25
37
  validateUploadFileMetadata,
26
38
  } from "../lib/upload.js";
@@ -45,6 +57,15 @@ import type { HostedControlPlaneClient } from "../lib/hosted-control-plane.js";
45
57
 
46
58
  const DEFAULT_MEDIA_POSITION = "a0";
47
59
 
60
+ /**
61
+ * Recycle-window length for deleted media storage objects. On delete we
62
+ * hard-remove the DB row but defer deleting the underlying storage object
63
+ * until this long afterwards (recorded in `storage_purge`), so an accidental
64
+ * delete is recoverable. R2 has no versioning/undelete, so this is our only
65
+ * safety net for the bytes.
66
+ */
67
+ const STORAGE_PURGE_RETENTION_SECONDS = 30 * 24 * 60 * 60;
68
+
48
69
  /**
49
70
  * MIME type stored on disk and on the `media` row for a Jant-composed text
50
71
  * attachment. Markdown is the canonical source — HTML is rendered on read,
@@ -183,21 +204,52 @@ export interface MediaService {
183
204
  */
184
205
  validateIds(ids: string[]): Promise<void>;
185
206
  /**
186
- * Delete a media record and its storage file.
207
+ * Delete a media record. The DB row is removed immediately. When `storage` is
208
+ * provided and supports server-side copy, the object is moved to a `trash/`
209
+ * key and its original key is deleted now (original URL 404s immediately,
210
+ * bytes recoverable until purge); otherwise the object is deleted immediately.
187
211
  *
188
212
  * @param id - Media record ID
189
- * @param storage - Optional storage driver; when provided the file is deleted from storage
213
+ * @param storage - Storage driver used to retire the object
190
214
  * @returns true if the record existed and was deleted
191
215
  */
192
216
  delete(id: string, storage?: StorageDriver | null): Promise<boolean>;
193
217
  /**
194
- * Delete multiple media records and their storage files.
218
+ * Delete multiple media records. Rows are removed immediately; their storage
219
+ * objects are retired (moved to trash, or deleted) via `storage`.
195
220
  *
196
221
  * @param ids - Media record IDs
197
- * @param storage - Optional storage driver; when provided the files are deleted from storage
222
+ * @param storage - Storage driver used to retire the objects
198
223
  */
199
224
  deleteByIds(ids: string[], storage?: StorageDriver | null): Promise<void>;
225
+ /**
226
+ * Physically delete trashed storage objects whose recycle window has elapsed.
227
+ * Called by the upload cleanup sweep; removes the queue entry for each.
228
+ *
229
+ * @param input.before - Unix-seconds cutoff; only entries with `purgeAfter <= before` are processed
230
+ * @param input.limit - Maximum number of queue entries to process (batch bound)
231
+ * @param input.provider - Only process entries for this storage provider (the active driver)
232
+ * @param storage - Storage driver used to delete the objects
233
+ * @returns Number of storage objects actually deleted
234
+ */
235
+ purgeDueStorageObjects(
236
+ input: { before: number; limit: number; provider: string },
237
+ storage: StorageDriver,
238
+ ): Promise<number>;
200
239
  getByStorageKey(storageKey: string, provider: string): Promise<Media | null>;
240
+ /**
241
+ * Return IDs of orphaned media — rows never attached to a post
242
+ * (`postId IS NULL`) created before the given cutoff. Used by the upload
243
+ * cleanup sweep to reap media uploaded during compose but never published.
244
+ *
245
+ * @param input.before - Unix-seconds cutoff; only rows with `createdAt < before` are returned
246
+ * @param input.limit - Maximum number of IDs to return (batch bound)
247
+ * @returns Orphaned media IDs, oldest first
248
+ */
249
+ listOrphanedMediaIds(input: {
250
+ before: number;
251
+ limit: number;
252
+ }): Promise<string[]>;
201
253
  createTextAttachment(
202
254
  data: CreateTextAttachmentData,
203
255
  deps: TextAttachmentDeps,
@@ -278,7 +330,91 @@ export function createMediaService(
278
330
  hostedControlPlane?: HostedControlPlaneClient | null;
279
331
  },
280
332
  ): MediaService {
281
- const { media } = databaseSchema;
333
+ const { media, storagePurge } = databaseSchema;
334
+
335
+ /**
336
+ * Build the recycle-bin key for a deleted object. Each retired object gets a
337
+ * unique `trash/<siteId>/<id>/<basename>` key so trash entries never collide
338
+ * and the original (public) key can be freed immediately.
339
+ */
340
+ function trashStorageKey(id: string, originalKey: string): string {
341
+ const basename = originalKey.split("/").pop() || "object";
342
+ return `trash/${siteId}/${id}/${basename}`;
343
+ }
344
+
345
+ /**
346
+ * Retire the storage object(s) backing the given media rows. When the driver
347
+ * supports server-side copy, each object is moved to a `trash/` key (recorded
348
+ * in `storage_purge`) and its original key is deleted immediately — so the
349
+ * original public URL 404s right away while the bytes stay recoverable until
350
+ * `purge_after`. Drivers without server-side copy (e.g. the R2 Workers
351
+ * binding) fall back to immediate deletion with no recycle window. All
352
+ * storage ops are best-effort so a backend hiccup never blocks the row delete.
353
+ */
354
+ async function retireStorageObjects(
355
+ records: Media[],
356
+ storage: StorageDriver | null | undefined,
357
+ reason: string,
358
+ ): Promise<void> {
359
+ if (!storage || records.length === 0) return;
360
+
361
+ const objects = records.flatMap((record) => {
362
+ const keys = [record.storageKey];
363
+ if (record.posterKey) keys.push(record.posterKey);
364
+ return keys.map((key) => ({ provider: record.provider, key }));
365
+ });
366
+
367
+ if (!supportsCopy(storage)) {
368
+ // No server-side copy: delete immediately (no recycle window).
369
+ await Promise.all(
370
+ objects.map((o) =>
371
+ storage.delete(o.key).catch((err) => {
372
+ // eslint-disable-next-line no-console -- Error logging is intentional
373
+ console.error("Storage delete error:", err);
374
+ }),
375
+ ),
376
+ );
377
+ return;
378
+ }
379
+
380
+ const purgeAfter = now() + STORAGE_PURGE_RETENTION_SECONDS;
381
+ const createdAt = now();
382
+ const entries: Array<typeof storagePurge.$inferInsert> = [];
383
+ for (const o of objects) {
384
+ const id = createEntityId("storagePurge");
385
+ const trashKey = trashStorageKey(id, o.key);
386
+ try {
387
+ await storage.copy(o.key, trashKey);
388
+ } catch (err) {
389
+ // Copy failed: delete the original anyway rather than leave it stranded
390
+ // at its public URL. No recycle entry for this object.
391
+ // eslint-disable-next-line no-console -- Error logging is intentional
392
+ console.error("Storage trash copy error:", err);
393
+ await storage.delete(o.key).catch((e) => {
394
+ // eslint-disable-next-line no-console -- Error logging is intentional
395
+ console.error("Storage delete error:", e);
396
+ });
397
+ continue;
398
+ }
399
+ await storage.delete(o.key).catch((err) => {
400
+ // eslint-disable-next-line no-console -- Error logging is intentional
401
+ console.error("Storage delete error:", err);
402
+ });
403
+ entries.push({
404
+ id,
405
+ siteId,
406
+ provider: o.provider,
407
+ storageKey: trashKey,
408
+ originalKey: o.key,
409
+ reason,
410
+ purgeAfter,
411
+ createdAt,
412
+ });
413
+ }
414
+ if (entries.length > 0) {
415
+ await db.insert(storagePurge).values(entries);
416
+ }
417
+ }
282
418
 
283
419
  async function getLastPosition(postId: string): Promise<string | null> {
284
420
  const rows = await db
@@ -501,6 +637,27 @@ export function createMediaService(
501
637
  return rows.map(toMedia);
502
638
  },
503
639
 
640
+ async listOrphanedMediaIds({ before, limit }) {
641
+ if (limit <= 0) return [];
642
+ const rows = await db
643
+ .select({ id: media.id })
644
+ .from(media)
645
+ .where(
646
+ and(
647
+ eq(media.siteId, siteId),
648
+ isNull(media.postId),
649
+ lt(media.createdAt, before),
650
+ // Site assets (avatars, favicons) are stored with postId = null and
651
+ // referenced from site settings, not posts. Exclude them so the
652
+ // reaper never deletes them as abandoned compose uploads.
653
+ sql`${media.storageKey} NOT LIKE ${SITE_ASSET_STORAGE_KEY_LIKE_PATTERN}`,
654
+ ),
655
+ )
656
+ .orderBy(asc(media.createdAt), asc(media.id))
657
+ .limit(limit);
658
+ return rows.map((r) => r.id);
659
+ },
660
+
504
661
  async getByPostId(postId) {
505
662
  const rows = await db
506
663
  .select()
@@ -890,22 +1047,9 @@ export function createMediaService(
890
1047
  const record = await this.getById(id);
891
1048
  if (!record) return false;
892
1049
 
893
- if (storage) {
894
- // Text attachments have a single `.md` object same shape as any
895
- // other media — so no sibling cleanup is needed. Only video/image
896
- // rows carry a companion poster.
897
- await storage.delete(record.storageKey).catch((err) => {
898
- // eslint-disable-next-line no-console -- Error logging is intentional
899
- console.error("Storage delete error:", err);
900
- });
901
- if (record.posterKey) {
902
- await storage.delete(record.posterKey).catch((err) => {
903
- // eslint-disable-next-line no-console -- Error logging is intentional
904
- console.error("Storage delete poster error:", err);
905
- });
906
- }
907
- }
908
-
1050
+ // Move the bytes to trash (recoverable) and free the original key now, so
1051
+ // the original public URL 404s immediately. Then remove the row.
1052
+ await retireStorageObjects([record], storage, "media-delete");
909
1053
  await db
910
1054
  .delete(media)
911
1055
  .where(and(eq(media.siteId, siteId), eq(media.id, id)));
@@ -915,28 +1059,41 @@ export function createMediaService(
915
1059
  async deleteByIds(ids, storage) {
916
1060
  if (ids.length === 0) return;
917
1061
 
918
- if (storage) {
919
- const records = await this.getByIds(ids);
920
- const keys: string[] = [];
921
- for (const record of records) {
922
- keys.push(record.storageKey);
923
- if (record.posterKey) {
924
- keys.push(record.posterKey);
925
- }
926
- }
927
- await Promise.all(
928
- keys.map((key) =>
929
- storage.delete(key).catch((err) => {
930
- // eslint-disable-next-line no-console -- Error logging is intentional
931
- console.error("Storage delete error:", err);
932
- }),
933
- ),
934
- );
935
- }
936
-
1062
+ const records = await this.getByIds(ids);
1063
+ await retireStorageObjects(records, storage, "media-delete");
937
1064
  await db
938
1065
  .delete(media)
939
1066
  .where(and(eq(media.siteId, siteId), inArray(media.id, ids)));
940
1067
  },
1068
+
1069
+ async purgeDueStorageObjects({ before, limit, provider }, storage) {
1070
+ if (limit <= 0) return 0;
1071
+ const dueRows = await db
1072
+ .select()
1073
+ .from(storagePurge)
1074
+ .where(
1075
+ and(
1076
+ eq(storagePurge.siteId, siteId),
1077
+ eq(storagePurge.provider, provider),
1078
+ lte(storagePurge.purgeAfter, before),
1079
+ ),
1080
+ )
1081
+ .orderBy(asc(storagePurge.purgeAfter), asc(storagePurge.id))
1082
+ .limit(limit);
1083
+
1084
+ let purged = 0;
1085
+ for (const row of dueRows) {
1086
+ // `storageKey` is the trash key; it is never referenced by a live media
1087
+ // row, so no liveness check is needed — just delete it and drop the entry.
1088
+ await storage.delete(row.storageKey).catch((err) => {
1089
+ // eslint-disable-next-line no-console -- Error logging is intentional
1090
+ console.error("Storage purge delete error:", err);
1091
+ });
1092
+ await db.delete(storagePurge).where(eq(storagePurge.id, row.id));
1093
+ purged += 1;
1094
+ }
1095
+
1096
+ return purged;
1097
+ },
941
1098
  };
942
1099
  }
@@ -112,6 +112,11 @@ export interface PostFilters {
112
112
  hasTitle?: boolean;
113
113
  /** Filter by rating presence */
114
114
  hasRating?: boolean;
115
+ /**
116
+ * Filter thread roots by whether their thread contains published replies.
117
+ * Only meaningful together with `excludeReplies` (replies never match).
118
+ */
119
+ hasReplies?: boolean;
115
120
  /** Explicit result sort order */
116
121
  sortOrder?: SortOrder;
117
122
  limit?: number;
@@ -527,7 +532,7 @@ export function createPostService(
527
532
 
528
533
  const effectiveVisibilityExpr = sql<string>`coalesce(
529
534
  ${posts.visibility},
530
- (SELECT root.visibility FROM post AS root WHERE root.id = ${posts.threadId})
535
+ (SELECT root.visibility FROM post AS root WHERE root.id = ${posts.threadId} AND root.site_id = ${siteId})
531
536
  )`;
532
537
 
533
538
  /** Check if a slug is available (not used by posts or path_registry) */
@@ -800,6 +805,21 @@ export function createPostService(
800
805
  filters.hasRating ? isNotNull(posts.rating) : isNull(posts.rating),
801
806
  );
802
807
  }
808
+ if (filters.hasReplies !== undefined) {
809
+ // Same notion of "reply" as getReplyCounts: published posts in the
810
+ // thread with reply_to_id set.
811
+ const repliesExist = sql`EXISTS (
812
+ SELECT 1
813
+ FROM post AS reply
814
+ WHERE reply.site_id = ${siteId}
815
+ AND reply.thread_id = ${posts.id}
816
+ AND reply.reply_to_id IS NOT NULL
817
+ AND reply.status = 'published'
818
+ )`;
819
+ conditions.push(
820
+ filters.hasReplies ? repliesExist : sql`NOT ${repliesExist}`,
821
+ );
822
+ }
803
823
 
804
824
  return conditions;
805
825
  }
@@ -1072,7 +1092,7 @@ export function createPostService(
1072
1092
  }
1073
1093
 
1074
1094
  function buildThreadRootPageConditions(options?: ThreadRootPageOptions) {
1075
- const conditions: SQL[] = [];
1095
+ const conditions: SQL[] = [eq(posts.siteId, siteId)];
1076
1096
  const status = options?.status;
1077
1097
 
1078
1098
  if (status) {
@@ -180,7 +180,7 @@ export function createSearchService(
180
180
  snippet(post_fts, 1, char(2), char(3), '...', 32) AS snippet
181
181
  FROM post_fts
182
182
  JOIN post ON post.rowid = post_fts.rowid
183
- JOIN post AS root_post ON root_post.id = post.thread_id
183
+ JOIN post AS root_post ON root_post.id = post.thread_id AND root_post.site_id = post.site_id
184
184
  JOIN path_registry
185
185
  ON path_registry.post_id = post.id
186
186
  AND path_registry.site_id = post.site_id
@@ -254,7 +254,7 @@ export function createSearchService(
254
254
  ) AS snippet
255
255
  FROM post
256
256
  CROSS JOIN search_query
257
- JOIN post AS root_post ON root_post.id = post.thread_id
257
+ JOIN post AS root_post ON root_post.id = post.thread_id AND root_post.site_id = post.site_id
258
258
  JOIN path_registry
259
259
  ON path_registry.post_id = post.id
260
260
  AND path_registry.site_id = post.site_id
@@ -300,7 +300,7 @@ export function createSearchService(
300
300
  ) AS rank,
301
301
  NULL AS snippet
302
302
  FROM post
303
- JOIN post AS root_post ON root_post.id = post.thread_id
303
+ JOIN post AS root_post ON root_post.id = post.thread_id AND root_post.site_id = post.site_id
304
304
  JOIN path_registry
305
305
  ON path_registry.post_id = post.id
306
306
  AND path_registry.site_id = post.site_id
@@ -342,7 +342,7 @@ export function createSearchService(
342
342
  0 AS rank,
343
343
  NULL AS snippet
344
344
  FROM post
345
- JOIN post AS root_post ON root_post.id = post.thread_id
345
+ JOIN post AS root_post ON root_post.id = post.thread_id AND root_post.site_id = post.site_id
346
346
  JOIN path_registry
347
347
  ON path_registry.post_id = post.id
348
348
  AND path_registry.site_id = post.site_id