diff --git a/js/web/src/components/stream/video-section.tsx b/js/web/src/components/stream/video-section.tsx
index 9be3ee1c1..d5e17053d 100644
--- a/js/web/src/components/stream/video-section.tsx
+++ b/js/web/src/components/stream/video-section.tsx
@@ -127,12 +127,20 @@ export function VideoSectionInner({
style={{
// In theatre mode the sidebar and header are hidden, so the video
// fills the full viewport. Otherwise constrain to aspect ratio.
+ // When offline, halve the height so the offline banner sits as a
+ // shorter block at the top of the page (matching the Twitch-style
+ // "offline card" layout) instead of a full video-sized box.
...(theatre
? { height: "100vh" }
- : {
- maxHeight: `min(calc(100vw / ${segRatio}), calc(100vh - 240px))`,
- aspectRatio: `${segRatio}`,
- }),
+ : offline
+ ? {
+ maxHeight: `min(calc(50vw / ${segRatio}), calc(50vh - 120px))`,
+ aspectRatio: `${segRatio * 2}`,
+ }
+ : {
+ maxHeight: `min(calc(100vw / ${segRatio}), calc(100vh - 240px))`,
+ aspectRatio: `${segRatio}`,
+ }),
}}
>
diff --git a/js/web/src/routes/$user/index.tsx b/js/web/src/routes/$user/index.tsx
index e7d578e91..e9cfd4a40 100644
--- a/js/web/src/routes/$user/index.tsx
+++ b/js/web/src/routes/$user/index.tsx
@@ -127,7 +127,12 @@ function StreamPage() {
function StreamBody({ store, user }: { store: LivestreamStore; user: string }) {
const liveness = useLivenessState(store);
const { theatre } = useFullscreen();
+ // Offline streams default to a closed chat — the chat has nothing
+ // to show, and the shorter offline player should take the page
+ // width. Live streams honor the user's saved preference.
+ const isOffline = liveness === "offline" || liveness === "never-live";
const [chatOpen, setChatOpen] = useState(() => {
+ if (isOffline) return false;
if (typeof localStorage === "undefined") return true;
return localStorage.getItem("streamplace:chat-open") !== "false";
});
@@ -140,16 +145,23 @@ function StreamBody({ store, user }: { store: LivestreamStore; user: string }) {
});
}, []);
- // Hide the chat sidebar when the stream is offline or the streamer
- // has never gone live — no chat to show in either case.
- const showChat = liveness !== "offline" && liveness !== "never-live";
+ // Auto-close the chat when a live stream goes offline. The user
+ // can still open it manually to see the backlog; this just gets
+ // the page back to a sensible default. (The reverse case — a
+ // closed chat reopening when the stream goes live — doesn't
+ // auto-open, the user has the toggle for that.)
+ useEffect(() => {
+ if (isOffline && chatOpen) {
+ setChatOpen(false);
+ }
+ }, [isOffline, chatOpen]);
return (
{/* Sidebar layout (wide viewport) */}
@@ -165,15 +177,13 @@ function StreamBody({ store, user }: { store: LivestreamStore; user: string }) {
- {showChat && (
-
-
-
- )}
+
+
+
{/* Stacked layout (portrait/tall) */}
@@ -182,11 +192,9 @@ function StreamBody({ store, user }: { store: LivestreamStore; user: string }) {
- {showChat && (
-
-
-
- )}
+
+
+
);