diff --git a/.gitignore b/.gitignore
index 4c2a515a..a8ebf016 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,3 @@ src/webtorrent-docs/
playwright-report/
test-results/
tests/visual/__snapshots__/
-css/tailwind.generated.css
diff --git a/components/profile-modal.html b/components/profile-modal.html
index 9018e1ac..be56000c 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -930,7 +930,7 @@
Whitelist
- Creators listed here remain visible when whitelist mode is enabled. Adding someone removes them from the blacklist.
+ Creators listed here remain visible when whitelist mode is enabled. Accounts can appear in both lists, and the blacklist always takes precedence.
@@ -966,7 +966,7 @@
Blacklist
- Creators on the blacklist are hidden from bitvid everywhere. Adding someone removes them from the whitelist.
+ Creators on the blacklist are hidden from bitvid everywhere. Accounts can appear in both lists, and the blacklist always takes precedence.
diff --git a/components/upload-modal.html b/components/upload-modal.html
index e997060f..6d19c2ba 100644
--- a/components/upload-modal.html
+++ b/components/upload-modal.html
@@ -89,7 +89,7 @@
-
+
Uploading...
@@ -133,45 +133,73 @@
-
+
+
+
Target Bucket Name (Required)
+
+
+
+ Copy
+
+
+
Create a bucket with this exact name in Cloudflare R2.
+
+
+
+
Account ID
-
-
Access Key ID
-
+
+
+ Next
-
-
Secret Access Key
-
+
+
+
+
+
Public Bucket URL
+
+
+ Enable Public Access in the Cloudflare dashboard to get your r2.dev URL.
+
+
+
+
+
+
+
+ Back
+ Verify & Save
+
-
-
-
-
Advanced R2 Options
-
-
- API Token (Optional)
-
-
-
-
-
-
-
- Save Credentials
-
diff --git a/components/video-modal.html b/components/video-modal.html
index 0a77155b..89a359c1 100644
--- a/components/video-modal.html
+++ b/components/video-modal.html
@@ -175,7 +175,13 @@
diff --git a/config/instance-config.js b/config/instance-config.js
index 15f519ed..a1074fd0 100644
--- a/config/instance-config.js
+++ b/config/instance-config.js
@@ -256,7 +256,7 @@ export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = 1;
* enforce community standards or raise it to tolerate more reports before
* hiding.
*/
-export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = 1;
+export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = 20;
/**
* Trusted spam/report threshold that hides videos globally.
diff --git a/content/community-guidelines.md b/content/community-guidelines.md
index adb217b1..d401d7d7 100644
--- a/content/community-guidelines.md
+++ b/content/community-guidelines.md
@@ -88,7 +88,7 @@ You can manage moderation directly from the client using the existing menus rend
### **5.5 Safety & Moderation thresholds**
-- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the defaults exported from [`config/instance-config.js`](../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). The upstream repo ships example values of 3 trusted reports for blur, 2 for autoplay blocking, 1 trusted mute to hide authors, and 3 trusted spam reports to hide videos—tune the config file before deployment if your community needs different baselines.
+- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the defaults exported from [`config/instance-config.js`](../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). The upstream repo ships example values of 1 trusted report for blur, 1 for autoplay blocking, 20 trusted mutes to hide authors, and 1 trusted spam report to hide videos—tune the config file before deployment if your community needs different baselines.
- These controls update immediately and sync with the moderation overlays applied by `moreMenuController` and `VideoCard` components. Operators can edit the config file to change the baseline before deploying their own instance.
## **6. Appeals and Feedback**
diff --git a/css/tailwind.generated.css b/css/tailwind.generated.css
new file mode 100644
index 00000000..7e658d19
--- /dev/null
+++ b/css/tailwind.generated.css
@@ -0,0 +1,8 @@
+:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner-base:var(--color-page);--surface-overlay-banner:var(--surface-overlay-banner-base);--surface-overlay-banner-opaque:var(--surface-overlay-banner-base);--surface-overlay-banner-strong:color-mix(in srgb,var(--surface-overlay-banner-base) 82%,transparent);--surface-overlay-banner-bottom:color-mix(in srgb,var(--surface-overlay-banner-base) 50%,transparent);--surface-overlay-banner-channel-mid:color-mix(in srgb,var(--surface-overlay-banner-base) 24%,transparent);--surface-overlay-banner-channel-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 10%,transparent);--surface-overlay-banner-soft:color-mix(in srgb,var(--surface-overlay-banner-base) 56%,transparent);--surface-overlay-banner-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 18%,transparent);--surface-overlay-banner-transparent:transparent;--surface-overlay-banner-gradient-header:linear-gradient(to bottom,var(--surface-overlay-banner-opaque) 0%,var(--surface-overlay-banner-strong) 38%,var(--surface-overlay-banner-soft) 70%,var(--surface-overlay-banner-faint) 88%,var(--surface-overlay-banner-transparent) 100%);--surface-overlay-banner-gradient-channel:linear-gradient(to top,var(--surface-overlay-banner-bottom) 0%,var(--surface-overlay-banner-channel-mid) 28%,var(--surface-overlay-banner-channel-faint) 42%,var(--surface-overlay-banner-transparent) 50%);--surface-overlay-banner-gradient:var(
+ --surface-overlay-banner-gradient-header
+ );--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--app-header-centerline-offset:calc(var(--space-sm) + var(--space-2xl-plus)/2);--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-app-header:40;--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--z-modal-top-root:140;--z-modal-top-overlay:150;--z-modal-top-content:160;--z-modal-peak-root:165;--z-modal-peak-overlay:175;--z-modal-peak-content:185;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded-base:16rem;--sidebar-dropup-content-width:0px;--sidebar-width-expanded:max(var(--sidebar-width-expanded-base),var(--sidebar-dropup-content-width));--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-sm);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-dropup-scroll-reserve:calc(var(--scrollbar-thin-width) + var(--space-sm) + var(--space-2xs));--sidebar-dropup-max-height:18rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-nav-padding-inline-start:var(--space-xs);--space-sidebar-nav-padding-inline-end:calc(var(--space-lg) + var(--space-2xs));--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-translate:0.35rem;--grid-card-min-width:20rem;--popover-backdrop-blur:12px;--overlay-panel-radius:var(--radius-lg);--overlay-panel-shadow:var(--shadow-lg);--overlay-panel-padding-inline:var(--popup-padding-inline);--overlay-panel-padding-block:var(--popup-padding-block);--toast-translate:-1.5rem;--sidebar-mobile-gutter:3rem;--popup-padding-inline:1rem;--popup-padding-block:0.75rem;--video-modal-primary-fr:1.85fr;--video-modal-secondary-min:320px;--video-modal-secondary-max:35rem;--video-modal-secondary-fr:1fr;--video-modal-layout-max:calc(var(--layout-player-max-width) + var(--video-modal-secondary-max) + var(--space-2xl-plus));--revert-modal-sidebar-min:240px;--revert-modal-sidebar-width:280px;--popover-inline-safe-max:calc(100vw - var(--space-xl));--progress-height-sm:0.25rem;--progress-height-md:0.5rem;--progress-radius:var(--radius-full);--progress-track-color:var(--color-border-subtle);--progress-fill-color:var(--color-info);--progress-transition-duration:var(--motion-duration-fast);--progress-transition-ease:var(--motion-ease-out);--space-4xs:0.0625rem;--space-3xs:0.125rem;--space-2xs:0.25rem;--space-xs:0.5rem;--space-xs-snug:0.4rem;--space-sm:0.75rem;--space-md:1rem;--space-md-plus:1.25rem;--space-lg:1.5rem;--space-xl:2rem;--space-xl-compact:2.25rem;--space-xl-plus:2.5rem;--space-2xl:3rem;--space-2xl-plus:3.5rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--radius-full:9999px;--radius-modal-xl:1.75rem;--radius-sidebar-shell:1.5rem;--radius-sidebar-panel:1.1rem;--radius-sidebar-nav:0.95rem;--radius-toggle-thumb:0.5625rem;--menu-min-width:12rem;--modal-max-width:62.5rem;--modal-sheet-inline-max:var(--modal-max-width);--modal-shell-max-height:90vh;--modal-sheet-max-height:92vh;--modal-body-max-height:calc(92vh - 4rem);--modal-pane-max-height:48vh;--modal-pane-min-height:20rem;--profile-modal-nav-min-width:max-content;--profile-modal-nav-max-width:max-content;--profile-modal-scrollbar-offset:var(--space-2xs);--modal-embed-height:80vh;--profile-history-max-height:26.25rem;--status-log-min-height:5rem;--layout-player-max-width:68.75rem;--layout-docs-max-width:92.5rem;--icon-size-sm:var(--font-size-lg);--icon-size-md:var(--space-md-plus);--icon-size-lg:var(--space-xl-plus);--avatar-size-lg:var(--space-2xl-plus);--form-textarea-min-height:8rem;--scrollbar-thin-width:0.4rem;--modal-progress-height:calc(var(--space-3xs)*3);--video-modal-action-min-width:2.75rem;--outline-thick-width:0.1875rem;--icon-button-ring-width:var(--outline-thick-width);--form-select-icon-offset:var(--space-sm);--form-select-icon-gap:var(--space-xs-snug);--form-select-icon-hit-area:var(--space-xl-plus);--form-select-icon-size:0.6rem;--form-select-divider-width:var(--border-width-hairline);--form-checkbox-checkmark-offset:calc(var(--space-3xs)*-1);--offscreen-offset:-9999px;--motion-card-enter-translate:var(--space-xs);--motion-fade-in-translate:var(--space-md-plus);--shadow-sm:0 0.0625rem 0.125rem 0 var(--color-shadow-floating);--shadow-md:0 0.25rem 0.375rem -0.0625rem var(--color-shadow-floating);--shadow-lg:0 0.625rem 0.9375rem -0.1875rem var(--color-shadow-elevated);--shadow-modal:0 2.75rem 6rem -1.25rem var(--color-shadow-modal);--shadow-sidebar-shell:0 1.875rem 3.75rem -1.5rem var(--color-shadow-sidebar);--shadow-sidebar-accent:0 1.65rem 3.25rem -1.35rem var(--color-sidebar-glow);--shadow-sidebar-trigger:0 1.25rem 2.5rem -1.35rem var(--color-shadow-panel);--shadow-sidebar-dropup:0 1.75rem 3.25rem -1.45rem var(--color-sidebar-glow);--shadow-sidebar-panel:0 2.25rem 4.5rem -1.85rem var(--color-shadow-sidebar);--shadow-sidebar-focus-ring:0 0 0 0.125rem var(--color-sidebar-glow),0 0 0 0.25rem var(--color-sidebar-hover-strong);--shadow-popover-zap:0 22px 48px -18px var(--color-shadow-intense);--shadow-video-modal:0 1rem 2.25rem var(--color-shadow-elevated);--font-family-sans:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;--font-family-mono:"Fira Code","SFMono-Regular",ui-monospace,monospace;--font-size-4xs:0.65rem;--font-size-3xs:0.6875rem;--font-size-2xs:0.7rem;--font-size-xs:0.75rem;--font-size-sm:0.875rem;--font-size-base:1rem;--font-size-lg:1.125rem;--font-size-xl:1.5rem;--font-size-2xl:2rem;--font-size-xl-plus:1.875rem;--font-size-modal-title-base:1.25rem;--font-size-sidebar-nav:0.95rem;--font-size-sidebar-subnav:0.85rem;--font-size-sidebar-trigger:0.9rem;--line-height-tight:1.1;--line-height-snug:1.25;--line-height-base:1.5;--line-height-relaxed:1.75;--tracking-extra-wide:0.2em;--grid-border-width:calc(var(--space-2xs)/4);--grid-canvas-padding-top:var(--grid-border-width);--grid-viewport-min-height:calc(var(--space-xl) - var(--space-sm));--grid-cell-padding-block:var(--space-sm);--grid-cell-padding-inline:var(--space-md);--grid-header-font-size:var(--font-size-xs);--grid-header-letter-spacing:0.08em;--grid-overlay-margin:10%;--grid-overlay-padding-block:var(--space-md);--grid-overlay-padding-inline:var(--space-xl);--grid-overlay-radius:var(--radius-lg);--grid-overlay-font-size:var(--font-size-lg);--grid-overlay-text-opacity:0.66;--grid-resizer-width:calc(var(--space-2xs)*1.25);--grid-resize-overlay-width:var(--grid-border-width);--grid-sort-indicator-size:calc(var(--space-lg) - var(--space-2xs));--grid-sort-indicator-offset:var(--space-xs);--grid-sort-indicator-stroke:calc(var(--space-2xs)/2);--grid-sort-indicator-thickness:var(--grid-border-width);--notify-padding-block:var(--space-md);--notify-padding-inline:var(--space-2xl);--notify-font-size:var(--font-size-base);--notify-dismiss-size:calc(var(--space-xl) + var(--space-xs));--notify-dismiss-size-compact:calc(var(--space-lg) + var(--space-2xs));--notify-dismiss-font-size:var(--font-size-sm);--notify-dismiss-radius:var(--radius-full);--notify-dismiss-color:var(--color-subtle);--notify-dismiss-background:var(--color-overlay-panel);--notify-dismiss-background-hover:var(--color-overlay-panel-soft);--z-notify:1200;--motion-duration-fast:120ms;--motion-duration-base:160ms;--motion-duration-slow:280ms;--motion-ease-out:cubic-bezier(0.16,1,0.3,1);--motion-ease-in-out:cubic-bezier(0.4,0,0.2,1);--modal-motion-duration:var(--motion-duration-base)}:root[data-theme=light]{--color-page:#f7f8fc;--color-page-alt:#f2f4f8;--color-surface:#fff;--color-surface-alt:#f8fafc;--color-bg:var(--color-surface);--color-card:#fff;--color-card-private:#f3ecff;--color-card-critical:#ffecee;--color-panel:#fcfdff;--color-panel-hover:#eff2f9;--color-border:#d2dbec;--color-border-subtle:#e5eaf2;--color-accent:#6366f1;--color-accent-strong:#4f46e5;--color-accent-pressed:#4338ca;--color-primary:var(--color-accent-strong);--color-secondary:var(--color-accent);--color-logo-accent:var(--color-accent-strong);--color-logo-wordmark:#28364d;--color-logo-wordmark-inverse:var(--color-white);--color-logo-background:#0f172a;--color-logo-background-inverse:var(--color-white);--color-info:#3b82f6;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-info-glass:rgba(59,130,246,.14);--color-info-border:rgba(59,130,246,.35);--color-info-on:#e3f2ff;--color-muted-strong:#475569;--color-text:#181f2c;--color-text-strong:#0c101a;--color-text-inverse:#fff;--color-text-muted:#667085;--color-muted:#8892a6;--color-muted-alt:#788296;--color-subtle-text:#8892a6;--color-subtle:#f3f5fa;--color-surface-sunken:#e8ebf4;--color-surface-muted:#ecf0f9;--color-critical:#dc3646;--color-critical-strong:#b92037;--color-danger-surface:#fce2e6;--color-danger-border:rgba(220,54,70,.35);--color-danger-on:#60121f;--color-warning:#eab308;--color-warning-strong:#ca8a04;--color-warning-surface:rgba(253,224,71,.35);--color-warning-border:rgba(234,179,8,.35);--color-warning-glass:rgba(253,224,71,.18);--color-warning-on:#5c3702;--color-success:#16a34a;--color-success-strong:#15803d;--color-success-surface:#defaec;--color-success-border:rgba(34,197,94,.3);--color-success-on:#125430;--color-private:#8b5cf6;--color-private-strong:#7c3aed;--color-private-surface:#ede9fe;--color-private-border:rgba(139,92,246,.32);--color-private-on:#4a1d96;--color-neutral-700:#475569;--color-neutral-600:#667085;--color-neutral-surface:#e8ecf4;--color-neutral-border:#cdd5e2;--color-neutral-on:#3e485c;--color-white:#fff;--color-black:#000;--color-overlay:rgba(15,23,42,.3);--color-overlay-black:rgba(15,23,42,.7);--color-overlay-muted:rgba(60,75,105,.18);--color-overlay-strong:rgba(15,23,42,.56);--color-overlay-darker:rgba(12,18,32,.68);--color-overlay-panel:rgba(248,250,255,.92);--color-overlay-panel-soft:rgba(244,247,255,.78);--color-overlay-panel-tint:rgba(146,170,211,.24);--color-overlay-panel-glass:hsla(0,0%,100%,.85);--color-overlay-panel-glass-soft:rgba(246,249,255,.7);--color-overlay-panel-gradient-start:hsla(0,0%,100%,.95);--color-overlay-panel-gradient-end:rgba(232,236,244,.92);--color-modal-backdrop:rgba(15,23,42,.28);--color-modal-surface:hsla(0,0%,100%,.94);--color-overlay-veil:rgba(103,112,133,.45);--color-overlay-banner-bottom:hsla(0,0%,100%,.6);--color-overlay-banner-transparent:hsla(0,0%,100%,0);--color-overlay-backdrop:rgba(15,23,42,.2);--scrollbar-track:color-mix(in srgb,var(--color-sidebar-border-strong) 40%,transparent);--scrollbar-thumb:color-mix(in srgb,var(--color-accent) 64%,transparent);--scrollbar-thumb-hover:color-mix(in srgb,var(--color-accent-strong) 76%,transparent);--color-sidebar-surface:hsla(0,0%,100%,.9);--color-sidebar-surface-alt:rgba(236,240,247,.72);--color-sidebar-border:rgba(184,194,212,.28);--color-sidebar-border-strong:rgba(148,163,184,.36);--color-sidebar-hover:color-mix(in srgb,var(--color-accent) 16%,transparent);--color-sidebar-hover-strong:color-mix(in srgb,var(--color-accent-strong) 24%,transparent);--color-sidebar-glow:color-mix(in srgb,var(--color-accent) 36%,transparent);--color-sidebar-accent-start:color-mix(in srgb,var(--color-accent-strong) 88%,transparent);--color-sidebar-accent-end:color-mix(in srgb,var(--color-accent) 82%,transparent);--color-sidebar-text-muted:var(--color-text-muted);--sidebar-icon-filter:brightness(0) saturate(100%);--color-surface-tint-subtle:rgba(15,23,42,.08);--color-border-translucent:rgba(148,163,184,.18);--color-border-translucent-strong:rgba(148,163,184,.22);--color-border-translucent-bright:rgba(148,163,184,.28);--color-border-translucent-stronger:rgba(148,163,184,.32);--color-border-translucent-strongest:rgba(148,163,184,.36);--color-border-translucent-glow:rgba(148,163,184,.45);--color-border-translucent-medium:rgba(148,163,184,.2);--color-border-neutral-soft:rgba(148,163,184,.25);--color-border-info-soft:rgba(59,130,246,.32);--color-border-sidebar-strong:rgba(102,112,133,.75);--color-ring-private:rgba(139,92,246,.5);--color-ring-private-hover:rgba(168,85,247,.6);--color-ring-critical:rgba(220,54,70,.35);--color-ring-critical-soft:rgba(220,54,70,.3);--color-ring-critical-hover:rgba(220,54,70,.4);--color-ring-critical-strong:rgba(220,54,70,.5);--color-ring-alert:rgba(239,68,68,.48);--color-gradient-visibility-start:rgba(99,102,241,.28);--color-gradient-visibility-end:rgba(129,140,248,.24);--color-white-soft:hsla(0,0%,100%,.08);--color-white-softer:hsla(0,0%,100%,.24);--color-white-softest:hsla(0,0%,100%,.32);--color-shadow-floating:rgba(15,23,42,.18);--color-shadow-elevated:rgba(15,23,42,.16);--color-shadow-panel:rgba(15,23,42,.12);--color-shadow-notification:rgba(15,23,42,.14);--color-shadow-intense:rgba(15,23,42,.22);--color-shadow-sidebar:rgba(15,23,42,.28);--color-shadow-modal:rgba(15,23,42,.28);--color-divider-strong:rgba(203,213,225,.6);--color-text-frosted-strong:rgba(40,54,77,.85);--color-text-frosted:rgba(61,74,98,.8);--color-text-frosted-soft:rgba(82,96,122,.72)}:root:not([data-theme]),:root[data-theme=dark]{--color-page:#060a12;--color-page-alt:#080c16;--color-surface:#0a0e1a;--color-surface-alt:#0e1422;--color-bg:var(--color-surface);--color-card:#121a2c;--color-card-private:#261c3e;--color-card-critical:#3a141c;--color-panel:#101828;--color-panel-hover:#182234;--color-border:#3d5070;--color-border-subtle:#26344c;--color-accent:#818cf8;--color-accent-strong:#6366f1;--color-accent-pressed:#4f46e5;--color-primary:var(--color-accent-strong);--color-secondary:#38bdf8;--color-logo-accent:var(--color-accent-strong);--color-logo-wordmark:#c6cfde;--color-logo-wordmark-inverse:var(--color-white);--color-logo-background:#0c121e;--color-logo-background-inverse:var(--color-white);--color-info:#60a5fa;--color-info-strong:#3884ff;--color-info-pressed:#2563eb;--color-info-glass:rgba(56,132,255,.22);--color-info-border:rgba(96,165,250,.42);--color-info-on:#ebf4ff;--color-muted-strong:#ced7e8;--color-text:#e2e8f0;--color-text-strong:#f4f7ff;--color-text-inverse:#0c121e;--color-text-muted:#a5b3c9;--color-muted:#94a3b8;--color-muted-alt:#808ba2;--color-subtle-text:#a8b4cd;--color-subtle:#1c263a;--color-surface-sunken:rgba(13,19,32,.92);--color-surface-muted:rgba(18,26,44,.82);--color-critical:#ef5362;--color-critical-strong:#dc2626;--color-danger-surface:rgba(220,38,38,.28);--color-danger-border:rgba(220,38,38,.45);--color-danger-on:#ffe4e9;--color-warning:#facc15;--color-warning-strong:#eab308;--color-warning-surface:rgba(253,224,71,.22);--color-warning-border:rgba(250,204,21,.42);--color-warning-glass:rgba(253,224,71,.2);--color-warning-on:#1a1200;--color-success:#22c55e;--color-success-strong:#16a34a;--color-success-surface:rgba(22,163,74,.24);--color-success-border:rgba(34,197,94,.45);--color-success-on:#d2fce4;--color-private:#a855f7;--color-private-strong:#8b5cf6;--color-private-surface:rgba(124,58,237,.26);--color-private-border:rgba(168,85,247,.45);--color-private-on:#efe6ff;--color-neutral-700:#3a4b6e;--color-neutral-600:#5a6985;--color-neutral-surface:rgba(99,114,144,.24);--color-neutral-border:rgba(99,114,144,.35);--color-neutral-on:#d2dcee;--color-white:#fff;--color-black:#000;--color-overlay:rgba(8,11,20,.72);--color-overlay-black:rgba(0,0,0,.88);--color-overlay-muted:rgba(12,18,30,.6);--color-overlay-strong:rgba(8,12,24,.82);--color-overlay-darker:rgba(5,8,16,.9);--color-overlay-panel:rgba(14,20,34,.92);--color-overlay-panel-soft:rgba(18,26,42,.78);--color-overlay-panel-tint:rgba(56,93,160,.36);--color-overlay-panel-glass:rgba(14,24,42,.86);--color-overlay-panel-glass-soft:rgba(16,28,48,.72);--color-overlay-panel-gradient-start:rgba(18,26,44,.92);--color-overlay-panel-gradient-end:rgba(10,16,30,.92);--color-modal-backdrop:rgba(5,8,14,.68);--color-modal-surface:rgba(14,22,38,.88);--color-overlay-veil:rgba(4,6,12,.94);--color-overlay-banner-bottom:rgba(0,0,0,.6);--color-overlay-banner-transparent:transparent;--color-overlay-backdrop:rgba(5,8,14,.75);--scrollbar-track:color-mix(in srgb,var(--color-sidebar-surface-alt) 54%,transparent);--scrollbar-thumb:color-mix(in srgb,var(--color-accent) 72%,transparent);--scrollbar-thumb-hover:color-mix(in srgb,var(--color-accent-strong) 84%,transparent);--color-sidebar-surface:rgba(12,18,30,.94);--color-sidebar-surface-alt:rgba(10,18,32,.8);--color-sidebar-border:rgba(86,106,142,.26);--color-sidebar-border-strong:rgba(112,133,170,.42);--color-sidebar-hover:color-mix(in srgb,var(--color-accent) 22%,transparent);--color-sidebar-hover-strong:color-mix(in srgb,var(--color-accent-strong) 32%,transparent);--color-sidebar-glow:color-mix(in srgb,var(--color-accent) 42%,transparent);--color-sidebar-accent-start:color-mix(in srgb,var(--color-accent) 92%,transparent);--color-sidebar-accent-end:color-mix(in srgb,var(--color-accent-strong) 88%,transparent);--color-sidebar-text-muted:var(--color-text-muted);--sidebar-icon-filter:brightness(0) invert(1);--color-surface-tint-subtle:rgba(94,113,161,.12);--color-border-translucent:rgba(99,114,144,.22);--color-border-translucent-strong:rgba(110,130,170,.28);--color-border-translucent-bright:rgba(148,163,184,.35);--color-border-translucent-stronger:rgba(148,163,184,.38);--color-border-translucent-strongest:rgba(198,207,222,.45);--color-border-translucent-glow:rgba(129,140,248,.55);--color-border-translucent-medium:rgba(99,114,144,.28);--color-border-neutral-soft:rgba(99,114,144,.3);--color-border-info-soft:rgba(96,165,250,.35);--color-border-sidebar-strong:rgba(32,46,68,.88);--color-ring-private:rgba(168,85,247,.55);--color-ring-private-hover:rgba(192,132,252,.65);--color-ring-critical:rgba(239,83,98,.42);--color-ring-critical-soft:rgba(239,83,98,.35);--color-ring-critical-hover:rgba(239,83,98,.5);--color-ring-critical-strong:rgba(220,38,38,.6);--color-ring-alert:rgba(220,38,38,.6);--color-gradient-visibility-start:rgba(129,140,248,.4);--color-gradient-visibility-end:rgba(192,132,252,.32);--color-white-soft:hsla(0,0%,100%,.1);--color-white-softer:hsla(0,0%,100%,.3);--color-white-softest:hsla(0,0%,100%,.4);--color-shadow-floating:rgba(8,11,30,.55);--color-shadow-elevated:rgba(8,11,30,.45);--color-shadow-panel:rgba(15,23,42,.35);--color-shadow-notification:rgba(15,23,42,.3);--color-shadow-intense:rgba(250,204,21,.48);--color-shadow-sidebar:rgba(0,0,0,.45);--color-shadow-modal:rgba(8,11,30,.55);--color-divider-strong:rgba(15,23,42,.35);--color-text-frosted-strong:rgba(226,232,240,.94);--color-text-frosted:rgba(226,232,240,.9);--color-text-frosted-soft:rgba(226,232,240,.82)}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
+/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:80%}@media (min-width:768px){html{font-size:100%}}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{--app-header-centerline:calc(env(safe-area-inset-top, 0) + var(--app-header-centerline-offset));font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:block}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.app-shell__header{position:sticky;top:env(safe-area-inset-top,0);z-index:var(--z-app-header);padding-top:calc(var(--space-sm) + env(safe-area-inset-top, 0));padding-bottom:var(--space-sm);background-color:transparent;background-image:var(--surface-overlay-banner-gradient-header);background-repeat:no-repeat;background-size:100% 100%}.ds-metric-probe{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;contain:layout size style;inline-size:0;block-size:var(--ds-metric-probe-length,0);overflow:hidden}.ds-header__logo{height:var(--space-2xl-plus);width:auto;max-width:none}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.author-pic{display:block;height:100%;width:100%;border-radius:var(--radius-full);-o-object-fit:cover;object-fit:cover}.header-icon-toggle{outline:2px solid transparent;outline-offset:2px}.header-icon-toggle:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.header-icon-toggle{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-timing-function:cubic-bezier(0,0,.2,1);width:var(--icon-size-lg);height:var(--icon-size-lg);min-width:var(--icon-size-lg);min-height:var(--icon-size-lg);padding:0;border:none;background:transparent;color:var(--color-text-muted);line-height:1}.header-icon-toggle[data-theme-state=light]{color:var(--color-text-strong)}.header-icon-toggle[data-theme-state=dark]{color:var(--color-text)}.header-icon-toggle__icon-wrapper{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--space-lg);block-size:var(--space-lg)}.header-icon-toggle__icon{inline-size:100%;block-size:100%;display:none;fill:currentColor}.header-icon-toggle__icon-wrapper:not([data-theme-icon-state]),.header-icon-toggle__icon-wrapper[data-theme-icon-state=light]{color:inherit}.header-icon-toggle__icon-wrapper:not([data-theme-icon-state]) .header-icon-toggle__icon--dark,.header-icon-toggle__icon-wrapper[data-theme-icon-state=dark] .header-icon-toggle__icon--light,.header-icon-toggle__icon-wrapper[data-theme-icon-state=light] .header-icon-toggle__icon--dark{display:block}.header-icon-toggle[data-theme-state=light]:focus-visible,.header-icon-toggle[data-theme-state=light]:hover{color:var(--color-text)}.header-icon-toggle[data-theme-state=dark]:focus-visible,.header-icon-toggle[data-theme-state=dark]:hover{color:var(--color-text-strong)}.header-icon-toggle[aria-pressed=true]{color:var(--color-text-strong)}.header-icon-toggle[aria-pressed=true]:focus-visible,.header-icon-toggle[aria-pressed=true]:hover{color:var(--color-text-strong)}.bv-modal.header-search__region,.profile-pane.header-search__region{display:none}.header-search__region{display:none;flex:1 1 0%;justify-content:center}@media (min-width:768px){.header-search__region#playerModal{display:block}.header-search__region{display:flex}}.header-search__region{min-width:0;padding-inline:clamp(var(--space-md),4vw,var(--space-3xl))}#playerModal.header-search{display:block}.header-search{display:flex;align-items:center;gap:.75rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.4;background-color:var(--surface-overlay-panel);padding-left:1rem;padding-right:1rem;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);backdrop-filter:blur(var(--popover-backdrop-blur));-webkit-backdrop-filter:blur(var(--popover-backdrop-blur));padding-block:calc(var(--space-sm)*.9);inline-size:min(100%,34rem)}.header-search:focus-within{border-color:var(--color-accent-strong)}.header-search__icon{display:block;height:1.25rem;width:1.25rem;background-color:currentColor;mask-repeat:no-repeat;mask-position:center;mask-size:contain;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;-webkit-mask-size:contain}.header-search__icon--filter{mask-image:url(../assets/svg/filter.svg);-webkit-mask-image:url(../assets/svg/filter.svg)}.header-search__icon--submit{mask-image:url(../assets/svg/search.svg);-webkit-mask-image:url(../assets/svg/search.svg)}.header-search__filter{outline:2px solid transparent;outline-offset:2px}.header-search__filter:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.header-search__filter{display:inline-flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:var(--radius-full);border-width:1px;border-color:transparent;background-color:transparent;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);color:var(--color-accent);inline-size:calc(var(--icon-size-lg)*.9);block-size:calc(var(--icon-size-lg)*.9);min-inline-size:calc(var(--icon-size-lg)*.9);min-block-size:calc(var(--icon-size-lg)*.9)}.header-search__filter:focus-visible,.header-search__filter:hover{color:var(--color-accent-strong)}.header-search__input{flex:1 1 0%;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);outline:2px solid transparent;outline-offset:2px}.header-search__input::-moz-placeholder{color:var(--text-muted)}.header-search__input::placeholder{color:var(--text-muted)}.header-search__input{min-width:0}.header-search__input:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.header-search__submit{outline:2px solid transparent;outline-offset:2px}.header-search__submit:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.header-search__submit{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);border-width:1px;border-color:transparent;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);background-color:var(--color-accent-strong);color:var(--color-white);inline-size:calc(var(--icon-size-lg)*.9);block-size:calc(var(--icon-size-lg)*.9);min-inline-size:calc(var(--icon-size-lg)*.9);min-block-size:calc(var(--icon-size-lg)*.9);padding:0}.header-search__submit:hover{background-color:var(--color-accent)}.header-search__submit:focus-visible{background-color:var(--color-accent-pressed)}.bv-logo{--logo-wordmark-color:var(--color-logo-wordmark);--logo-accent-color:var(--color-logo-accent);--logo-background-color:var(--color-logo-background)}.bv-logo[data-variant=inverse]{--logo-wordmark-color:var(--color-logo-wordmark-inverse);--logo-background-color:var(--color-logo-background-inverse)}.bv-logo[data-wordmark=current]{--logo-wordmark-color:currentColor}.bv-logo[data-accent=current]{--logo-accent-color:currentColor}.bv-logo[data-background=transparent]{--logo-background-color:transparent}.bv-logo__wordmark{fill:var(--logo-wordmark-color);color:var(--logo-wordmark-color)}.bv-logo__accent{fill:var(--logo-accent-color);color:var(--logo-accent-color)}.bv-logo__background{fill:var(--logo-background-color);color:var(--logo-background-color)}.markdown-prose{color:var(--color-text)}.markdown-prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:1rem}.markdown-prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.markdown-prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--color-primary);text-decoration:none;font-weight:500}.markdown-prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.markdown-prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.markdown-prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.markdown-prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.markdown-prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.markdown-prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.markdown-prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.markdown-prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.markdown-prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.markdown-prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.markdown-prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.markdown-prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.markdown-prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.markdown-prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.markdown-prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.markdown-prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--color-border-subtle);border-top-width:1px;margin-top:2rem;margin-bottom:2rem}.markdown-prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--color-muted);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1rem;margin-bottom:1rem;padding-inline-start:1em;border-left-width:4px;border-left-color:var(--color-border-subtle);padding-left:1rem}.markdown-prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.markdown-prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.markdown-prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:var(--font-size-2xl);margin-top:.5rem;margin-bottom:1rem;line-height:1.1111111}.markdown-prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.markdown-prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:var(--font-size-xl);margin-top:1.25rem;margin-bottom:.75rem;line-height:1.3333333}.markdown-prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.markdown-prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:var(--font-size-lg);margin-top:1rem;margin-bottom:.75rem;line-height:1.6}.markdown-prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.markdown-prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.markdown-prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.markdown-prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:1rem;border-radius:var(--radius-md);max-width:100%;height:auto}.markdown-prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.markdown-prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.markdown-prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows),0 3px 0 var(--tw-prose-kbd-shadows);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.markdown-prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--color-secondary);font-weight:500;font-size:var(--font-size-sm);background-color:var(--color-page-alt);padding:.2rem .4rem;border-radius:var(--radius-md);font-family:var(--font-family-mono)}.markdown-prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.markdown-prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.markdown-prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.markdown-prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.markdown-prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.markdown-prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--color-text-strong);background-color:var(--color-page-alt);overflow-x:auto;font-weight:400;font-size:var(--font-size-sm);line-height:1.7142857;margin-top:0;margin-bottom:1rem;border-radius:var(--radius-md);padding-inline-end:1.1428571em;padding-inline-start:1.1428571em;padding:1rem;font-family:var(--font-family-mono)}.markdown-prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.markdown-prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.markdown-prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.markdown-prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.markdown-prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.markdown-prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.markdown-prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.markdown-prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.markdown-prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.markdown-prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.markdown-prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.markdown-prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.markdown-prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.markdown-prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.markdown-prose{--tw-prose-body:var(--color-text);--tw-prose-headings:var(--color-text-strong);--tw-prose-lead:#4b5563;--tw-prose-links:var(--color-primary);--tw-prose-bold:var(--color-text-strong);--tw-prose-counters:var(--color-text-muted);--tw-prose-bullets:var(--color-text-muted);--tw-prose-hr:var(--color-border-subtle);--tw-prose-quotes:var(--color-muted);--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:rgba(17,24,39,.1);--tw-prose-code:var(--color-secondary);--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:var(--color-border-subtle);--tw-prose-td-borders:var(--color-border-subtle);--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:hsla(0,0%,100%,.1);--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.6}.markdown-prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.markdown-prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5rem;margin-bottom:.5rem}.markdown-prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.markdown-prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.markdown-prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.markdown-prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.markdown-prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.markdown-prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.markdown-prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.markdown-prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.markdown-prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.markdown-prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.markdown-prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.markdown-prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.markdown-prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.markdown-prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.markdown-prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.markdown-prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.markdown-prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.markdown-prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.markdown-prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.markdown-prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.markdown-prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.markdown-prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.markdown-prose{font-family:var(--font-family-sans)}.markdown-prose :where(h4,h5,h6):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:600;margin-top:.75rem;margin-bottom:.5rem}.markdown-prose :where(ul,ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:1rem;padding-left:1.25rem}.markdown-prose :where(a:hover):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration:underline}.markdown-prose{max-width:none;line-height:1.625;color:var(--text-primary);--tw-prose-body:var(--color-text);--tw-prose-headings:var(--color-text-strong);--tw-prose-invert-headings:var(--color-text-strong);--tw-prose-lead:var(--color-text);--tw-prose-links:var(--color-accent-strong);--tw-prose-bold:var(--color-text-strong);--tw-prose-counters:var(--color-text-muted);--tw-prose-bullets:var(--color-text-muted);--tw-prose-hr:var(--color-border-translucent-strong);--tw-prose-quotes:var(--color-text-strong);--tw-prose-quote-borders:var(--color-border-subtle);--tw-prose-captions:var(--color-text-muted);--tw-prose-code:var(--color-text-strong);--tw-prose-pre-code:var(--color-text);--tw-prose-pre-bg:var(--surface-muted);--tw-prose-th-borders:var(--color-border);--tw-prose-td-borders:var(--color-border-subtle)}.markdown-prose :where(h1,h2,h3,h4,h5,h6){color:var(--color-text-strong)}.markdown-prose :where(h1,h2,h3,h4,h5,h6) strong{color:inherit}.markdown-prose a{color:var(--color-accent-strong);text-decoration-color:color-mix(in srgb,var(--color-accent-strong) 65%,transparent);text-decoration-thickness:2px;text-underline-offset:.2em}.markdown-prose a:focus-visible,.markdown-prose a:hover{color:var(--color-accent);text-decoration-line:underline}.markdown-prose blockquote{border-radius:var(--radius-md);border-left-width:4px;border-color:var(--border-subtle);background-color:var(--surface-muted);--tw-bg-opacity:0.5;padding:.5rem 1rem;color:var(--text-muted)}.markdown-prose :where(pre,code){font-family:var(--font-family-mono)}.markdown-prose code{border-radius:var(--radius-md);background-color:var(--surface-muted);padding:.125rem .25rem;color:var(--color-accent-strong)}.markdown-prose pre{overflow-x:auto;border-radius:var(--radius-lg);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-muted);padding:1rem;color:var(--text-primary)}.markdown-prose pre code{background-color:transparent;color:inherit;padding:0}.\!popover,.popover{position:relative;display:inline-flex}.popover-panel,.popover__panel{outline:2px solid transparent;outline-offset:2px}.popover-panel:focus-visible,.popover__panel:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.popover-panel,.popover__panel{pointer-events:none;z-index:var(--z-overlay-popover);min-width:var(--menu-min-width);max-width:var(--popover-inline-safe-max);--tw-scale-x:.95;--tw-scale-y:.95;border-radius:var(--overlay-panel-radius);border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.4;background-color:var(--surface-overlay-panel);padding:var(--overlay-panel-padding-block) var(--overlay-panel-padding-inline);font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);opacity:0;--tw-shadow:var(--overlay-panel-shadow);--tw-shadow-colored:var(--overlay-panel-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-shadow-color:var(--surface-overlay-panel);--tw-shadow:var(--tw-shadow-colored);transition-property:opacity;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover-panel,.popover-panel[data-state=open],.popover__panel,.popover__panel[data-state=open]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.popover-panel[data-state=open],.popover__panel[data-state=open]{pointer-events:auto;--tw-scale-x:1;--tw-scale-y:1;opacity:1}.popover-panel[data-state=closed],.popover__panel[data-state=closed]{pointer-events:none;--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));opacity:0}.ds-overlay-backdrop{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--overlay-backdrop-color);backdrop-filter:blur(var(--overlay-backdrop-blur));-webkit-backdrop-filter:blur(var(--overlay-backdrop-blur))}.popover-panel[data-variant=zap],.popover__panel[data-variant=zap]{background:var(--color-overlay-veil);color:var(--color-subtle);border:var(--border-width-hairline) solid var(--color-border-translucent-stronger);box-shadow:var(--shadow-popover-zap);backdrop-filter:blur(var(--popover-backdrop-blur));-webkit-backdrop-filter:blur(var(--popover-backdrop-blur))}.\!btn{outline:2px solid transparent;outline-offset:2px}.\!btn:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.\!btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:transparent;background-color:var(--color-accent-strong);padding:.625rem 1.25rem;font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:600;color:var(--color-white);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn{outline:2px solid transparent;outline-offset:2px}.btn:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:transparent;background-color:var(--color-accent-strong);padding:.625rem 1.25rem;font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:600;color:var(--color-white);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.\!btn[data-size=sm],.btn-ghost[data-size=sm],.btn[data-size=sm]{padding:.375rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug)}.\!btn[data-size=sm],.btn-ghost[data-size=sm],.btn[data-size=sm]{gap:.375rem}.\!btn:hover,.btn:hover{background-color:var(--color-accent)}.\!btn:focus-visible{background-color:var(--color-accent-pressed)}.btn:focus-visible{background-color:var(--color-accent-pressed)}.\!btn:disabled,.btn:disabled{pointer-events:none;cursor:not-allowed;opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-link{outline:2px solid transparent;outline-offset:2px}.btn-link:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.btn-link{display:inline-flex;align-items:center;gap:.25rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;color:var(--color-accent);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.btn-link:focus-visible,.btn-link:hover{color:var(--color-accent-strong);text-decoration-line:underline}.progress,[data-progress-meter]{--progress-height:var(--progress-height-md);--progress-track:var(--progress-track-color);--progress-fill:var(--progress-fill-color);--progress-radius-current:var(--progress-radius);display:block;width:100%;height:var(--progress-height);border:none;border-radius:var(--progress-radius-current);overflow:hidden;background-color:var(--progress-track);-webkit-appearance:none;-moz-appearance:none;appearance:none}.progress::-webkit-progress-bar,[data-progress-meter]::-webkit-progress-bar{background-color:var(--progress-track);border-radius:var(--progress-radius-current)}.progress::-webkit-progress-value,[data-progress-meter]::-webkit-progress-value{background-color:var(--progress-fill);border-radius:var(--progress-radius-current);-webkit-transition:width var(--progress-transition-duration) var(--progress-transition-ease);transition:width var(--progress-transition-duration) var(--progress-transition-ease)}.progress::-moz-progress-bar,[data-progress-meter]::-moz-progress-bar{background-color:var(--progress-fill);border-radius:var(--progress-radius-current);-moz-transition:width var(--progress-transition-duration) var(--progress-transition-ease);transition:width var(--progress-transition-duration) var(--progress-transition-ease)}.progress.progress--surface,.progress[data-variant=surface],[data-progress-meter].progress--surface,[data-progress-meter][data-variant=surface]{--progress-track:var(--color-panel)}.progress.progress--accent,.progress[data-variant=accent],[data-progress-meter].progress--accent,[data-progress-meter][data-variant=accent]{--progress-fill:var(--color-info-strong)}.btn-ghost{outline:2px solid transparent;outline-offset:2px}.btn-ghost:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.btn-ghost{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-full);border-color:transparent;background-color:var(--color-accent-strong);padding:.625rem 1.25rem;font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:600;color:var(--color-white);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn-ghost[data-size=sm]{padding:.375rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);gap:.375rem}.btn-ghost:hover{background-color:var(--color-accent)}.btn-ghost:focus-visible{background-color:var(--color-accent-pressed)}.btn-ghost:disabled{cursor:not-allowed;opacity:.6}body.iframe-form button.btn-ghost{align-self:flex-start;margin-top:var(--space-md)}.btn-ghost{border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.6;background-color:transparent;color:var(--text-primary);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (prefers-reduced-motion:reduce){.btn-ghost{transition-property:none}}.btn-ghost:hover{background-color:var(--surface-panel-hover);color:var(--text-primary)}.btn-ghost:focus-visible{background-color:var(--surface-panel-hover)}.btn-ghost:disabled{pointer-events:none;opacity:.5;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-ghost[data-variant=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.6;color:var(--color-critical)}.btn-ghost[data-variant=critical]:focus-visible,.btn-ghost[data-variant=critical]:hover{background-color:var(--color-critical);--tw-bg-opacity:0.2;color:var(--color-critical-strong)}.login-option{outline:2px solid transparent;outline-offset:2px}.login-option:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.login-option{display:block}.login-option{position:relative;display:flex;height:100%;width:100%;flex-direction:column;gap:1rem;overflow:hidden;border-width:1px;border-color:var(--border-default);background-color:var(--surface-overlay-panel-soft);padding:1.25rem 1.5rem;text-align:left;color:var(--text-primary);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s;transition-timing-function:cubic-bezier(0,0,.2,1);border-radius:calc(var(--radius-xl)*1.12);isolation:isolate;backdrop-filter:blur(var(--blur-modal-backdrop));background-image:linear-gradient(140deg,color-mix(in srgb,var(--surface-overlay-panel-soft) 88%,transparent) 0,color-mix(in srgb,var(--surface-overlay-panel) 72%,transparent) 100%);transform:translateZ(0);transition-property:border-color,box-shadow,transform}.login-option:after{content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;opacity:0;transform:translate3d(0,14%,0);transition:opacity .22s ease,transform .22s ease;background:radial-gradient(115% 115% at 0 0,color-mix(in srgb,var(--status-neutral) 32%,transparent) 0,transparent 65%)}.login-option:focus-visible,.login-option:hover{border-color:var(--border-strong);box-shadow:var(--shadow-modal);transform:translate3d(0,-3px,0)}.login-option:focus-visible:after,.login-option:hover:after{opacity:.65;transform:translateZ(0)}.login-option:disabled,.login-option[aria-disabled=true]{cursor:not-allowed;opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transform:none}.login-option[data-tone=accent]:after{background:radial-gradient(120% 120% at 0 0,color-mix(in srgb,var(--status-info) 38%,transparent) 0,transparent 65%)}.login-option[data-tone=warning]:after{background:radial-gradient(120% 120% at 0 0,color-mix(in srgb,var(--status-warning) 42%,transparent) 0,transparent 65%)}.login-option--primary{border-color:var(--border-default)}.login-option--ghost{background-image:linear-gradient(145deg,color-mix(in srgb,var(--surface-overlay-panel-soft) 82%,transparent) 0,color-mix(in srgb,var(--surface-overlay-panel-soft) 68%,transparent) 100%)}.login-option--outline{border-color:var(--border-strong)}.login-option--link{border-width:1px;border-color:transparent;background-color:transparent;color:var(--color-accent);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.login-option__capabilities,.login-option__copy,.login-option__header,.login-option__status{position:relative;z-index:1}#playerModal.login-option__header{display:block}.login-option__header{display:flex;width:100%;align-items:flex-start;justify-content:space-between;gap:1rem}#playerModal.login-option__copy{display:block}.login-option__copy{display:flex;min-width:0;flex-direction:column;align-items:flex-start;gap:.5rem}.login-option__eyebrow{font-size:.7rem;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.18em}.login-option:focus-visible .login-option__eyebrow,.login-option:hover .login-option__eyebrow{color:var(--text-primary)}.login-option__label{font-size:var(--font-size-lg);line-height:var(--line-height-base);font-weight:600;color:var(--text-primary)}.login-option__description{font-size:var(--font-size-sm);line-height:var(--line-height-snug);line-height:1.625}.login-option__chevron,.login-option__description{color:var(--text-muted)}.login-option__chevron{display:inline-flex;height:2.25rem;width:2.25rem;align-items:center;justify-content:center;border-radius:var(--radius-full);background-color:var(--surface-overlay-panel);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--border-overlay) 60%,transparent)}.login-option:focus-visible .login-option__chevron,.login-option:hover .login-option__chevron{background-color:var(--color-accent);color:var(--color-white);box-shadow:none}#playerModal.login-option__status{display:block}.login-option__status{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.5rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted)}#playerModal.login-option__capabilities{display:block}.login-option__capabilities{display:flex;flex-wrap:wrap;gap:.5rem;position:relative}.btn-ghost[aria-pressed=true],.btn-ghost[data-state=active]{border-color:transparent;background-color:var(--color-primary);color:var(--color-white);--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-ghost[aria-pressed=true]:focus-visible,.btn-ghost[aria-pressed=true]:hover,.btn-ghost[data-state=active]:focus-visible,.btn-ghost[data-state=active]:hover{background-color:var(--color-accent-strong)}.\!badge,.badge{display:inline-flex;align-items:center;gap:.25rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);padding:.25rem .75rem;font-size:var(--font-size-2xs);line-height:var(--line-height-tight);font-weight:600;text-transform:uppercase;letter-spacing:var(--tracking-extra-wide);color:var(--status-neutral-on)}.\!badge[data-variant=info],.badge[data-variant=info]{border-color:var(--status-info-border);background-color:var(--status-info-surface);color:var(--status-info-on)}.\!badge[data-variant=success],.badge[data-variant=success]{border-color:var(--status-success-border);background-color:var(--status-success-surface);color:var(--status-success-on)}.\!badge[data-variant=warning],.badge[data-variant=warning]{border-color:var(--status-warning-border);background-color:var(--status-warning-surface);color:var(--status-warning-on)}.\!badge[data-variant=critical],.badge[data-variant=critical]{border-color:var(--status-danger-border);background-color:var(--status-danger-surface);color:var(--status-danger-on)}.\!badge[data-variant=private],.badge[data-variant=private]{border-color:var(--status-private-border);background-color:var(--status-private-surface);color:var(--status-private-on)}.\!badge[data-variant=neutral],.badge[data-variant=neutral]{border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);color:var(--status-neutral-on)}.moderation-badge{display:inline-flex;flex-direction:column;align-items:flex-start;gap:var(--space-sm);border-radius:var(--radius-xl);border-width:1px;padding:.375rem .75rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:600;text-transform:none;letter-spacing:-.025em;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.moderation-badge__actions{display:inline-flex;flex-wrap:nowrap;align-items:center;gap:var(--space-sm)}.moderation-badge__label{display:inline-flex;align-items:center;gap:var(--space-xs);white-space:nowrap}.moderation-badge[data-variant=warning]{border-color:var(--status-warning-border);background-color:var(--status-warning-surface);color:var(--status-warning-on);box-shadow:0 12px 30px color-mix(in srgb,var(--status-warning) 18%,transparent)}.moderation-badge[data-variant=neutral]{border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);color:var(--status-neutral-on);box-shadow:0 12px 30px color-mix(in srgb,var(--status-neutral) 18%,transparent)}.moderation-badge__icon{display:inline-flex;height:1.25rem;width:1.25rem;align-items:center;justify-content:center;border-radius:var(--radius-full);background-color:var(--status-warning-surface);background-color:color-mix(in srgb,var(--status-warning) 18%,transparent);color:var(--status-warning-on)}.moderation-badge[data-variant=neutral] .moderation-badge__icon{background-color:var(--status-neutral-surface);background-color:color-mix(in srgb,var(--status-neutral) 18%,transparent);color:var(--status-neutral-on)}.moderation-badge__icon-mark{height:.875rem;width:.875rem}.moderation-badge__text{white-space:nowrap}.moderation-badge__action,.moderation-badge__text{font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:600;letter-spacing:-.025em}.moderation-badge__action{display:inline-flex;align-items:center;gap:.375rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--status-warning-border);background-color:var(--status-warning-surface);padding:.25rem .75rem;text-transform:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-timing-function:cubic-bezier(0,0,.2,1);background-color:color-mix(in srgb,var(--status-warning) 16%,transparent);border-color:color-mix(in srgb,var(--status-warning-border) 80%,transparent);color:var(--status-warning-on)}.moderation-badge[data-variant=warning] .moderation-badge__action:hover{background-color:color-mix(in srgb,var(--status-warning) 24%,transparent)}.moderation-badge[data-variant=neutral] .moderation-badge__action{border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);background-color:color-mix(in srgb,var(--status-neutral) 16%,transparent);border-color:color-mix(in srgb,var(--status-neutral-border) 80%,transparent);color:var(--status-neutral-on)}.moderation-badge[data-variant=neutral] .moderation-badge__action:hover{background-color:color-mix(in srgb,var(--status-neutral) 24%,transparent)}.moderation-badge__action:focus-visible{outline:var(--outline-thick-width) solid color-mix(in srgb,var(--status-warning) 38%,transparent);outline-offset:var(--space-3xs)}.moderation-badge[data-variant=neutral] .moderation-badge__action:focus-visible{outline:var(--outline-thick-width) solid color-mix(in srgb,var(--status-neutral) 38%,transparent)}.moderation-badge__action[disabled]{cursor:wait;opacity:.7}.pill{display:inline-flex;align-items:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);padding:.25rem .75rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:500;color:var(--status-neutral-on)}.pill[data-size=compact]{padding:.125rem .5rem;font-size:var(--font-size-2xs);line-height:var(--line-height-tight)}.pill[data-variant=info]{border-color:var(--status-info-border);background-color:var(--status-info-surface);color:var(--status-info-on)}.pill[data-variant=success]{border-color:var(--status-success-border);background-color:var(--status-success-surface);color:var(--status-success-on)}.pill[data-variant=warning]{border-color:var(--status-warning-border);background-color:var(--status-warning-surface);color:var(--status-warning-on)}.pill[data-variant=critical]{border-color:var(--status-danger-border);background-color:var(--status-danger-surface);color:var(--status-danger-on)}.pill[data-variant=private]{border-color:var(--status-private-border);background-color:var(--status-private-surface);color:var(--status-private-on)}.pill[data-variant=neutral]{border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);color:var(--status-neutral-on)}#playerModal.\!menu{display:block}.\!menu{display:flex;flex-direction:column;gap:.25rem;padding-top:.5rem;padding-bottom:.5rem}#playerModal.menu{display:block}.menu{display:flex;flex-direction:column;gap:.25rem;padding-top:.5rem;padding-bottom:.5rem}.menu__heading{padding-left:1rem;padding-right:1rem;padding-bottom:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:600;text-transform:uppercase;letter-spacing:.025em;color:var(--text-muted)}.menu__separator{margin-top:.5rem;margin-bottom:.5rem;height:1px;background-color:var(--border-default);--tw-bg-opacity:0.2}.menu__item{outline:2px solid transparent;outline-offset:2px}.menu__item:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.menu__item{display:block}.menu__item{position:relative;display:flex;width:100%;align-items:center;gap:.75rem;border-radius:var(--radius-md);padding:.5rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:500;color:var(--text-primary);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:var(--motion-ease-in-out)}.menu__item:hover{background-color:var(--surface-panel-hover);color:var(--text-primary)}.menu__item[data-state=active]{background-color:var(--color-accent);--tw-bg-opacity:0.2;color:var(--color-accent-strong)}.menu__item[data-variant=critical]{color:var(--color-critical-strong)}.menu__item[data-variant=critical]:hover{background-color:var(--color-critical);--tw-bg-opacity:0.2}.menu__item:disabled,.menu__item[aria-disabled=true]{cursor:not-allowed;opacity:.6}.menu__item:disabled,.menu__item:disabled:hover,.menu__item[aria-disabled=true],.menu__item[aria-disabled=true]:hover{background-color:transparent;color:var(--text-muted)}.\!card{--video-card-lift-translate-y:0!important;--video-card-backdrop-opacity:0!important;--video-card-backdrop-translate-y:1.25rem!important;--video-card-backdrop-scale:1.08!important;--video-card-backdrop-blur:var(--blur-thumbnail-blur)!important}.card{--video-card-lift-translate-y:0;--video-card-backdrop-opacity:0;--video-card-backdrop-translate-y:1.25rem;--video-card-backdrop-scale:1.08;--video-card-backdrop-blur:var(--blur-thumbnail-blur)}.\!card{outline:2px solid transparent;outline-offset:2px}.\!card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.\!card{position:relative;border-radius:var(--radius-lg);border-width:1px;border-color:transparent;background-color:var(--surface-raised);color:var(--text-primary);--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.card{outline:2px solid transparent;outline-offset:2px}.card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.card{position:relative;border-radius:var(--radius-lg);border-width:1px;border-color:transparent;background-color:var(--surface-raised);color:var(--text-primary);--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.\!card{transform:translate3d(0,var(--video-card-lift-translate-y),0)!important;transition-property:transform,box-shadow,border-color,background-color,color!important;will-change:transform!important;isolation:isolate!important}.card{transform:translate3d(0,var(--video-card-lift-translate-y),0);transition-property:transform,box-shadow,border-color,background-color,color;will-change:transform;isolation:isolate}.\!card:before{content:""!important;position:absolute!important;inset:0!important;z-index:0!important;opacity:var(--video-card-backdrop-opacity)!important;background-image:var(--video-card-thumb-url,none)!important;background-position:50%!important;background-repeat:no-repeat!important;background-size:cover!important;transform:translate3d(0,var(--video-card-backdrop-translate-y),0) scale(var(--video-card-backdrop-scale))!important;filter:blur(var(--video-card-backdrop-blur))!important;transition:opacity var(--motion-duration-base) var(--motion-ease-in-out),transform var(--motion-duration-base) var(--motion-ease-in-out),filter var(--motion-duration-base) var(--motion-ease-in-out)!important;pointer-events:none!important;will-change:opacity,transform,filter!important}.card:before{content:"";position:absolute;inset:0;z-index:0;opacity:var(--video-card-backdrop-opacity);background-image:var(--video-card-thumb-url,none);background-position:50%;background-repeat:no-repeat;background-size:cover;transform:translate3d(0,var(--video-card-backdrop-translate-y),0) scale(var(--video-card-backdrop-scale));filter:blur(var(--video-card-backdrop-blur));transition:opacity var(--motion-duration-base) var(--motion-ease-in-out),transform var(--motion-duration-base) var(--motion-ease-in-out),filter var(--motion-duration-base) var(--motion-ease-in-out);pointer-events:none;will-change:opacity,transform,filter}.\!card>*{position:relative!important;z-index:1!important}.card>*{position:relative;z-index:1}#playerModal.card--compact{display:block}.card--compact{display:flex;flex-direction:row;align-items:flex-start;gap:var(--space-md)}.card--compact .video-card__media{inline-size:clamp(8rem,calc(var(--video-modal-secondary-min, 320px)*.4),12.5rem);flex-basis:clamp(8rem,calc(var(--video-modal-secondary-min, 320px)*0.4),12.5rem);display:block;flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal.card--compact .video-card__content{display:block}.card--compact .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm);padding:var(--space-md)}.card--compact .video-card__title{font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:600;line-height:1.375}.card--compact .video-card__author-name{font-size:var(--font-size-xs);line-height:var(--line-height-tight)}#playerModal.card--compact .video-card__meta{display:block}.card--compact .video-card__meta{margin-top:0;display:flex;align-items:center;gap:.5rem;color:var(--text-muted-strong)}.card--compact .video-card__meta,.card--compact .video-card__meta .view-count-text{font-size:var(--font-size-2xs);line-height:var(--line-height-tight)}.card:focus-within,.card:hover{--video-card-lift-translate-y:-0.35rem;--video-card-backdrop-opacity:0.425;--video-card-backdrop-translate-y:0.15rem;--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:24px}.\!card:focus-within,.\!card:hover{--video-card-lift-translate-y:-0.35rem!important;--video-card-backdrop-opacity:0.425!important;--video-card-backdrop-translate-y:0.15rem!important;--video-card-backdrop-scale:1.12!important;--video-card-backdrop-blur:24px!important;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card:focus-within,.card:hover{--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card:focus-within,.\!card:hover{--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card[data-state=critical],.card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.\!card[data-state=private],.card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card[data-state=disabled],.card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-card__title{transition:color var(--motion-duration-base) var(--motion-ease-in-out)}.video-card__title:focus-visible,.video-card__title:hover{color:var(--color-accent)}.form-control{outline:2px solid transparent;outline-offset:2px}.form-control:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.form-control{display:block;width:100%;border-radius:var(--radius-md);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);padding:.5rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.form-control::-moz-placeholder{color:var(--text-muted)}.form-control::placeholder{color:var(--text-muted)}.form-control:hover{border-color:var(--border-default)}.form-control:focus-visible{border-color:var(--color-accent-strong)}.form-control:disabled{cursor:not-allowed;background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.form-control:invalid{border-color:var(--color-critical);--tw-border-opacity:0.6}.\!input[data-state=locked],.form-control[data-state=locked],.input[data-state=locked],.select[data-state=locked],.textarea[data-state=locked],select.form-control[data-state=locked],textarea.form-control[data-state=locked]{cursor:default;border-color:var(--border-sidebar-strong);background-color:var(--surface-overlay-panel-soft);color:var(--text-muted)}.form-control[data-state=locked]:focus-visible,.input[data-state=locked]:focus-visible,.select[data-state=locked]:focus-visible,.textarea[data-state=locked]:focus-visible,select.form-control[data-state=locked]:focus-visible,textarea.form-control[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:0px}.\!input[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:0px}.\!input[data-state=locked]:disabled,.form-control[data-state=locked]:disabled,.input[data-state=locked]:disabled,.select[data-state=locked]:disabled,.textarea[data-state=locked]:disabled,select.form-control[data-state=locked]:disabled,textarea.form-control[data-state=locked]:disabled{cursor:default;opacity:1}.\!input{outline:2px solid transparent;outline-offset:2px}.\!input:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.\!input{display:block;width:100%;border-radius:var(--radius-md);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);padding:.5rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.\!input::-moz-placeholder{color:var(--text-muted)}.\!input::placeholder{color:var(--text-muted)}.\!input:hover{border-color:var(--border-default)}.\!input:focus-visible{border-color:var(--color-accent-strong)}.\!input:disabled{cursor:not-allowed;background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.\!input:invalid{border-color:var(--color-critical);--tw-border-opacity:0.6}.\!input[data-state=locked],select.\!input[data-state=locked],textarea.\!input[data-state=locked]{cursor:default;border-color:var(--border-sidebar-strong);background-color:var(--surface-overlay-panel-soft);color:var(--text-muted)}.\!input[data-state=locked]:focus-visible,select.\!input[data-state=locked]:focus-visible,textarea.\!input[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:0px}.\!input[data-state=locked]:disabled,select.\!input[data-state=locked]:disabled,textarea.\!input[data-state=locked]:disabled{cursor:default;opacity:1}@media (prefers-reduced-motion:reduce){.\!input{transition:none}}body.iframe-form label+.\!input{margin-top:var(--space-xs)}.input{outline:2px solid transparent;outline-offset:2px}.input:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.input{display:block;width:100%;border-radius:var(--radius-md);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);padding:.5rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.input::-moz-placeholder{color:var(--text-muted)}.input::placeholder{color:var(--text-muted)}.input:hover{border-color:var(--border-default)}.input:focus-visible{border-color:var(--color-accent-strong)}.input:disabled{cursor:not-allowed;background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.input:invalid{border-color:var(--color-critical);--tw-border-opacity:0.6}.input[data-state=locked],select.input[data-state=locked],textarea.input[data-state=locked]{cursor:default;border-color:var(--border-sidebar-strong);background-color:var(--surface-overlay-panel-soft);color:var(--text-muted)}.input[data-state=locked]:focus-visible,select.input[data-state=locked]:focus-visible,textarea.input[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:0px}.input[data-state=locked]:disabled,select.input[data-state=locked]:disabled,textarea.input[data-state=locked]:disabled{cursor:default;opacity:1}@media (prefers-reduced-motion:reduce){.input{transition:none}}body.iframe-form label+.input{margin-top:var(--space-xs)}.textarea{outline:2px solid transparent;outline-offset:2px}.textarea:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.textarea{display:block;width:100%;border-radius:var(--radius-md);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);padding:.5rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.textarea::-moz-placeholder{color:var(--text-muted)}.textarea::placeholder{color:var(--text-muted)}.textarea:hover{border-color:var(--border-default)}.textarea:focus-visible{border-color:var(--color-accent-strong)}.textarea:disabled{cursor:not-allowed;background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.textarea:invalid{border-color:var(--color-critical);--tw-border-opacity:0.6}.textarea[data-state=locked],select.textarea[data-state=locked],textarea.textarea[data-state=locked]{cursor:default;border-color:var(--border-sidebar-strong);background-color:var(--surface-overlay-panel-soft);color:var(--text-muted)}.textarea[data-state=locked]:focus-visible,select.textarea[data-state=locked]:focus-visible,textarea.textarea[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:0px}.textarea[data-state=locked]:disabled,select.textarea[data-state=locked]:disabled,textarea.textarea[data-state=locked]:disabled{cursor:default;opacity:1}@media (prefers-reduced-motion:reduce){.textarea{transition:none}}body.iframe-form label+.textarea{margin-top:var(--space-xs)}.textarea{min-height:var(--form-textarea-min-height);resize:vertical}.select{outline:2px solid transparent;outline-offset:2px}.select:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.select{display:block;width:100%;border-radius:var(--radius-md);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);padding:.5rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.select::-moz-placeholder{color:var(--text-muted)}.select::placeholder{color:var(--text-muted)}.select:hover{border-color:var(--border-default)}.select:focus-visible{border-color:var(--color-accent-strong)}.select:disabled{cursor:not-allowed;background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.select:invalid{border-color:var(--color-critical);--tw-border-opacity:0.6}.select[data-state=locked],select.select[data-state=locked],textarea.select[data-state=locked]{cursor:default;border-color:var(--border-sidebar-strong);background-color:var(--surface-overlay-panel-soft);color:var(--text-muted)}.select[data-state=locked]:focus-visible,select.select[data-state=locked]:focus-visible,textarea.select[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:0px}.select[data-state=locked]:disabled,select.select[data-state=locked]:disabled,textarea.select[data-state=locked]:disabled{cursor:default;opacity:1}@media (prefers-reduced-motion:reduce){.select{transition:none}}body.iframe-form label+.select{margin-top:var(--space-xs)}.select{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding-right:2.5rem;background-image:linear-gradient(135deg,transparent 50%,var(--select-arrow-color) 50%),linear-gradient(45deg,var(--select-arrow-color) 50%,transparent 50%),linear-gradient(to right,var(--color-border-subtle),var(--color-border-subtle));background-position:right var(--form-select-icon-offset) center,right var(--form-select-icon-gap) center,right var(--form-select-icon-hit-area) center;background-size:var(--form-select-icon-size) var(--form-select-icon-size),var(--form-select-icon-size) var(--form-select-icon-size),var(--form-select-divider-width) 60%;background-repeat:no-repeat}.select,.select:hover{--select-arrow-color:var(--color-border)}.select:focus-visible{--select-arrow-color:var(--color-accent-strong)}.select:disabled{--select-arrow-color:var(--color-border-subtle)}.select:invalid{--select-arrow-color:var(--color-critical)}.\!checkbox{outline:2px solid transparent;outline-offset:2px}.\!checkbox:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.\!checkbox{display:inline-flex;height:1.25rem;width:1.25rem;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;align-items:center;justify-content:center;border-radius:.25rem;border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);color:var(--color-white);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.checkbox{outline:2px solid transparent;outline-offset:2px}.checkbox:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.checkbox{display:inline-flex;height:1.25rem;width:1.25rem;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;align-items:center;justify-content:center;border-radius:.25rem;border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);color:var(--color-white);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.\!checkbox:hover,.checkbox:hover{border-color:var(--border-default)}.\!checkbox:disabled,.checkbox:disabled{cursor:not-allowed;border-color:var(--border-subtle);background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.\!checkbox:after{content:""!important}.checkbox:after{content:""}.\!checkbox:after,.checkbox:after{pointer-events:none;height:.5rem;width:.75rem;border-bottom-width:2px;border-right-width:2px;border-color:var(--color-white);opacity:0;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.\!checkbox:after{transform:rotate(45deg) translateY(var(--form-checkbox-checkmark-offset)) scale(.85)!important}.checkbox:after{transform:rotate(45deg) translateY(var(--form-checkbox-checkmark-offset)) scale(.85)}.\!checkbox:checked,.\!checkbox[aria-checked=true],.checkbox:checked,.checkbox[aria-checked=true]{border-color:var(--color-accent-strong);background-color:var(--color-accent-strong)}.\!checkbox:checked:after,.\!checkbox[aria-checked=true]:after,.checkbox:checked:after,.checkbox[aria-checked=true]:after{opacity:1}.checkbox:checked:after,.checkbox[aria-checked=true]:after{transform:rotate(45deg) translateY(var(--form-checkbox-checkmark-offset)) scale(1)}.\!checkbox:checked:after,.\!checkbox[aria-checked=true]:after{transform:rotate(45deg) translateY(var(--form-checkbox-checkmark-offset)) scale(1)!important}.switch{outline:2px solid transparent;outline-offset:2px}.switch:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.switch{position:relative;display:inline-flex;height:1.5rem;width:2.75rem;cursor:pointer;align-items:center;border-radius:var(--radius-full);border-width:1px;border-color:var(--border-subtle);background-color:var(--border-subtle);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.switch,.switch:before{transition-duration:.15s;transition-duration:var(--motion-duration-base)}.switch:before{content:"";pointer-events:none;position:absolute;left:.125rem;top:50%;height:1.25rem;width:1.25rem;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:var(--radius-full);background-color:var(--surface-base);--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.switch:disabled{cursor:not-allowed;border-color:var(--border-subtle);background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.switch.is-on,.switch[aria-checked=true]{border-color:var(--color-accent-strong);background-color:var(--color-accent-strong)}.switch.is-on:before,.switch[aria-checked=true]:before{--tw-translate-x:1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (prefers-reduced-motion:reduce){.\!checkbox,.\!checkbox:after,.checkbox,.checkbox:after,.form-control,.menu__item,.popover-panel,.popover__panel,.switch,.switch:before{transition:none!important}}#playerModal.bv-modal{display:block}.bv-modal{position:fixed;inset:0;z-index:var(--z-modal-root);display:flex;align-items:center;justify-content:center;padding:1.5rem}@media (min-width:640px){.bv-modal{padding:2rem}}.bv-modal.hidden,.bv-modal[hidden]{display:none!important}.bv-modal__panel{outline:2px solid transparent;outline-offset:2px}.bv-modal__panel:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.bv-modal__panel{position:relative;z-index:var(--z-modal-content);width:100%;max-width:var(--modal-max-width);border-radius:var(--radius-modal-xl);border-width:1px;border-color:var(--border-overlay);padding:1.75rem 2rem 2rem;text-align:left;color:var(--text-primary);--tw-shadow:var(--shadow-modal);--tw-shadow-colored:var(--shadow-modal);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);background-color:var(--color-modal-surface,var(--color-overlay-panel))}.bv-modal__panel:focus-visible{border-color:var(--color-border-translucent-bright)}.bv-modal-backdrop{position:fixed;inset:0;z-index:var(--z-modal-overlay);--tw-backdrop-saturate:saturate(1.5);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);background-color:var(--color-modal-backdrop,var(--color-overlay-muted));backdrop-filter:blur(var(--blur-modal-backdrop));-webkit-backdrop-filter:blur(var(--blur-modal-backdrop))}.bv-modal-backdrop[data-variant=veil]{--tw-backdrop-blur:blur(64px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);background-color:var(--color-overlay-veil)}.bv-modal-backdrop[data-variant=muted]{background-color:var(--color-overlay-muted)}.modal-sheet{outline:2px solid transparent;outline-offset:2px}.modal-sheet:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.modal-sheet{z-index:var(--z-modal-content);max-width:var(--modal-max-width);border-radius:var(--radius-modal-xl);border-width:1px;border-color:var(--border-overlay);padding:1.75rem 2rem 2rem;text-align:left;color:var(--text-primary);--tw-shadow:var(--shadow-modal);--tw-shadow-colored:var(--shadow-modal);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);background-color:var(--color-modal-surface,var(--color-overlay-panel))}.modal-sheet:focus-visible{border-color:var(--color-border-translucent-bright)}@media (max-width:calc(1024px - 0.02px)){.modal-sheet{inline-size:100vw;block-size:100vh;max-width:none;max-height:none;margin:0;border-radius:0;border:0;padding:calc(var(--space-lg) + env(safe-area-inset-top)) calc(var(--space-lg) + env(safe-area-inset-right)) calc(var(--space-lg) + env(safe-area-inset-bottom)) calc(var(--space-lg) + env(safe-area-inset-left));overflow:auto}}#playerModal>.modal-sheet{max-width:none;width:100%;min-height:100%;padding:0;border-radius:0;box-shadow:none;border:none;background-color:var(--color-surface);display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;flex:1 1 auto}#modalContainer>[id$=Modal]:not(#playerModal) .modal-sheet{z-index:var(--z-modal-content)}#modalContainer>#profileModal .modal-sheet{z-index:var(--z-modal-top-content)}#modalContainer>#loginModal .modal-sheet{z-index:var(--z-modal-peak-content)}.modal-sheet{position:relative;margin-top:0;margin-bottom:0;width:100%;overflow:hidden;max-width:var(--modal-sheet-inline-max);overscroll-behavior:contain}@media (max-width:calc(1024px - 0.02px)){.bv-modal{align-items:stretch;justify-content:stretch;padding:0}.bv-modal__panel,.modal-sheet{inline-size:100vw;block-size:100vh;max-width:none;max-height:none;margin:0;border-radius:0;border:0;padding:calc(var(--space-lg) + env(safe-area-inset-top)) calc(var(--space-lg) + env(safe-area-inset-right)) calc(var(--space-lg) + env(safe-area-inset-bottom)) calc(var(--space-lg) + env(safe-area-inset-left));overflow:auto}}#playerModal.modal-header{display:block}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-md);padding:1.75rem 2rem 1rem;background:transparent;border:0}.modal-header[data-variant=flush]{padding:0;background:transparent}.modal-close{outline:2px solid transparent;outline-offset:2px}.modal-close:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.modal-close{display:block}.modal-close{display:flex;height:2.5rem;width:2.5rem;align-items:center;justify-content:center;border-radius:var(--radius-full);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);border:var(--border-width-hairline) solid var(--color-border-translucent-medium);background:var(--color-surface-tint-subtle);color:var(--color-text-frosted-strong)}.modal-close:hover{background:var(--color-border-translucent);border-color:var(--color-border-translucent-stronger);color:var(--color-text)}.modal-back{display:inline-flex;align-items:center;gap:.5rem}@media (min-width:1024px){.modal-back{display:none!important}}.modal-body>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.modal-body{padding:.5rem 2rem 2rem;overscroll-behavior:contain;scrollbar-width:thin}.modal-body::-webkit-scrollbar{width:var(--scrollbar-thin-width)}.modal-body::-webkit-scrollbar-track{background:transparent}.modal-body::-webkit-scrollbar-thumb{background:var(--color-border-translucent);border-radius:var(--radius-full)}#playerModal.modal-embed-frame{display:block}.modal-embed-frame{display:flex;height:var(--modal-embed-height);max-height:var(--modal-embed-height);width:100%;flex-direction:column}#playerModal.modal-footer{display:block}.modal-footer{display:flex;flex-direction:column;gap:.75rem;padding:1rem 2rem 2rem}#playerModal.modal-alert{display:block}.modal-alert{display:flex;align-items:flex-start;gap:.75rem;border-radius:var(--radius-md);border-width:1px;border-color:var(--status-warning-strong);--tw-border-opacity:0.6;padding:.75rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);backdrop-filter:blur(var(--blur-modal-alert));background-color:var(--color-warning-glass)}.modal-alert,.modal-alert__icon{color:var(--status-warning-strong)}.modal-alert__icon{margin-top:.125rem;height:1.25rem;width:1.25rem;flex-shrink:0}.modal-tile{border-radius:var(--radius-xl);border-width:1px;border-color:var(--border-overlay);background-color:var(--surface-overlay-panel-soft);padding:1rem 1.25rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-frosted-primary);background-color:var(--color-overlay-panel-glass-soft)}.modal-tile>h3{font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:600;color:var(--text-primary)}.modal-primary{outline:2px solid transparent;outline-offset:2px}.modal-primary:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.modal-primary{display:inline-flex;align-items:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:transparent;background-color:var(--color-accent-strong);padding:.625rem 1.25rem;font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:600;color:var(--color-white);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.modal-primary[data-size=sm]{padding:.375rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);gap:.375rem}.modal-primary:hover{background-color:var(--color-accent)}.modal-primary:focus-visible{background-color:var(--color-accent-pressed)}.modal-primary:disabled{pointer-events:none;cursor:not-allowed;opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}body.iframe-form button.modal-primary{align-self:flex-start;margin-top:var(--space-md)}.modal-primary{width:100%;justify-content:center}@media (prefers-reduced-motion:reduce){.modal-primary{transition-property:none}}.grid-stack{display:grid;gap:var(--space-lg)}.grid-stack[data-variant=cards]{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.grid-stack[data-variant=cards]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1280px){.grid-stack[data-variant=cards]{grid-template-columns:repeat(3,minmax(0,1fr))}}#playerModal.grid-stack[data-orientation=vertical]{display:block}.grid-stack[data-orientation=vertical]{display:flex;flex-direction:column;gap:var(--space-md)}#playerModal.bv-stack{display:block}.bv-stack{display:flex;flex-direction:column;gap:var(--space-lg)}.bv-stack--tight{gap:var(--space-md)}.bv-grid-video{display:grid;gap:var(--space-xl);padding-top:var(--space-lg);padding-bottom:var(--space-lg);grid-template-columns:repeat(auto-fill,minmax(var(--grid-card-min-width),1fr))}.\!card[data-alert=nsfw-owner],.card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.\!card[data-alert=nsfw-owner]:hover,.card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card img[data-thumbnail-state=blurred],.\!card img[data-visual-state=blurred],.card img[data-thumbnail-state=blurred],.card img[data-visual-state=blurred],.channel-profile-container img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.card img[data-thumbnail-state=blurred],.card img[data-visual-state=blurred],.channel-profile-container img[data-visual-state=blurred]{filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.\!card img[data-thumbnail-state=blurred],.\!card img[data-visual-state=blurred]{filter:blur(var(--blur-thumbnail-blur))!important;transform:scale(1.05)!important}#playerModal.channel-moderation-badge-anchor{display:block}.channel-moderation-badge-anchor{display:flex;flex-direction:column;gap:var(--space-sm);pointer-events:none;width:100%}.channel-moderation-badge-anchor:empty{display:none}.channel-moderation-badge-anchor>[data-moderation-badge=true]{pointer-events:auto;max-width:min(100%,28rem)}#playerModal .video-modal__video{position:relative}#playerModal .video-modal__video[data-visual-state=blurred] .video-modal__media{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}#playerModal .video-modal__moderation-overlay{position:absolute;inset:0;z-index:var(--z-modal-content);display:flex;align-items:center;justify-content:center;padding:clamp(var(--space-sm),3vw,var(--space-xl));background:color-mix(in srgb,var(--color-overlay-strong) 62%,transparent);text-align:center}#playerModal .video-modal__moderation-overlay[hidden]{display:none}#playerModal .video-modal__moderation-overlay .moderation-badge{padding-bottom:1rem;pointer-events:auto}#playerModal .video-modal__moderation-overlay[data-overlay-state=override]{align-items:flex-start;justify-content:flex-start;background:none;pointer-events:none}#playerModal .video-modal__moderation-overlay[data-overlay-state=override] .moderation-badge{pointer-events:auto}.\!card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy],.card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.video-visibility-toggle{display:inline-flex;align-items:center;gap:.25rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--border-default);background-color:var(--surface-panel);padding:.25rem}.video-visibility-toggle__option{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);border-width:1px;border-color:transparent;background-color:var(--surface-panel);padding:.5rem .75rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:600;text-transform:uppercase;color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.video-visibility-toggle__option:not(.is-disabled):hover{background-color:var(--surface-panel-hover);color:var(--text-primary)}.video-visibility-toggle__option[data-active=true]{border-width:1px;border-color:var(--color-accent);--tw-border-opacity:0.6;color:var(--text-primary);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-visibility-toggle__option.is-disabled{cursor:not-allowed;opacity:.45}#playerModal.video-modal__container{display:block}.video-modal__container{display:flex;height:100%;min-height:100vh;width:100%;flex:1 1 0%;flex-direction:column}#playerModal.player-modal__container{display:block}.player-modal__container{display:flex;height:100%;min-height:100vh;width:100%;flex:1 1 0%;flex-direction:column;gap:1.5rem}#playerModal{--player-modal-vertical-padding:var(--space-md-plus);--player-modal-primary-inline-padding-lg:clamp(var(--space-sm),2.75vw,var(--space-lg))}#playerModal>.bv-modal__panel{max-width:none;border-radius:0;flex-direction:column;align-items:stretch;justify-content:flex-start}#playerModal>.bv-modal__panel,.video-modal__panel{width:100%;min-height:100%;padding:0;box-shadow:none;border:none;background-color:var(--color-surface);display:flex;flex:1 1 auto}.video-modal__panel{position:relative;z-index:var(--z-modal-content);flex-direction:column;overflow:visible;margin:0;border-radius:0}#playerModal .watch-container{width:100%;max-width:var(--player-modal-inline-max,var(--layout-player-max-width));margin-inline:auto;padding-inline:var(
+ --player-modal-inline-padding,clamp(var(--space-md),4vw,var(--space-xl-plus))
+ );overflow:visible}#playerModal .watch-container.video-modal__similar{padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .video-modal__primary>.watch-container:first-child{padding-block-start:var(--player-modal-vertical-padding)}#playerModal .video-modal__primary>.watch-container:last-child,#playerModal .video-modal__secondary>.watch-container:last-child{padding-block-end:var(--player-modal-vertical-padding)}#playerModal .modal-nav .watch-container{max-width:none;margin-inline:0}#playerModal .modal-info-panel{display:flex;flex-direction:column;gap:var(--space-md-plus)}#playerModal h2#videoTitle{margin:0;font-size:clamp(var(--font-size-xl),calc(var(--font-size-modal-title-base) + .5vw),var(--font-size-xl-plus));font-weight:600;line-height:1.3;color:var(--color-text);min-width:0;overflow-wrap:break-word;word-break:break-word}@media (min-width:1024px){.video-modal__headline #videoTitle.video-modal__title{flex:1 1 0%;min-width:0;max-width:calc(100% - var(--space-3xl) - var(--space-md))}.video-modal__headline .video-modal__status{flex:0 0 auto;max-width:clamp(var(--space-xl),25%,var(--space-3xl));margin-left:auto}}#playerModal .modal-meta{font-size:var(--font-size-sm);line-height:1.4;color:var(--color-text-muted)}#playerModal .modal-view-count{font-weight:600;color:var(--color-text)}#playerModal .channel-card{align-items:center;gap:var(--space-md);padding:var(--space-md) var(--space-lg)}#playerModal .channel-card,#playerModal .player-modal__module{display:flex;border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}#playerModal .player-modal__module{flex-direction:column;gap:clamp(var(--space-2xs),1vw,var(--space-sm));width:100%;padding:clamp(var(--space-xs),1vw,var(--space-sm))}#playerModal .player-modal__module-title{margin:0;font-size:var(--font-size-lg);font-weight:600;line-height:1.3;color:var(--color-text)}#playerModal .player-modal__module-list{display:flex;flex-direction:column;gap:var(--space-sm);width:100%;margin:0;padding:0;list-style:none}#playerModal .video-modal__similar .player-modal__module{gap:clamp(var(--space-3xs),.6vw,var(--space-xs));padding:clamp(var(--space-2xs),.6vw,var(--space-xs));border-width:0;box-shadow:none;background-color:transparent}#playerModal .video-modal__similar .player-modal__module-title{padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-3xs),.6vw,var(--space-xs));padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs));padding-bottom:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .player-modal__module-scroll{max-height:clamp(14rem,calc(100vh - var(--player-modal-vertical-padding)*2 - var(--space-2xl)),28rem);overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-gutter:stable;padding-inline-end:var(--space-xs)}#playerModal .video-modal__similar .player-modal__module-scroll{max-height:none;overflow:visible;padding-inline-end:0;scrollbar-gutter:auto}#playerModal .player-modal__module-scroll>li{scroll-margin-block:var(--space-xs)}@media (max-width:30rem){#playerModal .modal-nav .icon-button{width:var(--space-xl);height:var(--space-xl);min-width:var(--space-xl);min-height:var(--space-xl)}#playerModal .watch-container{padding-inline:var(--space-sm)}#playerModal .channel-card{gap:var(--space-sm);padding:var(--space-sm) var(--space-md)}}#playerModal .channel-card__avatar{flex-shrink:0;width:var(--avatar-size-lg);height:var(--avatar-size-lg);border-radius:var(--radius-full);overflow:hidden;background-color:var(--color-border-subtle)}#playerModal .channel-card__avatar img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}#playerModal .channel-card__name{margin:0;font-size:var(--font-size-lg);font-weight:600;color:var(--color-text)}#playerModal .channel-card__npub{margin:0;font-size:var(--font-size-sm);color:var(--color-text-muted)}#playerModal .description-panel{padding:var(--space-md-plus) var(--space-lg);border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}#playerModal .description-panel p{margin:0;font-size:var(--font-size-sm);line-height:1.6;color:var(--color-text)}.player-modal__content{position:relative;z-index:var(--z-modal-content);display:flex;flex:1 1 auto;width:100%;min-height:100%;flex-direction:column;overflow:visible;padding:0;margin:0;border:none;border-radius:0;background-color:var(--color-surface);box-shadow:none}@media (prefers-reduced-motion:reduce){.player-modal__content{transition-property:none}}#playerModal .modal-nav{position:sticky;top:0;z-index:var(--z-overlay-nav);--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--surface-overlay-panel);background-image:linear-gradient(180deg,color-mix(in srgb,var(--surface-overlay-panel) 94%,transparent) 0,color-mix(in srgb,var(--surface-overlay-panel) 88%,transparent) 100%);border-bottom:var(--border-width-hairline) solid var(--border-overlay);backdrop-filter:blur(var(--modal-nav-backdrop-blur));-webkit-backdrop-filter:blur(var(--modal-nav-backdrop-blur))}#playerModal#playerModal .modal-nav__bar{display:block}#playerModal .modal-nav__bar{display:flex;width:100%;align-items:center;justify-content:flex-start;gap:.5rem;padding-block:var(--space-xs);padding-inline:clamp(var(--space-sm),2.5vw,var(--space-lg))}#playerModal#playerModal .modal-nav__rail{display:block}#playerModal .modal-nav__rail{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.5rem}#playerModal#playerModal .modal-nav__slot{display:block}#playerModal .modal-nav__slot{display:flex;align-items:center;gap:.5rem}#playerModal .modal-nav__slot--context{justify-content:flex-end;margin-inline-start:auto;min-inline-size:0}#playerModal .modal-nav__context{font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:600;text-transform:uppercase;letter-spacing:.025em;color:var(--text-muted)}#playerModal#playerModal .modal-nav .modal-actions{display:block}#playerModal .modal-nav .modal-actions{display:flex;align-items:center;gap:.5rem;margin-left:0;align-self:stretch}#playerModal .modal-nav .icon-button{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-timing-function:cubic-bezier(0,0,.2,1);inline-size:var(--icon-size-md);block-size:var(--icon-size-md);background:var(--surface-overlay-panel-tint);border:var(--border-width-hairline) solid var(--border-overlay);color:var(--color-text-muted)}#playerModal .modal-nav .icon-button:focus-visible,#playerModal .modal-nav .icon-button:hover{background:var(--surface-overlay-panel-soft);border-color:var(--border-overlay-strong);color:var(--color-text)}.modal-nav--hidden{--tw-translate-y:-100%}.modal-nav--hidden,.modal-nav--visible{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-nav--visible{--tw-translate-y:0px}.video-modal__layout{--video-modal-secondary-size:clamp(var(--video-modal-secondary-min),46vw,var(--video-modal-secondary-max));--video-modal-gap:clamp(var(--space-2xs),0.85vw,var(--space-sm));display:grid;gap:var(--video-modal-gap);width:min(100%,var(--video-modal-layout-max));margin-inline:auto;grid-template-columns:minmax(0,1fr);grid-template-areas:"primary" "secondary";align-items:start}@media (min-width:1024px){.video-modal__layout{--video-modal-secondary-size:clamp(var(--video-modal-secondary-min),30vw,var(--video-modal-secondary-max));grid-template-columns:minmax(0,1fr) minmax(var(--video-modal-secondary-min),var(--video-modal-secondary-size));grid-template-areas:"primary secondary";align-items:stretch;grid-auto-flow:column}.video-modal__primary{min-width:0;min-height:0;grid-area:primary}.video-modal__secondary{min-width:var(--video-modal-secondary-min);min-height:0;max-width:var(--video-modal-secondary-size);grid-area:secondary;height:100%}#playerModal .video-modal__layout .watch-container{max-width:none;margin-inline:0;width:100%}}@media (min-width:1280px){.video-modal__layout{--video-modal-secondary-size:clamp(var(--video-modal-secondary-min),28vw,var(--video-modal-secondary-max))}}#playerModal .video-modal__layout{display:grid}@media (min-width:1024px){#playerModal .video-modal__layout{grid-template-columns:minmax(0,1fr) minmax(var(--video-modal-secondary-min),var(--video-modal-secondary-size));grid-template-areas:"primary secondary";align-items:stretch;grid-auto-flow:column}#playerModal .video-modal__primary{grid-area:primary}#playerModal .video-modal__secondary{grid-area:secondary}#playerModal .modal-nav__bar,#playerModal .video-modal__primary{--player-modal-inline-padding:var(
+ --player-modal-primary-inline-padding-lg,clamp(var(--space-sm),2.75vw,var(--space-lg))
+ )}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem;min-width:0;min-height:0}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem;min-width:0;min-height:0;width:100%;align-self:stretch}.video-modal__similar{display:none}@media (min-width:1024px){#playerModal.video-modal__similar{display:block}.video-modal__similar{display:flex;flex-direction:column;gap:1.5rem;align-self:stretch;flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module{height:100%;display:flex;flex:1 1 auto;min-height:0;flex-direction:column}#playerModal .video-modal__similar .player-modal__module-list{flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module-scroll{height:100%;max-height:none;min-height:0}}#playerModal .player-modal__module-item{width:100%}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{width:clamp(5.75rem,18vw,8rem)}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media{width:clamp(5.75rem,18vw,8rem)!important}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media,#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))!important}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-2xs),.85vw,var(--space-sm))}.player-modal__similar-card{outline:2px solid transparent;outline-offset:2px}.player-modal__similar-card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.player-modal__similar-card{display:block}.player-modal__similar-card{position:relative;display:flex;width:100%;min-width:0;align-items:flex-start;gap:.75rem;border-radius:var(--radius-xl);padding:.5rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.player-modal__similar-card:focus-within,.player-modal__similar-card:hover{background-color:var(--surface-panel-hover)}.player-modal__similar-card-media{position:relative;display:block;flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg);background-color:var(--surface-alt);width:168px;aspect-ratio:16/9}@media (max-width:640px){.player-modal__similar-card-media{width:140px}}.player-modal__similar-card-media img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;border-radius:inherit}#playerModal.player-modal__similar-card-content{display:block}.player-modal__similar-card-content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:.25rem}.player-modal__similar-card-title{font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;line-height:1.375;color:var(--text-primary);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.player-modal__similar-card-title:focus-visible,.player-modal__similar-card:hover .player-modal__similar-card-title{color:var(--color-accent-strong)}#playerModal.player-modal__similar-card-author{display:block}.player-modal__similar-card-author{display:flex;min-width:0;align-items:center}#playerModal.player-modal__similar-card-author-meta{display:block}.player-modal__similar-card-author-meta{display:flex;min-width:0;flex-direction:column}.player-modal__similar-card-author-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:500;color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.player-modal__similar-card:hover .player-modal__similar-card-author-name{color:var(--color-text-strong)}.player-modal__similar-card-author-npub{display:none}#playerModal.player-modal__similar-card-meta{display:block}.player-modal__similar-card-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem;font-size:var(--font-size-2xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.player-modal__similar-card-timestamp{white-space:nowrap}.video-modal__stage{position:relative;isolation:isolate;overflow:visible;--video-modal-ambient-bleed:clamp(var(--space-2xl),12vw,var(--space-5xl));--video-modal-ambient-scale:1.38;--video-modal-ambient-blur:clamp(128px,20vw,256px);--video-modal-ambient-opacity:1}.video-modal__video{position:relative;z-index:1;border-radius:var(--radius-xl);padding-block:clamp(var(--space-xs),2.5vw,var(--space-md-plus));background-color:transparent;box-shadow:0 0 0 1px color-mix(in srgb,var(--color-border-strong) 8%,transparent) inset;overflow:visible}.video-modal__ambient{position:absolute;inset:calc(var(--video-modal-ambient-bleed)*-1);border-radius:calc(var(--radius-3xl) + var(--video-modal-ambient-bleed));pointer-events:none;z-index:0;display:block;overflow:visible}.video-modal__ambient:before{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--color-overlay-strong) 18%,transparent) 0,color-mix(in srgb,var(--color-overlay) 34%,transparent) 62%,transparent 100%);opacity:.9;mix-blend-mode:screen;pointer-events:none}.video-modal__ambient-canvas{position:relative;width:100%;height:100%;display:block;transform:scale(var(--video-modal-ambient-scale));filter:blur(var(--video-modal-ambient-blur)) saturate(2) brightness(1.24);opacity:var(--video-modal-ambient-opacity)}@media (min-width:1024px){.video-modal__video{position:sticky;top:calc(var(--space-2xl) + var(--space-3xl));z-index:1}.video-modal__status{flex-basis:clamp(var(--space-xl),25%,var(--space-3xl));max-width:clamp(var(--space-xl),25%,var(--space-3xl))}.video-modal__details{position:relative;z-index:2;margin-top:var(--space-lg)}}#playerModal .video-modal__video video{position:relative;z-index:1;background-color:var(--color-black);border-radius:var(--radius-lg);box-shadow:var(--shadow-video-modal);max-height:min(80vh,56.25vw);-o-object-fit:contain;object-fit:contain}.video-modal__media{display:block;aspect-ratio:16/9;width:100%;-o-object-fit:contain;object-fit:contain}#playerModal.video-modal__details{display:block}.video-modal__details{display:flex;flex-direction:column;gap:1.5rem}.video-modal__meta{border-bottom-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;padding-bottom:1rem}#playerModal.video-modal__tags{display:block}.video-modal__tags{margin-left:-.25rem;margin-right:-.25rem;display:flex;gap:.5rem;padding-left:.25rem;padding-right:.25rem}.video-modal__similar .video-modal__tags{margin-bottom:clamp(var(--space-xs),.9vw,var(--space-md))}.video-modal__tags[hidden]{display:none}.video-modal__tags[data-fade]{position:relative}.video-modal__tags[data-fade]:after,.video-modal__tags[data-fade]:before{content:"";position:sticky;top:0;bottom:0;width:var(--space-lg);pointer-events:none;z-index:1}.video-modal__tags[data-fade]:before{left:0;background:linear-gradient(to right,var(--surface-overlay-muted),transparent)}.video-modal__tags[data-fade]:after{right:0;background:linear-gradient(to left,var(--surface-overlay-muted),transparent)}.video-tag-strip{display:flex;flex-wrap:nowrap;align-items:center;gap:var(--space-xs);overflow:hidden}.video-tag-pill{display:inline-flex;align-items:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--status-neutral-border);background-color:var(--status-neutral-surface);padding:.25rem .75rem;font-size:var(--font-size-2xs);line-height:var(--line-height-tight);font-weight:500;color:var(--status-neutral-on);position:relative;justify-content:center;white-space:nowrap;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;min-height:calc(var(--space-sm) + var(--space-xs));margin-inline-end:.25rem}.video-tag-pill__label{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:1}#playerModal.video-tag-pill__icon{display:block}.video-tag-pill__icon{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;transform:scale(.8)}.video-tag-pill__icon svg{height:.75rem;width:.75rem}.video-tag-pill:focus-visible .video-tag-pill__label,.video-tag-pill:hover .video-tag-pill__label{opacity:0}.video-tag-pill:focus-visible .video-tag-pill__icon,.video-tag-pill:hover .video-tag-pill__icon{opacity:1;transform:scale(1)}#playerModal.video-modal__actions-row{display:block}.video-modal__actions-row{display:flex;width:100%;flex-wrap:wrap;align-items:flex-start;row-gap:.75rem;-moz-column-gap:.75rem;column-gap:.75rem}@media (min-width:640px){.video-modal__actions-row{-moz-column-gap:1rem;column-gap:1rem}}.video-modal__actions-group,.video-modal__reactions{flex:1 1 var(--menu-min-width);min-inline-size:0}#playerModal.video-modal__reactions{display:block}.video-modal__reactions{display:flex;flex-basis:100%;flex-wrap:wrap;align-items:flex-start;gap:.75rem}@media (min-width:640px){.video-modal__reactions{gap:1rem}}@media (min-width:768px){.video-modal__reactions{flex-basis:auto}}.video-modal__reaction-buttons{display:inline-flex;flex-wrap:wrap;align-items:center;gap:.5rem}.video-modal__reaction-button{--video-modal-reaction-active-bg:color-mix(in srgb,var(--color-accent) 22%,var(--surface-overlay-panel));--video-modal-reaction-active-border:color-mix(in srgb,var(--color-accent) 38%,var(--border-overlay))}.video-modal__reaction-button[aria-pressed=true]{color:var(--color-accent-strong);background-color:var(--video-modal-reaction-active-bg);border-color:var(--video-modal-reaction-active-border)}.video-modal__reaction-button[aria-pressed=true] .video-modal__reaction-count{color:var(--color-accent-strong)}.video-modal__reaction-count{font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;color:var(--text-muted)}.video-modal__reaction-meter{display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap;flex:1 1 var(--menu-min-width);max-inline-size:100%}.video-modal__reaction-meter-label{font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:500;color:var(--text-muted);min-inline-size:6rem}.video-modal__reaction-meter-track{position:relative;flex:1 1 auto;inline-size:100%;block-size:var(--space-xs);border-radius:var(--radius-full);background-color:var(--color-border-muted);overflow:hidden}.video-modal__reaction-meter-fill{position:absolute;inset-inline-start:0;inset-block-start:0;block-size:100%;inline-size:0;max-inline-size:100%;background-image:linear-gradient(90deg,var(--color-accent) 0,var(--color-accent-strong) 100%);transition:inline-size var(--motion-duration-base) var(--motion-ease-out)}#playerModal.video-modal__actions-group{display:block}.video-modal__actions-group{display:flex;flex-basis:100%;flex-wrap:wrap;align-items:flex-start;justify-content:flex-start;gap:.75rem}@media (min-width:768px){.video-modal__actions-group{flex-basis:auto;justify-content:flex-end;margin-inline-start:auto}}.video-modal__action-button{display:inline-flex;height:2.75rem;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-full);font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);min-width:var(--video-modal-action-min-width);padding-inline:var(--space-md);border-width:1px;border-style:solid}.bv-modal.video-modal__action-label,.profile-pane.video-modal__action-label,.video-modal__action-label{display:none}@media (min-width:640px){.video-modal__action-label{display:inline}}.video-modal__action-icon{height:1.25rem;width:1.25rem}.video-modal__channel,.video-modal__description,.video-modal__stats{border-radius:1rem;border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;background-color:var(--surface-base);padding:1rem 1.25rem;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-modal__description-body{overflow-wrap:anywhere;word-break:break-word}.video-modal__description-body a{color:var(--color-accent-strong);text-decoration:underline;text-decoration-color:currentColor;word-break:break-word}.video-modal__description-body a:focus-visible,.video-modal__description-body a:hover{color:var(--color-accent-pressed)}.video-modal__comments{border-radius:1rem;border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;background-color:var(--surface-base);padding:1.25rem;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.comment-thread,.video-modal__comments{display:flex;flex-direction:column;gap:var(--space-lg)}.comment-thread__header{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-sm)}.comment-thread__title{font-size:var(--font-size-lg);line-height:var(--line-height-base);font-weight:600;color:var(--text-primary)}.comment-thread__count,.comment-thread__empty{font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted);margin:0}.comment-thread__empty{padding:var(--space-md);border-radius:var(--radius-lg);border:1px dashed color-mix(in srgb,var(--border-default) 40%,transparent);background-color:var(--surface-alt)}.comment-thread__list{display:flex;flex-direction:column;gap:var(--space-md);margin:0;padding:0;list-style:none}.comment-thread__item{display:grid;grid-template-columns:auto 1fr;gap:var(--space-md);padding-block:var(--space-sm);border-bottom:1px solid var(--border-subtle)}.comment-thread__item:last-child{border-bottom:none;padding-bottom:0}.comment-thread__avatar{inline-size:2.5rem;block-size:2.5rem}.comment-thread__avatar img{border-radius:var(--radius-full);-o-object-fit:cover;object-fit:cover;inline-size:100%;block-size:100%}.comment-thread__content{display:flex;flex-direction:column;gap:var(--space-xs);min-inline-size:0}.comment-thread__meta{font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted);display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-xs)}.comment-thread__author{font-weight:600;color:var(--text-primary)}.comment-thread__timestamp{color:var(--text-muted)}.comment-thread__body{color:var(--text-primary);line-height:1.6;word-break:break-word}.comment-thread__actions{font-size:var(--font-size-xs);line-height:var(--line-height-tight);display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-sm)}.comment-thread__actions button{outline:2px solid transparent;outline-offset:2px}.comment-thread__actions button:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.comment-thread__actions button{display:inline-flex;align-items:center;gap:.25rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-base);padding:.25rem .75rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:600;color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.comment-thread__actions button:focus-visible,.comment-thread__actions button:hover{color:var(--text-primary);border-color:var(--border-default)}.comment-thread__replies{display:flex;flex-direction:column;gap:var(--space-md);margin:0;margin-inline-start:calc(var(--space-lg) + var(--space-sm));padding-inline-start:var(--space-md);padding-block-start:var(--space-md);border-inline-start:1px solid var(--border-subtle);list-style:none}.comment-thread__load-more{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-base);padding:.5rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;color:var(--text-primary);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.comment-thread__load-more:focus-visible,.comment-thread__load-more:hover{border-color:var(--border-default);color:var(--text-primary);background-color:color-mix(in srgb,var(--surface-alt) 70%,transparent)}.comment-thread__disabled{font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted);border-radius:var(--radius-lg);border:1px dashed color-mix(in srgb,var(--border-default) 32%,transparent);background-color:var(--surface-alt);padding:var(--space-md)}.comment-composer{display:flex;flex-direction:column;gap:var(--space-md);border-top:1px solid var(--border-subtle);padding-top:var(--space-lg)}.comment-composer__field{display:flex;flex-direction:column;gap:var(--space-2xs)}.comment-composer__label{font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;color:var(--text-primary)}.comment-composer__textarea{min-block-size:7rem}.comment-composer__footer{display:flex;flex-direction:column;gap:var(--space-sm)}.comment-composer__hint{font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted);margin:0}.comment-composer__actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:var(--space-sm)}.comment-composer__char-count{font-size:var(--font-size-xs);line-height:var(--line-height-tight);font-weight:500;color:var(--text-muted)}.comment-composer__submit{outline:2px solid transparent;outline-offset:2px}.comment-composer__submit:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.comment-composer__status{margin:0}@media (min-width:640px){.comment-composer__footer{flex-direction:row;align-items:center;justify-content:space-between}}.video-modal__progress{height:.5rem;width:100%;overflow:hidden;border-radius:var(--radius-full);background-color:var(--border-subtle)}#playerModal.video-modal__stat-metrics{display:block}.video-modal__stat-metrics{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted)}.profile-modal{display:flex;flex-direction:column;inline-size:100%;block-size:100%;min-block-size:100%;max-block-size:none;overflow:hidden;--profile-modal-scale:0.9;--profile-modal-gap:calc((var(--space-md) - var(--space-3xs))*var(--profile-modal-scale));--profile-modal-padding-inline:calc((var(--space-lg) - var(--space-md))*var(--profile-modal-scale));--profile-modal-padding-block-start:calc((var(--space-md) - var(--space-2xs))*var(--profile-modal-scale));--profile-modal-padding-block-end:calc((var(--space-lg) - var(--space-sm))*var(--profile-modal-scale));--profile-modal-nav-gap:calc(var(--space-xs)*var(--profile-modal-scale))}.profile-modal__content{display:flex;flex:1 1 auto;flex-direction:column;block-size:100%;min-block-size:0;transform:scale(var(--profile-modal-scale));transform-origin:top center}.profile-modal__active-profile{outline:2px solid transparent;outline-offset:2px}.profile-modal__active-profile:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.profile-modal__active-profile{display:block}.profile-modal__active-profile{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:1rem}.profile-modal__active-details{display:flex;flex:1 1 12rem;flex-direction:column;gap:var(--space-2xs);min-inline-size:0}.profile-modal__active-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-xs)}.profile-modal__channel-link{margin-inline-start:auto;align-self:center;white-space:nowrap}.profile-hashtag-list{display:flex;flex-wrap:wrap;gap:var(--space-xs);margin:0;padding:0;list-style:none}.profile-hashtag-item{display:inline-flex;align-items:center;gap:var(--space-3xs);padding-block:var(--space-3xs);padding-inline:var(--space-sm);border-radius:var(--radius-full);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-panel);color:var(--color-text);font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);line-height:var(--font-lineheight-tight)}.profile-hashtag-remove{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--space-md);block-size:var(--space-md);border-radius:var(--radius-full);color:var(--color-text-muted);background:transparent;border:none;padding:0;cursor:pointer}.profile-hashtag-remove:hover{color:var(--color-text)}.profile-modal__remove-button{border-width:2px}.profile-modal__remove-button:focus-visible{--tw-ring-color:var(--color-critical-strong)}.profile-modal__add-profile{--tw-ring-color:var(--color-ring-alert);outline:2px solid transparent;outline-offset:2px}.profile-modal__add-profile:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.profile-modal__add-profile{display:block}.profile-modal__add-profile{position:relative;display:flex;width:100%;align-items:center;justify-content:flex-start;gap:1rem;border-radius:var(--radius-xl);padding:1rem 1.25rem;text-align:left;color:var(--text-primary);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);border:var(--outline-thick-width) solid var(--color-ring-alert);background-color:var(--surface-panel-hover)}.profile-modal__add-profile:hover{background-color:color-mix(in srgb,var(--surface-panel-hover) 88%,var(--color-ring-alert) 12%)}.profile-modal__add-profile:focus-visible{background-color:color-mix(in srgb,var(--surface-panel-hover) 80%,var(--color-ring-alert) 20%)}#playerModal.profile-modal__add-profile-icon{display:block}.profile-modal__add-profile-icon{display:flex;height:2.5rem;width:2.5rem;align-items:center;justify-content:center;border-radius:var(--radius-full);background-color:color-mix(in srgb,var(--color-ring-alert) 18%,var(--surface-panel));color:var(--color-ring-alert)}.profile-modal__body{overflow:hidden;overflow-x:hidden;padding-block-start:var(--profile-modal-padding-block-start);padding-block-end:var(--profile-modal-padding-block-end);padding-inline:var(--profile-modal-padding-inline)}.profile-modal__body,.profile-modal__layout{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--profile-modal-gap);inline-size:100%;min-block-size:0}.profile-modal__header{align-items:center}.profile-modal__header-title{display:flex;align-items:center;gap:var(--space-sm)}.profile-modal__menu{display:flex;flex-direction:column;gap:var(--space-sm)}.profile-modal__pane-wrapper{display:flex;flex:1 1 auto;flex-direction:column;min-block-size:0}.profile-modal__nav{display:flex;flex-wrap:wrap;gap:var(--profile-modal-nav-gap);align-self:flex-start;overflow-x:hidden}.profile-modal__panes{flex:1 1 auto;block-size:100%;min-block-size:0;overflow-y:auto;padding-inline-end:var(--profile-modal-scrollbar-offset);scrollbar-gutter:stable}.profile-modal__panes,.profile-pane{display:flex;flex-direction:column;overflow-x:hidden}.profile-pane{min-block-size:auto}.profile-pane.hidden{display:none!important}@supports not (scrollbar-gutter:stable){.profile-modal__panes{padding-inline-end:calc(var(--profile-modal-scrollbar-offset) + var(--space-2xs))}}@media (max-width:calc(1024px - 0.02px)){.profile-modal__layout[data-mobile-view=menu] .profile-modal__pane-wrapper,.profile-modal__layout[data-mobile-view=pane] .profile-modal__menu{display:none}}@media (min-width:1024px){.profile-modal{max-block-size:var(--modal-shell-max-height)}.profile-modal__body{padding-inline:calc(var(--profile-modal-padding-inline) + var(--space-xs))}.profile-modal__body,.profile-modal__layout{gap:calc(var(--profile-modal-gap) + var(--space-2xs))}.profile-modal__layout{display:grid;align-items:stretch;block-size:100%;grid-template-columns:minmax(var(--profile-modal-nav-min-width),var(--profile-modal-nav-max-width)) minmax(0,1fr);grid-auto-rows:minmax(0,1fr)}.profile-modal__nav{flex-direction:column;flex-wrap:nowrap;gap:var(--profile-modal-nav-gap);justify-self:start;inline-size:var(--profile-modal-nav-max-width);min-inline-size:var(--profile-modal-nav-min-width)}.profile-modal__pane-wrapper{block-size:100%}}.beacon-hidden-download,.torrent-download-anchor,[data-beacon-hidden-download]{display:none!important}.beacon-clipboard-offscreen,.torrent-clipboard-textarea,[data-beacon-clipboard-state=offscreen],[data-beacon-clipboard=offscreen]{position:fixed;top:0;left:var(--offscreen-offset);opacity:0;width:0;height:0;pointer-events:none}.beacon-toast-motion,.torrent-toast-motion{transition-property:opacity,transform;will-change:opacity,transform;opacity:0;transform:translateY(var(--toast-translate))}.beacon-toast-motion[data-beacon-motion=enter],.beacon-toast-motion[data-beacon-motion=show],.torrent-toast-motion[data-beacon-motion=enter],.torrent-toast-motion[data-beacon-motion=show]{opacity:1;transform:translateY(0)}.beacon-toast-motion[data-beacon-motion=exit],.torrent-toast-motion[data-beacon-motion=exit]{opacity:0;transform:translateY(var(--toast-translate))}.torrent-toast{pointer-events:auto;width:100%;max-width:28rem;border-radius:var(--radius-lg);padding:.75rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:500;letter-spacing:-.025em;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset}.torrent-toast--info{border-width:1px;border-color:var(--border-overlay);background-color:var(--surface-base);color:var(--text-primary)}.torrent-toast--success{border-width:1px;border-color:var(--color-info);--tw-border-opacity:0.6;background-color:var(--color-info);color:var(--color-white)}.torrent-toast--error{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.6;background-color:var(--color-critical);color:var(--color-white)}.torrent-toast--warn{border-width:1px;border-color:var(--status-warning-strong);--tw-border-opacity:0.7;background-color:var(--status-warning-strong);color:var(--status-warning-strong)}.watch-history-card{--video-card-lift-translate-y:0;--video-card-backdrop-opacity:0;--video-card-backdrop-translate-y:1.25rem;--video-card-backdrop-scale:1.08;--video-card-backdrop-blur:var(--blur-thumbnail-blur);outline:2px solid transparent;outline-offset:2px}.watch-history-card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.watch-history-card{position:relative;border-radius:var(--radius-lg);border-width:1px;border-color:transparent;background-color:var(--surface-raised);color:var(--text-primary);--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transform:translate3d(0,var(--video-card-lift-translate-y),0);transition-property:transform,box-shadow,border-color,background-color,color;will-change:transform;isolation:isolate}.watch-history-card:before{content:"";position:absolute;inset:0;z-index:0;opacity:var(--video-card-backdrop-opacity);background-image:var(--video-card-thumb-url,none);background-position:50%;background-repeat:no-repeat;background-size:cover;transform:translate3d(0,var(--video-card-backdrop-translate-y),0) scale(var(--video-card-backdrop-scale));filter:blur(var(--video-card-backdrop-blur));transition:opacity var(--motion-duration-base) var(--motion-ease-in-out),transform var(--motion-duration-base) var(--motion-ease-in-out),filter var(--motion-duration-base) var(--motion-ease-in-out);pointer-events:none;will-change:opacity,transform,filter}.watch-history-card>*{position:relative;z-index:1}.watch-history-card:focus-within,.watch-history-card:hover{--video-card-lift-translate-y:-0.35rem;--video-card-backdrop-opacity:0.425;--video-card-backdrop-translate-y:0.15rem;--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:24px;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.watch-history-card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.watch-history-card[data-state=disabled],.watch-history-card[data-state=private]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.watch-history-card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.watch-history-card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card img[data-thumbnail-state=blurred],.watch-history-card img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.watch-history-card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__media{width:clamp(5.75rem,18vw,8rem);flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.watch-history-card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}#playerModal.watch-history-card{display:block}.watch-history-card{display:flex;flex-wrap:wrap;align-items:stretch;gap:1.25rem;padding:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}@media (prefers-reduced-motion:reduce){.watch-history-card,.watch-history-card[data-motion=enter]{transition-property:none}.watch-history-card[data-motion=enter]{opacity:1;animation:none}}.watch-history-card:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.watch-history-card{gap:1.5rem;padding:1.25rem}}.watch-history-card--private{border-color:var(--color-info);--tw-border-opacity:0.6}#playerModal.watch-history-card__primary{display:block}.watch-history-card__primary{display:flex;min-width:0;flex:1 1 0%;flex-wrap:wrap;align-items:stretch;gap:1rem}@media (min-width:640px){.watch-history-card__primary{flex-wrap:nowrap}}.watch-history-card__thumbnail{display:block;flex:none;color:var(--text-primary);text-decoration-line:none}@media (min-width:640px){.watch-history-card__thumbnail{width:14rem}}.watch-history-card__thumbnailInner{aspect-ratio:16/9;width:100%;overflow:hidden;border-radius:var(--radius-lg);background-color:var(--surface-base)}.watch-history-card__thumbnailInner img{height:100%;width:100%;-o-object-fit:cover;object-fit:cover}.watch-history-card [data-thumbnail-state=blurred] img,.watch-history-card [data-visual-state=blurred] img{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}#playerModal.watch-history-card__details{display:block}.watch-history-card__details{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;justify-content:center;gap:var(--space-sm)}.watch-history-card__title{cursor:pointer;font-size:var(--font-size-lg);line-height:var(--line-height-base);font-weight:600;line-height:1.375;color:var(--text-primary);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.watch-history-card__title:focus,.watch-history-card__title:hover{color:var(--color-accent)}.watch-history-card__created{font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted)}#playerModal.watch-history-card__meta{display:block}.watch-history-card__meta{display:flex;min-width:0;flex-direction:column;justify-content:space-between;gap:1rem}@media (min-width:640px){.watch-history-card__meta{min-width:14rem;flex:none;flex-basis:16rem}}.watch-history-card__watched{font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--border-subtle)}#playerModal.watch-history-card__creator{display:block}.watch-history-card__creator{display:flex;align-items:center;gap:.75rem}#playerModal.watch-history-card__creatorAvatar{display:block}.watch-history-card__creatorAvatar{display:flex;height:2.5rem;width:2.5rem;align-items:center;justify-content:center;overflow:hidden;border-radius:var(--radius-full);background-color:var(--surface-panel);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.watch-history-card__creatorAvatar:hover{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.5}.watch-history-card__creatorAvatar:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.5}.watch-history-card__creatorAvatar:focus-visible,.watch-history-card__creatorAvatar:hover{--tw-ring-color:color-mix(in srgb,var(--color-accent) 38%,transparent)}.watch-history-card__creatorAvatar img{height:100%;width:100%;border-radius:var(--radius-full);-o-object-fit:cover;object-fit:cover}.watch-history-card__creatorName{cursor:pointer;background-color:transparent;padding:0;text-align:left;font-size:var(--font-size-base);line-height:var(--line-height-base);font-weight:500;color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.watch-history-card__creatorName:hover{color:var(--color-accent)}.watch-history-card__creatorName:focus-visible{color:var(--color-accent)}#playerModal.watch-history-card__actions{display:block}.watch-history-card__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}.watch-history-card__action{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-full);border-width:1px;border-color:transparent;background-color:var(--surface-panel);padding:.5rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:500;color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}.watch-history-card__action:hover{border-color:var(--border-default);background-color:var(--surface-alt);color:var(--text-primary)}.watch-history-card__action:focus-visible{border-color:var(--border-default);background-color:var(--surface-alt);color:var(--text-primary)}.watch-history-card__action--primary{background-color:var(--color-accent-strong);color:var(--color-white)}.watch-history-card__action--primary:hover{background-color:var(--color-accent-pressed)}.watch-history-card__action--primary:focus-visible{background-color:var(--color-accent-pressed)}.watch-history-card__action--danger{background-color:var(--color-critical);color:var(--color-white)}.watch-history-card__action--danger:hover{background-color:var(--color-critical-strong)}.watch-history-card__action--danger:focus-visible{background-color:var(--color-critical-strong)}#playerModal.watch-history-card__moderation{display:block}.watch-history-card__moderation{margin-top:var(--space-sm);display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-sm)}.watch-history-card__hidden{margin-top:var(--space-md);border-radius:var(--radius-lg);border-width:1px;border-style:dashed;border-color:var(--border-default);background-color:var(--surface-overlay-panel-soft);font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary)}.watch-history-card__hidden .moderation-badge{align-self:flex-start}.\!container{max-width:var(--layout-docs-max-width)!important;margin:0 auto!important;padding:var(--space-md)!important}.container{max-width:var(--layout-docs-max-width);margin:0 auto;padding:var(--space-md)}.fade-in{opacity:0;animation:interface-fade-in .28s ease-out forwards;animation-delay:var(--fade-in-delay,0ms)}.fade-in-delay-100{--fade-in-delay:100ms}.fade-in-delay-200{--fade-in-delay:200ms}.fade-in-delay-300{--fade-in-delay:300ms}.\!card[data-motion=enter]{opacity:0!important;animation:card-motion-enter .22s ease-out forwards!important}.card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}.icon-button{display:inline-flex;align-items:center;justify-content:center;width:var(--icon-size-lg);height:var(--icon-size-lg);min-width:var(--icon-size-lg);min-height:var(--icon-size-lg);flex:0 0 var(--icon-size-lg);line-height:0;background-color:var(--color-neutral-700);color:var(--color-white);border:none;border-radius:var(--radius-full);cursor:pointer;transition:background-color .2s ease,box-shadow .2s ease}.icon-button:hover{background-color:var(--color-neutral-600)}.icon-button:active,.icon-button:focus{outline:none;box-shadow:0 0 0 var(--icon-button-ring-width) var(--color-ring-alert)}.icon-image{width:var(--icon-size-md);height:var(--icon-size-md);pointer-events:none}.accent-action-button,.channel-action-button{--accent-action-bg:color-mix(in srgb,var(--surface-overlay-panel) 78%,transparent);--accent-action-border:color-mix(in srgb,var(--border-overlay) 92%,transparent);--accent-action-bg-hover:color-mix(in srgb,var(--color-accent) 22%,var(--surface-overlay-panel));--accent-action-ring:color-mix(in srgb,var(--color-accent) 48%,transparent);--accent-action-bg-active:color-mix(in srgb,var(--color-accent-pressed) 28%,var(--surface-overlay-panel));color:var(--color-text);background-color:var(--accent-action-bg);border-color:var(--accent-action-border);border-style:solid;border-width:1px}.accent-action-button:focus-visible,.accent-action-button:hover,.channel-action-button:focus-visible,.channel-action-button:hover{color:var(--color-accent-strong);background-color:var(--accent-action-bg-hover)}.accent-action-button:focus,.accent-action-button:focus-visible,.channel-action-button:focus,.channel-action-button:focus-visible{box-shadow:0 0 0 var(--icon-button-ring-width) var(--accent-action-ring)}.accent-action-button:active,.channel-action-button:active{color:var(--color-accent-pressed);background-color:var(--accent-action-bg-active)}.modal-nav .icon-button{box-shadow:none;padding:0;gap:0;width:var(--icon-size-lg);height:var(--icon-size-lg);min-width:var(--icon-size-lg);min-height:var(--icon-size-lg)}.modal-nav .icon-button:not(.accent-action-button):not(.channel-action-button){border:none}.modal-meta{gap:var(--space-xs)}.modal-meta,.modal-view-count{display:inline-flex;align-items:center}.modal-view-count{gap:var(--space-2xs);font-size:var(--font-size-sm);line-height:var(--space-md-plus);color:var(--color-muted-alt);white-space:nowrap}.view-count-text{white-space:nowrap;color:var(--color-muted-strong)}.ratio-16-9{position:relative;width:100%;padding-top:56.25%;background-color:var(--color-card)}.ratio-16-9>img{position:absolute;inset:0;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}#modalContainer{position:fixed;inset:0;z-index:var(--z-modal-root);pointer-events:none;isolation:isolate}#modalContainer>*{pointer-events:auto}#modalContainer>[id$=Modal]:not(#playerModal){z-index:var(--z-modal-root)}#modalContainer>[id$=Modal]:not(#playerModal)>.bv-modal-backdrop{z-index:var(--z-modal-overlay)}#modalContainer>[id$=Modal]:not(#playerModal) .bv-modal__panel{z-index:var(--z-modal-content)}#modalContainer>#profileModal{z-index:var(--z-modal-top-root)}#modalContainer>#profileModal>.bv-modal-backdrop{z-index:var(--z-modal-top-overlay)}#modalContainer>#profileModal .bv-modal__panel{z-index:var(--z-modal-top-content)}#modalContainer>#loginModal{z-index:var(--z-modal-peak-root)}#modalContainer>#loginModal>.bv-modal-backdrop{z-index:var(--z-modal-peak-overlay)}#modalContainer>#loginModal .bv-modal__panel{z-index:var(--z-modal-peak-content)}#modalContainer>#playerModal{z-index:var(--z-modal-player)}#modalContainer>#playerModal .player-modal__container,#modalContainer>#playerModal .player-modal__content{z-index:var(--z-modal-content)}#playerModal{position:fixed;inset:0;z-index:var(--z-modal-player);display:none;flex-direction:column;align-items:stretch;justify-content:flex-start;width:100vw;height:100vh;padding:0;background-color:var(--color-surface);color:var(--color-subtle-text);--player-modal-inline-max:var(--layout-player-max-width);--player-modal-inline-padding:clamp(var(--space-md-plus),4vw,var(--space-2xl));overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}#playerModal.flex{display:block}#modalVideo{width:100%;aspect-ratio:16/9;background-color:var(--color-black)}.notification-portal{position:fixed;top:0;left:0;right:0;z-index:var(--z-overlay-toast);display:flex;flex-direction:column;align-items:center;gap:var(--space-md);padding:calc(var(--space-xl) + env(safe-area-inset-top)) var(--space-lg) var(--space-lg);pointer-events:none;opacity:0;transform:translateY(var(--toast-translate));transition:transform var(--motion-duration-base) var(--motion-ease-out),opacity var(--motion-duration-base) var(--motion-ease-out)}@media (min-width:640px){.notification-portal{padding-inline:var(--space-xl)}}.notification-portal:has(.notification-banner:not(.hidden)){opacity:1;transform:translateY(0)}.notification-portal--active{opacity:1;transform:translateY(0)}.notification-banner,.torrent-toast{--notify-accent:var(--color-accent-strong);--notify-accent-strong:var(--color-accent);--notify-border:var(--surface-overlay-panel-tint);--notify-background:var(--surface-overlay-panel);--notify-foreground:var(--color-text);position:relative;display:flex;align-items:center;gap:var(--space-md);width:min(28rem,100%);padding:var(--space-md) var(--space-xl);border-radius:var(--radius-xl);border:1px solid var(--notify-border);background:var(--notify-background);color:var(--notify-foreground);box-shadow:0 32px 64px var(--color-shadow-notification);backdrop-filter:blur(var(--overlay-backdrop-blur));pointer-events:auto;isolation:isolate;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;line-height:1.375;transition:transform var(--motion-duration-base) var(--motion-ease-out),opacity var(--motion-duration-base) var(--motion-ease-out)}.notification-banner:before,.torrent-toast:before{content:"";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg,var(--notify-accent) 0,var(--notify-accent-strong) 100%);opacity:.18;pointer-events:none}.notification-banner>*,.torrent-toast>*{position:relative;z-index:1}.notification-dismiss{display:inline-flex;height:2rem;width:2rem;align-items:center;justify-content:center;border-radius:var(--radius-full);border-width:1px;border-color:transparent;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);color:inherit;opacity:.75;background-color:var(--notify-dismiss-background)}.notification-dismiss:focus-visible,.notification-dismiss:hover{opacity:1;background-color:var(--notify-dismiss-background-hover)}.notification-dismiss svg{height:1rem;width:1rem}.notification-banner:not(.hidden){animation:notification-slide-in var(--motion-duration-base) var(--motion-ease-out)}.notification-banner--critical,.torrent-toast--error{--notify-accent:var(--color-critical-strong);--notify-accent-strong:var(--color-critical);--notify-border:var(--color-ring-critical-soft);--notify-background:var(--color-card-critical);--notify-foreground:var(--color-critical-strong)}.notification-banner--success,.torrent-toast--success{--notify-accent:var(--status-success-strong);--notify-accent-strong:var(--status-success);--notify-border:var(--status-success-border);--notify-background:var(--status-success-surface);--notify-foreground:var(--status-success-on)}.notification-banner--info,.torrent-toast--info{--notify-accent:var(--color-info-strong);--notify-accent-strong:var(--color-info);--notify-border:var(--color-info-border);--notify-background:var(--status-info-surface)}.notification-banner--warning,.torrent-toast--warn{--notify-accent:var(--color-warning-strong);--notify-accent-strong:var(--color-warning);--notify-border:var(--status-warning-border);--notify-background:var(--status-warning-surface);--notify-foreground:var(--status-warning-on)}.status-banner .status-spinner{animation:spin 1s linear infinite;border:var(--space-3xs) solid var(--color-info-border);border-radius:var(--radius-full);border-top-color:var(--color-info);flex-shrink:0;height:var(--icon-size-md);width:var(--icon-size-md)}.status-banner .status-message{flex:1 1 auto}@keyframes notification-slide-in{0%{opacity:0;transform:translateY(var(--toast-translate))}to{opacity:1;transform:translateY(0)}}.status-spinner--inline{animation:spin 1s linear infinite;border:var(--space-3xs) solid var(--color-border-info-soft);border-radius:var(--radius-full);border-top-color:var(--color-info);display:inline-block;height:var(--space-md);width:var(--space-md)}body.iframe-form{background-color:transparent;color:var(--color-text);font-family:var(--font-family-sans);margin:var(--space-lg)}body.iframe-form .container{width:100%;max-width:100%;margin:0;padding:0}body.iframe-form .\!container{width:100%!important;max-width:100%!important;margin:0!important;padding:0!important}body.iframe-form .form-container{background-color:var(--color-surface-alt);border-radius:var(--radius-md);box-shadow:var(--shadow-md);color:var(--color-text);display:flex;flex-direction:column;gap:var(--space-md);padding:var(--space-lg)}body.iframe-form .form-container>p{margin:0}body.iframe-form form{display:flex;flex-direction:column}body.iframe-form form>*+*{margin-top:var(--space-sm)}body.iframe-form form h1,body.iframe-form form h2{color:var(--color-text);margin-top:var(--space-md)}body.iframe-form label{color:var(--color-subtle-text);display:block;font-weight:600;margin-top:var(--space-sm)}body.iframe-form label+.form-control,body.iframe-form label+.select,body.iframe-form label+.textarea{margin-top:var(--space-xs)}body.iframe-form .checkbox-group,body.iframe-form .radio-group{display:flex;flex-wrap:wrap;gap:var(--space-sm);margin-top:var(--space-xs)}body.iframe-form .checkbox-group label,body.iframe-form .radio-group label{align-items:center;color:var(--color-subtle-text);display:inline-flex;font-weight:500;gap:var(--space-xs);margin:0}body.iframe-form .checkbox-group input[type=checkbox],body.iframe-form .radio-group input[type=radio]{margin:0}body.iframe-form .radio-group input[type=radio]{accent-color:var(--color-accent-strong)}body.iframe-form button.btn{align-self:flex-start;margin-top:var(--space-md)}body.iframe-form button.\!btn{align-self:flex-start!important;margin-top:var(--space-md)!important}body.iframe-form #status{background:var(--color-surface);border-radius:var(--radius-sm);box-shadow:inset 0 0 0 var(--border-width-hairline) var(--color-border-translucent);color:var(--color-subtle-text);min-height:calc(var(--space-2xl) + var(--space-xl));padding:var(--space-sm);white-space:pre-wrap}body.iframe-form .status-line{color:var(--color-subtle-text);margin:var(--space-2xs) 0}body.iframe-form .status-line.error{color:var(--color-critical-strong)}body.iframe-form .status-line.\!error{color:var(--color-critical-strong)!important}body.iframe-form .status-line.success{color:var(--color-info-strong)}body.iframe-form .status-line.\!success{color:var(--color-info-strong)!important}body.iframe-form .status-line.warn{color:var(--color-warning-strong)}@media (max-width:640px){body.iframe-form{margin:var(--space-md)}body.iframe-form .form-container{padding:var(--space-md)}}#sidebarContainer{position:relative;overflow:visible}#sidebar{position:fixed;top:0;left:0;width:var(--sidebar-width);min-width:var(--sidebar-width);height:100vh;height:100dvh;color:var(--color-text-frosted-strong);overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;overscroll-behavior-inline:contain;touch-action:pan-y;scrollbar-gutter:stable both-edges;background:transparent;border-right:none;backdrop-filter:none;-webkit-backdrop-filter:none;transition:width .2s ease,transform .3s ease;z-index:var(--z-sidebar);box-shadow:none;isolation:isolate}#app.sidebar-collapsed,#sidebar.sidebar-collapsed,#sidebar[data-state=collapsed],body.sidebar-collapsed{--sidebar-width:var(--sidebar-width-collapsed)}#app.sidebar-expanded,#sidebar.sidebar-expanded,#sidebar[data-state=expanded],body.sidebar-expanded{--sidebar-width:var(--sidebar-width-expanded)}#sidebar .sidebar__inner{padding-block-start:max(var(--space-sidebar-shell-block-start),calc(var(--app-header-centerline) - var(--sidebar-control-size)/2));padding-block-end:var(--space-sidebar-shell-block-end);padding-inline:var(--space-sidebar-shell-inline);border-radius:var(--radius-sidebar-shell);border:none;background:none;box-shadow:none;backdrop-filter:none;-webkit-backdrop-filter:none;isolation:isolate;transition:color .2s ease}#sidebar .sidebar__inner,.sidebar-scroll{display:flex;flex-direction:column;height:100%;width:100%;overflow:visible}.sidebar-scroll{gap:var(--space-sidebar-scroll-gap);min-height:0;flex:1 1 auto}.sidebar-panel{position:relative;width:100%;display:flex;flex-direction:column;gap:var(--space-sidebar-panel-gap);padding:var(--space-sidebar-panel-padding-block) var(--space-sidebar-panel-padding-inline);border-radius:var(--radius-sidebar-panel);border:none;background:transparent;box-shadow:none;color:inherit;backdrop-filter:none;-webkit-backdrop-filter:none;overflow:hidden;isolation:isolate;transition:border-color .2s ease,box-shadow .22s ease,background .22s ease}.sidebar-panel--primary{flex:1 1 auto;min-height:0;padding-block-start:0}.sidebar-panel__header{display:flex;justify-content:flex-end;margin-bottom:var(--space-md)}.sidebar-collapse-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--sidebar-control-size);height:var(--sidebar-control-size);border-radius:var(--radius-full);border:var(--border-width-hairline) solid var(--color-border-translucent-medium);background:var(--color-surface-tint-subtle);color:inherit;cursor:pointer;transition:background .18s ease,color .18s ease,border-color .18s ease;flex-shrink:0;box-sizing:border-box}.sidebar-collapse-toggle:hover{background:var(--color-border-translucent);border-color:var(--color-border-translucent-stronger);color:var(--color-text)}.sidebar-collapse-toggle:focus-visible{outline:var(--space-3xs) solid var(--color-secondary);outline-offset:var(--space-2xs)}.sidebar-collapse-toggle__icon{width:var(--icon-size-sm);height:var(--icon-size-sm);transition:transform .2s ease}.sidebar-nav{display:flex;flex-direction:column;gap:var(--space-xs);opacity:1;transition:opacity .16s ease;width:100%}.sidebar-nav-link{--sidebar-nav-translate-x:0;--sidebar-nav-link-highlight-opacity:0;--sidebar-nav-link-highlight-scale:0.96;display:flex;align-items:center;gap:var(--space-sidebar-footer-gap);padding-block:var(--space-sidebar-nav-padding-block);padding-inline-start:var(--space-sidebar-nav-padding-inline-start);padding-inline-end:var(--space-sidebar-nav-padding-inline-end);min-height:var(--sidebar-control-size);border-radius:var(--radius-sidebar-nav);font-size:var(--font-size-sidebar-nav);font-weight:600;letter-spacing:.015em;color:var(--color-text);text-decoration:none;width:100%;position:relative;border:var(--border-width-hairline) solid transparent;background:none;box-shadow:none;transform:translateX(var(--sidebar-nav-translate-x));transition:color .18s ease,border-color .2s ease,box-shadow .22s ease,transform .22s ease;overflow:hidden;isolation:isolate;backdrop-filter:none;-webkit-backdrop-filter:none}.sidebar-nav-link:after,.sidebar-nav-link:before{content:"";position:absolute;inset:calc(var(--border-width-hairline)*-1);border-radius:inherit;pointer-events:none;transition:opacity .22s ease,transform .24s ease;z-index:-1}.sidebar-nav-link:before{background:linear-gradient(120deg,var(--color-sidebar-accent-start) 0,var(--color-sidebar-accent-end) 100%);opacity:var(--sidebar-nav-link-highlight-opacity);transform:scale(var(--sidebar-nav-link-highlight-scale));filter:saturate(140%)}.sidebar-nav-link:after{background:linear-gradient(105deg,var(--color-sidebar-hover) 0,transparent 75%);opacity:.22;mix-blend-mode:screen;z-index:-2}.sidebar-nav-link:focus-visible,.sidebar-nav-link:hover{--sidebar-nav-link-highlight-opacity:1;--sidebar-nav-link-highlight-scale:1.02;--sidebar-nav-translate-x:var(--space-sidebar-nav-translate);color:var(--color-white);border-color:var(--color-sidebar-hover-strong);box-shadow:0 0 0 var(--border-width-hairline) var(--color-sidebar-hover-strong),var(--shadow-sidebar-accent)}.sidebar-nav-link:focus-visible{outline:none;box-shadow:var(--shadow-sidebar-focus-ring),var(--shadow-sidebar-accent)}.sidebar-nav-link:focus-visible:after,.sidebar-nav-link:hover:after{opacity:.35}.sidebar-nav-link--sub{font-size:var(--font-size-sidebar-subnav);font-weight:500;color:var(--color-sidebar-text-muted);padding-block:var(--space-sidebar-subnav-padding-block);letter-spacing:.02em;--sidebar-nav-link-highlight-opacity:0.25;--sidebar-nav-link-highlight-scale:1}.sidebar-icon{width:var(--space-lg);height:var(--space-lg);min-width:var(--space-lg);min-height:var(--space-lg);flex-shrink:0;display:block;filter:var(--sidebar-icon-filter);transition:filter .18s ease}.sidebar-footer{flex-direction:column;margin-top:auto;z-index:2}.sidebar-dropup-trigger,.sidebar-footer{display:flex;gap:var(--space-sidebar-footer-gap);position:relative}.sidebar-dropup-trigger{align-items:center;justify-content:space-between;width:100%;padding-block:var(--space-sidebar-trigger-padding-block);padding-inline-start:var(--space-sidebar-nav-padding-inline-start);padding-inline-end:var(--space-sidebar-nav-padding-inline-end);min-height:var(--sidebar-control-size);border-radius:var(--radius-sidebar-nav);border:var(--border-width-hairline) solid transparent;background:none;color:var(--color-text);font-size:var(--font-size-sidebar-trigger);font-weight:600;cursor:pointer;box-sizing:border-box;flex-shrink:0;overflow:hidden;isolation:isolate;backdrop-filter:none;-webkit-backdrop-filter:none;box-shadow:none;transition:border-color .2s ease,color .2s ease,box-shadow .22s ease}.sidebar-dropup-trigger:before{background:linear-gradient(120deg,var(--color-sidebar-accent-start) 0,var(--color-sidebar-accent-end) 100%);opacity:0;transform:scale(.96);transition:opacity .2s ease,transform .22s ease;z-index:-1;filter:saturate(140%)}.sidebar-dropup-trigger:after,.sidebar-dropup-trigger:before{content:"";position:absolute;inset:calc(var(--border-width-hairline)*-1);border-radius:inherit;pointer-events:none}.sidebar-dropup-trigger:after{background:linear-gradient(110deg,var(--color-sidebar-hover) 0,transparent 80%);opacity:.18;mix-blend-mode:screen;transition:opacity .2s ease;z-index:-2}.sidebar-dropup-trigger:focus-visible,.sidebar-dropup-trigger:hover{color:var(--color-white);border-color:var(--color-sidebar-hover-strong);box-shadow:0 0 0 var(--border-width-hairline) var(--color-sidebar-hover-strong),var(--shadow-sidebar-dropup)}.sidebar-dropup-trigger:focus-visible:before,.sidebar-dropup-trigger:hover:before{opacity:1;transform:scale(1.02)}.sidebar-dropup-trigger:focus-visible:after,.sidebar-dropup-trigger:hover:after{opacity:.35}.sidebar-dropup-trigger:focus-visible{outline:none;box-shadow:var(--shadow-sidebar-focus-ring),var(--shadow-sidebar-dropup)}.sidebar-dropup-trigger__glyph{width:var(--icon-size-md);height:var(--icon-size-md);opacity:.9}.sidebar-dropup-trigger__chevron{width:var(--icon-size-sm);height:var(--icon-size-sm);transition:transform .18s ease}.sidebar-dropup-trigger[aria-expanded=true] .sidebar-dropup-trigger__chevron,.sidebar-dropup-trigger__chevron.is-rotated{transform:rotate(180deg)}.sidebar-dropup-panel{position:absolute;left:0;right:0;bottom:calc(100% + var(--space-sidebar-popover-offset));border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-sidebar-border-strong);background:linear-gradient(160deg,color-mix(in srgb,var(--color-sidebar-surface) 88%,transparent) 0,color-mix(in srgb,var(--color-sidebar-surface-alt) 68%,transparent) 100%);box-shadow:inset 0 0 0 var(--border-width-fine) var(--color-sidebar-border),var(--shadow-sidebar-panel);padding:var(--space-sidebar-popover-padding-block) var(--space-sidebar-popover-padding-inline);color:var(--color-text-frosted);opacity:0;visibility:hidden;pointer-events:none;transform:translateY(var(--space-sm));transition:opacity .18s ease,transform .18s ease,visibility .18s ease,box-shadow .22s ease;backdrop-filter:blur(var(--sidebar-panel-backdrop-blur)) saturate(var(--sidebar-panel-backdrop-saturation));-webkit-backdrop-filter:blur(var(--sidebar-panel-backdrop-blur)) saturate(var(--sidebar-panel-backdrop-saturation));z-index:5;isolation:isolate}.sidebar-dropup-panel[data-state=expanded]{opacity:1;visibility:visible;pointer-events:auto;transform:translateY(0)}.sidebar-dropup-panel__inner{display:flex;flex-direction:column;gap:var(--space-sidebar-popover-gap);width:100%;padding-inline-end:var(--space-sidebar-dropup-scroll-reserve);max-height:var(--sidebar-dropup-max-height);overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable both-edges}.sidebar-dropup-panel__inner>*{width:100%;min-width:0}#sidebar[data-footer-state=expanded] .sidebar-nav{opacity:0;pointer-events:none}#sidebar[data-footer-state=collapsed] .sidebar-nav{opacity:1}.sidebar-loading-wrapper{display:flex;justify-content:center;align-items:center;padding:var(--space-2xl) 0;width:100%;grid-column:1/-1}.sidebar-loading-indicator{display:inline-flex;align-items:center;gap:var(--space-sm);color:var(--color-muted-strong);font-size:var(--font-size-sm);font-weight:500;letter-spacing:.01em}.sidebar-loading-text{white-space:nowrap}#app{box-sizing:border-box;transition:margin-left .2s ease,width .2s ease,max-width .2s ease}#app[data-sidebar-state=collapsed]{margin-left:var(--sidebar-width-collapsed);width:calc(100vw - var(--sidebar-width-collapsed));max-width:calc(100vw - var(--sidebar-width-collapsed))}#app[data-sidebar-state=expanded]{margin-left:max(var(--sidebar-width-expanded-base),var(--sidebar-dropup-content-width));width:min(calc(100vw - var(--sidebar-width-expanded-base)),calc(100vw - var(--sidebar-dropup-content-width)));max-width:min(calc(100vw - var(--sidebar-width-expanded-base)),calc(100vw - var(--sidebar-dropup-content-width)))}#sidebar.sidebar-collapsed .sidebar-nav-link,body.sidebar-collapsed #sidebar .sidebar-nav-link{justify-content:center;width:var(--sidebar-control-size);padding:0;margin-inline:auto;border-radius:var(--radius-full);--sidebar-nav-translate-x:0;--sidebar-nav-link-highlight-scale:1}#sidebar.sidebar-expanded .sidebar-nav-link,body.sidebar-expanded #sidebar .sidebar-nav-link{justify-content:flex-start}#sidebar.sidebar-collapsed .sidebar-text,body.sidebar-collapsed #sidebar .sidebar-text{display:none}#sidebar.sidebar-collapsed .sidebar__inner,body.sidebar-collapsed #sidebar .sidebar__inner{padding-inline:var(--space-2xs)}#sidebar.sidebar-collapsed .sidebar-panel,body.sidebar-collapsed #sidebar .sidebar-panel{padding-inline:calc(var(--space-3xs)*3)}#sidebar.sidebar-collapsed .sidebar-panel__header,body.sidebar-collapsed #sidebar .sidebar-panel__header{justify-content:center}#sidebar.sidebar-collapsed .sidebar-collapse-toggle,body.sidebar-collapsed #sidebar .sidebar-collapse-toggle{margin-inline:auto}#sidebar .sidebar-toggle-icon--rotated{transform:rotate(180deg)}#sidebar.sidebar-collapsed .sidebar-toggle-icon--rotated,body.sidebar-collapsed #sidebar .sidebar-toggle-icon--rotated{transform:rotate(0deg)}#sidebar.sidebar-collapsed .sidebar-dropup-trigger,body.sidebar-collapsed #sidebar .sidebar-dropup-trigger{justify-content:center;width:var(--sidebar-control-size);height:var(--sidebar-control-size);padding:0;margin-inline:auto;border-radius:var(--radius-full)}#sidebar.sidebar-collapsed .sidebar-dropup-trigger .sidebar-text,body.sidebar-collapsed #sidebar .sidebar-dropup-trigger .sidebar-text,body.sidebar-collapsed #sidebar .sidebar-dropup-trigger__chevron{display:none}#sidebar.sidebar-collapsed .sidebar-dropup-trigger__glyph,body.sidebar-collapsed #sidebar .sidebar-dropup-trigger__glyph{margin:0}#sidebar.sidebar-collapsed .sidebar-nav-link:focus-visible,#sidebar.sidebar-collapsed .sidebar-nav-link:hover,body.sidebar-collapsed #sidebar .sidebar-nav-link:focus-visible,body.sidebar-collapsed #sidebar .sidebar-nav-link:hover{--sidebar-nav-translate-x:0}@media (min-width:768px){.sidebar-panel__header{display:flex}.sidebar-collapse-toggle{display:inline-flex}#sidebar{transform:translateX(0)!important;width:var(--sidebar-width);overflow:visible}#app[data-sidebar-state=collapsed]{margin-left:calc(var(--sidebar-width-collapsed) + var(--sidebar-content-gap));width:calc(100vw - var(--sidebar-width-collapsed) - var(--sidebar-content-gap));max-width:calc(100vw - var(--sidebar-width-collapsed) - var(--sidebar-content-gap))}#app[data-sidebar-state=expanded]{margin-left:max(calc(var(--sidebar-width-expanded-base) + var(--sidebar-content-gap)),calc(var(--sidebar-dropup-content-width) + var(--sidebar-content-gap)));width:min(calc(100vw - var(--sidebar-width-expanded-base) - var(--sidebar-content-gap)),calc(100vw - var(--sidebar-dropup-content-width) - var(--sidebar-content-gap)));max-width:min(calc(100vw - var(--sidebar-width-expanded-base) - var(--sidebar-content-gap)),calc(100vw - var(--sidebar-dropup-content-width) - var(--sidebar-content-gap)))}}@keyframes card-motion-enter{0%{opacity:0;transform:translateY(var(--motion-card-enter-translate))}to{opacity:1;transform:translateY(0)}}@keyframes video-thumbnail-fade-in{0%{opacity:0}to{opacity:1}}@keyframes interface-fade-in{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0;transform:translateY(var(--motion-fade-in-translate))}to{opacity:1;transform:translateY(0)}}.status-line{margin-top:.25rem;margin-bottom:.25rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);line-height:1.375;color:var(--text-primary)}.status-line.error{color:var(--color-critical-strong)}.status-line.\!error{color:var(--color-critical-strong)!important}.status-line.warn{color:var(--color-warning-strong)}@keyframes ztr-rotate{to{transform:rotate(1turn)}}@keyframes ztr-dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}to{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes svelte-181fgen-loader-animation{0%{transform:rotate(0)}to{transform:rotate(1turn)}}@keyframes svelte-181fgen-fade-in{to{opacity:1}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.bottom-4{bottom:1rem}.left-4{left:1rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-3{top:.75rem}.z-overlay-floating{z-index:var(--z-overlay-floating)}.z-overlay-toast{z-index:var(--z-overlay-toast)}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-ml-2{margin-left:-.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-sm{margin-top:var(--space-sm)}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2xl{height:var(--space-2xl)}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-auto{height:auto}.h-full{height:100%}.max-h-modal-body{max-height:var(--modal-body-max-height)}.max-h-modal-pane{max-height:var(--modal-pane-max-height)}.max-h-modal-sheet{max-height:var(--modal-sheet-max-height)}.max-h-modal-shell{max-height:var(--modal-shell-max-height)}.min-h-0{min-height:0}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-44{width:11rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-docs{max-width:var(--layout-docs-max-width)}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-popover-safe{max-width:var(--popover-inline-safe-max)}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.rotate-90{--tw-rotate:90deg}.rotate-90,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.cursor-wait{cursor:wait}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.resize-y{resize:vertical}.resize{resize:both}.list-disc{list-style-type:disc}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-sm{gap:var(--space-sm)}.gap-xl{gap:var(--space-xl)}.gap-xs{gap:var(--space-xs)}.gap-y-6{row-gap:1.5rem}.gap-y-8{row-gap:2rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-border-translucent>:not([hidden])~:not([hidden]){border-color:var(--border-overlay)}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:var(--radius-full)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-border{border-color:var(--border-default)}.border-border-strong{border-color:var(--border-strong)}.border-border-translucent{border-color:var(--border-overlay)}.border-border-translucent-bright{border-color:var(--color-border-translucent-bright)}.border-border-translucent-medium{border-color:var(--color-border-translucent-medium)}.border-overlay{border-color:var(--border-overlay)}.border-overlay-strong{border-color:var(--border-overlay-strong)}.border-status-danger-border{border-color:var(--status-danger-border)}.border-status-info-border{border-color:var(--status-info-border)}.border-status-success-border{border-color:var(--status-success-border)}.border-status-warning-border{border-color:var(--status-warning-border)}.border-surface-strong{border-color:var(--border-strong)}.border-opacity-20{--tw-border-opacity:0.2}.bg-overlay-muted{background-color:var(--surface-overlay-muted)}.bg-overlay-panel{background-color:var(--surface-overlay-panel)}.bg-overlay-panel-soft{background-color:var(--surface-overlay-panel-soft)}.bg-overlay-strong{background-color:var(--surface-overlay-strong)}.bg-panel{background-color:var(--surface-panel)}.bg-status-danger-surface{background-color:var(--status-danger-surface)}.bg-status-info-surface{background-color:var(--status-info-surface)}.bg-status-success-surface{background-color:var(--status-success-surface)}.bg-status-warning-surface{background-color:var(--status-warning-surface)}.bg-surface{background-color:var(--surface-base)}.bg-surface-alt{background-color:var(--surface-alt)}.bg-transparent{background-color:transparent}.bg-white{background-color:var(--color-white)}.bg-overlay-channel{background-image:var(--surface-overlay-banner-gradient-channel)}.stroke-border-translucent{stroke:var(--border-overlay)}.stroke-current{stroke:currentColor}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-lg{padding:var(--space-lg)}.p-md{padding:var(--space-md)}.px-0{padding-left:0;padding-right:0}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-lg{padding-left:var(--space-lg);padding-right:var(--space-lg)}.px-md{padding-left:var(--space-md);padding-right:var(--space-md)}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-md{padding-top:var(--space-md);padding-bottom:var(--space-md)}.py-xl{padding-top:var(--space-xl);padding-bottom:var(--space-xl)}.pb-16{padding-bottom:4rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-2{padding-left:.5rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-1{padding-right:.25rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pt-md{padding-top:var(--space-md)}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-family-mono)}.font-sans{font-family:var(--font-family-sans)}.text-2xl{font-size:var(--font-size-2xl);line-height:var(--line-height-relaxed)}.text-2xs{font-size:var(--font-size-2xs);line-height:var(--line-height-tight)}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-3xs{font-size:var(--font-size-3xs);line-height:var(--line-height-tight)}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[0\.75rem\]{font-size:.75rem}.text-\[10px\]{font-size:10px}.text-base{font-size:var(--font-size-base)}.text-base,.text-lg{line-height:var(--line-height-base)}.text-lg{font-size:var(--font-size-lg)}.text-sm{font-size:var(--font-size-sm);line-height:var(--line-height-snug)}.text-xl{font-size:var(--font-size-xl);line-height:var(--line-height-relaxed)}.text-xs{font-size:var(--font-size-xs);line-height:var(--line-height-tight)}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.ordinal{--tw-ordinal:ordinal}.ordinal,.tabular-nums{font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.tabular-nums{--tw-numeric-spacing:tabular-nums}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-extra-wide{letter-spacing:var(--tracking-extra-wide)}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-accent{color:var(--color-accent)}.text-critical{color:var(--color-critical)}.text-critical-strong{color:var(--color-critical-strong)}.text-info{color:var(--color-info)}.text-info-strong{color:var(--color-info-strong)}.text-muted{color:var(--text-muted)}.text-muted-strong{color:var(--text-muted-strong)}.text-primary{color:var(--text-primary)}.text-status-danger{color:var(--status-danger)}.text-status-danger-on{color:var(--status-danger-on)}.text-status-info{color:var(--status-info)}.text-status-info-on{color:var(--status-info-on)}.text-status-neutral{color:var(--status-neutral)}.text-status-private-on{color:var(--status-private-on)}.text-status-success{color:var(--status-success)}.text-status-success-on{color:var(--status-success-on)}.text-status-warning{color:var(--status-warning)}.text-status-warning-on{color:var(--status-warning-on)}.text-subtle{color:var(--text-subtle)}.text-text{color:var(--text-primary)}.text-text-frosted{color:var(--text-frosted-primary)}.text-text-muted{color:var(--text-muted)}.text-text-strong{color:var(--color-text-strong)}.text-warning{color:var(--status-warning)}.text-warning-strong{color:var(--status-warning-strong)}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.underline-offset-2{text-underline-offset:2px}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-inner,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.shadow-md{--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-fast{transition-duration:var(--motion-duration-fast)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@media (max-width:640px){.bv-grid-video{gap:var(--space-md);grid-template-columns:1fr}}@media (prefers-reduced-motion:reduce){:root{--motion-duration-fast:0s;--motion-duration-base:0s;--motion-duration-slow:0s;--modal-motion-duration:0s}*,:after,:before{animation-delay:0s!important;animation-duration:0s!important;animation-iteration-count:1!important;animation-timing-function:linear!important;transition-delay:0s!important;transition-duration:0s!important;transition-timing-function:linear!important}.player-modal__similar-card:before{transition:none!important}#app,#playerModal .player-modal__content,#sidebar,.btn,.btn-ghost,.card,.card[data-motion=enter],.fade-in,.icon-button,.menu__item,.modal-nav,.notification-banner,.notification-portal,.popover-panel,.popover__panel,.progress-bar-fill,.sidebar-collapse-toggle,.sidebar-collapse-toggle__icon,.sidebar-dropup-panel,.sidebar-dropup-trigger,.sidebar-dropup-trigger__chevron,.sidebar-loading-indicator,.sidebar-loading-wrapper,.sidebar-nav,.sidebar-nav-link,.status-banner .status-spinner,.status-spinner--inline,.torrent-toast,.video-modal__panel,.watch-history-card,.watch-history-card__action,.watch-history-card__creatorAvatar,.watch-history-card__creatorName,.watch-history-card__title{transition-property:none!important}.popover-panel,.popover__panel{transform:none!important}.popover-panel[data-state=open],.popover__panel[data-state=open]{opacity:1}.popover-panel[data-state=closed],.popover__panel[data-state=closed]{opacity:0}.card[data-motion=enter],.fade-in,[data-video-thumbnail][data-thumbnail-loaded=true]{opacity:1;animation:none!important}.notification-banner,.status-banner .status-spinner,.status-spinner--inline,.torrent-toast{animation:none!important}.notification-portal{transform:none!important;opacity:1!important}}@media (min-width:768px){.md\:flex#playerModal{display:block}}.hover\:border-status-danger-border:hover{border-color:var(--status-danger-border)}.hover\:bg-overlay-panel:hover{background-color:var(--surface-overlay-panel)}.hover\:bg-panel-hover:hover{background-color:var(--surface-panel-hover)}.hover\:text-accent:hover{color:var(--color-accent)}.hover\:text-critical:hover{color:var(--color-critical)}.hover\:text-info-strong:hover{color:var(--color-info-strong)}.hover\:text-text:hover{color:var(--text-primary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-0:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-info:focus{--tw-ring-color:var(--color-info)}.focus\:ring-status-info:focus{--tw-ring-color:var(--status-info)}.focus\:ring-status-success:focus{--tw-ring-color:var(--status-success)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus\:ring-offset-surface:focus{--tw-ring-offset-color:var(--surface-base)}.focus-visible\:bg-primary:focus-visible{background-color:var(--color-primary)}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-info:focus-visible{outline-color:var(--color-info)}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (min-width:640px){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:w-1\/3{width:33.333333%}.sm\:w-auto{width:auto}.sm\:max-w-md{max-width:28rem}.sm\:max-w-xl{max-width:36rem}.sm\:flex-none{flex:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-start{align-items:flex-start}.sm\:items-end{align-items:flex-end}.sm\:items-center{align-items:center}.sm\:items-stretch{align-items:stretch}.sm\:justify-end{justify-content:flex-end}.sm\:justify-between{justify-content:space-between}.sm\:self-end{align-self:flex-end}.sm\:p-5{padding:1.25rem}.sm\:p-8{padding:2rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0;padding-bottom:0}.sm\:pt-8{padding-top:2rem}}@media (min-width:768px){.md\:col-span-2{grid-column:span 2/span 2}.md\:flex{display:flex}.md\:h-64{height:16rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:items-center{align-items:center}.md\:justify-end{justify-content:flex-end}.md\:gap-md{gap:var(--space-md)}.md\:pb-12{padding-bottom:3rem}}@media (min-width:1024px){.lg\:ml-auto{margin-left:auto}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:flex-wrap{flex-wrap:wrap}.lg\:items-start{align-items:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:text-right{text-align:right}}@media (min-width:1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
\ No newline at end of file
diff --git a/css/tailwind.source.css b/css/tailwind.source.css
index 3332a09d..7fdf447b 100644
--- a/css/tailwind.source.css
+++ b/css/tailwind.source.css
@@ -1602,6 +1602,10 @@
padding-block-start: var(--player-modal-vertical-padding);
}
+ #playerModal .video-modal__primary > .watch-container:last-child {
+ padding-block-end: var(--player-modal-vertical-padding);
+ }
+
#playerModal .video-modal__secondary > .watch-container:last-child {
padding-block-end: var(--player-modal-vertical-padding);
}
@@ -1897,7 +1901,7 @@
.video-modal__layout {
--video-modal-secondary-size: clamp(
var(--video-modal-secondary-min),
- 40vw,
+ 30vw,
var(--video-modal-secondary-max)
);
grid-template-columns:
@@ -1936,7 +1940,7 @@
.video-modal__layout {
--video-modal-secondary-size: clamp(
var(--video-modal-secondary-min),
- 36vw,
+ 28vw,
var(--video-modal-secondary-max)
);
}
diff --git a/css/tokens.css b/css/tokens.css
index 12b2ae90..e74b8e9d 100644
--- a/css/tokens.css
+++ b/css/tokens.css
@@ -211,7 +211,7 @@
--popup-padding-block: 0.75rem;
--video-modal-primary-fr: 1.85fr;
--video-modal-secondary-min: 320px;
- --video-modal-secondary-max: 44rem;
+ --video-modal-secondary-max: 35rem;
--video-modal-secondary-fr: 1fr;
--video-modal-layout-max: calc(
var(--layout-player-max-width) + var(--video-modal-secondary-max) +
diff --git a/docs/instance-config.md b/docs/instance-config.md
index 338d7f67..c8997e51 100644
--- a/docs/instance-config.md
+++ b/docs/instance-config.md
@@ -139,7 +139,7 @@ an operator might change it.
- **`DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`**
- **What it does:** Trusted mute count required to hide a creator globally.
- - **Why change it:** Lower for stricter enforcement, higher for more leniency.
+ - **Why change it:** Lower for stricter enforcement, higher for more leniency. The upstream default is 20, allowing most content to blur before hiding.
- **`DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`**
- **What it does:** Trusted spam/report count required to hide a video
diff --git a/docs/moderation/README.md b/docs/moderation/README.md
index 528bae17..f9dff94c 100644
--- a/docs/moderation/README.md
+++ b/docs/moderation/README.md
@@ -6,7 +6,7 @@ bitvid is follow-centric. Your Home feed comes from people you follow (F1). Disc
## Core principles
- **Freedom to choose**: User picks filters. Defaults are safe but reversible.
- **Explain decisions**: Every blur/hide shows a “why” badge and a “show anyway” control.
-- **Whitelist > blacklist**: Prefer showing content from trusted networks over fighting global spam.
+- **Blacklist > whitelist**: Admin blacklists and moderation gates always win; whitelists only influence Discovery ranking.
- **Minimal central power**: Admin lists are opt-in; users can unsubscribe.
## Threat model (short)
@@ -50,7 +50,7 @@ Thread new moderation behaviors through the same service → stage → app → U
- Opt-in admin lists (30000 with `d=bitvid:admin:*`) can hard-hide content, but whitelist entries no longer bypass moderation gates—they only influence Discovery rankings when a viewer opts into that list.
- Trust seeds now come from the Super Admin plus every active moderator, so their reports shape anonymous/default visitor filters automatically. The `DEFAULT_TRUST_SEED_NPUBS` export only activates as an emergency fallback when those live lists cannot be loaded (toggle via `FEATURE_TRUST_SEEDS`).
-> Default thresholds now come directly from [`config/instance-config.js`](../../config/instance-config.js). Look for the exports `DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, and `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` to adjust the instance-wide behavior before deploying. The upstream repo currently publishes example values of 3 trusted reports for blur, 2 for autoplay blocking, 1 trusted mute to hide authors, and 3 trusted spam reports to hide videos.
+> Default thresholds now come directly from [`config/instance-config.js`](../../config/instance-config.js). Look for the exports `DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, and `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` to adjust the instance-wide behavior before deploying. The upstream repo currently publishes example values of 1 trusted report for blur, 1 for autoplay blocking, 20 trusted mutes to hide authors, and 1 trusted spam report to hide videos.
> Operators can still override these thresholds on a per-viewer basis in **Settings → Safety & Moderation**; leaving the UI fields blank restores the config-driven defaults. Update [`config/instance-config.js`](../../config/instance-config.js) to change what new viewers receive out of the box.
@@ -67,7 +67,7 @@ Operators can delegate hard-hide decisions to trusted curators without giving th
1. The super admin maintains a `kind 30000` list with `d=${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_SOURCES}`. Each `a` tag references a community curator’s blacklist as `30000::${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_PREFIX}:`.
2. Every curator publishes their own `kind 30000` list using the referenced `d` tag (`${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_PREFIX}:`) and fills it with `p` tags for accounts they want hidden.
-3. bitvid automatically fetches each referenced list, merges the entries into the global blacklist, and removes any duplicates or guard-protected accounts (super admin, editors, or whitelist members).
+3. bitvid automatically fetches each referenced list, merges the entries into the global blacklist, and removes any duplicates or guard-protected accounts (super admin or editors).
To add a community list, append a new `a` tag to the super-admin source list and ask the curator to publish their companion `p` list. To remove a list, delete the `a` tag or ask the curator to clear their `p` entries; the client stops ingesting it on the next refresh.
@@ -86,8 +86,8 @@ The profile modal now exposes blur, autoplay, and hide thresholds so operators c
### Runtime flags
-- `TRUSTED_MUTE_HIDE_THRESHOLD` — numeric default for the trusted mute hide control. Initialized from `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); the upstream config sets this to `1`, but adjust the export to match your policy.
-- `TRUSTED_SPAM_HIDE_THRESHOLD` — numeric default for the trusted spam hide control. Initialized from `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); the upstream config example is `3`.
+- `TRUSTED_MUTE_HIDE_THRESHOLD` — numeric default for the trusted mute hide control. Initialized from `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); the upstream config sets this to `20`, but adjust the export to match your policy.
+- `TRUSTED_SPAM_HIDE_THRESHOLD` — numeric default for the trusted spam hide control. Initialized from `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); the upstream config example is `1`.
- `FEATURE_TRUSTED_HIDE_CONTROLS` — boolean toggle to hide/show the new trusted hide controls in the UI (default `true`).
## Files in this folder
diff --git a/docs/moderation/testing.md b/docs/moderation/testing.md
index bc436ce7..27c85eb2 100644
--- a/docs/moderation/testing.md
+++ b/docs/moderation/testing.md
@@ -41,7 +41,7 @@ Expected:
* After 1 report → no blur; autoplay allowed.
* Once reports reach `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD` → autoplay blocked (the upstream configuration currently sets this to 2, but rely on your `config/instance-config.js` value).
-* Once reports reach `DEFAULT_BLUR_THRESHOLD` → thumbnail blurred; reason chip shown (upstream example: 3). At the spam threshold (`TRUSTED_SPAM_HIDE_THRESHOLD` sourced from `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js)) the card hides entirely and the badge copy switches to "Hidden · {count} trusted spam report(s)" until the viewer overrides it.
+* Once reports reach `DEFAULT_BLUR_THRESHOLD` → thumbnail blurred; reason chip shown (upstream example: 1). At the spam threshold (`TRUSTED_SPAM_HIDE_THRESHOLD` sourced from `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js)) the card hides entirely and the badge copy switches to "Hidden · {count} trusted spam report(s)" until the viewer overrides it.
**Exercises:** [`ModerationService.ingestReportEvent()`](../../js/services/moderationService.js) and [`ModerationService.getTrustedReportSummary()`](../../js/services/moderationService.js) wire through [`createModerationStage()`](../../js/feedEngine/stages.js) so [`bitvidApp.decorateVideoModeration()`](../../js/app.js) can hand the summary to [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js).
@@ -56,7 +56,7 @@ Expected:
}
```
-Expected: author’s items are downranked/hidden for this viewer. When the mute threshold (`TRUSTED_MUTE_HIDE_THRESHOLD`, sourced from `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); upstream example: 1) is met, cards render with "Hidden · {count} trusted mute(s)" and require a "Show anyway" override to display content plus a "Restore default moderation" control to revert.
+Expected: author’s items are downranked/blurred for this viewer. When the mute threshold (`TRUSTED_MUTE_HIDE_THRESHOLD`, sourced from `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); upstream example: 20) is met, cards render with "Hidden · {count} trusted mute(s)" and require a "Show anyway" override to display content plus a "Restore default moderation" control to revert.
**Exercises:** [`ModerationService.ingestTrustedMuteEvent()`](../../js/services/moderationService.js) updates [`ModerationService.isAuthorMutedByTrusted()`](../../js/services/moderationService.js) / [`getTrustedMutersForAuthor()`](../../js/services/moderationService.js), which [`createModerationStage()`](../../js/feedEngine/stages.js) propagates to the video item before [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js) renders the mute state.
diff --git a/docs/moderation/web-of-trust-examples.md b/docs/moderation/web-of-trust-examples.md
index e692d23e..4111688d 100644
--- a/docs/moderation/web-of-trust-examples.md
+++ b/docs/moderation/web-of-trust-examples.md
@@ -54,8 +54,8 @@ list. Blocking still follows the precedence rules above.
- A video has 3 `nudity` reports from those seed accounts.
Result:
-- Thumbnail is blurred if `DEFAULT_BLUR_THRESHOLD` is 3.
-- Autoplay is blocked if `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD` is 2.
+- Thumbnail is blurred if `DEFAULT_BLUR_THRESHOLD` is 1 (met).
+- Autoplay is blocked if `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD` is 1 (met).
- No hard hide unless trusted mute/spam thresholds are met.
### Example 2: Logged-in viewer with personal blocks
@@ -81,13 +81,13 @@ Result:
### Example 4: Trusted mute hide
-- Viewer follows `npub_a`, `npub_b`, and `npub_c`.
-- Both `npub_a` and `npub_b` have muted `npub_author_y`.
-- `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` is 1.
+- Viewer follows 25 accounts.
+- 20 of them have muted `npub_author_y`.
+- `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` is 20.
Result:
-- The author is hidden (trusted mute count ≥ 1).
-- The badge reads: `Hidden · 2 trusted mutes` and a “Show anyway” override is
+- The author is hidden (trusted mute count ≥ 20).
+- The badge reads: `Hidden · 20 trusted mutes` and a “Show anyway” override is
available.
### Example 5: Mixed nudity + spam reports
diff --git a/docs/moderation/web-of-trust.md b/docs/moderation/web-of-trust.md
index 9472ebaa..88dc7060 100644
--- a/docs/moderation/web-of-trust.md
+++ b/docs/moderation/web-of-trust.md
@@ -20,7 +20,7 @@
- `hideIfTrustedMuteCount(author) >= DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`
- `hideIfTrustedSpamReports(event) >= DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`
-Threshold constants are exported from [`config/instance-config.js`](../../config/instance-config.js) so operators can change the defaults without touching moderation code. Inspect the `DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, and `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` exports to set your policy. The upstream repo includes example values (blur at 3, autoplay block at 2, trusted mute hide at 1, trusted spam hide at 3), but treat those as guidance rather than hard-coded requirements.
+Threshold constants are exported from [`config/instance-config.js`](../../config/instance-config.js) so operators can change the defaults without touching moderation code. Inspect the `DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, and `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD` exports to set your policy. The upstream repo includes example values (blur at 1, autoplay block at 1, trusted mute hide at 20, trusted spam hide at 1), but treat those as guidance rather than hard-coded requirements.
### Why these numbers?
- F1-only reports resist Sybil attacks.
diff --git a/js/accessControl.js b/js/accessControl.js
index 0ac932dc..96dd35d8 100644
--- a/js/accessControl.js
+++ b/js/accessControl.js
@@ -117,6 +117,7 @@ class AccessControl {
this._hydratedFromCache = false;
this._whitelistListeners = new Set();
this._editorListeners = new Set();
+ this._blacklistListeners = new Set();
this._scheduleHydrateFromCache();
}
@@ -242,6 +243,7 @@ class AccessControl {
}
_applyState(state, options = {}) {
+ userLogger.info("[accessControl] _applyState called", { state, options });
const markLoaded = options.markLoaded !== false;
const editors = Array.isArray(state?.editors) ? state.editors : [];
const whitelist = Array.isArray(state?.whitelist) ? state.whitelist : [];
@@ -251,6 +253,8 @@ class AccessControl {
this.editors instanceof Set ? new Set(this.editors) : new Set();
const previousWhitelist =
this.whitelist instanceof Set ? new Set(this.whitelist) : new Set();
+ const previousBlacklist =
+ this.blacklist instanceof Set ? new Set(this.blacklist) : new Set();
this.editors = new Set(
dedupeNpubs([...ADMIN_EDITORS_NPUBS, ...editors])
@@ -261,7 +265,6 @@ class AccessControl {
const whitelistChanged = !areSetsEqual(previousWhitelist, this.whitelist);
const blacklistDedupe = dedupeNpubs(blacklist);
- const whitelistSet = new Set(normalizedWhitelist.map(normalizeNpub));
const adminGuardSet = new Set([
normalizeNpub(ADMIN_SUPER_NPUB),
...Array.from(this.editors),
@@ -271,15 +274,13 @@ class AccessControl {
if (!normalized) {
return false;
}
- if (whitelistSet.has(normalized)) {
- return false;
- }
if (adminGuardSet.has(normalized)) {
return false;
}
return true;
});
this.blacklist = new Set(sanitizedBlacklist);
+ const blacklistChanged = !areSetsEqual(previousBlacklist, this.blacklist);
const toolkitCandidate =
(typeof window !== "undefined" ? window?.NostrTools : null) ||
@@ -326,6 +327,9 @@ class AccessControl {
if (editorsChanged) {
this._emitEditorsChange(Array.from(this.editors));
}
+ if (blacklistChanged) {
+ this._emitBlacklistChange(Array.from(this.blacklist));
+ }
}
_emitWhitelistChange(whitelistValues) {
@@ -346,6 +350,24 @@ class AccessControl {
}
}
+ _emitBlacklistChange(blacklistValues) {
+ if (!this._blacklistListeners.size) {
+ return;
+ }
+
+ const snapshot = Array.isArray(blacklistValues)
+ ? [...blacklistValues]
+ : this.getBlacklist();
+
+ for (const listener of Array.from(this._blacklistListeners)) {
+ try {
+ listener(snapshot);
+ } catch (error) {
+ userLogger.error("accessControl blacklist listener failed", error);
+ }
+ }
+ }
+
_emitEditorsChange(editorsValues) {
if (!this._editorListeners.size) {
return;
@@ -396,9 +418,11 @@ class AccessControl {
refresh() {
if (this._isRefreshing) {
+ userLogger.info("[accessControl] refresh ignored (already refreshing)");
return this._refreshPromise;
}
+ userLogger.info("[accessControl] refresh started");
const operation = this._performRefresh();
const tracked = operation.catch((error) => {
userLogger.error("Failed to refresh admin lists:", error);
@@ -415,8 +439,10 @@ class AccessControl {
try {
await this._refreshPromise;
+ userLogger.info("[accessControl] ensureReady resolved");
} catch (error) {
if (!this.hasLoaded) {
+ userLogger.warn("[accessControl] ensureReady retry");
await this.refresh();
await this._refreshPromise;
} else {
@@ -485,6 +511,18 @@ class AccessControl {
};
}
+ onBlacklistChange(listener) {
+ if (typeof listener !== "function") {
+ return () => {};
+ }
+
+ this._blacklistListeners.add(listener);
+
+ return () => {
+ this._blacklistListeners.delete(listener);
+ };
+ }
+
async addModerator(requestorNpub, moderatorNpub) {
if (!this.isSuperAdmin(requestorNpub)) {
return { ok: false, error: "forbidden" };
@@ -543,9 +581,7 @@ class AccessControl {
}
const nextWhitelist = dedupeNpubs([...this.getWhitelist(), normalized]);
- const nextBlacklist = this.getBlacklist().filter(
- (value) => value !== normalized
- );
+ const nextBlacklist = this.getBlacklist();
try {
await persistAdminState(actorNpub, {
@@ -604,9 +640,7 @@ class AccessControl {
}
const nextBlacklist = dedupeNpubs([...this.getBlacklist(), normalized]);
- const nextWhitelist = this.getWhitelist().filter(
- (value) => value !== normalized
- );
+ const nextWhitelist = this.getWhitelist();
try {
await persistAdminState(actorNpub, {
@@ -656,15 +690,9 @@ class AccessControl {
if (!normalized) {
return false;
}
- if (this.whitelist.has(normalized)) {
- return false;
- }
const normalizedHexCandidate = normalizeHexKey(normalized);
if (normalizedHexCandidate) {
- if (this.whitelistPubkeys.has(normalizedHexCandidate)) {
- return false;
- }
return this.blacklistPubkeys.has(normalizedHexCandidate);
}
@@ -712,14 +740,14 @@ class AccessControl {
return false;
}
- if (this.whitelistPubkeys.has(hex)) {
- return true;
- }
-
if (this.blacklistPubkeys.has(hex)) {
return false;
}
+ if (this.whitelistPubkeys.has(hex)) {
+ return true;
+ }
+
if (this.whitelistEnabled && !this.whitelistPubkeys.has(hex)) {
return false;
}
@@ -736,22 +764,22 @@ class AccessControl {
}
if (hex) {
- if (this.whitelistPubkeys.has(hex)) {
- return true;
- }
if (this.blacklistPubkeys.has(hex)) {
return false;
}
- }
-
- if (this.whitelist.has(normalized)) {
- return true;
+ if (this.whitelistPubkeys.has(hex)) {
+ return true;
+ }
}
if (this.blacklist.has(normalized)) {
return false;
}
+ if (this.whitelist.has(normalized)) {
+ return true;
+ }
+
if (this.whitelistEnabled && !this.whitelist.has(normalized)) {
return false;
}
diff --git a/js/adminListStore.js b/js/adminListStore.js
index 5d070bf1..4e1aadf7 100644
--- a/js/adminListStore.js
+++ b/js/adminListStore.js
@@ -193,7 +193,6 @@ function sanitizeNpubList(values) {
function sanitizeAdminState(state = {}) {
const sanitizedEditors = sanitizeNpubList(state.editors || []);
const sanitizedWhitelist = sanitizeNpubList(state.whitelist || []);
- const whitelistSet = new Set(sanitizedWhitelist.map(normalizeNpub));
const adminGuardSet = new Set([
normalizeNpub(ADMIN_SUPER_NPUB),
@@ -207,9 +206,6 @@ function sanitizeAdminState(state = {}) {
if (!normalized) {
return false;
}
- if (whitelistSet.has(normalized)) {
- return false;
- }
if (adminGuardSet.has(normalized)) {
return false;
}
@@ -848,9 +844,6 @@ async function persistNostrState(actorNpub, updates = {}) {
}
if (Array.isArray(updates.blacklist)) {
- const whitelistSet = new Set(
- (sanitizedUpdates.whitelist || []).map(normalizeNpub)
- );
const editorGuard = new Set([
normalizeNpub(ADMIN_SUPER_NPUB),
...ADMIN_EDITORS_NPUBS.map(normalizeNpub),
@@ -867,9 +860,6 @@ async function persistNostrState(actorNpub, updates = {}) {
if (!normalized) {
return false;
}
- if (whitelistSet.has(normalized)) {
- return false;
- }
if (editorGuard.has(normalized)) {
return false;
}
diff --git a/js/app.js b/js/app.js
index 7686c6e1..6a1fa2d4 100644
--- a/js/app.js
+++ b/js/app.js
@@ -76,6 +76,7 @@ import {
import { splitAndZap as splitAndZapDefault } from "./payments/zapSplit.js";
import {
formatAbsoluteTimestamp as formatAbsoluteTimestampUtil,
+ formatAbsoluteDateWithOrdinal as formatAbsoluteDateWithOrdinalUtil,
formatTimeAgo as formatTimeAgoUtil,
truncateMiddle,
formatShortNpub,
@@ -328,7 +329,24 @@ class Application {
isAuthorBlocked(pubkey) {
try {
if (userBlocks && typeof userBlocks.isBlocked === "function") {
- return userBlocks.isBlocked(pubkey);
+ if (userBlocks.isBlocked(pubkey)) {
+ return true;
+ }
+ }
+
+ if (!this.isUserLoggedIn()) {
+ const normalized = this.normalizeHexPubkey(pubkey);
+ if (
+ normalized &&
+ moderationService &&
+ typeof moderationService.getTrustedMutersForAuthor === "function"
+ ) {
+ const muters = moderationService.getTrustedMutersForAuthor(normalized);
+ const threshold = getTrustedMuteHideThreshold();
+ if (Array.isArray(muters) && muters.length >= threshold) {
+ return true;
+ }
+ }
}
} catch (error) {
devLogger.warn("[Application] Failed to evaluate block status:", error);
@@ -467,6 +485,112 @@ class Application {
await Promise.all([accessControlPromise, adminPanePromise]);
+ const syncSessionActorBlacklist = async (trigger) => {
+ if (this.pubkey) {
+ return;
+ }
+
+ const sessionActorPubkey = nostrClient.sessionActor?.pubkey;
+ if (!sessionActorPubkey) {
+ return;
+ }
+
+ const blacklist = accessControl.getBlacklist();
+ try {
+ await userBlocks.seedBaselineDelta(
+ sessionActorPubkey,
+ Array.from(blacklist || []),
+ );
+ } catch (error) {
+ devLogger.warn(
+ `[app.init()] Failed to sync session actor blacklist${
+ trigger ? ` (${trigger})` : ""
+ }:`,
+ error,
+ );
+ }
+ };
+
+ const handleSessionActorReady = async ({ pubkey, reason } = {}) => {
+ if (this.pubkey) {
+ return;
+ }
+
+ const normalizedPubkey = this.normalizeHexPubkey(pubkey);
+ if (!normalizedPubkey) {
+ return;
+ }
+
+ const triggerLabel = reason ? `session-actor-${reason}` : "session-actor";
+ await syncSessionActorBlacklist(triggerLabel);
+
+ const refreshReason = "session-actor-ready";
+ if (typeof this.refreshVisibleModerationUi === "function") {
+ try {
+ this.refreshVisibleModerationUi({ reason: refreshReason });
+ } catch (error) {
+ devLogger.warn(
+ "[app.init()] Failed to refresh moderation UI after session actor:",
+ error,
+ );
+ }
+ } else {
+ this.refreshAllVideoGrids({
+ reason: refreshReason,
+ forceMainReload: true,
+ }).catch((error) => {
+ devLogger.warn(
+ "[app.init()] Failed to refresh video grids after session actor:",
+ error,
+ );
+ });
+ }
+ };
+
+ if (typeof nostrClient.onSessionActorChange === "function") {
+ nostrClient.onSessionActorChange((detail) => {
+ handleSessionActorReady(detail).catch((error) => {
+ devLogger.warn(
+ "[app.init()] Failed to process session actor change:",
+ error,
+ );
+ });
+ });
+ }
+
+ await syncSessionActorBlacklist("post-refresh");
+
+ if (typeof accessControl.onBlacklistChange === "function") {
+ accessControl.onBlacklistChange(() => {
+ syncSessionActorBlacklist("blacklist-change");
+ if (this.isUserLoggedIn()) {
+ return;
+ }
+
+ const refreshReason = "admin-blacklist-change";
+ this.refreshAllVideoGrids({
+ reason: refreshReason,
+ forceMainReload: true,
+ }).catch((error) => {
+ devLogger.warn(
+ "[app.init()] Failed to refresh video grids after admin blacklist change:",
+ error,
+ );
+ });
+
+ if (typeof this.refreshVisibleModerationUi === "function") {
+ try {
+ this.refreshVisibleModerationUi({ reason: refreshReason });
+ } catch (error) {
+ devLogger.warn(
+ "[app.init()] Failed to refresh moderation UI after admin blacklist change:",
+ error,
+ );
+ }
+ }
+ });
+ }
+
// Grab the "Subscriptions" link by its id in the sidebar
this.subscriptionsLink = document.getElementById("subscriptionsLink");
@@ -2651,6 +2775,22 @@ class Application {
* Setup general event listeners for logout, modals, etc.
*/
setupEventListeners() {
+ if (moderationService && typeof moderationService.on === "function") {
+ moderationService.on("trusted-mutes", () => {
+ if (!this.isUserLoggedIn()) {
+ this.refreshAllVideoGrids({ reason: "trusted-mutes" });
+ } else {
+ this.refreshVisibleModerationUi({ reason: "trusted-mutes" });
+ }
+ });
+ moderationService.on("user-blocks", () => {
+ this.refreshVisibleModerationUi({ reason: "user-blocks" });
+ });
+ moderationService.on("summary", () => {
+ this.refreshVisibleModerationUi({ reason: "moderation-summary" });
+ });
+ }
+
if (this.appChromeController) {
this.appChromeController.initialize();
return;
@@ -8622,7 +8762,9 @@ class Application {
(normalizedPostedAt === null || normalizedEditedAt - normalizedPostedAt >= 60);
if (shouldShowEdited) {
- payload.edited = `Last edited ${this.formatTimeAgo(normalizedEditedAt)}`;
+ const abs = formatAbsoluteDateWithOrdinalUtil(normalizedEditedAt);
+ const rel = this.formatTimeAgo(normalizedEditedAt);
+ payload.edited = `Last edited: ${abs} (${rel})`;
}
return payload;
@@ -8707,6 +8849,21 @@ class Application {
return null;
}
+ // Prioritize NIP-71 published_at metadata if available
+ const rawNip71PublishedAt =
+ video?.nip71?.publishedAt ||
+ video?.nip71?.published_at ||
+ video?.nip71?.["published-at"];
+ const parsedNip71PublishedAt = Number(rawNip71PublishedAt);
+ const nip71PublishedAt = Number.isFinite(parsedNip71PublishedAt)
+ ? Math.floor(parsedNip71PublishedAt)
+ : null;
+
+ if (nip71PublishedAt !== null) {
+ this.cacheVideoRootCreatedAt(video, nip71PublishedAt);
+ return nip71PublishedAt;
+ }
+
const cached = this.getKnownVideoPostedAt(video);
if (cached !== null) {
return cached;
diff --git a/js/constants.js b/js/constants.js
index c92d95ea..d719447a 100644
--- a/js/constants.js
+++ b/js/constants.js
@@ -123,7 +123,7 @@ const SANITIZED_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = coerceNonNegativeInteger(
);
const SANITIZED_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = coerceNonNegativeInteger(
CONFIG_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
- 1,
+ 4,
);
const SANITIZED_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = coerceNonNegativeInteger(
CONFIG_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
diff --git a/js/feedEngine/sources.js b/js/feedEngine/sources.js
index b8994fe7..bf7bf2a3 100644
--- a/js/feedEngine/sources.js
+++ b/js/feedEngine/sources.js
@@ -66,6 +66,7 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
const resolvedService = resolveService(service, nostrService);
return async function subscriptionAuthorsSource(context = {}) {
+ context?.log?.("[subscriptions-source] START");
const runtimeAuthors = toArray(
context?.runtime?.subscriptionAuthors || context?.runtime?.authors
).map(normalizeAuthor);
@@ -88,6 +89,8 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
[...runtimeAuthors, ...configAuthors, ...hookAuthors].filter(Boolean)
);
+ context?.log?.(`[subscriptions-source] Resolved ${authors.size} authors.`);
+
if (!authors.size) {
return [];
}
@@ -115,12 +118,16 @@ export function createSubscriptionAuthorsSource({ service } = {}) {
let videos = [];
try {
if (hasTargetedLookup) {
+ context?.log?.("[subscriptions-source] Calling getActiveVideosByAuthors...");
videos = await Promise.resolve(
resolvedService.getActiveVideosByAuthors(authorList, options)
);
+ context?.log?.(`[subscriptions-source] getActiveVideosByAuthors returned ${videos.length} videos.`);
if (!videos.length && typeof resolvedService.fetchVideosByAuthors === "function") {
+ context?.log?.("[subscriptions-source] Cache empty. Calling fetchVideosByAuthors...");
const fetched = await resolvedService.fetchVideosByAuthors(authorList, options);
+ context?.log?.(`[subscriptions-source] fetchVideosByAuthors returned ${fetched ? fetched.length : 0} videos.`);
if (Array.isArray(fetched) && fetched.length > 0) {
videos = fetched;
}
diff --git a/js/nostr/client.js b/js/nostr/client.js
index 4790e0bd..31210d98 100644
--- a/js/nostr/client.js
+++ b/js/nostr/client.js
@@ -1247,6 +1247,7 @@ export class NostrClient {
this.countUnsupportedRelays = new Set();
this.nip46Client = null;
this.remoteSignerListeners = new Set();
+ this.sessionActorListeners = new Set();
const storedRemoteSigner = this.getStoredNip46Metadata();
this.remoteSignerStatus = {
state: storedRemoteSigner.hasSession ? "stored" : "idle",
@@ -1398,6 +1399,53 @@ export class NostrClient {
};
}
+ emitSessionActorChange(actor, context = {}) {
+ const resolvedActor =
+ actor && typeof actor === "object" ? actor : this.sessionActor;
+ const pubkey =
+ typeof resolvedActor?.pubkey === "string" ? resolvedActor.pubkey.trim() : "";
+ if (!pubkey) {
+ return null;
+ }
+
+ const reason =
+ typeof context.reason === "string" && context.reason.trim()
+ ? context.reason.trim()
+ : "";
+ const createdAt = Number.isFinite(resolvedActor?.createdAt)
+ ? resolvedActor.createdAt
+ : null;
+ const snapshot = {
+ pubkey,
+ source:
+ typeof resolvedActor?.source === "string" ? resolvedActor.source : "",
+ persisted: resolvedActor?.persisted === true,
+ createdAt,
+ reason,
+ };
+
+ for (const listener of Array.from(this.sessionActorListeners)) {
+ try {
+ listener(snapshot);
+ } catch (error) {
+ devLogger.warn("[nostr] Session actor listener threw:", error);
+ }
+ }
+
+ return snapshot;
+ }
+
+ onSessionActorChange(listener) {
+ if (typeof listener !== "function") {
+ return () => {};
+ }
+
+ this.sessionActorListeners.add(listener);
+ return () => {
+ this.sessionActorListeners.delete(listener);
+ };
+ }
+
async createNip46KeyPair(existingPrivateKey = "", existingPublicKey = "") {
let privateKey =
typeof existingPrivateKey === "string" && existingPrivateKey.trim()
@@ -3167,6 +3215,7 @@ export class NostrClient {
const restored = this.restoreSessionActorFromStorage();
if (restored) {
this.sessionActor = restored;
+ this.emitSessionActorChange(restored, { reason: "restored" });
return restored.pubkey;
}
}
@@ -3179,6 +3228,7 @@ export class NostrClient {
this.sessionActorCipherClosures = null;
this.sessionActorCipherClosuresPrivateKey = null;
this.persistSessionActor(minted);
+ this.emitSessionActorChange(minted, { reason: "minted" });
return minted.pubkey;
}
diff --git a/js/nostr/toolkit.js b/js/nostr/toolkit.js
index f3314803..829be9c8 100644
--- a/js/nostr/toolkit.js
+++ b/js/nostr/toolkit.js
@@ -581,6 +581,7 @@ export function shimLegacySimplePoolMethods(pool) {
: 7000;
timer = setTimeout(() => {
+ devLogger.warn(`[toolkit] pool.list timed out after ${timeoutMs}ms.`);
timer = null;
try {
subscription.unsub?.();
diff --git a/js/r2.js b/js/r2.js
index 777165f0..fed1bdd7 100644
--- a/js/r2.js
+++ b/js/r2.js
@@ -10,8 +10,12 @@ function sanitizeBaseDomain(domain) {
return "";
}
let value = String(domain).trim().toLowerCase();
- value = value.replace(/^https?:\/\//, "");
- value = value.replace(/\/.*$/, "");
+ // Ensure it starts with https:// if it doesn't already
+ if (!/^https?:\/\//.test(value)) {
+ value = `https://${value}`;
+ }
+ // Remove trailing slashes
+ value = value.replace(/\/+$/, "");
return value;
}
diff --git a/js/services/moderationService.js b/js/services/moderationService.js
index 6562567c..98b77ae4 100644
--- a/js/services/moderationService.js
+++ b/js/services/moderationService.js
@@ -446,6 +446,19 @@ function ensureNumber(value) {
return Number.isFinite(parsed) ? parsed : 0;
}
+function resolveStorage() {
+ if (typeof localStorage !== "undefined") {
+ return localStorage;
+ }
+ if (typeof window !== "undefined" && window.localStorage) {
+ return window.localStorage;
+ }
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
+ return globalThis.localStorage;
+ }
+ return null;
+}
+
export class ModerationService {
constructor({
nostrClient: client = null,
@@ -459,6 +472,7 @@ export class ModerationService {
this.userLogger = normalizeUserLogger(userChannel);
this.viewerPubkey = "";
+ this.viewerIsSessionActor = false;
this.trustedContacts = new Set();
this.trustedSeedContacts = new Set();
this.viewerContacts = new Set();
@@ -520,9 +534,11 @@ export class ModerationService {
}
if (this.trustedSeedContacts instanceof Set) {
+ const adminSnapshot = this.getAdminListSnapshot();
+ const blacklistHex = adminSnapshot.blacklistHex;
for (const seed of this.trustedSeedContacts) {
const normalized = normalizeHex(seed);
- if (normalized) {
+ if (normalized && (!blacklistHex || !blacklistHex.has(normalized))) {
merged.add(normalized);
}
}
@@ -634,19 +650,23 @@ export class ModerationService {
}
setTrustedSeeds(seeds = []) {
+ this.log("[moderationService] setTrustedSeeds called", { count: seeds?.length || seeds?.size || 0 });
const sanitizedSeeds = new Set();
+ const adminSnapshot = this.getAdminListSnapshot();
if (seeds instanceof Set || Array.isArray(seeds)) {
for (const candidate of seeds) {
const normalized = normalizeToHex(candidate);
- if (normalized) {
+ const status = this.getAccessControlStatus(candidate, adminSnapshot);
+ if (normalized && !status.blacklisted) {
sanitizedSeeds.add(normalized);
}
}
} else if (seeds && typeof seeds[Symbol.iterator] === "function") {
for (const candidate of seeds) {
const normalized = normalizeToHex(candidate);
- if (normalized) {
+ const status = this.getAccessControlStatus(candidate, adminSnapshot);
+ if (normalized && !status.blacklisted) {
sanitizedSeeds.add(normalized);
}
}
@@ -812,12 +832,7 @@ export class ModerationService {
}
clearTrustedMuteTracking({ previousContacts = null } = {}) {
- const previous =
- previousContacts instanceof Set
- ? new Set(previousContacts)
- : this.trustedContacts instanceof Set
- ? new Set(this.trustedContacts)
- : new Set();
+ void previousContacts;
for (const [pubkey, entry] of this.trustedMuteSubscriptions.entries()) {
if (entry && typeof entry.unsub === "function") {
@@ -845,7 +860,8 @@ export class ModerationService {
this.emit("trusted-mutes", { total: 0 });
- this.rebuildTrustedContacts(new Set(), { previous });
+ const emptyPrevious = new Set();
+ this.rebuildTrustedContacts(new Set(), { previous: emptyPrevious });
}
isTrustedMuteOwner(pubkey) {
@@ -863,6 +879,11 @@ export class ModerationService {
const previous = previousSet instanceof Set ? new Set(previousSet) : new Set();
const next = nextSet instanceof Set ? new Set(nextSet) : new Set();
+ this.log("[moderationService] reconcileTrustedMuteSubscriptions", {
+ previous: previous.size,
+ next: next.size,
+ });
+
if (this.viewerPubkey) {
if (this.trustedMuteSubscriptions.has(this.viewerPubkey)) {
previous.add(this.viewerPubkey);
@@ -877,14 +898,12 @@ export class ModerationService {
}
for (const value of next) {
- if (!previous.has(value)) {
- this.subscribeToTrustedMuteList(value).catch((error) => {
- this.log(
- `(moderationService) failed to subscribe to trusted mute list for ${value}`,
- error,
- );
- });
- }
+ this.subscribeToTrustedMuteList(value).catch((error) => {
+ this.log(
+ `(moderationService) failed to subscribe to trusted mute list for ${value}`,
+ error,
+ );
+ });
}
}
@@ -923,9 +942,12 @@ export class ModerationService {
}
return;
} else if (typeof record.unsub === "function") {
+ this.log(`[moderationService] already subscribed to ${normalized}`);
return;
}
+ this.log(`[moderationService] subscribing to trusted mute list for ${normalized}`);
+
record.promise = (async () => {
try {
await this.ensurePool();
@@ -943,6 +965,7 @@ export class ModerationService {
const relays = resolveRelayList(this.nostrClient);
if (!relays.length) {
+ this.log(`[moderationService] no relays available for ${normalized}`);
return;
}
@@ -987,6 +1010,7 @@ export class ModerationService {
});
sub.on("eose", () => {});
record.unsub = typeof sub.unsub === "function" ? () => sub.unsub() : null;
+ this.log(`[moderationService] subscribed to trusted mute list for ${normalized}`);
} catch (error) {
this.log(
`(moderationService) failed to subscribe to trusted mute list for ${normalized}`,
@@ -1149,6 +1173,7 @@ export class ModerationService {
return;
}
+ this.log(`[moderationService] ingesting trusted mute event from ${owner} (${event.id})`);
this.applyTrustedMuteEvent(owner, event);
}
@@ -1164,6 +1189,85 @@ export class ModerationService {
return this.viewerMuteList.has(normalized);
}
+ loadLocalMutes(pubkey) {
+ const normalized = normalizeHex(pubkey);
+ if (!normalized) {
+ return;
+ }
+
+ const storage = resolveStorage();
+ if (!storage) {
+ return;
+ }
+
+ const key = `bitvid:user-mutes:local:v1:${normalized}`;
+ let loaded = [];
+
+ try {
+ const raw = storage.getItem(key);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (Array.isArray(parsed)) {
+ loaded = parsed;
+ }
+ }
+ } catch (error) {
+ this.log(
+ `[moderationService] failed to load local mutes for ${normalized}`,
+ error,
+ );
+ }
+
+ const mutes = new Set();
+ for (const item of loaded) {
+ const target = normalizeHex(item);
+ if (target && target !== normalized) {
+ mutes.add(target);
+ }
+ }
+
+ this.replaceTrustedMuteList(normalized, mutes, {
+ createdAt: Date.now() / 1000,
+ eventId: `local-${Date.now()}`,
+ });
+ }
+
+ saveLocalMutes(pubkey, muteSet) {
+ const normalized = normalizeHex(pubkey);
+ if (!normalized) {
+ return;
+ }
+
+ const storage = resolveStorage();
+ if (!storage) {
+ return;
+ }
+
+ const list = [];
+ if (muteSet instanceof Set || Array.isArray(muteSet)) {
+ for (const item of muteSet) {
+ const target = normalizeHex(item);
+ if (target && target !== normalized) {
+ list.push(target);
+ }
+ }
+ }
+
+ const key = `bitvid:user-mutes:local:v1:${normalized}`;
+ try {
+ if (!list.length) {
+ storage.removeItem(key);
+ } else {
+ storage.setItem(key, JSON.stringify(list));
+ }
+ } catch (error) {
+ this.log(
+ `[moderationService] failed to save local mutes for ${normalized}`,
+ error,
+ );
+ }
+ }
+
async ensureViewerMuteListLoaded(pubkey = this.viewerPubkey) {
const normalized = normalizeHex(pubkey);
if (!normalized) {
@@ -1177,6 +1281,11 @@ export class ModerationService {
return;
}
+ if (this.viewerIsSessionActor && normalized === this.viewerPubkey) {
+ this.loadLocalMutes(normalized);
+ return;
+ }
+
if (this.viewerMutePromise) {
try {
await this.viewerMutePromise;
@@ -1210,6 +1319,15 @@ export class ModerationService {
throw error;
}
+ if (this.viewerIsSessionActor && viewer === this.viewerPubkey) {
+ this.saveLocalMutes(viewer, muted);
+ this.replaceTrustedMuteList(viewer, muted, {
+ createdAt: Date.now() / 1000,
+ eventId: `local-${Date.now()}`,
+ });
+ return { ok: true, event: null, results: [] };
+ }
+
await this.ensurePool();
const extension = typeof window !== "undefined" ? window.nostr : null;
@@ -1516,7 +1634,9 @@ export class ModerationService {
}
async refreshViewerFromClient() {
- const clientPubkey = normalizeHex(this.nostrClient?.pubkey);
+ const clientPubkey = normalizeHex(
+ this.nostrClient?.pubkey || this.nostrClient?.sessionActor?.pubkey,
+ );
if (clientPubkey === this.viewerPubkey) {
if (this.contactListPromise) {
try {
@@ -1546,6 +1666,13 @@ export class ModerationService {
this.trustedContacts instanceof Set ? new Set(this.trustedContacts) : new Set();
this.viewerPubkey = normalized;
+
+ const loggedInPubkey = normalizeHex(this.nostrClient?.pubkey);
+ const sessionPubkey = normalizeHex(this.nostrClient?.sessionActor?.pubkey);
+
+ this.viewerIsSessionActor =
+ !!normalized && !loggedInPubkey && normalized === sessionPubkey;
+
this.clearTrustedMuteTracking({ previousContacts });
if (this.contactSubscription && typeof this.contactSubscription.unsub === "function") {
@@ -2116,7 +2243,10 @@ export class ModerationService {
throw error;
}
- const reporterPubkey = normalizeHex(this.nostrClient?.pubkey);
+ const reporterPubkey = normalizeHex(
+ this.nostrClient?.pubkey || this.nostrClient?.sessionActor?.pubkey,
+ );
+
if (!reporterPubkey) {
const error = new Error("viewer-not-logged-in");
error.code = "viewer-not-logged-in";
@@ -2130,14 +2260,19 @@ export class ModerationService {
throw error;
}
- const extension = typeof window !== "undefined" ? window.nostr : null;
- if (!extension) {
- const error = new Error("nostr-extension-missing");
- error.code = "nostr-extension-missing";
+ const signer = await this.nostrClient.ensureActiveSignerForPubkey(
+ reporterPubkey,
+ );
+ if (!signer || typeof signer.signEvent !== "function") {
+ const error = new Error("signer-unavailable");
+ error.code = "signer-unavailable";
throw error;
}
- if (typeof this.nostrClient?.ensureExtensionPermissions === "function") {
+ if (
+ signer.type === "extension" &&
+ typeof this.nostrClient?.ensureExtensionPermissions === "function"
+ ) {
const permissionResult = await this.nostrClient.ensureExtensionPermissions([
"sign_event",
"get_public_key",
@@ -2174,7 +2309,7 @@ export class ModerationService {
let signedEvent;
try {
- signedEvent = await extension.signEvent(event);
+ signedEvent = await signer.signEvent(event);
} catch (error) {
const wrapped = new Error("signature-failed");
wrapped.code = "signature-failed";
diff --git a/js/services/nostrService.js b/js/services/nostrService.js
index 5c420515..7a5ea3f0 100644
--- a/js/services/nostrService.js
+++ b/js/services/nostrService.js
@@ -157,6 +157,17 @@ function extractSnapshotPreview(message) {
return sanitizeSnapshotPreview(selected || "");
}
+function getActiveKey(video) {
+ if (video.videoRootId) {
+ return `ROOT:${video.videoRootId}`;
+ }
+ const dTag = video.tags?.find((t) => Array.isArray(t) && t[0] === "d");
+ if (dTag && typeof dTag[1] === "string") {
+ return `${video.pubkey}:${dTag[1]}`;
+ }
+ return `LEGACY:${video.id}`;
+}
+
function resolveDirectMessageRemotePubkey(message, actorPubkey = "") {
const normalizedActor = normalizeHexPubkey(actorPubkey);
@@ -1165,10 +1176,11 @@ export class NostrService {
return [];
}
+ this.log(`[nostrService] fetchVideosByAuthors START. Authors: ${authorList.length}`);
+
const requestedLimit = Number(options?.limit);
const resolvedLimit = this.nostrClient.clampVideoRequestLimit(
- requestedLimit,
- DEFAULT_VIDEO_REQUEST_LIMIT
+ requestedLimit
);
const filter = {
@@ -1183,31 +1195,38 @@ export class NostrService {
const invalidNotes = [];
try {
+ this.log(`[nostrService] Querying ${this.nostrClient.relays.length} relays...`);
await Promise.all(
this.nostrClient.relays.map(async (url) => {
- const events = await this.nostrClient.pool.list([url], [filter]);
- for (const evt of events) {
- if (evt && evt.id) {
- this.nostrClient.rawEvents.set(evt.id, evt);
- }
- const vid = convertEventToVideo(evt);
- if (vid.invalid) {
- invalidNotes.push({ id: vid.id, reason: vid.reason });
- } else {
- if (
- this.nostrClient &&
- typeof this.nostrClient.applyRootCreatedAt === "function"
- ) {
- this.nostrClient.applyRootCreatedAt(vid);
+ try {
+ this.log(`[nostrService] Querying relay: ${url}`);
+ const events = await this.nostrClient.pool.list([url], [filter]);
+ this.log(`[nostrService] Relay ${url} returned ${events.length} events.`);
+ for (const evt of events) {
+ if (evt && evt.id) {
+ this.nostrClient.rawEvents.set(evt.id, evt);
}
- const activeKey = getActiveKey(vid);
- if (vid.deleted) {
- this.recordTombstone(activeKey, vid.created_at);
+ const vid = convertEventToVideo(evt);
+ if (vid.invalid) {
+ invalidNotes.push({ id: vid.id, reason: vid.reason });
} else {
- this.applyTombstoneGuard(vid);
+ if (
+ this.nostrClient &&
+ typeof this.nostrClient.applyRootCreatedAt === "function"
+ ) {
+ this.nostrClient.applyRootCreatedAt(vid);
+ }
+ const activeKey = getActiveKey(vid);
+ if (vid.deleted) {
+ this.nostrClient.recordTombstone(activeKey, vid.created_at);
+ } else {
+ this.nostrClient.applyTombstoneGuard(vid);
+ }
+ localAll.set(evt.id, vid);
}
- localAll.set(evt.id, vid);
}
+ } catch (relayErr) {
+ this.log(`[nostrService] Relay ${url} failed:`, relayErr);
}
})
);
diff --git a/js/services/r2Service.js b/js/services/r2Service.js
index 55b40593..ed55df9b 100644
--- a/js/services/r2Service.js
+++ b/js/services/r2Service.js
@@ -19,6 +19,8 @@ import {
makeR2Client,
multipartUpload,
ensureBucketCors,
+ ensureBucketExists,
+ deleteObject,
} from "../storage/r2-s3.js";
import { truncateMiddle } from "../utils/formatters.js";
import { userLogger } from "../utils/logger.js";
@@ -35,9 +37,7 @@ function createDefaultSettings() {
accountId: "",
accessKeyId: "",
secretAccessKey: "",
- apiToken: "",
- zoneId: "",
- baseDomain: "",
+ baseDomain: "", // Now interpreted as the public URL base
buckets: {},
};
}
@@ -46,7 +46,6 @@ class R2Service {
constructor() {
this.listeners = new Map();
this.cloudflareSettings = null;
- this.cloudflareAdvancedVisible = false;
}
on(event, handler) {
@@ -89,19 +88,6 @@ class R2Service {
this.emit("settingsChanged", { settings: this.getSettings() });
}
- getCloudflareAdvancedVisibility() {
- return Boolean(this.cloudflareAdvancedVisible);
- }
-
- setCloudflareAdvancedVisibility(visible) {
- const isVisible = Boolean(visible);
- if (this.cloudflareAdvancedVisible === isVisible) {
- return;
- }
- this.cloudflareAdvancedVisible = isVisible;
- this.emit("advancedVisibilityChange", { visible: isVisible });
- }
-
normalizeStatusVariant(variant) {
if (STATUS_VARIANTS.has(variant)) {
return variant;
@@ -203,23 +189,9 @@ class R2Service {
accountId: settings?.accountId || "",
accessKeyId: settings?.accessKeyId || "",
secretAccessKey: settings?.secretAccessKey || "",
- apiToken: settings?.apiToken || "",
- zoneId: settings?.zoneId || "",
baseDomain: settings?.baseDomain || "",
};
- const hasAdvancedValues = Boolean(
- (data.apiToken && data.apiToken.length > 0) ||
- (data.zoneId && data.zoneId.length > 0) ||
- (data.baseDomain && data.baseDomain.length > 0)
- );
-
- if (hasAdvancedValues) {
- this.setCloudflareAdvancedVisibility(true);
- } else if (!this.cloudflareAdvancedVisible) {
- this.setCloudflareAdvancedVisibility(false);
- }
-
this.setCloudflareSettingsStatus("");
this.emit("settingsPopulated", { settings: data });
}
@@ -246,9 +218,7 @@ class R2Service {
const accountId = String(formValues.accountId || "").trim();
const accessKeyId = String(formValues.accessKeyId || "").trim();
const secretAccessKey = String(formValues.secretAccessKey || "").trim();
- const apiToken = String(formValues.apiToken || "").trim();
- const zoneId = String(formValues.zoneId || "").trim();
- const baseDomain = sanitizeBaseDomain(formValues.baseDomain || "");
+ const baseDomain = sanitizeBaseDomain(formValues.baseDomain || ""); // This is the Public Bucket URL
if (!accountId || !accessKeyId || !secretAccessKey) {
if (!quiet) {
@@ -260,14 +230,23 @@ class R2Service {
return false;
}
+ if (!baseDomain) {
+ if (!quiet) {
+ this.setCloudflareSettingsStatus(
+ "Public Bucket URL is required (e.g., https://pub-xxx.r2.dev).",
+ "error"
+ );
+ }
+ return false;
+ }
+
let buckets = { ...(this.getSettings().buckets || {}) };
const previousAccount = this.getSettings().accountId || "";
const previousBaseDomain = this.getSettings().baseDomain || "";
- const previousZoneId = this.getSettings().zoneId || "";
+
if (
previousAccount !== accountId ||
- previousBaseDomain !== baseDomain ||
- previousZoneId !== zoneId
+ previousBaseDomain !== baseDomain
) {
buckets = {};
}
@@ -276,8 +255,6 @@ class R2Service {
accountId,
accessKeyId,
secretAccessKey,
- apiToken,
- zoneId,
baseDomain,
buckets,
};
@@ -312,7 +289,6 @@ class R2Service {
const refreshed = await loadR2Settings();
this.setSettings(refreshed);
this.populateCloudflareSettingsInputs(refreshed);
- this.setCloudflareAdvancedVisibility(false);
this.setCloudflareSettingsStatus("Settings cleared.", "success");
return true;
} catch (err) {
@@ -340,297 +316,171 @@ class R2Service {
return Array.from(origins);
}
- deriveSubdomainForNpub(npub) {
- try {
- return deriveShortSubdomain(npub);
- } catch (err) {
- userLogger.warn("Failed to derive short subdomain, falling back:", err);
- }
-
- const base = String(npub || "user")
- .toLowerCase()
- .replace(/[^a-z0-9-]/g, "")
- .replace(/^-+|[-]+$/g, "");
- return base.slice(0, 32) || "user";
- }
-
async ensureBucketConfigForNpub(npub) {
if (!npub || !this.cloudflareSettings) {
return null;
}
const accountId = (this.cloudflareSettings.accountId || "").trim();
- const apiToken = (this.cloudflareSettings.apiToken || "").trim();
- const zoneId = (this.cloudflareSettings.zoneId || "").trim();
const accessKeyId = (this.cloudflareSettings.accessKeyId || "").trim();
const secretAccessKey = (this.cloudflareSettings.secretAccessKey || "").trim();
- const corsOrigins = this.getCorsOrigins();
const baseDomain = this.cloudflareSettings.baseDomain || "";
+ const corsOrigins = this.getCorsOrigins();
if (!accountId) {
throw new Error("Cloudflare account ID is missing.");
}
-
- let entry = this.cloudflareSettings.buckets?.[npub] || null;
-
- if (entry && entry.publicBaseUrl) {
- if (apiToken) {
- try {
- await ensureBucket({
- accountId,
- bucket: entry.bucket,
- token: apiToken,
- });
- await putCors({
- accountId,
- bucket: entry.bucket,
- token: apiToken,
- origins: corsOrigins,
- });
- } catch (err) {
- userLogger.warn("Failed to refresh bucket configuration:", err);
- }
- } else if (accessKeyId && secretAccessKey && corsOrigins.length > 0) {
- try {
- const s3 = makeR2Client({
- accountId,
- accessKeyId,
- secretAccessKey,
- });
- await ensureBucketCors({
- s3,
- bucket: entry.bucket,
- origins: corsOrigins,
- });
- } catch (err) {
- userLogger.warn("Failed to refresh bucket CORS via access keys:", err);
- }
- }
- return {
- entry,
- usedManagedFallback: entry.domainType !== "custom",
- customDomainStatus: entry.domainType === "custom" ? "active" : "skipped",
- };
+ if (!baseDomain) {
+ throw new Error("Public Bucket URL is missing.");
}
- if (!apiToken) {
- const bucketName = entry?.bucket || sanitizeBucketName(npub);
- const manualCustomDomain = baseDomain
- ? `https://${this.deriveSubdomainForNpub(npub)}.${baseDomain}`
- : "";
+ // We no longer support automated bucket creation or domain management via API token.
+ // We assume the user has created the bucket with the correct name and configured the public domain.
+ const bucketName = sanitizeBucketName(npub);
- let publicBaseUrl = entry?.publicBaseUrl || manualCustomDomain;
- if (!publicBaseUrl) {
- publicBaseUrl = `https://${bucketName}.${accountId}.r2.dev`;
- }
-
- if (!publicBaseUrl) {
- throw new Error(
- "No public bucket domain configured. Please add a Cloudflare API token to automate domain setup."
- );
- }
-
- const manualEntry = {
- bucket: bucketName,
- publicBaseUrl,
- domainType: publicBaseUrl.includes(".r2.dev") ? "managed" : "custom",
- lastUpdated: Date.now(),
- };
-
- if (accessKeyId && secretAccessKey && corsOrigins.length > 0) {
+ // We attempt to ensure the bucket exists and CORS is set up using the S3 keys if possible.
+ if (accessKeyId && secretAccessKey) {
try {
const s3 = makeR2Client({
accountId,
accessKeyId,
secretAccessKey,
});
- await ensureBucketCors({
- s3,
- bucket: bucketName,
- origins: corsOrigins,
- });
+
+ // Attempt to auto-create the bucket (requires Admin keys, but harmless if fails)
+ try {
+ await ensureBucketExists({ s3, bucket: bucketName });
+ } catch (createErr) {
+ // 403 Forbidden is expected if keys are "Object Read & Write" only.
+ // We proceed assuming the user might have created it manually.
+ userLogger.debug("Auto-creation of bucket failed (likely permission issue), proceeding...", createErr);
+ }
+
+ if (corsOrigins.length > 0) {
+ await ensureBucketCors({
+ s3,
+ bucket: bucketName,
+ origins: corsOrigins,
+ });
+ }
} catch (corsErr) {
userLogger.warn(
- "Failed to ensure R2 CORS rules via access keys. Configure the bucket's CORS policy manually if uploads continue to fail.",
+ "Failed to ensure R2 bucket/CORS configuration via access keys. Ensure the bucket exists and you have permissions.",
corsErr
);
}
- }
+ }
- let savedEntry = entry;
- if (
+ // Use the user-provided baseDomain as the public URL root.
+ // Clean up trailing slash if present.
+ const cleanBaseUrl = baseDomain.replace(/\/+$/, "");
+
+ const manualEntry = {
+ bucket: bucketName,
+ publicBaseUrl: cleanBaseUrl,
+ domainType: "manual",
+ lastUpdated: Date.now(),
+ };
+
+ let entry = this.cloudflareSettings.buckets?.[npub];
+ let savedEntry = entry;
+
+ if (
!entry ||
entry.bucket !== manualEntry.bucket ||
- entry.publicBaseUrl !== manualEntry.publicBaseUrl ||
- entry.domainType !== manualEntry.domainType
- ) {
+ entry.publicBaseUrl !== manualEntry.publicBaseUrl
+ ) {
const updatedSettings = await saveR2Settings(
mergeBucketEntry(this.getSettings(), npub, manualEntry)
);
this.setSettings(updatedSettings);
savedEntry = updatedSettings.buckets?.[npub] || manualEntry;
- }
-
- return {
- entry: savedEntry,
- usedManagedFallback: manualEntry.domainType !== "custom",
- customDomainStatus:
- manualEntry.domainType === "custom" ? "manual" : "managed",
- };
}
- const bucketName = entry?.bucket || sanitizeBucketName(npub);
+ return {
+ entry: savedEntry,
+ usedManagedFallback: false,
+ customDomainStatus: "manual",
+ };
+ }
- await ensureBucket({ accountId, bucket: bucketName, token: apiToken });
+ async verifyPublicAccess({ settings, npub }) {
+ if (!settings || !npub) {
+ return { success: false, error: "Missing settings or npub." };
+ }
+
+ const { accountId, accessKeyId, secretAccessKey, baseDomain } = settings;
+ if (!accountId || !accessKeyId || !secretAccessKey || !baseDomain) {
+ return { success: false, error: "Incomplete credentials or missing public URL." };
+ }
+
+ const bucketName = sanitizeBucketName(npub);
+ const verifyKey = `.verify-${Date.now()}-${Math.random().toString(36).substring(7)}.txt`;
+ const verifyContent = "bitvid-verification";
+ const publicUrl = buildPublicUrl(baseDomain, verifyKey);
try {
- await putCors({
- accountId,
- bucket: bucketName,
- token: apiToken,
- origins: corsOrigins,
- });
- } catch (err) {
- userLogger.warn("Failed to apply R2 CORS rules:", err);
- }
+ // 1. Initialize S3
+ const s3 = makeR2Client({ accountId, accessKeyId, secretAccessKey });
- let publicBaseUrl = entry?.publicBaseUrl || "";
- let domainType = entry?.domainType || "managed";
- let usedManagedFallback = false;
- let customDomainStatus = "skipped";
-
- if (baseDomain && zoneId) {
- const domain = `${this.deriveSubdomainForNpub(npub)}.${baseDomain}`;
+ // 2. Ensure bucket/cors (best effort)
try {
- const custom = await attachCustomDomainAndWait({
- accountId,
- bucket: bucketName,
- token: apiToken,
- zoneId,
- domain,
- pollInterval: 2500,
- timeoutMs: 120000,
- });
- customDomainStatus = custom?.status || "unknown";
- if (custom?.active && custom?.url) {
- publicBaseUrl = custom.url;
- domainType = "custom";
- try {
- await setManagedDomain({
- accountId,
- bucket: bucketName,
- token: apiToken,
- enabled: false,
- });
- } catch (disableErr) {
- userLogger.warn("Failed to disable managed domain:", disableErr);
- }
- } else {
- usedManagedFallback = true;
- }
- } catch (err) {
- if (/already exists/i.test(err.message || "")) {
- publicBaseUrl = `https://${domain}`;
- domainType = "custom";
- customDomainStatus = "active";
- try {
- await setManagedDomain({
- accountId,
- bucket: bucketName,
- token: apiToken,
- enabled: false,
- });
- } catch (disableErr) {
- userLogger.warn("Failed to disable managed domain:", disableErr);
- }
- } else {
- userLogger.warn("Failed to attach custom domain, falling back:", err);
- usedManagedFallback = true;
- customDomainStatus = "error";
+ await ensureBucketExists({ s3, bucket: bucketName });
+ const corsOrigins = this.getCorsOrigins();
+ if (corsOrigins.length > 0) {
+ await ensureBucketCors({ s3, bucket: bucketName, origins: corsOrigins });
}
+ } catch (setupErr) {
+ userLogger.warn("Bucket setup warning during verification:", setupErr);
+ // Continue, assuming bucket might already exist and be configured
}
- }
- if (!publicBaseUrl) {
- const managed = await setManagedDomain({
- accountId,
+ // 3. Upload Test File
+ const file = new File([verifyContent], "verify.txt", { type: "text/plain" });
+ await multipartUpload({
+ s3,
bucket: bucketName,
- token: apiToken,
- enabled: true,
+ key: verifyKey,
+ file,
+ contentType: "text/plain",
});
- publicBaseUrl = managed?.url || `https://${bucketName}.${accountId}.r2.dev`;
- domainType = "managed";
- usedManagedFallback = true;
- customDomainStatus =
- customDomainStatus === "skipped" ? "managed" : customDomainStatus;
- }
- const mergedEntry = {
- bucket: bucketName,
- publicBaseUrl,
- domainType,
- lastUpdated: Date.now(),
- };
- const updatedSettings = await saveR2Settings(
- mergeBucketEntry(this.getSettings(), npub, mergedEntry)
- );
- this.setSettings(updatedSettings);
- return { entry: mergedEntry, usedManagedFallback, customDomainStatus };
+ // 4. Verify Public Access (Fetch)
+ // Wait a moment for propagation (R2 is usually instant-ish but helpful to wait)
+ await new Promise((r) => setTimeout(r, 500));
+
+ const response = await fetch(publicUrl, { method: "GET", cache: "no-cache" });
+
+ if (!response.ok) {
+ // Cleanup attempt
+ try { await deleteObject({ s3, bucket: bucketName, key: verifyKey }); } catch (e) {}
+
+ if (response.status === 404) {
+ return { success: false, error: "File not found. Check your Public Bucket URL." };
+ }
+ return { success: false, error: `Public access failed (HTTP ${response.status}). Is the bucket public?` };
+ }
+
+ const text = await response.text();
+
+ // Cleanup
+ try { await deleteObject({ s3, bucket: bucketName, key: verifyKey }); } catch (e) {}
+
+ if (text.trim() !== verifyContent) {
+ return { success: false, error: "Content mismatch. URL might be pointing elsewhere." };
+ }
+
+ return { success: true };
+
+ } catch (err) {
+ userLogger.error("Verification failed:", err);
+ return { success: false, error: err.message || "Unknown error during verification." };
+ }
}
async updateCloudflareBucketPreview({ hasPubkey = false, npub = "" } = {}) {
- if (!this.cloudflareSettings) {
- const detail = {
- text: "Save your credentials to configure R2.",
- title: "",
- };
- this.emit("bucketPreview", detail);
- return detail;
- }
-
- if (!hasPubkey) {
- const detail = { text: "Login to preview your R2 bucket.", title: "" };
- this.emit("bucketPreview", detail);
- return detail;
- }
-
- if (!npub) {
- const detail = { text: "Unable to encode npub.", title: "" };
- this.emit("bucketPreview", detail);
- return detail;
- }
-
- const entry = this.cloudflareSettings.buckets?.[npub];
- if (!entry || !entry.publicBaseUrl) {
- const detail = {
- text: "Bucket will be auto-created on your next upload.",
- title: "",
- };
- this.emit("bucketPreview", detail);
- return detail;
- }
-
- const sampleKey = buildR2Key(npub, { name: "sample.mp4" });
- const publicUrl = buildPublicUrl(entry.publicBaseUrl, sampleKey);
- const fullPreview = `${entry.bucket} • ${publicUrl}`;
-
- let displayHostAndPath = truncateMiddle(publicUrl, 72);
- try {
- const parsed = new URL(publicUrl);
- const cleanPath = parsed.pathname.replace(/^\//, "");
- const truncatedPath = truncateMiddle(cleanPath || sampleKey, 32);
- displayHostAndPath = `${truncateMiddle(parsed.host, 32)}/${truncatedPath}`;
- } catch (err) {
- // ignore URL parse issues and fall back to the raw string
- }
-
- const truncatedBucket = truncateMiddle(entry.bucket, 28);
- const detail = {
- text: `${truncatedBucket} • ${displayHostAndPath}`,
- title: fullPreview,
- };
- this.emit("bucketPreview", detail);
- return detail;
+ // No-op for now or just simplified text, since we rely on user input mostly.
+ return;
}
async handleCloudflareUploadSubmit({
@@ -638,6 +488,7 @@ class R2Service {
file = null,
thumbnailFile = null,
metadata = {},
+ infoHash = "",
settingsInput = null,
publishVideoNote,
onReset,
@@ -717,7 +568,7 @@ class R2Service {
if (!bucketEntry || !bucketEntry.publicBaseUrl) {
this.setCloudflareUploadStatus(
- "Bucket is missing a public domain. Check your Cloudflare settings.",
+ "Bucket is missing a public URL. Check your settings.",
"error"
);
this.setCloudflareUploading(false);
@@ -726,22 +577,7 @@ class R2Service {
}
let statusMessage = `Uploading to ${bucketEntry.bucket}…`;
- if (bucketResult?.usedManagedFallback) {
- const baseDomain = this.cloudflareSettings?.baseDomain || "";
- if (baseDomain) {
- const customStatus = bucketResult?.customDomainStatus
- ? ` (custom domain status: ${bucketResult.customDomainStatus})`
- : "";
- statusMessage = `Using managed r2.dev domain for ${bucketEntry.bucket}. Verify your Cloudflare zone${customStatus}. Uploading…`;
- } else {
- statusMessage = `Using managed r2.dev domain for ${bucketEntry.bucket}. Uploading…`;
- }
- }
-
- this.setCloudflareUploadStatus(
- statusMessage,
- bucketResult?.usedManagedFallback ? "warning" : "info"
- );
+ this.setCloudflareUploadStatus(statusMessage, "info");
const key = buildR2Key(npub, file);
const publicUrl = buildPublicUrl(bucketEntry.publicBaseUrl, key);
@@ -771,10 +607,7 @@ class R2Service {
}
}
- this.setCloudflareUploadStatus(
- statusMessage,
- bucketResult?.usedManagedFallback ? "warning" : "info"
- );
+ this.setCloudflareUploadStatus(statusMessage, "info");
await multipartUpload({
s3,
@@ -795,13 +628,25 @@ class R2Service {
);
publishOutcome = false;
} else {
+ // Construct the Magnet Link
+ // magnet:?xt=urn:btih:&dn=&ws=
+ let generatedMagnet = "";
+ let generatedWs = "";
+
+ if (infoHash) {
+ const encodedDn = encodeURIComponent(file.name);
+ const encodedWs = encodeURIComponent(publicUrl);
+ generatedMagnet = `magnet:?xt=urn:btih:${infoHash}&dn=${encodedDn}&ws=${encodedWs}`;
+ generatedWs = publicUrl;
+ }
+
const rawVideoPayload = {
title,
- url: publicUrl,
- magnet: metadata?.magnet ?? "",
+ url: publicUrl, // Primary URL
+ magnet: generatedMagnet || (metadata?.magnet ?? ""),
thumbnail: metadata?.thumbnail ?? "",
description: metadata?.description ?? "",
- ws: metadata?.ws ?? "",
+ ws: generatedWs || (metadata?.ws ?? ""),
xs: metadata?.xs ?? "",
enableComments: metadata?.enableComments,
isNsfw: metadata?.isNsfw,
diff --git a/js/services/trustBootstrap.js b/js/services/trustBootstrap.js
index 8ac79c6a..24ba4f67 100644
--- a/js/services/trustBootstrap.js
+++ b/js/services/trustBootstrap.js
@@ -92,6 +92,7 @@ async function waitForAccessControl({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS }
}
export async function bootstrapTrustedSeeds() {
+ devLogger.info("[trustBootstrap] bootstrapTrustedSeeds called");
if (
!(
FEATURE_TRUST_SEEDS &&
@@ -99,6 +100,7 @@ export async function bootstrapTrustedSeeds() {
typeof moderationService.setTrustedSeeds === "function"
)
) {
+ devLogger.warn("[trustBootstrap] Trust seeds feature disabled or service missing");
return;
}
@@ -113,6 +115,7 @@ export async function bootstrapTrustedSeeds() {
editors,
fallbackSeeds: DEFAULT_TRUST_SEED_NPUBS,
});
+ devLogger.info(`[trustBootstrap] Applying ${seeds.size} trusted seeds`, Array.from(seeds));
moderationService.setTrustedSeeds(seeds);
if (
moderationService &&
@@ -130,6 +133,7 @@ export async function bootstrapTrustedSeeds() {
attempts += 1;
try {
const result = await waitForAccessControl();
+ devLogger.info("[trustBootstrap] waitForAccessControl result", result);
applySeeds();
return result;
} catch (error) {
@@ -178,4 +182,8 @@ export async function bootstrapTrustedSeeds() {
if (accessControl && typeof accessControl.onEditorsChange === "function") {
accessControl.onEditorsChange(applyOnChange);
}
+
+ if (accessControl && typeof accessControl.onBlacklistChange === "function") {
+ accessControl.onBlacklistChange(applyOnChange);
+ }
}
diff --git a/js/storage/r2-s3.js b/js/storage/r2-s3.js
index eb71b5dc..f638f4c2 100644
--- a/js/storage/r2-s3.js
+++ b/js/storage/r2-s3.js
@@ -25,6 +25,7 @@ function assignAwsSdk(module) {
AbortMultipartUploadCommand: module.AbortMultipartUploadCommand,
HeadBucketCommand: module.HeadBucketCommand,
PutBucketCorsCommand: module.PutBucketCorsCommand,
+ DeleteObjectCommand: module.DeleteObjectCommand,
};
}
@@ -129,7 +130,7 @@ export function makeR2Client({ accountId, accessKeyId, secretAccessKey }) {
});
}
-async function ensureBucketExists({ s3, bucket }) {
+export async function ensureBucketExists({ s3, bucket }) {
const { CreateBucketCommand, HeadBucketCommand } = requireAwsSdk();
try {
@@ -199,6 +200,30 @@ export async function ensureBucketCors({ s3, bucket, origins }) {
await s3.send(command);
}
+export async function deleteObject({ s3, bucket, key }) {
+ if (!s3 || !bucket || !key) {
+ throw new Error("Missing required parameters for deleteObject");
+ }
+
+ const { DeleteObjectCommand } = requireAwsSdk();
+
+ try {
+ await s3.send(
+ new DeleteObjectCommand({
+ Bucket: bucket,
+ Key: key,
+ })
+ );
+ } catch (error) {
+ // We treat 404 (NoSuchKey) as success since the object is gone.
+ const status = error?.$metadata?.httpStatusCode || 0;
+ const code = error?.name || error?.Code || "";
+ if (status !== 404 && code !== "NoSuchKey") {
+ throw error;
+ }
+ }
+}
+
export async function multipartUpload({
s3,
bucket,
diff --git a/js/subscriptions.js b/js/subscriptions.js
index 570c5343..e3c042a9 100644
--- a/js/subscriptions.js
+++ b/js/subscriptions.js
@@ -398,17 +398,20 @@ class SubscriptionsManager {
}
async ensureLoaded(actorHex) {
+ devLogger.log("[SubscriptionsManager] ensureLoaded start", actorHex);
const normalizedActor = normalizeHexPubkey(actorHex) || actorHex;
if (!normalizedActor) {
return;
}
if (this.loaded && this.currentUserPubkey === normalizedActor) {
+ devLogger.log("[SubscriptionsManager] ensureLoaded already loaded");
return;
}
if (this.loadingPromise) {
try {
+ devLogger.log("[SubscriptionsManager] ensureLoaded awaiting existing promise");
await this.loadingPromise;
} catch (error) {
throw error;
@@ -431,6 +434,7 @@ class SubscriptionsManager {
)
),
]);
+ devLogger.log("[SubscriptionsManager] ensureLoaded success");
} catch (error) {
userLogger.warn("[SubscriptionsManager] ensureLoaded timed out or failed:", error);
} finally {
@@ -856,7 +860,9 @@ class SubscriptionsManager {
return this.lastResult;
}
- container.innerHTML = getSidebarLoadingMarkup("Fetching subscriptions…");
+ if (!this.hasRenderedOnce) {
+ container.innerHTML = getSidebarLoadingMarkup("Fetching subscriptions…");
+ }
this.lastRunOptions = {
actorPubkey: userPubkey,
@@ -870,7 +876,9 @@ class SubscriptionsManager {
if (typeof nostrService?.awaitInitialLoad === "function") {
try {
+ devLogger.log("[SubscriptionsManager] awaiting nostrService initial load...");
await nostrService.awaitInitialLoad();
+ devLogger.log("[SubscriptionsManager] nostrService initial load done.");
} catch (error) {
devLogger.warn(
"[SubscriptionsManager] Failed to await nostrService initial load:",
@@ -906,7 +914,9 @@ class SubscriptionsManager {
try {
this.isRunningFeed = true;
+ devLogger.log("[SubscriptionsManager] Calling engine.run('subscriptions')...");
const result = await engine.run("subscriptions", runOptions);
+ devLogger.log("[SubscriptionsManager] engine.run complete. Items:", result?.items?.length);
const videos = Array.isArray(result?.items)
? result.items.map((item) => item?.video).filter(Boolean)
diff --git a/js/ui/components/EditModal.js b/js/ui/components/EditModal.js
index 92d1c70d..cb9ba308 100644
--- a/js/ui/components/EditModal.js
+++ b/js/ui/components/EditModal.js
@@ -438,9 +438,38 @@ export class EditModal {
this.applyVideoToForm(editContext);
this.nip71FormManager.resetSection(this.nip71SectionKey);
- if (video.nip71 && typeof video.nip71 === "object") {
- this.nip71FormManager.hydrateSection(this.nip71SectionKey, video.nip71);
+
+ let nip71Data = video.nip71 && typeof video.nip71 === "object" ? { ...video.nip71 } : {};
+
+ // Backfill published_at if missing, using the original root creation time
+ if (
+ !nip71Data.publishedAt &&
+ !nip71Data.published_at &&
+ !nip71Data["published-at"]
+ ) {
+ const rootCreated = video.rootCreatedAt || video.created_at;
+ if (rootCreated) {
+ nip71Data.publishedAt = rootCreated;
+ }
}
+
+ // Format publishedAt for datetime-local input (YYYY-MM-DDThh:mm)
+ const rawPublishedAt =
+ nip71Data.publishedAt || nip71Data.published_at || nip71Data["published-at"];
+ if (rawPublishedAt) {
+ const numeric = Number(rawPublishedAt);
+ if (Number.isFinite(numeric)) {
+ const dt = new Date(numeric * 1000);
+ const pad = (n) => String(n).padStart(2, "0");
+ const formatted = `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(
+ dt.getDate()
+ )}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
+ nip71Data.publishedAt = formatted;
+ }
+ }
+
+ this.nip71FormManager.hydrateSection(this.nip71SectionKey, nip71Data);
+
const initialNip71 = this.nip71FormManager.collectSection(this.nip71SectionKey);
this.originalNip71Metadata = this.cloneNip71Metadata(initialNip71);
this.originalNip71MetadataJson = JSON.stringify(this.originalNip71Metadata);
@@ -926,9 +955,23 @@ export class EditModal {
const originalIsNsfw = original.isNsfw === true;
const originalIsForKids = original.isForKids === true;
- const currentNip71 = this.cloneNip71Metadata(
- this.nip71FormManager.collectSection(this.nip71SectionKey)
- );
+ const rawNip71 = this.nip71FormManager.collectSection(this.nip71SectionKey);
+ const currentNip71 = this.cloneNip71Metadata(rawNip71);
+
+ // Convert publishedAt back to Unix timestamp (seconds)
+ if (currentNip71 && typeof currentNip71.publishedAt === "string") {
+ const dateStr = currentNip71.publishedAt;
+ if (dateStr) {
+ const dt = new Date(dateStr);
+ if (!isNaN(dt.getTime())) {
+ currentNip71.publishedAt = Math.floor(dt.getTime() / 1000);
+ } else {
+ delete currentNip71.publishedAt;
+ }
+ } else {
+ delete currentNip71.publishedAt;
+ }
+ }
const hasImetaSource = Array.isArray(currentNip71?.imeta)
? currentNip71.imeta.some((variant) => {
if (!variant || typeof variant !== "object") {
diff --git a/js/ui/components/SimilarContentCard.js b/js/ui/components/SimilarContentCard.js
index 66c615a9..a759724d 100644
--- a/js/ui/components/SimilarContentCard.js
+++ b/js/ui/components/SimilarContentCard.js
@@ -2,6 +2,7 @@ import { normalizeDesignSystemContext } from "../../designSystem.js";
import { formatShortNpub } from "../../utils/formatters.js";
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
+const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
export class SimilarContentCard {
@@ -66,6 +67,7 @@ export class SimilarContentCard {
this.authorNpubEl = null;
this.timeEl = null;
this.viewCountEl = null;
+ this.discussionCountEl = null;
this.build();
}
@@ -82,6 +84,10 @@ export class SimilarContentCard {
return this.viewCountEl;
}
+ getDiscussionCountElement() {
+ return this.discussionCountEl;
+ }
+
closeMoreMenu() {}
closeSettingsMenu() {}
@@ -469,9 +475,9 @@ export class SimilarContentCard {
titleLink.title = this.video.title || "Untitled";
titleLink.style.minWidth = "0";
- // Modified order: Title -> Author -> Meta
const authorStack = this.buildAuthorStack();
const metaRow = this.buildMetaRow();
+ const engagement = this.buildEngagementSection();
content.appendChild(titleLink);
if (authorStack) {
@@ -480,6 +486,9 @@ export class SimilarContentCard {
if (metaRow) {
content.appendChild(metaRow);
}
+ if (engagement) {
+ content.appendChild(engagement);
+ }
this.contentEl = content;
this.titleEl = titleLink;
@@ -556,24 +565,6 @@ export class SimilarContentCard {
row.classList.add("player-modal__similar-card-meta");
row.style.minWidth = "0";
- // Reordered: Views -> Separator -> Time
-
- const viewEl = this.document.createElement("span");
- viewEl.classList.add("player-modal__similar-card-views", "view-count-text");
- viewEl.dataset.viewCount = "";
- viewEl.textContent = "– views";
- if (this.pointerInfo?.key) {
- viewEl.dataset.viewPointer = this.pointerInfo.key;
- }
- row.appendChild(viewEl);
- this.viewCountEl = viewEl;
-
- const separator = this.document.createElement("span");
- separator.classList.add("player-modal__similar-card-separator");
- separator.setAttribute("aria-hidden", "true");
- separator.textContent = "•";
- row.appendChild(separator);
-
const timeEl = this.document.createElement("time");
timeEl.classList.add("player-modal__similar-card-timestamp");
if (this.postedAt !== null) {
@@ -592,6 +583,146 @@ export class SimilarContentCard {
return row;
}
+ createEyeIcon(classNames = []) {
+ const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("aria-hidden", "true");
+ svg.setAttribute("focusable", "false");
+ svg.setAttribute("stroke", "currentColor");
+ svg.setAttribute("stroke-width", "2");
+ svg.setAttribute("stroke-linecap", "round");
+ svg.setAttribute("stroke-linejoin", "round");
+
+ classNames.forEach((className) => {
+ if (className) {
+ svg.classList.add(className);
+ }
+ });
+
+ const path = this.document.createElementNS(SVG_NAMESPACE, "path");
+ path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");
+ svg.appendChild(path);
+
+ const circle = this.document.createElementNS(SVG_NAMESPACE, "circle");
+ circle.setAttribute("cx", "12");
+ circle.setAttribute("cy", "12");
+ circle.setAttribute("r", "3");
+ svg.appendChild(circle);
+
+ return svg;
+ }
+
+ createMessageIcon(classNames = []) {
+ const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("aria-hidden", "true");
+ svg.setAttribute("focusable", "false");
+ svg.setAttribute("stroke", "currentColor");
+ svg.setAttribute("stroke-width", "2");
+ svg.setAttribute("stroke-linecap", "round");
+ svg.setAttribute("stroke-linejoin", "round");
+
+ classNames.forEach((className) => {
+ if (className) {
+ svg.classList.add(className);
+ }
+ });
+
+ const path = this.document.createElementNS(SVG_NAMESPACE, "path");
+ path.setAttribute(
+ "d",
+ "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
+ );
+ svg.appendChild(path);
+
+ return svg;
+ }
+
+ buildEngagementSection() {
+ const hasPointer = this.pointerInfo && this.pointerInfo.key;
+ const hasDiscussion =
+ this.video.enableComments !== false &&
+ (typeof this.video.discussionCount === "number" ||
+ (typeof this.video.discussionCount === "string" &&
+ this.video.discussionCount.trim()));
+
+ if (!hasPointer && !hasDiscussion) {
+ return null;
+ }
+
+ const container = this.document.createElement("div");
+ container.classList.add(
+ "flex",
+ "items-center",
+ "gap-4",
+ "text-xs",
+ "text-muted-strong",
+ "mt-1.5",
+ "video-card__engagement" // Reuse this class for consistent styling if global, otherwise it's just a marker
+ );
+ // Add specific class for scoping if needed, but Tailwind classes usually suffice.
+
+ // Views
+ if (hasPointer) {
+ const wrapper = this.document.createElement("div");
+ wrapper.classList.add("flex", "items-center", "gap-1.5");
+ wrapper.setAttribute("title", "Views");
+
+ const icon = this.createEyeIcon(["w-3.5", "h-3.5"]);
+ wrapper.appendChild(icon);
+
+ const view = this.document.createElement("span");
+ view.classList.add("view-count-text");
+ view.textContent = "–";
+ view.dataset.viewCount = "";
+ view.dataset.viewPointer = this.pointerInfo.key;
+
+ wrapper.appendChild(view);
+ container.appendChild(wrapper);
+
+ this.viewCountEl = view;
+ }
+
+ // Discussion
+ if (hasDiscussion) {
+ let initialCount = 0;
+ if (typeof this.video.discussionCount === "number") {
+ initialCount = this.video.discussionCount;
+ } else if (typeof this.video.discussionCount === "string") {
+ const parsed = Number.parseInt(this.video.discussionCount.trim(), 10);
+ if (Number.isFinite(parsed)) {
+ initialCount = parsed;
+ }
+ }
+
+ if (Number.isFinite(initialCount) && initialCount >= 0) {
+ const wrapper = this.document.createElement("div");
+ wrapper.classList.add("flex", "items-center", "gap-1.5");
+ wrapper.dataset.discussionCount = this.video.id;
+ wrapper.dataset.countState = "ready";
+ wrapper.setAttribute("title", "Comments");
+
+ const icon = this.createMessageIcon(["w-3.5", "h-3.5"]);
+ wrapper.appendChild(icon);
+
+ const displayValue = initialCount.toLocaleString();
+
+ const valueEl = this.document.createElement("span");
+ valueEl.textContent = displayValue;
+ valueEl.dataset.discussionCountValue = "";
+
+ wrapper.appendChild(valueEl);
+ container.appendChild(wrapper);
+
+ this.discussionCountEl = wrapper;
+ }
+ }
+
+ return container;
+ }
+
applyPointerDatasets() {
if (!this.root || !this.pointerInfo) {
return;
diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js
index a1738570..bf73a5ff 100644
--- a/js/ui/components/UploadModal.js
+++ b/js/ui/components/UploadModal.js
@@ -7,6 +7,8 @@ import {
normalizeVideoNotePayload,
VIDEO_NOTE_ERROR_CODES,
} from "../../services/videoNotePayload.js";
+import { calculateTorrentInfoHash } from "../../utils/torrentHash.js";
+import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
export class UploadModal {
constructor({
@@ -137,7 +139,6 @@ export class UploadModal {
external: $("#section-source-external"),
settings: $("#section-storage-settings"),
advanced: $("#section-advanced"),
- r2Advanced: $("#section-r2-advanced"),
progress: $("#upload-progress-container"),
};
@@ -152,12 +153,11 @@ export class UploadModal {
magnet: $("#input-magnet"),
// Settings
+ r2BucketName: $("#input-r2-bucket-name"),
r2Account: $("#input-r2-account"),
r2Key: $("#input-r2-key"),
r2Secret: $("#input-r2-secret"),
- r2Token: $("#input-r2-token"),
- r2Zone: $("#input-r2-zone"),
- r2Domain: $("#input-r2-domain"),
+ r2Domain: $("#input-r2-domain"), // Public URL
// Advanced (Manual or NIP71 managed)
ws: $("#input-ws"),
@@ -170,6 +170,17 @@ export class UploadModal {
progress: $("#input-progress"),
};
+ // Wizard Containers
+ this.wizard = {
+ step1: $("#step-credentials"),
+ step2: $("#step-verification"),
+ nextBtn: $("#btn-next-step"),
+ backBtn: $("#btn-back-step"),
+ errorContainer: $("#container-verification-error"),
+ errorText: $("#text-verification-error"),
+ guideLink: $("#link-cloudflare-guide"),
+ };
+
// Toggles/Buttons
this.toggles = {
nsfw: $("#check-nsfw"),
@@ -179,10 +190,10 @@ export class UploadModal {
advanced: $("#btn-advanced-toggle"),
storageSettings: $("#btn-storage-settings"),
- r2Advanced: $("#btn-r2-advanced"),
- saveSettings: $("#btn-save-settings"),
+ saveSettings: $("#btn-save-settings"), // Verify & Save
browseThumbnail: $("#btn-thumbnail-file"),
r2HelpLink: $("#link-r2-help"),
+ copyBucket: $("#btn-copy-bucket"),
};
// Status text
@@ -203,7 +214,6 @@ export class UploadModal {
// Toggles
this.setupAccordion(this.toggles.advanced, this.sourceSections.advanced);
this.setupAccordion(this.toggles.storageSettings, this.sourceSections.settings);
- this.setupAccordion(this.toggles.r2Advanced, this.sourceSections.r2Advanced);
// Form Submission
this.form.addEventListener("submit", (e) => {
@@ -211,9 +221,19 @@ export class UploadModal {
this.handleSubmit();
});
- // Settings Save
+ // Wizard: Step 1 Next
+ if (this.wizard.nextBtn) {
+ this.wizard.nextBtn.addEventListener("click", () => this.goToVerificationStep());
+ }
+
+ // Wizard: Step 2 Back
+ if (this.wizard.backBtn) {
+ this.wizard.backBtn.addEventListener("click", () => this.goToCredentialsStep());
+ }
+
+ // Settings Save (Verify & Save)
this.toggles.saveSettings.addEventListener("click", async () => {
- await this.handleSaveSettings();
+ await this.handleVerifyAndSave();
});
// Help Link
@@ -223,6 +243,20 @@ export class UploadModal {
});
}
+ // Copy Bucket Name
+ if (this.toggles.copyBucket && this.inputs.r2BucketName) {
+ this.toggles.copyBucket.addEventListener("click", () => {
+ const val = this.inputs.r2BucketName.value;
+ if (val) {
+ navigator.clipboard.writeText(val).then(() => {
+ const original = this.toggles.copyBucket.textContent;
+ this.toggles.copyBucket.textContent = "Copied!";
+ setTimeout(() => this.toggles.copyBucket.textContent = original, 1500);
+ });
+ }
+ });
+ }
+
// Automation
this.setupDescriptionMirror();
this.setupMutuallyExclusiveCheckboxes(this.toggles.nsfw, this.toggles.kids);
@@ -270,6 +304,10 @@ export class UploadModal {
if (this.toggles.browseThumbnail) {
this.toggles.browseThumbnail.classList.remove("hidden");
}
+
+ // Populate derived bucket name
+ this.populateBucketName();
+
} else {
this.sourceSections.upload.classList.add("hidden");
this.sourceSections.external.classList.remove("hidden");
@@ -304,6 +342,15 @@ export class UploadModal {
// --- Automation Helpers ---
+ populateBucketName() {
+ const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
+ if (pubkey && this.safeEncodeNpub && this.inputs.r2BucketName) {
+ const npub = this.safeEncodeNpub(pubkey);
+ const bucketName = sanitizeBucketName(npub);
+ this.inputs.r2BucketName.value = bucketName;
+ }
+ }
+
setupDescriptionMirror() {
const { description, summary } = this.inputs;
const { summaryUnlock } = this.toggles;
@@ -382,15 +429,13 @@ export class UploadModal {
hasValidR2Settings() {
const s = this.cloudflareSettings;
- return Boolean(s?.accountId && s?.accessKeyId && s?.secretAccessKey);
+ return Boolean(s?.accountId && s?.accessKeyId && s?.secretAccessKey && s?.baseDomain);
}
fillSettingsForm(s) {
if (this.inputs.r2Account) this.inputs.r2Account.value = s.accountId || "";
if (this.inputs.r2Key) this.inputs.r2Key.value = s.accessKeyId || "";
if (this.inputs.r2Secret) this.inputs.r2Secret.value = s.secretAccessKey || "";
- if (this.inputs.r2Token) this.inputs.r2Token.value = s.apiToken || "";
- if (this.inputs.r2Zone) this.inputs.r2Zone.value = s.zoneId || "";
if (this.inputs.r2Domain) this.inputs.r2Domain.value = s.baseDomain || "";
}
@@ -399,28 +444,103 @@ export class UploadModal {
accountId: this.inputs.r2Account?.value?.trim() || "",
accessKeyId: this.inputs.r2Key?.value?.trim() || "",
secretAccessKey: this.inputs.r2Secret?.value?.trim() || "",
- apiToken: this.inputs.r2Token?.value?.trim() || "",
- zoneId: this.inputs.r2Zone?.value?.trim() || "",
- baseDomain: this.inputs.r2Domain?.value?.trim() || "",
+ baseDomain: this.inputs.r2Domain?.value?.trim() || "", // Public URL
};
}
- async handleSaveSettings() {
- const settings = this.collectSettingsForm();
- await this.r2Service.saveSettings(settings);
- this.cloudflareSettings = settings;
- this.updateStorageStatus(this.hasValidR2Settings());
+ goToVerificationStep() {
+ // Validate Step 1
+ const accountId = this.inputs.r2Account?.value?.trim();
+ const keyId = this.inputs.r2Key?.value?.trim();
+ const secret = this.inputs.r2Secret?.value?.trim();
- // Visual feedback
+ if (!accountId || !keyId || !secret) {
+ alert("Please fill in Account ID, Access Key ID, and Secret Access Key.");
+ return;
+ }
+
+ // Transition
+ this.wizard.step1.classList.add("hidden");
+ this.wizard.step2.classList.remove("hidden");
+
+ // Auto-focus URL if empty
+ if (!this.inputs.r2Domain.value) {
+ this.inputs.r2Domain.focus();
+ }
+ }
+
+ goToCredentialsStep() {
+ this.wizard.step2.classList.add("hidden");
+ this.wizard.step1.classList.remove("hidden");
+ this.wizard.errorContainer.classList.add("hidden");
+ }
+
+ async handleVerifyAndSave() {
const btn = this.toggles.saveSettings;
const originalText = btn.textContent;
- btn.textContent = "Saved!";
- setTimeout(() => btn.textContent = originalText, 2000);
- // Collapse if valid
- if (this.hasValidR2Settings()) {
- this.sourceSections.settings.classList.add("hidden");
- this.toggles.storageSettings.setAttribute("aria-expanded", "false");
+ // Clear previous error
+ this.wizard.errorContainer.classList.add("hidden");
+
+ const settings = this.collectSettingsForm();
+ const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
+ const npub = pubkey ? this.safeEncodeNpub(pubkey) : null;
+
+ if (!settings.baseDomain) {
+ this.wizard.errorText.textContent = "Public Bucket URL is required.";
+ this.wizard.errorContainer.classList.remove("hidden");
+ return;
+ }
+
+ // 1. Verify
+ btn.disabled = true;
+ btn.textContent = "Verifying...";
+
+ try {
+ const verification = await this.r2Service.verifyPublicAccess({ settings, npub });
+
+ if (!verification.success) {
+ btn.disabled = false;
+ btn.textContent = originalText;
+
+ this.wizard.errorText.textContent = verification.error || "Verification failed.";
+ this.wizard.errorContainer.classList.remove("hidden");
+
+ // Dynamic Cloudflare Link
+ // https://dash.cloudflare.com/?to=/:account/r2/default/buckets/:bucket/settings
+ if (settings.accountId && npub) {
+ const bucketName = this.inputs.r2BucketName?.value || "";
+ const cfLink = `https://dash.cloudflare.com/?to=/${settings.accountId}/r2/default/buckets/${bucketName}/settings`;
+ this.wizard.guideLink.href = cfLink;
+ } else {
+ this.wizard.guideLink.href = "https://dash.cloudflare.com";
+ }
+ return;
+ }
+
+ // 2. Save on Success
+ btn.textContent = "Saving...";
+ await this.r2Service.saveSettings(settings);
+ this.cloudflareSettings = settings;
+ this.updateStorageStatus(true);
+
+ btn.disabled = false;
+ btn.textContent = "Saved & Ready!";
+
+ setTimeout(() => {
+ btn.textContent = originalText;
+ // Collapse
+ this.sourceSections.settings.classList.add("hidden");
+ this.toggles.storageSettings.setAttribute("aria-expanded", "false");
+ // Reset wizard to start for next edit
+ this.goToCredentialsStep();
+ }, 1500);
+ } catch (err) {
+ userLogger.error("Verification crashed:", err);
+ btn.disabled = false;
+ btn.textContent = originalText;
+ this.wizard.errorText.textContent = "Unexpected error during verification.";
+ this.wizard.errorContainer.classList.remove("hidden");
}
}
@@ -456,7 +576,7 @@ export class UploadModal {
const bar = this.inputs.progress;
const txt = this.statusText.uploadPercent;
- if (fraction === null || fraction < 0) {
+ if (fraction === null || fraction < 0 || isNaN(fraction)) {
container.classList.add("hidden");
return;
}
@@ -530,11 +650,30 @@ export class UploadModal {
const npub = this.safeEncodeNpub(pubkey);
+ // 1. Calculate Torrent Info Hash (Client-side)
+ this.isUploading = true;
+ this.submitButton.disabled = true;
+ this.updateUploadStatus("Calculating Info Hash...", "info");
+ this.updateProgress(0); // Show bar at 0
+
+ let infoHash = "";
+ try {
+ infoHash = await calculateTorrentInfoHash(file);
+ } catch (hashErr) {
+ userLogger.warn("Failed to calculate info hash:", hashErr);
+ // We can proceed without it, or fail. The plan says we need it.
+ // Let's warn but proceed? Or fail? The user said "Also lets not forget that we calculate a torrent hash".
+ // I will proceed but log it, maybe the upload handles it gracefully (missing magnet).
+ // But passing empty infoHash means no magnet link generated in R2Service.
+ }
+
+ // 2. Upload
await this.r2Service.uploadVideo({
npub,
file,
thumbnailFile,
metadata,
+ infoHash,
settingsInput: this.collectSettingsForm(),
publishVideoNote: this.publishVideoNote,
onReset: () => this.resetForm(),
@@ -589,6 +728,7 @@ export class UploadModal {
this.toggles.nsfw.checked = false;
this.toggles.kids.checked = false;
this.updateProgress(null);
+ this.populateBucketName(); // Re-populate if user logged in
// Reset thumbnail UI
if (this.inputs.thumbnail) {
@@ -607,6 +747,7 @@ export class UploadModal {
this.root.classList.remove("hidden");
this.setGlobalModalState("upload", true);
this.isVisible = true;
+ this.populateBucketName(); // Ensure bucket name is fresh on open
this.modalAccessibility?.activate({ triggerElement });
}
diff --git a/js/ui/components/VideoCard.js b/js/ui/components/VideoCard.js
index efd19df1..b068745c 100644
--- a/js/ui/components/VideoCard.js
+++ b/js/ui/components/VideoCard.js
@@ -476,9 +476,9 @@ export class VideoCard {
content.appendChild(badgesContainer);
}
- const discussion = this.buildDiscussionCount();
- if (discussion) {
- content.appendChild(discussion);
+ const engagement = this.buildEngagementSection();
+ if (engagement) {
+ content.appendChild(engagement);
}
if (this.showUnsupportedTorrentBadge) {
@@ -926,29 +926,24 @@ export class VideoCard {
metadata.appendChild(timeEl);
this.timestampEl = timeEl;
- if (this.pointerInfo && this.pointerInfo.key) {
- const dotClassNames = ["text-muted-strong"];
- if (!isCompact) {
- dotClassNames.push("mx-1");
- }
+ // (View count moved to engagement section)
- const dot = this.createElement("span", {
- classNames: dotClassNames,
- textContent: "•"
+ if (
+ this.postedAt !== null &&
+ Number.isFinite(this.video?.created_at) &&
+ this.video.created_at > this.postedAt + 60
+ ) {
+ const separator = this.createElement("span", {
+ classNames: ["text-muted-strong", isCompact ? "" : "mx-1"],
+ textContent: "•",
});
- dot.setAttribute("aria-hidden", "true");
+ separator.setAttribute("aria-hidden", "true");
+ metadata.appendChild(separator);
- const view = this.createElement("span", {
- classNames: ["view-count-text"],
- textContent: "– views"
+ const edited = this.createElement("span", {
+ textContent: "Edited",
});
- view.dataset.viewCount = "";
- view.dataset.viewPointer = this.pointerInfo.key;
-
- metadata.appendChild(dot);
- metadata.appendChild(view);
-
- this.viewCountEl = view;
+ metadata.appendChild(edited);
}
authorMeta.appendChild(authorName);
@@ -2312,62 +2307,152 @@ export class VideoCard {
}
}
- buildDiscussionCount() {
- if (this.variant === "compact") {
+ createEyeIcon(classNames = []) {
+ const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("aria-hidden", "true");
+ svg.setAttribute("focusable", "false");
+ svg.setAttribute("stroke", "currentColor");
+ svg.setAttribute("stroke-width", "2");
+ svg.setAttribute("stroke-linecap", "round");
+ svg.setAttribute("stroke-linejoin", "round");
+
+ classNames.forEach((className) => {
+ if (className) {
+ svg.classList.add(className);
+ }
+ });
+
+ const path = this.document.createElementNS(SVG_NAMESPACE, "path");
+ path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");
+ svg.appendChild(path);
+
+ const circle = this.document.createElementNS(SVG_NAMESPACE, "circle");
+ circle.setAttribute("cx", "12");
+ circle.setAttribute("cy", "12");
+ circle.setAttribute("r", "3");
+ svg.appendChild(circle);
+
+ return svg;
+ }
+
+ createMessageIcon(classNames = []) {
+ const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("aria-hidden", "true");
+ svg.setAttribute("focusable", "false");
+ svg.setAttribute("stroke", "currentColor");
+ svg.setAttribute("stroke-width", "2");
+ svg.setAttribute("stroke-linecap", "round");
+ svg.setAttribute("stroke-linejoin", "round");
+
+ classNames.forEach((className) => {
+ if (className) {
+ svg.classList.add(className);
+ }
+ });
+
+ const path = this.document.createElementNS(SVG_NAMESPACE, "path");
+ path.setAttribute(
+ "d",
+ "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
+ );
+ svg.appendChild(path);
+
+ return svg;
+ }
+
+ buildEngagementSection() {
+ // Check availability
+ const hasPointer = this.pointerInfo && this.pointerInfo.key;
+ const hasDiscussion =
+ this.variant !== "compact" &&
+ this.video.enableComments !== false &&
+ (typeof this.video.discussionCount === "number" ||
+ (typeof this.video.discussionCount === "string" &&
+ this.video.discussionCount.trim()));
+
+ if (!hasPointer && !hasDiscussion) {
return null;
}
- if (this.video.enableComments === false) {
- return null;
+ const container = this.createElement("div", {
+ classNames: [
+ "flex",
+ "items-center",
+ "gap-4",
+ "text-xs",
+ "text-muted-strong",
+ "mt-3",
+ "video-card__engagement",
+ ],
+ });
+
+ // Views
+ if (hasPointer) {
+ const wrapper = this.createElement("div", {
+ classNames: ["flex", "items-center", "gap-1.5"],
+ });
+ wrapper.setAttribute("title", "Views");
+
+ const icon = this.createEyeIcon(["w-4", "h-4"]);
+ wrapper.appendChild(icon);
+
+ const view = this.createElement("span", {
+ classNames: ["view-count-text"],
+ textContent: "–",
+ });
+ view.dataset.viewCount = "";
+ view.dataset.viewPointer = this.pointerInfo.key;
+
+ wrapper.appendChild(view);
+ container.appendChild(wrapper);
+
+ this.viewCountEl = view;
}
- let initialCount = null;
- if (typeof this.video.discussionCount === "number") {
- initialCount = this.video.discussionCount;
- } else if (typeof this.video.discussionCount === "string") {
- const trimmed = this.video.discussionCount.trim();
- if (trimmed) {
- const parsed = Number.parseInt(trimmed, 10);
+ // Discussion
+ if (hasDiscussion) {
+ let initialCount = 0;
+ if (typeof this.video.discussionCount === "number") {
+ initialCount = this.video.discussionCount;
+ } else if (typeof this.video.discussionCount === "string") {
+ const parsed = Number.parseInt(this.video.discussionCount.trim(), 10);
if (Number.isFinite(parsed)) {
initialCount = parsed;
}
}
+
+ if (Number.isFinite(initialCount) && initialCount >= 0) {
+ const wrapper = this.createElement("div", {
+ classNames: ["flex", "items-center", "gap-1.5"],
+ });
+ wrapper.dataset.discussionCount = this.video.id;
+ wrapper.dataset.countState = "ready";
+ wrapper.setAttribute("title", "Comments");
+
+ const icon = this.createMessageIcon(["w-4", "h-4"]);
+ wrapper.appendChild(icon);
+
+ const safeCount = Math.floor(initialCount);
+ const displayValue = this.helpers.toLocaleString
+ ? this.helpers.toLocaleString(safeCount)
+ : safeCount.toLocaleString();
+
+ const valueEl = this.createElement("span", {
+ textContent: displayValue,
+ });
+ valueEl.dataset.discussionCountValue = "";
+
+ wrapper.appendChild(valueEl);
+ container.appendChild(wrapper);
+
+ this.discussionCountEl = wrapper;
+ }
}
- if (
- initialCount === null ||
- !Number.isFinite(initialCount) ||
- initialCount < 0
- ) {
- return null;
- }
-
- const safeCount = Math.floor(initialCount);
- const displayValue = this.helpers.toLocaleString
- ? this.helpers.toLocaleString(safeCount)
- : safeCount.toLocaleString();
-
- const container = this.createElement("div", {
- classNames: ["flex", "items-center", "text-xs", "text-muted-strong"]
- });
- container.dataset.discussionCount = this.video.id;
- container.dataset.countState = "ready";
-
- const valueEl = this.createElement("span", {
- textContent: displayValue
- });
- valueEl.dataset.discussionCountValue = "";
-
- const labelEl = this.createElement("span", {
- classNames: ["ml-1"],
- textContent: "notes"
- });
-
- container.appendChild(valueEl);
- container.appendChild(labelEl);
-
- this.discussionCountEl = container;
-
return container;
}
diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js
index 89fd1d93..e1501194 100644
--- a/js/ui/components/VideoModal.js
+++ b/js/ui/components/VideoModal.js
@@ -3959,8 +3959,17 @@ export class VideoModal {
}
updateViewCountLabel(text) {
- if (this.videoViewCountEl) {
- this.videoViewCountEl.textContent = text || "";
+ if (!this.videoViewCountEl) {
+ return;
+ }
+ const textEl = this.videoViewCountEl.querySelector(
+ "[data-view-count-text]"
+ );
+ if (textEl) {
+ textEl.textContent = text || "–";
+ } else {
+ // Fallback if span is missing
+ this.videoViewCountEl.textContent = text || "–";
}
}
@@ -5438,15 +5447,14 @@ export class VideoModal {
return this.formatViewCountLabel(Number(total));
}
if (status === "hydrating") {
- return "Loading views…";
+ return "Loading…";
}
- return "– views";
+ return "–";
}
formatViewCountLabel(total) {
const numeric = Number.isFinite(total) ? Math.max(0, Number(total)) : 0;
- const label = numeric === 1 ? "view" : "views";
- return `${formatViewCount(numeric)} ${label}`;
+ return formatViewCount(numeric);
}
syncMoreMenuData({ currentVideo, canManageBlacklist }) {
diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js
index 23aa5c1b..aa6e962e 100644
--- a/js/ui/profileModalController.js
+++ b/js/ui/profileModalController.js
@@ -7033,8 +7033,14 @@ export class ProfileModalController {
return;
}
- const { onRemove, removeLabel = "Remove", confirmMessage, removable = true } =
- options;
+ const {
+ onRemove,
+ removeLabel = "Remove",
+ confirmMessage,
+ removable = true,
+ overlapSet,
+ overlapLabel,
+ } = options;
const formatNpub =
typeof this.formatShortNpub === "function"
@@ -7076,6 +7082,8 @@ export class ProfileModalController {
"card flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between";
const normalizedNpub = typeof npub === "string" ? npub.trim() : "";
+ const comparableNpub =
+ this.normalizeNpubValue(normalizedNpub) || normalizedNpub;
const decodedHex =
normalizedNpub && normalizedNpub.startsWith("npub1")
? this.safeDecodeNpub(normalizedNpub)
@@ -7110,6 +7118,20 @@ export class ProfileModalController {
avatarSrc,
});
+ if (
+ summary &&
+ overlapLabel &&
+ overlapSet instanceof Set &&
+ comparableNpub &&
+ overlapSet.has(comparableNpub)
+ ) {
+ const overlapBadge = document.createElement("span");
+ overlapBadge.className = "badge whitespace-nowrap";
+ overlapBadge.dataset.variant = "warning";
+ overlapBadge.textContent = overlapLabel;
+ summary.appendChild(overlapBadge);
+ }
+
const actions = document.createElement("div");
actions.className =
"flex flex-wrap items-center justify-end gap-2 sm:flex-none";
@@ -7175,6 +7197,15 @@ export class ProfileModalController {
const blacklist = this.normalizeAdminListEntries(
this.services.accessControl.getBlacklist(),
);
+ const normalizeForCompare = (value) =>
+ this.normalizeNpubValue(value) ||
+ (typeof value === "string" ? value.trim() : "");
+ const whitelistCompare = new Set(
+ whitelist.map(normalizeForCompare).filter(Boolean),
+ );
+ const blacklistCompare = new Set(
+ blacklist.map(normalizeForCompare).filter(Boolean),
+ );
this.renderAdminList(
this.adminModeratorList,
@@ -7199,6 +7230,8 @@ export class ProfileModalController {
removeLabel: "Remove",
confirmMessage: "Remove {npub} from the whitelist?",
removable: true,
+ overlapSet: blacklistCompare,
+ overlapLabel: "Also blacklisted",
},
);
@@ -7212,6 +7245,8 @@ export class ProfileModalController {
removeLabel: "Unblock",
confirmMessage: "Remove {npub} from the blacklist?",
removable: true,
+ overlapSet: whitelistCompare,
+ overlapLabel: "Also whitelisted",
},
);
}
diff --git a/js/userBlocks.js b/js/userBlocks.js
index 5d2ea62b..36b840c0 100644
--- a/js/userBlocks.js
+++ b/js/userBlocks.js
@@ -5,7 +5,7 @@ import {
} from "./nostrClientFacade.js";
import { getActiveSigner } from "./nostr/index.js";
import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER } from "./nostrEventSchemas.js";
-import { userLogger } from "./utils/logger.js";
+import { devLogger, userLogger } from "./utils/logger.js";
import {
publishEventToRelays,
assertAnyRelayAccepted,
@@ -67,6 +67,20 @@ const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
const BLOCKLIST_STORAGE_PREFIX = "bitvid:user-blocks";
const BLOCKLIST_SEEDED_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:seeded:v1`;
const BLOCKLIST_REMOVALS_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:removals:v1`;
+const BLOCKLIST_LOCAL_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:local:v1`;
+
+function resolveStorage() {
+ if (typeof localStorage !== "undefined") {
+ return localStorage;
+ }
+ if (typeof window !== "undefined" && window.localStorage) {
+ return window.localStorage;
+ }
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
+ return globalThis.localStorage;
+ }
+ return null;
+}
function normalizeEncryptionToken(value) {
if (typeof value !== "string") {
@@ -286,6 +300,32 @@ function serializeBlockListTagMatrix(values, ownerPubkey, options = {}) {
return JSON.stringify(tags);
}
+function extractPubkeysFromTags(tags, ownerPubkey = null) {
+ if (!Array.isArray(tags)) {
+ return [];
+ }
+ const collected = [];
+ const seen = new Set();
+ const owner = ownerPubkey ? normalizeHex(ownerPubkey) : null;
+
+ for (const entry of tags) {
+ if (!Array.isArray(entry) || entry.length < 2) {
+ continue;
+ }
+ const label = typeof entry[0] === "string" ? entry[0].trim().toLowerCase() : "";
+ if (label !== "p") {
+ continue;
+ }
+ const normalized = normalizeHex(entry[1]);
+ if (!normalized || (owner && normalized === owner) || seen.has(normalized)) {
+ continue;
+ }
+ seen.add(normalized);
+ collected.push(normalized);
+ }
+ return collected;
+}
+
function parseBlockListPlaintext(plaintext, ownerPubkey) {
if (typeof plaintext !== "string" || !plaintext) {
return [];
@@ -304,34 +344,13 @@ function parseBlockListPlaintext(plaintext, ownerPubkey) {
const owner = normalizeHex(ownerPubkey);
- const extractFromTags = (tags) => {
- const collected = [];
- const seen = new Set();
- for (const entry of tags) {
- if (!Array.isArray(entry) || entry.length < 2) {
- continue;
- }
- const label = typeof entry[0] === "string" ? entry[0].trim().toLowerCase() : "";
- if (label !== "p") {
- continue;
- }
- const normalized = normalizeHex(entry[1]);
- if (!normalized || normalized === owner || seen.has(normalized)) {
- continue;
- }
- seen.add(normalized);
- collected.push(normalized);
- }
- return collected;
- };
-
if (Array.isArray(parsed)) {
- return extractFromTags(parsed);
+ return extractPubkeysFromTags(parsed, owner);
}
if (parsed && typeof parsed === "object") {
if (Array.isArray(parsed.tags) && parsed.tags.length) {
- return extractFromTags(parsed.tags);
+ return extractPubkeysFromTags(parsed.tags, owner);
}
const legacy = Array.isArray(parsed.blockedPubkeys) ? parsed.blockedPubkeys : [];
@@ -367,12 +386,202 @@ function isUserBlockListEvent(event) {
return false;
}
+function isTaggedBlockListEvent(event) {
+ if (!event || !Array.isArray(event.tags)) {
+ return false;
+ }
+ return event.tags.some(
+ (tag) =>
+ Array.isArray(tag) &&
+ tag.length >= 2 &&
+ typeof tag[0] === "string" &&
+ tag[0].trim().toLowerCase() === "d" &&
+ typeof tag[1] === "string" &&
+ tag[1].trim() === BLOCK_LIST_IDENTIFIER,
+ );
+}
+
+const BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
+const BECH32_CHARSET_MAP = (() => {
+ const map = new Map();
+ for (let index = 0; index < BECH32_CHARSET.length; index += 1) {
+ map.set(BECH32_CHARSET[index], index);
+ }
+ return map;
+})();
+
+function bytesToHex(bytes) {
+ if (!bytes || typeof bytes.length !== "number") {
+ return "";
+ }
+
+ let hex = "";
+ for (let index = 0; index < bytes.length; index += 1) {
+ const value = bytes[index];
+ if (typeof value !== "number") {
+ return "";
+ }
+ const normalized = value & 0xff;
+ hex += normalized.toString(16).padStart(2, "0");
+ }
+ return hex;
+}
+
+function bech32Polymod(values) {
+ let chk = 1;
+ const generators = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
+
+ for (const value of values) {
+ const top = chk >>> 25;
+ chk = ((chk & 0x1ffffff) << 5) ^ value;
+ for (let bit = 0; bit < generators.length; bit += 1) {
+ if ((top >>> bit) & 1) {
+ chk ^= generators[bit];
+ }
+ }
+ }
+
+ return chk;
+}
+
+function bech32HrpExpand(hrp) {
+ const expanded = [];
+ for (let index = 0; index < hrp.length; index += 1) {
+ const code = hrp.charCodeAt(index);
+ expanded.push(code >>> 5);
+ }
+ expanded.push(0);
+ for (let index = 0; index < hrp.length; index += 1) {
+ const code = hrp.charCodeAt(index);
+ expanded.push(code & 31);
+ }
+ return expanded;
+}
+
+function bech32VerifyChecksum(hrp, data) {
+ return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 1;
+}
+
+function bech32Decode(value) {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return null;
+ }
+
+ const lower = trimmed.toLowerCase();
+ const upper = trimmed.toUpperCase();
+ if (trimmed !== lower && trimmed !== upper) {
+ return null;
+ }
+
+ const normalized = lower;
+ const separatorIndex = normalized.lastIndexOf("1");
+ if (separatorIndex < 1 || separatorIndex + 7 > normalized.length) {
+ return null;
+ }
+
+ const hrp = normalized.slice(0, separatorIndex);
+ const data = [];
+ for (let index = separatorIndex + 1; index < normalized.length; index += 1) {
+ const char = normalized[index];
+ if (!BECH32_CHARSET_MAP.has(char)) {
+ return null;
+ }
+ data.push(BECH32_CHARSET_MAP.get(char));
+ }
+
+ if (!bech32VerifyChecksum(hrp, data)) {
+ return null;
+ }
+
+ return { hrp, words: data.slice(0, -6) };
+}
+
+function convertBits(data, fromBits, toBits, pad = true) {
+ const result = [];
+ let acc = 0;
+ let bits = 0;
+ const maxv = (1 << toBits) - 1;
+ const maxAcc = (1 << (fromBits + toBits - 1)) - 1;
+
+ for (const value of data) {
+ if (value < 0 || value >> fromBits) {
+ return null;
+ }
+ acc = ((acc << fromBits) | value) & maxAcc;
+ bits += fromBits;
+ while (bits >= toBits) {
+ bits -= toBits;
+ result.push((acc >>> bits) & maxv);
+ }
+ }
+
+ if (pad) {
+ if (bits) {
+ result.push((acc << (toBits - bits)) & maxv);
+ }
+ } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
+ return null;
+ }
+
+ return result;
+}
+
+function fallbackDecodeNpubToHex(value) {
+ const decoded = bech32Decode(value);
+ if (!decoded || decoded.hrp !== "npub") {
+ return "";
+ }
+
+ const bytes = convertBits(decoded.words, 5, 8, false);
+ if (!bytes || !bytes.length) {
+ return "";
+ }
+
+ return bytesToHex(bytes);
+}
+
+function normalizeDecodedHex(value) {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const trimmed = value.trim();
+ if (!/^[0-9a-f]{64}$/i.test(trimmed)) {
+ return null;
+ }
+
+ return trimmed.toLowerCase();
+}
+
+function normalizeNpubInput(value) {
+ if (typeof value !== "string") {
+ return "";
+ }
+
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return "";
+ }
+
+ const lower = trimmed.toLowerCase();
+ if (lower.startsWith("nostr:")) {
+ return trimmed.slice(6).trim();
+ }
+
+ return trimmed;
+}
+
function decodeNpubToHex(npub) {
if (typeof npub !== "string") {
return null;
}
- const trimmed = npub.trim();
+ const trimmed = normalizeNpubInput(npub);
if (!trimmed) {
return null;
}
@@ -381,29 +590,25 @@ function decodeNpubToHex(npub) {
return trimmed.toLowerCase();
}
- if (!trimmed.toLowerCase().startsWith("npub1")) {
- return null;
- }
-
- const tools = window?.NostrTools;
+ const tools = typeof globalThis !== "undefined" ? globalThis?.NostrTools : null;
const decoder = tools?.nip19?.decode;
- if (typeof decoder !== "function") {
- return null;
- }
-
- try {
- const decoded = decoder(trimmed);
- if (decoded?.type === "npub" && typeof decoded.data === "string") {
- const hex = decoded.data.trim();
- if (/^[0-9a-f]{64}$/i.test(hex)) {
- return hex.toLowerCase();
+ if (typeof decoder === "function") {
+ try {
+ const decoded = decoder(trimmed);
+ if (!decoded || decoded.type !== "npub") {
+ return normalizeDecodedHex(fallbackDecodeNpubToHex(trimmed));
}
+ if (typeof decoded.data === "string") {
+ const normalized = normalizeDecodedHex(decoded.data);
+ return normalized || normalizeDecodedHex(fallbackDecodeNpubToHex(trimmed));
+ }
+ return normalizeDecodedHex(bytesToHex(decoded.data));
+ } catch (error) {
+ return normalizeDecodedHex(fallbackDecodeNpubToHex(trimmed));
}
- } catch (error) {
- return null;
}
- return null;
+ return normalizeDecodedHex(fallbackDecodeNpubToHex(trimmed));
}
function readSeededFlag(actorHex) {
@@ -434,7 +639,8 @@ function writeSeededFlag(actorHex, seeded) {
return;
}
- if (typeof localStorage === "undefined") {
+ const storage = resolveStorage();
+ if (!storage) {
return;
}
@@ -496,7 +702,8 @@ function writeRemovalSet(actorHex, removals) {
return;
}
- if (typeof localStorage === "undefined") {
+ const storage = resolveStorage();
+ if (!storage) {
return;
}
@@ -539,6 +746,73 @@ function normalizeHex(pubkey) {
return null;
}
+function readLocalBlocks(actorHex) {
+ if (typeof actorHex !== "string" || !actorHex) {
+ return null;
+ }
+
+ const storage = resolveStorage();
+ if (!storage) {
+ return null;
+ }
+
+ const key = `${BLOCKLIST_LOCAL_KEY_PREFIX}:${actorHex}`;
+
+ try {
+ const raw = storage.getItem(key);
+ if (raw === null) {
+ return null;
+ }
+
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) {
+ return new Set();
+ }
+
+ const normalized = parsed
+ .map((entry) =>
+ typeof entry === "string" ? entry.trim().toLowerCase() : "",
+ )
+ .filter((entry) => /^[0-9a-f]{64}$/.test(entry));
+
+ return new Set(normalized);
+ } catch (error) {
+ userLogger.warn(
+ `[UserBlockList] Failed to read local blocks for ${actorHex}:`,
+ error,
+ );
+ return null;
+ }
+}
+
+function writeLocalBlocks(actorHex, blocks) {
+ if (typeof actorHex !== "string" || !actorHex) {
+ return;
+ }
+
+ const storage = resolveStorage();
+ if (!storage) {
+ return;
+ }
+
+ const key = `${BLOCKLIST_LOCAL_KEY_PREFIX}:${actorHex}`;
+
+ try {
+ if (!blocks || !blocks.size) {
+ // We persist empty sets as [] to distinguish from "not found" (null)
+ storage.setItem(key, "[]");
+ return;
+ }
+
+ storage.setItem(key, JSON.stringify(Array.from(blocks)));
+ } catch (error) {
+ userLogger.warn(
+ `[UserBlockList] Failed to persist local blocks for ${actorHex}:`,
+ error,
+ );
+ }
+}
+
class UserBlockListManager {
constructor() {
this.blockedPubkeys = new Set();
@@ -631,6 +905,19 @@ class UserBlockListManager {
emitStatus({ status: "loading", relays: Array.from(nostrClient.relays || []) });
+ const localBlocks = this._loadLocal(normalized);
+ if (localBlocks) {
+ this.blockedPubkeys = localBlocks;
+ this.loaded = true;
+ this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
+ action: "sync",
+ blockedPubkeys: Array.from(this.blockedPubkeys),
+ source: "local",
+ });
+ emitStatus({ status: "settled" });
+ return;
+ }
+
const applyBlockedPubkeys = (nextValues, meta = {}) => {
const nextSet = new Set(Array.isArray(nextValues) ? nextValues : []);
this.blockedPubkeys = nextSet;
@@ -854,7 +1141,10 @@ class UserBlockListManager {
if (skipIfEmpty) {
return;
}
- if (this.lastPublishedCreatedAt !== null || this.blockEventCreatedAt !== null) {
+ if (
+ this.lastPublishedCreatedAt !== null ||
+ this.blockEventCreatedAt !== null
+ ) {
emitStatus({ status: "stale", reason: "empty-result", source });
return;
}
@@ -865,18 +1155,19 @@ class UserBlockListManager {
return;
}
- const sorted = events
- .filter(
- (event) =>
- event && event.pubkey === normalized && isUserBlockListEvent(event),
- )
- .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
+ const validEvents = events.filter(
+ (event) =>
+ event && event.pubkey === normalized && isUserBlockListEvent(event),
+ );
- if (!sorted.length) {
+ if (!validEvents.length) {
if (skipIfEmpty) {
return;
}
- if (this.lastPublishedCreatedAt !== null || this.blockEventCreatedAt !== null) {
+ if (
+ this.lastPublishedCreatedAt !== null ||
+ this.blockEventCreatedAt !== null
+ ) {
emitStatus({ status: "stale", reason: "empty-result", source });
return;
}
@@ -887,221 +1178,194 @@ class UserBlockListManager {
return;
}
- const newest = sorted[0];
- const newestCreatedAt = Number.isFinite(newest?.created_at)
- ? newest.created_at
+ // Separate by kind to merge distinct lists (Block vs Mute)
+ const muteEvents = validEvents.filter((e) => e.kind === 10000);
+
+ // Prioritize tagged mute lists (Bitvid) over plain ones (others)
+ const taggedMutes = muteEvents
+ .filter((e) => isTaggedBlockListEvent(e))
+ .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
+
+ const plainMutes = muteEvents
+ .filter((e) => !isTaggedBlockListEvent(e))
+ .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
+
+ const newestMute =
+ taggedMutes.length > 0 ? taggedMutes[0] : plainMutes[0] || null;
+
+ const blockEvents = validEvents
+ .filter((e) => e.kind === 30002)
+ .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
+
+ const newestBlock = blockEvents[0] || null;
+
+ const newestMuteTime = Number.isFinite(newestMute?.created_at)
+ ? newestMute.created_at
: 0;
+ const newestBlockTime = Number.isFinite(newestBlock?.created_at)
+ ? newestBlock.created_at
+ : 0;
+
+ // The "canonical" newest event for stale checks is simply the latest of either.
+ const newestOverall =
+ newestMuteTime >= newestBlockTime ? newestMute : newestBlock;
+ const newestCreatedAt = Math.max(newestMuteTime, newestBlockTime);
+
const guardCreatedAt = Math.max(
this.blockEventCreatedAt ?? 0,
this.lastPublishedCreatedAt ?? 0,
);
+ // If both are stale, skip
if (newestCreatedAt < guardCreatedAt) {
- emitStatus({ status: "stale", event: newest, guardCreatedAt, source });
+ emitStatus({
+ status: "stale",
+ event: newestOverall,
+ guardCreatedAt,
+ source,
+ });
return;
}
if (
newestCreatedAt === guardCreatedAt &&
this.blockEventId &&
- newest?.id &&
- newest.id !== this.blockEventId
+ newestOverall?.id &&
+ newestOverall.id !== this.blockEventId
) {
- emitStatus({ status: "stale", event: newest, guardCreatedAt, source });
+ // If equal time but different ID, we might be seeing a race or propagation delay.
+ // But since we are merging, we should proceed unless it's strictly older context.
+ // For safety against flapping, we treat strict equality with different ID as stale
+ // if we already have a confirmed ID.
+ emitStatus({
+ status: "stale",
+ event: newestOverall,
+ guardCreatedAt,
+ source,
+ });
return;
}
- if (newest?.id && newest.id === this.blockEventId) {
- this.blockEventCreatedAt = Number.isFinite(newestCreatedAt)
- ? newestCreatedAt
- : this.blockEventCreatedAt;
- emitStatus({ status: "confirmed", event: newest, source });
+ if (newestOverall?.id && newestOverall.id === this.blockEventId) {
+ // Already applied this event. Idempotency check.
return;
}
- this.blockEventId = newest?.id || null;
- this.blockEventCreatedAt = Number.isFinite(newestCreatedAt)
- ? newestCreatedAt
- : null;
+ // We accept this state.
+ this.blockEventId = newestOverall?.id || null;
+ this.blockEventCreatedAt = newestCreatedAt;
- if (!newest?.content) {
- applyBlockedPubkeys([], { source, reason: "empty-event", event: newest });
- emitStatus({ status: "applied-empty", event: newest, source });
- return;
- }
+ // Helper to decrypt a single event
+ const decryptEvent = async (ev) => {
+ if (!ev) return [];
+ const isContentEmpty = !ev.content || !ev.content.trim();
- const {
- decryptors,
- order,
- sources,
- permissionError,
- } = await resolveDecryptors(newest);
-
- if (!decryptors.size) {
- if (permissionError) {
- userLogger.warn(
- "[UserBlockList] Unable to load block list via extension decryptor; permissions denied.",
- permissionError,
- );
- this.reset();
- this.loaded = true;
- emitStatus({ status: "error", error: permissionError, decryptor: "extension" });
- emitStatus({ status: "settled" });
- return;
+ if (isContentEmpty) {
+ // Handle tag-only lists (e.g. Kind 10000 with empty content but populated p tags)
+ if (Array.isArray(ev.tags) && ev.tags.length) {
+ return extractPubkeysFromTags(ev.tags, normalized);
+ }
+ return [];
}
+ const { decryptors, order, sources, permissionError } =
+ await resolveDecryptors(ev);
+
+ if (!decryptors.size) {
+ if (permissionError) {
+ throw permissionError;
+ }
+ return [];
+ }
+
+ let decryptedText = "";
+ for (const scheme of order) {
+ const handler = decryptors.get(scheme);
+ if (typeof handler !== "function") continue;
+ try {
+ const plaintext = await handler(ev.content);
+ if (typeof plaintext === "string") {
+ decryptedText = plaintext;
+ break;
+ }
+ } catch {
+ // ignore
+ }
+ }
+
+ if (!decryptedText) {
+ // If decryption fails completely for a present event, we might want to log it
+ // but return empty for that specific event.
+ return [];
+ }
+
+ return parseBlockListPlaintext(decryptedText, normalized);
+ };
+
+ const mergedPubkeys = new Set();
+ let decryptionError = null;
+
+ try {
+ const [muteList, blockList] = await Promise.all([
+ decryptEvent(newestMute),
+ decryptEvent(newestBlock),
+ ]);
+
+ muteList.forEach((pk) => mergedPubkeys.add(pk));
+ blockList.forEach((pk) => mergedPubkeys.add(pk));
+ } catch (error) {
+ // If permission denied or critical failure
+ decryptionError = error;
+ }
+
+ if (decryptionError) {
userLogger.warn(
- "[UserBlockList] No decryptors available; treating block list as empty.",
+ "[UserBlockList] Decryption failed during merge.",
+ decryptionError,
);
this.reset();
this.loaded = true;
- const error = new Error(
- "No decryptors are available to load the block list.",
- );
- emitStatus({ status: "error", error, decryptor: "unavailable" });
+ emitStatus({
+ status: "error",
+ error: decryptionError,
+ decryptor: "mixed",
+ });
emitStatus({ status: "settled" });
return;
}
- let decrypted = "";
- let schemeUsed = "";
- let schemeSource = "unavailable";
- const attemptErrors = [];
+ const hasMute = Boolean(newestMute);
+ const hasBlock = Boolean(newestBlock);
- for (const scheme of order) {
- const handler = decryptors.get(scheme);
- if (typeof handler !== "function") {
- continue;
- }
- const sourceLabel = sources.get(scheme) || "unknown";
- try {
- const plaintext = await handler(newest.content);
- if (typeof plaintext !== "string") {
- throw new Error("Decryption returned a non-string payload.");
- }
- decrypted = plaintext;
- schemeUsed = scheme;
- schemeSource = sourceLabel;
- break;
- } catch (error) {
- attemptErrors.push({ scheme, source: sourceLabel, error });
- }
- }
+ // Check if we should log "empty-events" reason
+ // Effectively empty means: missing content AND missing valid tags
+ const isMuteEffectiveEmpty = !newestMute || ((!newestMute.content || !newestMute.content.trim()) && (!newestMute.tags || !extractPubkeysFromTags(newestMute.tags, normalized).length));
+ const isBlockEffectiveEmpty = !newestBlock || ((!newestBlock.content || !newestBlock.content.trim()) && (!newestBlock.tags || !extractPubkeysFromTags(newestBlock.tags, normalized).length));
- if (!decrypted) {
- const error = new Error(
- "Failed to decrypt block list with available schemes.",
- );
- error.code = "block-list-decrypt-failed";
- if (attemptErrors.length) {
- error.cause = attemptErrors;
- }
- const lastAttempt = attemptErrors.length
- ? attemptErrors[attemptErrors.length - 1]
- : null;
- const decryptPath = lastAttempt?.source || "unavailable";
- userLogger.error(
- `[UserBlockList] Failed to decrypt block list via available schemes (last: ${decryptPath}).`,
- error,
- );
- applyBlockedPubkeys([], {
- source,
- reason: "decrypt-error",
- event: newest,
- error,
- decryptor: decryptPath,
- errors: attemptErrors,
- });
- emitStatus({
- status: "error",
- event: newest,
- error,
- source,
- decryptor: decryptPath,
- });
- return;
- }
+ const effectiveReason = (isMuteEffectiveEmpty && isBlockEffectiveEmpty)
+ ? "empty-events"
+ : "applied-merge";
- try {
- const sanitized = parseBlockListPlaintext(decrypted, normalized);
- applyBlockedPubkeys(sanitized, {
- source,
- reason: "applied",
- event: newest,
- scheme: schemeUsed,
- decryptor: schemeSource,
- });
- emitStatus({
- status: "applied",
- event: newest,
- blockedPubkeys: Array.from(this.blockedPubkeys),
- source,
- scheme: schemeUsed,
- decryptor: schemeSource,
- });
- } catch (err) {
- userLogger.error("[UserBlockList] Failed to parse block list:", err);
- applyBlockedPubkeys([], {
- source,
- reason: "parse-error",
- event: newest,
- error: err,
- decryptor: schemeSource,
- scheme: schemeUsed,
- });
- emitStatus({
- status: "error",
- event: newest,
- error: err,
- source,
- decryptor: schemeSource,
- scheme: schemeUsed,
- });
+ applyBlockedPubkeys(Array.from(mergedPubkeys), {
+ source,
+ reason: effectiveReason,
+ events: [newestMute?.id, newestBlock?.id].filter(Boolean),
+ });
+
+ if (effectiveReason === "empty-events") {
+ emitStatus({ status: "applied-empty", event: newestOverall, source });
+ } else {
+ emitStatus({
+ status: "applied",
+ event: newestOverall,
+ blockedPubkeys: Array.from(this.blockedPubkeys),
+ source,
+ });
}
};
- const background = Promise.allSettled([
- ...fastPromises,
- ...backgroundPromises,
- ])
- .then(async (outcomes) => {
- const aggregated = [];
- for (const outcome of outcomes) {
- if (outcome.status === "fulfilled") {
- const events = Array.isArray(outcome.value?.events)
- ? outcome.value.events
- : [];
- if (events.length) {
- aggregated.push(...events);
- }
- } else {
- const reason = outcome.reason;
- if (reason?.code === "timeout") {
- userLogger.warn(
- `[UserBlockList] Relay ${reason.relay} timed out while loading block list (${reason.timeoutMs}ms)`
- );
- } else {
- const relay = reason?.relay || reason?.relayUrl;
- userLogger.error(
- `[UserBlockList] Relay error at ${relay}:`,
- reason?.error ?? reason
- );
- }
- }
- }
-
- if (!aggregated.length) {
- return { foundEvents: false };
- }
-
- await applyEvents(aggregated, { skipIfEmpty: true, source: "background" });
- return { foundEvents: true };
- })
- .catch((error) => {
- userLogger.error("[UserBlockList] background block list refresh failed:", error);
- return { foundEvents: false, error };
- });
-
+ // Sequential promise handling to avoid race conditions but preserve background updates.
+ // 1. Try fast path.
let fastResult = null;
if (fastPromises.length) {
try {
@@ -1123,29 +1387,54 @@ class UserBlockListManager {
if (fastResult?.events?.length) {
await applyEvents(fastResult.events, { source: "fast" });
- background.catch(() => {});
- return;
+ // NOTE: We do NOT return here. We allow background relays to potentially provide newer data.
}
- if (backgroundRelays.length || fastPromises.length) {
+ // 2. Wait for background (and failed fast relays).
+ if (backgroundRelays.length || (!fastResult?.events?.length && fastPromises.length)) {
emitStatus({
status: "awaiting-background",
relays: backgroundRelays.length ? backgroundRelays : fastRelays,
});
}
- const backgroundOutcome = await background;
+ const allPromises = [...fastPromises, ...backgroundPromises];
+ const outcomes = await Promise.allSettled(allPromises);
- if (backgroundOutcome?.foundEvents) {
+ const aggregated = [];
+ for (const outcome of outcomes) {
+ if (outcome.status === "fulfilled") {
+ const events = Array.isArray(outcome.value?.events)
+ ? outcome.value.events
+ : [];
+ if (events.length) {
+ aggregated.push(...events);
+ }
+ } else {
+ const reason = outcome.reason;
+ if (reason?.code === "timeout") {
+ userLogger.warn(
+ `[UserBlockList] Relay ${reason.relay} timed out while loading block list (${reason.timeoutMs}ms)`
+ );
+ } else {
+ const relay = reason?.relay || reason?.relayUrl;
+ userLogger.error(
+ `[UserBlockList] Relay error at ${relay}:`,
+ reason?.error ?? reason
+ );
+ }
+ }
+ }
+
+ if (!aggregated.length) {
+ // Only apply empty if fast path also found nothing (already handled by logic inside applyEvents,
+ // but explicit call ensures we settle state if everything failed).
+ await applyEvents([], { source: "background" });
return;
}
- if (backgroundOutcome?.error) {
- emitStatus({ status: "error", error: backgroundOutcome.error, source: "background" });
- return;
- }
+ await applyEvents(aggregated, { skipIfEmpty: true, source: "background" });
- await applyEvents([], { source: "background" });
} catch (error) {
userLogger.error("[UserBlockList] loadBlocks failed:", error);
applyBlockedPubkeys([], { source: "fast", reason: "load-error", error });
@@ -1158,6 +1447,14 @@ class UserBlockListManager {
}
}
+ _loadLocal(actorHex) {
+ return readLocalBlocks(actorHex);
+ }
+
+ _saveLocal(actorHex) {
+ writeLocalBlocks(actorHex, this.blockedPubkeys);
+ }
+
_getSeedState(actorHex) {
const normalized = normalizeHex(actorHex);
if (!normalized) {
@@ -1226,22 +1523,23 @@ class UserBlockListManager {
return { ok: true, seeded: false, reason: "already-seeded" };
}
- if (this.blockedPubkeys.size > 0) {
- return { ok: true, seeded: false, reason: "non-empty" };
- }
-
const removals = state.removals;
const additions = new Set();
+ let invalidCount = 0;
const candidates = Array.isArray(candidateNpubs) ? candidateNpubs : [];
for (const candidate of candidates) {
const candidateHex = normalizeHex(candidate);
if (!candidateHex) {
+ invalidCount += 1;
continue;
}
if (candidateHex === actorHex) {
continue;
}
+ if (this.blockedPubkeys.has(candidateHex)) {
+ continue;
+ }
if (removals.has(candidateHex)) {
continue;
}
@@ -1249,6 +1547,18 @@ class UserBlockListManager {
}
if (!additions.size) {
+ if (candidates.length) {
+ devLogger.info(
+ "[UserBlockList] Seed candidates resolved to zero additions.",
+ {
+ actorPubkey: actorHex,
+ candidateCount: candidates.length,
+ invalidCount,
+ blockedCount: this.blockedPubkeys.size,
+ removalCount: removals.size,
+ },
+ );
+ }
return { ok: true, seeded: false, reason: "no-candidates" };
}
@@ -1264,6 +1574,7 @@ class UserBlockListManager {
this._setSeeded(actorHex, true);
additions.forEach((hex) => this._clearSeedRemoval(actorHex, hex));
+ this._saveLocal(actorHex);
try {
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
@@ -1279,6 +1590,150 @@ class UserBlockListManager {
return { ok: true, seeded: true, addedPubkeys: Array.from(additions) };
}
+ async seedLocalBaseline(userPubkey, candidateNpubs = []) {
+ const actorHex = normalizeHex(userPubkey);
+ if (!actorHex) {
+ return { ok: false, seeded: false, reason: "invalid-user" };
+ }
+
+ await this.ensureLoaded(actorHex);
+
+ const state = this._getSeedState(actorHex);
+ if (state.seeded) {
+ return { ok: true, seeded: false, reason: "already-seeded" };
+ }
+
+ if (this.blockedPubkeys.size > 0) {
+ return { ok: true, seeded: false, reason: "non-empty" };
+ }
+
+ const removals = state.removals;
+ const additions = new Set();
+ let invalidCount = 0;
+
+ const candidates = Array.isArray(candidateNpubs) ? candidateNpubs : [];
+ for (const candidate of candidates) {
+ const candidateHex = normalizeHex(candidate);
+ if (!candidateHex) {
+ invalidCount += 1;
+ continue;
+ }
+ if (candidateHex === actorHex) {
+ continue;
+ }
+ if (removals.has(candidateHex)) {
+ continue;
+ }
+ additions.add(candidateHex);
+ }
+
+ if (!additions.size) {
+ if (candidates.length) {
+ devLogger.info(
+ "[UserBlockList] Local seed candidates resolved to zero additions.",
+ {
+ actorPubkey: actorHex,
+ candidateCount: candidates.length,
+ invalidCount,
+ removalCount: removals.size,
+ },
+ );
+ }
+ this._setSeeded(actorHex, true);
+ return { ok: true, seeded: true, addedPubkeys: [] };
+ }
+
+ additions.forEach((hex) => this.blockedPubkeys.add(hex));
+
+ this._setSeeded(actorHex, true);
+ additions.forEach((hex) => this._clearSeedRemoval(actorHex, hex));
+
+ try {
+ this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
+ action: "seed",
+ actorPubkey: actorHex,
+ blockedPubkeys: Array.from(this.blockedPubkeys),
+ addedPubkeys: Array.from(additions),
+ localOnly: true,
+ });
+ } catch (error) {
+ userLogger.warn("[UserBlockList] Failed to emit local seed change event:", error);
+ }
+
+ return { ok: true, seeded: true, addedPubkeys: Array.from(additions) };
+ }
+
+ async seedBaselineDelta(userPubkey, candidateNpubs = []) {
+ const actorHex = normalizeHex(userPubkey);
+ if (!actorHex) {
+ return { ok: false, seeded: false, reason: "invalid-user" };
+ }
+
+ await this.ensureLoaded(actorHex);
+
+ const state = this._getSeedState(actorHex);
+ const removals = state.removals;
+ const additions = new Set();
+ let invalidCount = 0;
+
+ const candidates = Array.isArray(candidateNpubs) ? candidateNpubs : [];
+ for (const candidate of candidates) {
+ const candidateHex = normalizeHex(candidate);
+ if (!candidateHex) {
+ invalidCount += 1;
+ continue;
+ }
+ if (candidateHex === actorHex) {
+ continue;
+ }
+ if (this.blockedPubkeys.has(candidateHex)) {
+ continue;
+ }
+ if (removals.has(candidateHex)) {
+ continue;
+ }
+ additions.add(candidateHex);
+ }
+
+ if (!additions.size) {
+ if (candidates.length) {
+ devLogger.info(
+ "[UserBlockList] Baseline delta candidates resolved to zero additions.",
+ {
+ actorPubkey: actorHex,
+ candidateCount: candidates.length,
+ invalidCount,
+ blockedCount: this.blockedPubkeys.size,
+ removalCount: removals.size,
+ },
+ );
+ }
+ if (!state.seeded) {
+ this._setSeeded(actorHex, true);
+ }
+ return { ok: true, seeded: state.seeded, reason: "no-candidates" };
+ }
+
+ additions.forEach((hex) => this.blockedPubkeys.add(hex));
+
+ this._setSeeded(actorHex, true);
+ this._saveLocal(actorHex);
+
+ try {
+ this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
+ action: "seed-delta",
+ actorPubkey: actorHex,
+ blockedPubkeys: Array.from(this.blockedPubkeys),
+ addedPubkeys: Array.from(additions),
+ localOnly: true,
+ });
+ } catch (error) {
+ userLogger.warn("[UserBlockList] Failed to emit delta seed change event:", error);
+ }
+
+ return { ok: true, seeded: true, addedPubkeys: Array.from(additions) };
+ }
+
async addBlock(targetPubkey, userPubkey) {
const actorHex = normalizeHex(userPubkey);
if (!actorHex) {
@@ -1307,6 +1762,22 @@ class UserBlockListManager {
const snapshot = new Set(this.blockedPubkeys);
this.blockedPubkeys.add(targetHex);
+ const loggedInPubkey = normalizeHex(nostrClient?.pubkey);
+ const sessionPubkey = normalizeHex(nostrClient?.sessionActor?.pubkey);
+ const isSessionActor =
+ !!actorHex && !loggedInPubkey && actorHex === sessionPubkey;
+
+ if (isSessionActor) {
+ this._saveLocal(actorHex);
+ this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
+ action: "block",
+ targetPubkey: targetHex,
+ actorPubkey: actorHex,
+ });
+ this._clearSeedRemoval(actorHex, targetHex);
+ return { ok: true };
+ }
+
try {
await this.publishBlockList(actorHex);
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
@@ -1342,6 +1813,22 @@ class UserBlockListManager {
const snapshot = new Set(this.blockedPubkeys);
this.blockedPubkeys.delete(targetHex);
+ const loggedInPubkey = normalizeHex(nostrClient?.pubkey);
+ const sessionPubkey = normalizeHex(nostrClient?.sessionActor?.pubkey);
+ const isSessionActor =
+ !!actorHex && !loggedInPubkey && actorHex === sessionPubkey;
+
+ if (isSessionActor) {
+ this._saveLocal(actorHex);
+ this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
+ action: "unblock",
+ targetPubkey: targetHex,
+ actorPubkey: actorHex,
+ });
+ this._addSeedRemoval(actorHex, targetHex);
+ return { ok: true };
+ }
+
try {
await this.publishBlockList(actorHex);
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
diff --git a/js/utils/formatters.js b/js/utils/formatters.js
index a2af7f95..278bee49 100644
--- a/js/utils/formatters.js
+++ b/js/utils/formatters.js
@@ -90,3 +90,37 @@ export function formatTimeAgo(timestamp) {
}
return "just now";
}
+
+/**
+ * Formats a timestamp (in seconds) to a long absolute date with ordinal day
+ * (e.g. "January 6th, 2026").
+ */
+export function formatAbsoluteDateWithOrdinal(timestamp) {
+ if (!Number.isFinite(timestamp)) {
+ return "";
+ }
+
+ const date = new Date(timestamp * 1000);
+ if (Number.isNaN(date.getTime())) {
+ return "";
+ }
+
+ // Helper for English ordinal suffixes
+ const getOrdinal = (n) => {
+ const s = ["th", "st", "nd", "rd"];
+ const v = n % 100;
+ return s[(v - 20) % 10] || s[v] || s[0];
+ };
+
+ try {
+ const month = date.toLocaleString("en-US", { month: "long" });
+ const day = date.getDate();
+ const year = date.getFullYear();
+ const suffix = getOrdinal(day);
+
+ return `${month} ${day}${suffix}, ${year}`;
+ } catch (err) {
+ // Fallback if locale fails
+ return date.toDateString();
+ }
+}
diff --git a/js/utils/torrentHash.js b/js/utils/torrentHash.js
new file mode 100644
index 00000000..b7b715c8
--- /dev/null
+++ b/js/utils/torrentHash.js
@@ -0,0 +1,62 @@
+import WebTorrent from "../webtorrent.min.js";
+import { userLogger } from "./logger.js";
+
+/**
+ * Calculates the torrent infoHash for a given file client-side.
+ * This operation requires reading and hashing the entire file, so it may take time for large files.
+ *
+ * @param {File} file - The file object to hash.
+ * @returns {Promise} - Resolves with the hex encoded infoHash.
+ */
+export async function calculateTorrentInfoHash(file) {
+ if (!file) {
+ throw new Error("No file provided for hashing.");
+ }
+
+ // Create a temporary client just for hashing.
+ // We disable trackers and dht/lsd to keep it quiet and fast (local only).
+ const client = new WebTorrent({
+ tracker: false,
+ dht: false,
+ lsd: false,
+ webSeeds: false,
+ });
+
+ return new Promise((resolve, reject) => {
+ // Seed the file. This calculates the infoHash.
+ // We use destroyStoreOnDestroy: true to clean up memory/storage afterwards.
+ try {
+ client.seed(
+ file,
+ {
+ announce: [], // No trackers
+ destroyStoreOnDestroy: true,
+ name: file.name,
+ },
+ (torrent) => {
+ const infoHash = torrent.infoHash;
+ // Destroy the client and the torrent data immediately.
+ client.destroy((err) => {
+ if (err) {
+ userLogger.warn("Error destroying temporary hashing client:", err);
+ }
+ if (infoHash) {
+ resolve(infoHash);
+ } else {
+ reject(new Error("Failed to calculate info hash."));
+ }
+ });
+ }
+ );
+
+ client.on("error", (err) => {
+ userLogger.error("WebTorrent client error during hashing:", err);
+ client.destroy();
+ reject(err);
+ });
+ } catch (err) {
+ client.destroy();
+ reject(err);
+ }
+ });
+}
diff --git a/js/viewCounter.js b/js/viewCounter.js
index 17638a36..f9bf799b 100644
--- a/js/viewCounter.js
+++ b/js/viewCounter.js
@@ -49,7 +49,7 @@ restoreCacheSnapshot();
* @property {number} total
* @property {Map} dedupeBuckets
* @property {number} lastSyncedAt
- * @property {"idle"|"hydrating"|"live"} status
+ * @property {"idle"|"hydrating"|"live"|"stale"} status
*/
/**
@@ -482,7 +482,8 @@ async function hydratePointer(key, listeners) {
}
}
let mutated = false;
- if (Array.isArray(listResult)) {
+ const hasListEvents = Array.isArray(listResult) && listResult.length > 0;
+ if (hasListEvents) {
for (const event of listResult) {
mutated = applyEventToState(key, event) || mutated;
}
@@ -502,6 +503,13 @@ async function hydratePointer(key, listeners) {
mutated = true;
}
}
+ if (!hasListEvents && bestCount === null) {
+ const state = ensurePointerState(key);
+ state.status = "stale";
+ schedulePersist();
+ notifyHandlers(key);
+ return;
+ }
if (mutated) {
schedulePersist();
notifyHandlers(key);
@@ -518,8 +526,11 @@ function ensureHydration(key, listeners) {
if (!listeners.hydrationPromise) {
listeners.hydrationPromise = hydratePointer(key, listeners).finally(() => {
listeners.hydrationPromise = null;
+ const state = ensurePointerState(key);
if (listeners.liveUnsub) {
setPointerStatus(key, "live");
+ } else if (state.status === "stale") {
+ setPointerStatus(key, "stale");
} else {
setPointerStatus(key, "idle");
}
diff --git a/js/watchHistoryService.js b/js/watchHistoryService.js
index 04274f4b..90f90e47 100644
--- a/js/watchHistoryService.js
+++ b/js/watchHistoryService.js
@@ -1032,6 +1032,159 @@ function scheduleWatchHistoryRefresh(actorKey, cacheEntry = {}) {
return promise;
}
+function resolveEntryPointer(entry) {
+ if (!entry) {
+ return null;
+ }
+ const candidate = entry.pointer || entry;
+ return normalizePointerInput(candidate);
+}
+
+function resolveEntryWatchedAt(entry, pointer) {
+ const watchedAtRaw = Number.isFinite(entry?.watchedAt)
+ ? entry.watchedAt
+ : Number.isFinite(entry?.pointer?.watchedAt)
+ ? entry.pointer.watchedAt
+ : Number.isFinite(entry?.timestamp)
+ ? entry.timestamp
+ : Number.isFinite(pointer?.watchedAt)
+ ? pointer.watchedAt
+ : 0;
+ return Math.max(0, Math.floor(Number(watchedAtRaw) || 0));
+}
+
+function getLatestWatchedAt(items) {
+ if (!Array.isArray(items) || !items.length) {
+ return 0;
+ }
+ let latest = 0;
+ for (const entry of items) {
+ const pointer = resolveEntryPointer(entry);
+ if (!pointer) {
+ continue;
+ }
+ const watchedAt = resolveEntryWatchedAt(entry, pointer);
+ if (watchedAt > latest) {
+ latest = watchedAt;
+ }
+ }
+ return latest;
+}
+
+function buildMergedEntry(entry, pointer, watchedAt) {
+ const normalizedPointer = pointer ? { ...pointer } : null;
+ if (normalizedPointer) {
+ normalizedPointer.watchedAt = watchedAt;
+ if (pointer?.session === true) {
+ normalizedPointer.session = true;
+ }
+ }
+
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
+ const next = { ...entry };
+ if (entry.pointer && typeof entry.pointer === "object") {
+ next.pointer = {
+ ...(entry.pointer || {}),
+ ...(normalizedPointer || {}),
+ watchedAt,
+ };
+ } else if (normalizedPointer) {
+ Object.assign(next, normalizedPointer);
+ next.watchedAt = watchedAt;
+ } else {
+ next.watchedAt = watchedAt;
+ }
+ if (normalizedPointer?.session === true) {
+ if (next.pointer) {
+ next.pointer.session = true;
+ } else {
+ next.session = true;
+ }
+ }
+ return next;
+ }
+
+ if (normalizedPointer) {
+ return normalizedPointer;
+ }
+
+ return entry;
+}
+
+function mergeWatchHistoryItems(baseItems, queueItems) {
+ const merged = new Map();
+
+ const ingest = (entry, pointer, watchedAt) => {
+ const key = pointerKey(pointer);
+ if (!key) {
+ return;
+ }
+ merged.set(key, {
+ entry,
+ watchedAt,
+ key,
+ });
+ };
+
+ for (const entry of Array.isArray(baseItems) ? baseItems : []) {
+ const pointer = resolveEntryPointer(entry);
+ if (!pointer) {
+ continue;
+ }
+ const watchedAt = resolveEntryWatchedAt(entry, pointer);
+ ingest(entry, pointer, watchedAt);
+ }
+
+ for (const entry of Array.isArray(queueItems) ? queueItems : []) {
+ const pointer = resolveEntryPointer(entry);
+ if (!pointer) {
+ continue;
+ }
+ const watchedAt = resolveEntryWatchedAt(entry, pointer);
+ const key = pointerKey(pointer);
+ if (!key) {
+ continue;
+ }
+ const existing = merged.get(key);
+ if (!existing || watchedAt > existing.watchedAt) {
+ const mergedEntry = buildMergedEntry(existing?.entry || entry, pointer, watchedAt);
+ merged.set(key, {
+ entry: mergedEntry,
+ watchedAt,
+ key,
+ });
+ }
+ }
+
+ const ordered = Array.from(merged.values());
+ ordered.sort((a, b) => {
+ if (a.watchedAt !== b.watchedAt) {
+ return b.watchedAt - a.watchedAt;
+ }
+ return a.key.localeCompare(b.key);
+ });
+
+ return ordered.map((entry) => entry.entry);
+}
+
+function mergeQueuedItemsIfNeeded(actorKey, items) {
+ const queueItems = collectQueueItems(actorKey);
+ if (!queueItems.length) {
+ return items;
+ }
+
+ const snapshotInflight = state.inflightSnapshots.has(actorKey);
+ if (!snapshotInflight) {
+ const latestQueue = getLatestWatchedAt(queueItems);
+ const latestBase = getLatestWatchedAt(items);
+ if (latestQueue <= latestBase) {
+ return items;
+ }
+ }
+
+ return mergeWatchHistoryItems(items, queueItems);
+}
+
async function loadLatest(actorInput, options = {}) {
const actorKey = resolveActorKey(actorInput);
if (!actorKey) {
@@ -1087,7 +1240,7 @@ async function loadLatest(actorInput, options = {}) {
itemCount: cacheEntry.items.length,
}
);
- return cacheEntry.items;
+ return mergeQueuedItemsIfNeeded(actorKey, cacheEntry.items);
}
if (cacheEntry?.promise) {
@@ -1105,13 +1258,15 @@ async function loadLatest(actorInput, options = {}) {
itemCount: cacheEntry.items.length,
}
);
- return cacheEntry.items;
+ return mergeQueuedItemsIfNeeded(actorKey, cacheEntry.items);
}
- return cacheEntry.promise;
+ const resolved = await cacheEntry.promise;
+ return mergeQueuedItemsIfNeeded(actorKey, resolved);
}
if (!allowStale || !hasCachedItems) {
- return scheduleWatchHistoryRefresh(actorKey, cacheEntry);
+ const resolved = await scheduleWatchHistoryRefresh(actorKey, cacheEntry);
+ return mergeQueuedItemsIfNeeded(actorKey, resolved);
}
const refreshPromise = scheduleWatchHistoryRefresh(actorKey, cacheEntry);
@@ -1125,7 +1280,7 @@ async function loadLatest(actorInput, options = {}) {
}
);
- return cacheEntry.items || [];
+ return mergeQueuedItemsIfNeeded(actorKey, cacheEntry.items || []);
}
async function getFingerprint(actorInput) {
diff --git a/tests/moderation/trusted-mute-lists.test.mjs b/tests/moderation/trusted-mute-lists.test.mjs
index f6e211c2..0b3dca66 100644
--- a/tests/moderation/trusted-mute-lists.test.mjs
+++ b/tests/moderation/trusted-mute-lists.test.mjs
@@ -150,7 +150,7 @@ test("trusted mute lists from seeds hide authors for anonymous viewers", async (
service,
autoplayThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
blurThreshold: DEFAULT_BLUR_THRESHOLD,
- trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
+ trustedMuteHideThreshold: 1, // Override to ensure hiding triggers with a single mute
reportType: "nudity",
});
diff --git a/tests/user-blocks.test.mjs b/tests/user-blocks.test.mjs
index 3a418e39..91187114 100644
--- a/tests/user-blocks.test.mjs
+++ b/tests/user-blocks.test.mjs
@@ -13,6 +13,17 @@ if (typeof globalThis.window === "undefined") {
globalThis.window = {};
}
+// Fix environment for NostrTools
+if (typeof globalThis.NostrTools === "undefined") {
+ // Polyfill enough of NostrTools for the tests to function if actual library missing or failing bootstrap
+ // The test below overwrites window.NostrTools later, but we need globalThis.NostrTools to exist for js/userBlocks.js module scope checks
+ globalThis.NostrTools = {
+ nip19: {
+ decode: () => { throw new Error("Mock not implemented"); }
+ }
+ };
+}
+
await (async () => {
const actor = "f".repeat(64);
const blocked = "e".repeat(64);
@@ -87,6 +98,16 @@ await (async () => {
"loadBlocks should initiate queries for all relays concurrently"
);
+ // Reverting to original test data:
+ // relay-one (fast): OLD event
+ // relay-two (fast): NEW event
+ // This ensures that even if fast relay returns old data, eventual consistency (or Promise.any randomness?) picks it up.
+ // Wait, Promise.any picks the FIRST resolved.
+ // The test resolves them in order: relay-one (old), relay-two (new).
+ // So 'fast' path gets 'event-old'.
+ // Background path (Promise.allSettled) gets BOTH.
+ // Background path merges and finds 'event-new'.
+ // So if the code correctly implements eventual consistency, userBlocks.blockEventId will settle on 'event-new'.
const responses = {
[relays[0]]: [
{
@@ -313,7 +334,7 @@ await (async () => {
const originalMuteEventCreatedAt = userBlocks.muteEventCreatedAt;
const originalLoaded = userBlocks.loaded;
const originalSeedStateCache = userBlocks.seedStateCache;
- const originalNostrTools = window.NostrTools;
+ const originalNostrTools = globalThis.NostrTools;
const publishCalls = [];
@@ -324,7 +345,8 @@ await (async () => {
[seedTwoNpub, seedTwoHex],
]);
- window.NostrTools = {
+ // Patch both globalThis and window to ensure userBlocks sees it
+ globalThis.NostrTools = {
...(originalNostrTools || {}),
nip19: {
...((originalNostrTools && originalNostrTools.nip19) || {}),
@@ -336,6 +358,7 @@ await (async () => {
},
},
};
+ window.NostrTools = globalThis.NostrTools;
userBlocks.ensureLoaded = async () => {
userBlocks.loaded = true;
@@ -454,7 +477,7 @@ await (async () => {
userBlocks.muteEventCreatedAt = originalMuteEventCreatedAt;
userBlocks.loaded = originalLoaded;
userBlocks.seedStateCache = originalSeedStateCache;
- window.NostrTools = originalNostrTools;
+ globalThis.NostrTools = originalNostrTools;
localStorage.clear();
}
})();
@@ -492,6 +515,8 @@ await (async () => {
let latestCiphertext = "cipher:[]";
let latestEventId = "event-initial";
let latestCreatedAt = 1_000;
+ // Capture tags for faithful mock
+ let latestTags = [];
const decryptPayloads = new Map([[latestCiphertext, JSON.stringify([])]]);
@@ -502,6 +527,7 @@ await (async () => {
nostrClient.pool = {
publish: (_targets, event) => {
latestCiphertext = event.content;
+ latestTags = event.tags; // Store tags
latestEventId = event.id || `event-${Date.now()}`;
latestCreatedAt = event.created_at || Math.floor(Date.now() / 1000);
return {
@@ -521,6 +547,7 @@ await (async () => {
created_at: latestCreatedAt,
pubkey: actor,
content: latestCiphertext,
+ tags: latestTags || [], // Return tags so encryption hints work
},
],
};
@@ -1044,3 +1071,170 @@ await (async () => {
}
}
})();
+
+await (async () => {
+ // Test for tag-only Kind 10000 events (Primal-style)
+ const actor = "f".repeat(64);
+ const blockedHex = "a".repeat(64);
+
+ const UserBlockListManager = userBlocks.constructor;
+ const manager = new UserBlockListManager();
+
+ const originalRelays = Array.isArray(nostrClient.relays) ? [...nostrClient.relays] : nostrClient.relays;
+ const originalPool = nostrClient.pool;
+
+ nostrClient.relays = ["wss://tag-only.example"];
+ nostrClient.pool = {
+ list: async () => [
+ {
+ id: "event-tag-only",
+ kind: 10000,
+ created_at: 10_000,
+ pubkey: actor,
+ content: "", // Empty content
+ tags: [["p", blockedHex]],
+ },
+ ],
+ };
+
+ try {
+ await manager.loadBlocks(actor);
+
+ assert(manager.blockedPubkeys.has(blockedHex), "should load blocked pubkeys from p-tags when content is empty");
+ assert.equal(manager.blockedPubkeys.size, 1, "should have exactly one blocked pubkey");
+ assert.equal(manager.blockEventId, "event-tag-only", "should track the correct event ID");
+ } finally {
+ nostrClient.relays = originalRelays;
+ nostrClient.pool = originalPool;
+ }
+ })();
+
+ await (async () => {
+ // Test for tag-only Kind 10000 events with whitespace content
+ const actor = "f".repeat(64);
+ const blockedHex = "b".repeat(64);
+
+ const UserBlockListManager = userBlocks.constructor;
+ const manager = new UserBlockListManager();
+
+ const originalRelays = Array.isArray(nostrClient.relays) ? [...nostrClient.relays] : nostrClient.relays;
+ const originalPool = nostrClient.pool;
+
+ nostrClient.relays = ["wss://tag-only-whitespace.example"];
+ nostrClient.pool = {
+ list: async () => [
+ {
+ id: "event-tag-only-space",
+ kind: 10000,
+ created_at: 11_000,
+ pubkey: actor,
+ content: " ", // Whitespace content
+ tags: [["p", blockedHex]],
+ },
+ ],
+ };
+
+ try {
+ await manager.loadBlocks(actor);
+
+ assert(manager.blockedPubkeys.has(blockedHex), "should load blocked pubkeys from p-tags when content is whitespace");
+ } finally {
+ nostrClient.relays = originalRelays;
+ nostrClient.pool = originalPool;
+ }
+ })();
+
+ await (async () => {
+ // Test for empty content AND no tags -> should clear
+ const actor = "f".repeat(64);
+ const previouslyBlocked = "c".repeat(64);
+
+ const UserBlockListManager = userBlocks.constructor;
+ const manager = new UserBlockListManager();
+ manager.blockedPubkeys.add(previouslyBlocked);
+
+ const originalRelays = Array.isArray(nostrClient.relays) ? [...nostrClient.relays] : nostrClient.relays;
+ const originalPool = nostrClient.pool;
+
+ nostrClient.relays = ["wss://empty-clear.example"];
+ nostrClient.pool = {
+ list: async () => [
+ {
+ id: "event-empty",
+ kind: 10000,
+ created_at: 12_000,
+ pubkey: actor,
+ content: "",
+ tags: [], // No p tags
+ },
+ ],
+ };
+
+ try {
+ await manager.loadBlocks(actor);
+
+ assert.equal(manager.blockedPubkeys.size, 0, "should clear block list when content and tags are empty");
+ } finally {
+ nostrClient.relays = originalRelays;
+ nostrClient.pool = originalPool;
+ }
+ })();
+
+await (async () => {
+ // Test: Prioritize 'd=user-blocks' tagged lists over newer plain lists
+ const actor = "a".repeat(64);
+ const taggedBlocked = "b".repeat(64);
+ const plainBlocked = "c".repeat(64);
+
+ const originalRelays = nostrClient.relays;
+ const originalPool = nostrClient.pool;
+ const originalNostr = window.nostr;
+
+ nostrClient.relays = ["wss://repro.example"];
+
+ window.nostr = {
+ nip04: {
+ decrypt: async (_pubkey, ciphertext) => {
+ if (ciphertext === "encrypted-tagged") {
+ return JSON.stringify({ blockedPubkeys: [taggedBlocked] });
+ }
+ return "{}";
+ }
+ }
+ };
+
+ nostrClient.pool = {
+ list: async () => [
+ {
+ id: "event-tagged",
+ kind: 10000,
+ created_at: 1000,
+ pubkey: actor,
+ content: "encrypted-tagged",
+ tags: [["d", "user-blocks"]],
+ },
+ {
+ id: "event-plain",
+ kind: 10000,
+ created_at: 2000, // Newer!
+ pubkey: actor,
+ content: "",
+ tags: [["p", plainBlocked]],
+ }
+ ]
+ };
+
+ userBlocks.reset();
+
+ try {
+ await userBlocks.loadBlocks(actor);
+
+ const blocked = userBlocks.getBlockedPubkeys();
+ assert.ok(blocked.includes(taggedBlocked), "Should include pubkey from tagged list");
+ assert.ok(!blocked.includes(plainBlocked), "Should NOT include pubkey from plain list (fallback ignored)");
+ } finally {
+ nostrClient.relays = originalRelays;
+ nostrClient.pool = originalPool;
+ window.nostr = originalNostr;
+ }
+})();