/*
Room pages: the full-screen video UI.

What belongs here:
Rules that carry a decision about how a room looks — the sizes, crops, and
overlays that make the stage feel like a video call rather than a web page.
Page-specific by design, so a name only has to make sense within a room.

What does not:
Rules that change what a Bootstrap class does (bootstrap_overrides.css),
generic reusable utilities (bootstrap_additions.css), and components that
belong to some other page (application.css).

Layout vocabulary — source, framing, crop, layout — is defined in SYSTEM.md.
room_layout_controller.js writes data-layout, data-framing, and --video-aspect
onto .room; everything below reacts to those and nothing computes for itself.
*/

/*
Rail beside the room, never above it. Its own element rather than <main>: the
layout hardcodes Bootstrap's .flex-column there, which is declared after
.flex-row and equally !important, so a flex-row passed in via main_class
loses the tiebreak and the rail stacks on top.
*/
.room-page {
  display: flex;
  flex-direction: row;
  flex-grow: 1;
  min-height: 0;
  overflow: hidden;
}

/*
The room's layer stack, declared in one place so the order is readable without
hunting. Tens, so a layer can be inserted later without renumbering.

ONLY IMMERSIVE STACKS. In beside and below every piece of UI is on the same
plane, in normal flow with its own background — nothing floats over anything,
and these values go unused.

Two rules that are easy to get wrong, both explained in SYSTEM.md:
- Layers must be SIBLINGS. filter and transform each create a stacking
  context, and we use both (.overlay-shadow, the mirrored preview), so a
  nested layer is sealed in and cannot rise above its parent's siblings.
- Transient layers pass clicks through (pointer-events: none), with individual
  interactive children turning it back on. Confetti that eats a click while
  someone is typing is worse than no confetti.

There is no layer for face effects: those are composited on the streamer's
device and muxed into the outgoing track, so they are part of the picture
rather than something positioned over it.

Declared so that each UI element that lands has a slot to go in instead of
inventing a number; the celebrations layer was the first to be filled.
*/
.room {
  --z-placeholder: 0;
  --z-video: 10;
  --z-chat: 30;
  --z-composer: 40;
  --z-room-ui: 50;
  --z-banner: 60;
  --z-creator-ui: 70;
  --z-celebration: 80;

  display: flex;
  flex-grow: 1;
  isolation: isolate;
  min-width: 0;
  overflow: hidden;
  position: relative;
}

/*
The stage never scrolls and never forces the room taller.
min-* 0 because a flex item will not shrink below its content otherwise, and
the content here is a video with intrinsic dimensions.
*/
/*
The stage stacks rather than lays out: placeholder underneath, video over it,
both filling. So a <video> with no stream yet — the broadcaster before they
start a camera, a viewer before the first track arrives — is simply
transparent and reveals what is under it, with nothing to toggle.
*/
.room-stage {
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  position: relative;
}

.room-video {
  display: block;
  height: 100%;
  inset: 0;
  position: absolute;
  width: 100%;
  z-index: var(--z-video);
}

/*
LAYER 0 — the stage with no picture on it. Never takes a click: there is
nothing here to press, and in immersive this spans the whole room.
*/
.room-placeholder {
  align-items: center;
  display: flex;
  inset: 0;
  justify-content: center;
  padding: 2rem;
  pointer-events: none;
  position: absolute;
  z-index: var(--z-placeholder);
}

.room-placeholder-message {
  align-items: center;
  color: var(--bs-secondary-color);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 0;
  max-width: 22rem;
  text-align: center;
  text-wrap: balance;
}

/*
IMMERSIVE — aspects agree closely enough that the leftover is not worth
splitting. Video fills every edge and crops, the way FaceTime and TikTok do;
by definition we are cropping less than the minimum, so little is lost. Chat
floats on top rather than taking space away.
*/
.room[data-layout="immersive"] .room-stage {
  flex: 1 1 auto;
}

.room[data-layout="immersive"] .room-video {
  object-fit: cover;
}

/*
Chat floats over the picture, anchored to the bottom so messages rise off the
floor rather than hanging from a fixed box.

pointer-events is the fiddly part: the layer spans the whole width, so a solid
one would swallow every click meant for the video. The layer passes clicks
through and the log itself takes them back, which keeps the log scrollable
while the empty space above it stays transparent to the touch.

The scrim is what makes white text survive an arbitrary bright frame — a
gradient rather than a flat panel so it fades out where there is nothing to
read, and taller than the log so text never crosses its edge.

Sizing is decided by the log, not the panel. The panel's cap is only a
backstop for a landscape phone; everything else in it (scrim fade, tip form
with its balance line, composer) is fixed height, so a tight panel cap was
silently starving the log down to two lines on a portrait phone.
*/
.room[data-layout="immersive"] .room-chat {
  background: linear-gradient(to top, rgb(0 0 0 / 70%), rgb(0 0 0 / 0%));
  bottom: 0;
  color: #fff;
  inset-inline: 0;
  justify-content: flex-end;
  max-height: 70%;
  padding-top: 3rem;
  pointer-events: none;
  position: absolute;
  z-index: var(--z-chat);
}

/*
About six lines at the default line height, plus the log's own padding. Grows
only as far as its messages and shrinks under the panel cap, so an empty
room shows no dark block and a landscape phone still gets a composer.
*/
.room[data-layout="immersive"] .room-chat-log {
  --chat-line-color: #fff;
  flex: 0 1 auto;
  max-height: 10.5rem;
  pointer-events: auto;
}

/*
The message partial is written for a normal page, so its Bootstrap text and
link colors have to be pulled back to white over the video.
*/
.room[data-layout="immersive"] .room-chat-log a,
.room[data-layout="immersive"] .room-chat-log .text-secondary {
  color: #fff;
}

/*
The guest note and the unavailable-chat lines sit on the scrim rather than on
a panel, so they need the same treatment as the messages above them.
*/
.room[data-layout="immersive"] .room-composer-note,
.room[data-layout="immersive"] .room-composer-note a {
  color: #fff;
}

/*
BESIDE — portrait video in a wide window. The stage takes exactly the video's
shape at full height (hence --video-aspect), chat takes the leftover up to a
cap, and the pair centers with background either side. Uncapped, a 1920 window
would give a 608px video against a 1312px chat.
*/
.room[data-layout="beside"] {
  flex-direction: row;
  justify-content: center;
}

.room[data-layout="beside"] .room-stage {
  aspect-ratio: var(--video-aspect);
  flex: 0 1 auto;
  height: 100%;
}

.room[data-layout="beside"] .room-chat {
  flex: 1 1 20rem;
  max-width: 45rem;
}

/*
Split layouts: an ordinary panel, so the log fills it and scrolls inside.
The lines still sit at its foot, against the composer, the way they do in
immersive: the log is a column and its first line takes the slack above as
margin, which is nothing once there is enough chat to scroll. No scrim —
nothing is over the video here.
*/
.room[data-layout="beside"] .room-chat-log,
.room[data-layout="below"] .room-chat-log {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
}

.room[data-layout="beside"] .room-chat-log > :first-child,
.room[data-layout="below"] .room-chat-log > :first-child {
  margin-top: auto;
}

/*
BELOW — landscape video in a tall window. Video across the top at full width,
chat gets everything under it. No cap here: a chat that is too tall is just
more history, where one that is too wide is unreadable line lengths.
*/
.room[data-layout="below"] {
  flex-direction: column;
}

.room[data-layout="below"] .room-stage {
  aspect-ratio: var(--video-aspect);
  flex: 0 1 auto;
  width: 100%;
}

.room[data-layout="below"] .room-chat {
  flex: 1 1 12rem;
  min-height: 0;
}

/*
Both split layouts show the whole frame: chat already has its own space, and
the streamer may be demonstrating something that must not be cropped away.
*/
.room[data-layout="beside"] .room-video,
.room[data-layout="below"] .room-video {
  object-fit: contain;
}

/*
LAYER 50 — the room's own chrome: the way out, whose room this is, and how
many people are in it. One strip across the top of the stage in every layout.

Passes clicks through, since it spans the full width and most of it is empty
space over the picture; the two things you can actually press take them back.

The scrim is not decoration. With no stream the stage shows the placeholder on
the page background, so white text would vanish in light mode — the gradient
is what makes this readable over anything, video or not.
*/
.room-header {
  align-items: flex-start;
  background: linear-gradient(to bottom, rgb(0 0 0 / 55%), rgb(0 0 0 / 0%));
  color: #fff;
  display: flex;
  gap: 0.75rem;
  inset-inline: 0;
  padding: 0.5rem 0.75rem 2.5rem;
  pointer-events: none;
  position: absolute;
  top: 0;
  z-index: var(--z-room-ui);
}

/*
The strip is top-aligned so the controls sit beside the host line, not the
middle of the two-line identity block. Every control therefore takes the
host line's own height (1rem at Bootstrap's 1.5 line-height) and centers
its content in it, so a number and an icon share one center line instead
of each bringing its own box height.
*/
/*
min-width 0 so the title can truncate: a flex item will not shrink below its
content without it, and a long room title would otherwise push the occupancy
count off the edge.
*/
.room-identity {
  flex: 1 1 auto;
  min-width: 0;
}

/*
One line: the host (or the streamer's own clock) bold, then the room's
title after it, smaller and lighter, on the same baseline. The host is the
one thing everyone in the room came for; the title is the byline. The
avatar centers itself, since an image has no baseline worth aligning to.
*/
.room-host {
  align-items: baseline;
  display: flex;
  font-size: 1rem;
  font-weight: 600;
  gap: 0.375rem;
  /* The strip's buttons and the avatar are 1.5rem tall; the same line
     height puts the text's middle on theirs rather than its top. */
  line-height: 1.5rem;
  margin: 0;
  min-width: 0;
}

.room-host img {
  align-self: center;
}

.room-host a {
  color: #fff;
  pointer-events: auto;
  text-decoration: none;
}

/*
The streamer's menu: their name in the heading is the summary of a <details>
whose panel drops from the strip's left edge a row down, the mirror of the
gift panel and the who-is-here list on the right, on the page colors. In it
the room's facts (visibility and title) and its controls (Close room, the
Paused words or the Stop live button, the clock for the show), stacked.
*/
.room-identity-summary {
  cursor: pointer;
  list-style: none;
  pointer-events: auto;
}

.room-identity-summary::-webkit-details-marker {
  display: none;
}

/* A Bootstrap card, dropped from the strip; its body is the column. */
.room-identity-panel {
  box-shadow: var(--bs-box-shadow);
  left: 0.75rem;
  pointer-events: auto;
  position: absolute;
  top: 2.75rem;
  /* Wide enough for the footer's "about 2 hours ago" beside Pause stream
     on one line; the phone's width less the strip's padding otherwise. */
  width: min(24rem, calc(100vw - 1.5rem));
}

/* The clock's words stay on one line beside the footer's button. */
.room-started-at {
  white-space: nowrap;
}

/* Off the air, the clock's place: a chevron and the streamer's avatar,
   the way home, in the footer's own color. */
.room-identity-home {
  align-items: center;
  color: inherit;
  display: inline-flex;
  gap: 0.125rem;
}

.room-identity-panel .card-body {
  align-items: flex-start;
  display: flex;
  flex-direction: column;
  font-size: 0.875rem;
  gap: 0.5rem;
}

/* The viewer's card reads at the page's size throughout: its body, and
   the host's card inside it, which is written small for the popover and
   the who-is-here list. */
#room-viewer-card .card-body,
#room-viewer-card .small {
  font-size: 1rem;
}

.room-identity-facts {
  display: flex;
  gap: 0.25rem;
  margin: 0;
  max-width: 100%;
  min-width: 0;
}

/* The host's card, inline in a viewer's menu: the panel's full width, so
   its name can truncate and its bio can wrap, like it does in the popover. */
.room-identity-card {
  align-self: stretch;
  min-width: 0;
}

/* The room's name: its own line, the page's size, wrapping rather than
   cut, since the panel has the width the strip's one line did not. */
.room-identity-name {
  font-size: 1rem;
  margin: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* The owner's settings form in the menu: name, who can see it, when the
   next stream starts, Save; one column, the panel's width. */
.room-identity-form {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* The icons in the card's header (copy, and the viewer's sound and
   picture-in-picture): bare, in the header's own color, buttons only in
   the markup. */
.room-identity-icon {
  background: none;
  border: 0;
  color: inherit;
  line-height: 1;
  padding: 0;
}

.room-identity-icon:hover,
.room-identity-icon:focus-visible {
  opacity: 0.7;
}

.room-identity-schedule {
  align-self: stretch;
}

/* The lobby's countdown, on the placeholder: the words on their own line
   under the icon, like the other placeholder messages. */
.room-placeholder-schedule {
  display: block;
}

/*
The title truncates, the ended-at does not: a long room name gives way so
"ended about 1 hour ago" always fits beside it on an archive.
*/
.room-visibility,
.room-ended {
  font-size: 0.875rem;
  font-weight: 400;
}

/* The owner's "Public" or "Followers", before the title in their menu. */
.room-visibility {
  flex: 0 0 auto;
}

/* On an archive, how old it is, under the name in the menu. */
.room-ended {
  color: var(--bs-secondary-color);
  margin: 0;
}

/*
Takes clicks back from the strip, which passes them through — these are inert
buttons, but a disabled control still wants its tooltip on hover.
*/
.room-media-state {
  flex: 0 0 auto;
  pointer-events: auto;
}

.room-occupancy {
  align-items: center;
  display: flex;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  height: 1.5rem;
  line-height: 1;
}

/*
The count is a <details>: the disclosure marker goes, so the number looks
like a plain number, and the summary takes clicks back from the strip. The
list drops over the picture on the page colors rather than the strip's
white-on-scrim, because it is a panel with names in it and not a caption on
the video.

Where it drops: not from the count's own corner but from the strip's right
edge, a row down, as if it hung from the strip's last icon. The count sits
left of three icons, and a panel anchored to it ran off the left of a phone.
The <details> is not positioned, so the strip (absolute) is the containing
block; the gift panel hangs in the same place, so only one is ever open.
*/
.room-occupancy-summary {
  cursor: pointer;
  list-style: none;
  pointer-events: auto;
}

.room-occupancy-summary::-webkit-details-marker {
  display: none;
}

.room-viewer-list {
  background: var(--bs-body-bg);
  border-radius: var(--bs-border-radius);
  box-shadow: var(--bs-box-shadow);
  color: var(--bs-body-color);
  display: block;
  font-size: 0.875rem;
  max-height: 50vh;
  /* As wide as its longest line, so the card's history rows read as rows
     rather than wrapping into paragraphs; capped to the screen on a phone. */
  max-width: min(26rem, calc(100vw - 1.5rem));
  min-width: 18rem;
  overflow-y: auto;
  padding: 0.5rem 0.75rem;
  pointer-events: auto;
  position: absolute;
  /* The strip's own side padding, and the creator bar's row. */
  right: 0.75rem;
  top: 2.75rem;
  width: max-content;
}

.room-viewer-list a {
  color: inherit;
}

/*
The card a tap on a chat name opens. A popover, so it lives in the top layer
and no z value here could matter. Centred, which is what the browser does
unasked and reads as a sheet on a phone. Same panel treatment as the viewer
list. pointer-events is inherited, and in immersive the chat layer passes
clicks through, so it is switched back on here.
*/
.room-person-card {
  background: var(--bs-body-bg);
  border: 0;
  border-radius: var(--bs-border-radius-lg);
  box-shadow: var(--bs-box-shadow-lg);
  color: var(--bs-body-color);
  font-size: 0.875rem;
  max-height: 70vh;
  max-width: min(26rem, calc(100vw - 1.5rem));
  min-width: 18rem;
  overflow-y: auto;
  padding: 0.75rem 1rem;
  pointer-events: auto;
}

.room-person-card::backdrop {
  background: rgb(0 0 0 / 30%);
}

.room-person-card turbo-frame[busy] {
  opacity: 0.5;
}

/*
The strip is a stacking context, so the list can never rise past the creator
bar (70) or a celebration (80) on its own. While the disclosure is open the
whole strip goes above everything instead — the one moment the layer table
bends, and it is the streamer's own hand that bent it.
*/
.room-header:has(.room-identity-menu[open]),
.room-header:has(.room-occupancy[open]),
.room-header:has(.room-gift-toggles[open]) {
  z-index: calc(var(--z-celebration) + 10);
}

/*
The stage clips its contents (the video fills it, and nothing inside should
spill), which cut a drop-down taller than the picture off at the chat's
edge in the split layouts. While one is open the stage lets its overflow
show; the strip is already above everything then, and the video stays
inside its box regardless.
*/
.room-stage:has(.room-identity-menu[open]),
.room-stage:has(.room-occupancy[open]),
.room-stage:has(.room-gift-toggles[open]) {
  overflow: visible;
}

/*
The owner's clock in the menu's footer is a control: a tap turns it over,
and the cursor says so.
*/
.room-started-at {
  cursor: pointer;
  pointer-events: auto;
}

/*
Icon buttons in the strip: the gift panel's. (Share is in the menu behind
the host's name, the viewer's picture-in-picture is a chip in the viewer
bar, and the chat toggle leads the composer row.)
*/
.room-gifts-button {
  align-items: center;
  background: none;
  border: 0;
  color: #fff;
  display: grid;
  flex: 0 0 auto;
  font-size: 0.875rem;
  height: 1.5rem;
  justify-items: center;
  line-height: 1;
  padding: 0 0.125rem;
  pointer-events: auto;
  white-space: nowrap;
}

.room-gifts-button:hover,
.room-gifts-button:focus-visible {
  opacity: 0.7;
}

/*
Chat pushed aside (chat_visibility_controller writes data-chat="hidden" on
.room). In immersive the panel slides off the bottom; in the splits it is
simply gone and the stage has the room to itself. The toggle (at the start
of the composer row, rooms/_chat_toggle) dims to say it is pressed, and the
count of lines that arrived meanwhile rides on it.
*/
.room-chat-toggle {
  position: relative;
}

.room-chat-toggle[aria-pressed="true"] {
  opacity: 0.5;
}

.room-chat-unread {
  background: var(--bs-danger);
  border-radius: var(--bs-border-radius-pill);
  color: #fff;
  font-size: 0.625rem;
  font-weight: 600;
  line-height: 1;
  min-width: 1rem;
  padding: 0.125rem 0.25rem;
  position: absolute;
  right: -0.375rem;
  top: -0.375rem;
}

/*
Hidden: the log, the field, the send button, and any note go; the toggle
stays, bare, exactly where the composer row began, so there is always a
way back and it is the same button in the same place. The panel keeps its
shape and its size around that one control, pushed to the panel's floor:
in immersive the scrim goes so the stage is clear and the icon is white on
the picture; in the splits the panel keeps its column or its band under
the video (collapsing it put the icon at the top of the emptied space, not
the bottom-left corner of the chat's area) and the icon sits at its foot.
*/
.room[data-chat="hidden"] .room-chat-log,
.room[data-chat="hidden"] .room-composer-note,
.room[data-chat="hidden"] .room-composer .chat-composer,
.room[data-chat="hidden"] .room-composer [type="submit"] {
  display: none;
}

.room[data-chat="hidden"] .room-chat {
  justify-content: flex-end;
}

.room[data-chat="hidden"] .room-chat-toggle,
.room[data-chat="hidden"] .room-chat-toggle:hover,
.room[data-chat="hidden"] .room-chat-toggle:focus-visible {
  background: none;
  border: 0;
  box-shadow: none;
  color: var(--bs-body-color);
}

.room[data-layout="immersive"][data-chat="hidden"] .room-chat {
  background: none;
  padding-top: 0;
}

/*
In immersive the toggle may be over the picture or, with no stream yet, over
the placeholder on the page background, and the page cannot tell which (the
streamer's preview sits over the placeholder either way). Plain white
vanished on the light page (#158); plain gray would vanish on a dark
picture. So: the secondary gray on a faint pill of the page's own color,
which reads on both and all but disappears into the page when there is no
picture under it.
*/
.room[data-layout="immersive"][data-chat="hidden"] .room-chat-toggle,
.room[data-layout="immersive"][data-chat="hidden"] .room-chat-toggle:hover,
.room[data-layout="immersive"][data-chat="hidden"] .room-chat-toggle:focus-visible {
  background: color-mix(in srgb, var(--bs-body-bg) 65%, transparent);
  border-radius: var(--bs-border-radius-pill);
  color: var(--bs-secondary-color);
  padding: 0.25rem 0.375rem;
}

/*
Big chat (data-chat="big"), the third state after shown and hidden: larger
type and line height for a creator with bad vision or across the kitchen from
a propped-up phone. Fewer lines fit, which is the point. The composer grows
with it so a reply is as readable as the log, and in immersive the log's cap
grows so big text still shows a few lines. The toggle's icon grows too, to
say which state it is in.
*/
.room[data-chat="big"] .room-chat-log {
  font-size: 1.875rem;
  line-height: 1.3;
}

.room[data-chat="big"] .chat-composer {
  font-size: 1.5rem;
}

.room[data-layout="immersive"][data-chat="big"] .room-chat-log {
  max-height: 22rem;
}

.room-chat-toggle[data-state="big"] svg {
  transform: scale(1.3);
}

/*
RAIL — the bonus nav, in the margin the wide layouts leave over. Keyed to the
window rather than to that margin on purpose: deriving it from the leftover
space would make the two mutually dependent (margin sizes rail, rail changes
width, width changes layout, layout changes margin). Keyed to the window, the
rail simply takes its column first and .room measures what remains.

Everything in it is duplicated elsewhere. Nothing may live only here, since it
is absent at most window sizes.
*/
.room-rail {
  display: none;
}

@media (min-width: 90rem) {
  .room-rail {
    align-items: center;
    display: flex;
    flex: 0 0 4rem;
    flex-direction: column;
    gap: 0.25rem;
    overflow-y: auto;
    padding-block: 0.75rem;
  }
}

.room-rail-link {
  border-radius: 0.375rem;
  color: var(--bs-secondary-color);
  display: block;
  line-height: 1;
  padding: 0.625rem;
}

.room-rail-link:hover,
.room-rail-link:focus-visible {
  background-color: var(--bs-secondary-bg);
  color: var(--bs-body-color);
}

/*
LAYER 30 — chat. The only element that changes kind between layouts: an
overlay floating on the video in immersive, an ordinary panel in beside and
below. Everything specific to one of those lives in that layout's block above;
this is what they share.
*/
.room-chat {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
}

.room-chat-log {
  /* What a line's text fades back to after a landing tint: the page's
     color in the split layouts, white over the scrim in immersive. */
  --chat-line-color: var(--bs-body-color);
  min-height: 0;
  overflow-y: auto;
  padding: 0.75rem;
}

/*
LAYER 50 — the viewer's "Unmute the stream" button, shown only while the
browser is refusing to start audio on its own. Bottom center of the stage:
over the picture, clear of the header strip and of the creator bar's spot,
and where a thumb already is on a phone. On the page's own colors, so it
reads in light and dark alike, with a shadow to lift it off the picture.
*/
.room-unmute {
  --bs-btn-bg: var(--bs-body-bg);
  --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-hover-bg: var(--bs-tertiary-bg);
  --bs-btn-hover-border-color: var(--bs-border-color);
  --bs-btn-hover-color: var(--bs-body-color);
  --bs-btn-active-bg: var(--bs-secondary-bg);
  --bs-btn-active-border-color: var(--bs-border-color);
  --bs-btn-active-color: var(--bs-body-color);
  bottom: 1rem;
  box-shadow: var(--bs-box-shadow);
  left: 50%;
  position: absolute;
  transform: translateX(-50%);
  z-index: var(--z-room-ui);
}

/*
LAYER 70 — creator controls, the owner's only. Two positions for two states:
the start control centered while off air, because going live is the whole job
then; a strip along the top once the picture matters more than the buttons.

Both sit inside .room-stage, so they stay over the video rather than reaching
across the chat panel in the split layouts.
*/
.room-creator-start {
  inset: 0;
  place-content: center;
  pointer-events: none;
  position: absolute;
  z-index: var(--z-creator-ui);
}

/*
display is set here rather than in the markup because broadcaster_controller
toggles the `hidden` attribute, and [hidden] in bootstrap_overrides.css is
display: none !important — so this can safely claim the element's display
without fighting the controller.
*/
.room-creator-start:not([hidden]) {
  display: grid;
}

/* Pushed down past the placeholder's message, which is centered in the same
   box: an icon and two lines of text on a phone, reaching about 2.9rem
   below center, so the buttons start half a rem under it. A three-line
   wrap (large system type) just touches. */
.room-creator-start .btn-group {
  pointer-events: auto;
  transform: translateY(5rem);
}

/*
In immersive the chat panel lies over the bottom of the stage, so "the
middle of the stage" is really the middle of what is left between the header
strip and the panel's top. room-layout measures the panel into --chat-height;
the placeholder and the start control center in that gap, and they move as
the panel grows (big chat) or shrinks. A hidden panel is off screen, so the
whole stage is free again.
*/
.room[data-layout="immersive"] .room-placeholder,
.room[data-layout="immersive"] .room-creator-start {
  bottom: var(--chat-height, 0px);
  top: 3.5rem;
}

.room[data-layout="immersive"][data-chat="hidden"] .room-placeholder,
.room[data-layout="immersive"][data-chat="hidden"] .room-creator-start {
  bottom: 0;
}

/*
Below the header strip, aligned right so it stays clear of the title. The
buttons are light-on-video rather than themed: this floats over a picture,
not over a panel.
*/
.room-creator-bar {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  inset-inline: 0.75rem;
  pointer-events: none;
  position: absolute;
  /* The header's line ends 2rem down (0.5rem padding, 1.5rem of content);
     this leaves 0.75rem under it. */
  top: 2.75rem;
  z-index: var(--z-creator-ui);
}

/* The stream's controls sit at the far end of the bar, the room's facts
   and Close room at the near end. */
.room-creator-media {
  align-items: center;
  display: flex;
  gap: 0.5rem;
  margin-inline-start: auto;
}

.room-creator-bar > * {
  pointer-events: auto;
}

/*
The owner's gift panel (rooms/_gifts_panel), a <details> in the header strip
beside the occupancy count. The summary is a bare icon like the strip's
others (see .room-gifts-button); the panel drops over the picture on the page colors,
like the who-is-here list, and in the same place: from the strip's right
edge a row down, not from its own icon (see .room-viewer-list). Inside, two
tabs (Gifts, Wishlist) are a pair of radio buttons; :has() shows the pane
whose tab is checked, so no JavaScript is involved.
*/
.room-gifts-button {
  cursor: pointer;
  list-style: none;
}

.room-gifts-button::-webkit-details-marker {
  display: none;
}

/*
A gift switch: outline when off, solid when on, in the page's emphasis color
(black on light, white on dark) so the pair reads the same in both color
modes. Bootstrap's -dark pair is dark in both, which vanishes on dark. Set
through the button's own variables so hover and active follow.
*/
.room-gift-toggle-button {
  --bs-btn-active-bg: var(--bs-emphasis-color);
  --bs-btn-active-border-color: var(--bs-emphasis-color);
  --bs-btn-active-color: var(--bs-body-bg);
  /* Off is quiet: the page's ordinary border color for the outline and the
     words both, and the face dimmed below, so what the room offers is what
     stands out. */
  --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-color: var(--bs-border-color);
  /* Hover in secondary, the gray the outline pair used, so a hover is a
     hint and not the same as being on. */
  --bs-btn-hover-bg: var(--bs-secondary);
  --bs-btn-hover-border-color: var(--bs-secondary);
  --bs-btn-hover-color: #fff;
}

.room-gift-toggle-button:not([data-offered]) .room-gift-emoji {
  opacity: 0.5;
}

/* On: the emphasis color for the outline and the words, over the page's
   secondary surface (a light gray on light, a dark gray on dark), rather
   than a solid block of the emphasis color, which was more contrast than a
   switch needs. */
.room-gift-toggle-button[data-offered] {
  --bs-btn-bg: var(--bs-secondary-bg);
  --bs-btn-border-color: var(--bs-emphasis-color);
  --bs-btn-color: var(--bs-emphasis-color);
}

.room-gifts-pane {
  display: none;
}

.room-gifts-panel:has(#room-gifts-tab-gifts:checked) .room-gifts-pane-gifts,
.room-gifts-panel:has(#room-gifts-tab-wishlist:checked) .room-gifts-pane-wishlist {
  display: block;
}

.room-gift-toggles-summary {
  cursor: pointer;
  list-style: none;
}

.room-gift-toggles-summary::-webkit-details-marker {
  display: none;
}

.room-gift-toggles-panel {
  background: var(--bs-body-bg);
  border-radius: var(--bs-border-radius);
  box-shadow: var(--bs-box-shadow-lg);
  color: var(--bs-body-color);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-height: 50vh;
  overflow-y: auto;
  padding: 0.5rem;
  pointer-events: auto;
  position: absolute;
  right: 0.75rem;
  top: 2.75rem;
  width: min(16rem, calc(100vw - 1.5rem));
}

.room-creator-close {
  margin: 0;
}

/*
LAYER 60 — banners, at the top of the chat panel. In flow rather than
positioned, so they sit above the log wherever the panel is: over the video in
immersive, in the column in beside, in the row in below.

Spacing lives on the children rather than as padding here, so an empty
container collapses to nothing instead of leaving a gap on every room.

Takes clicks back from the chat layer, which passes them through — one of
these carries a Resend button.

Bootstrap's .alert brings its own background, so nothing here needs a scrim.
*/
.room-banners {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  pointer-events: auto;
}

.room-banners > * {
  margin: 0.75rem 0.75rem 0;
}

/*
LAYER 80 — celebrations. Gifts arrive here by broadcast (Tip#celebrate_in_room)
and remove themselves after a few seconds; empty the rest of the time.

Never takes a click. It spans the stage, and a burst that swallows a tap while
someone is trying to press something underneath is worse than no burst at all.
*/
.room-celebrations {
  align-items: center;
  bottom: 10%;
  display: flex;
  /* Newest at the bottom, so a second gift's caption takes a row above the
     first's rather than printing on top of it. */
  flex-direction: column-reverse;
  gap: 0.5rem;
  inset-inline: 0;
  pointer-events: none;
  position: absolute;
  z-index: var(--z-celebration);
}

/* In immersive the chat panel sits over the bottom of the stage, so the
   caption stays up where it clears the tip form and composer; the emoji
   alone start lower, down among the chat lines, and fly up past it. */
.room[data-layout="immersive"] .room-celebrations {
  bottom: 25%;
}

.room[data-layout="immersive"] .room-celebration-burst {
  bottom: -15vh;
}

/*
One gift landing (gifts/_celebration). The emoji float up from near the
bottom of the stage and fade; the caption with the sender's name sits still
beneath them and fades last. Each celebration takes a row of the layer (its
caption's height) and the burst is absolute within it, so two gifts in a row
keep their captions apart while their flights overlap into a shower. --drift,
set per gift in the markup, leans the whole flight left or right; --spread is
how long the launches go on, so the caption stays for the last one.
*/
.room-celebration {
  align-items: center;
  display: flex;
  flex-direction: column;
  pointer-events: none;
  position: relative;
  width: 100%;
}

/* The starting rows: nearly as wide as the stage, the emoji shoulder to
   shoulder and centered, so four sit in a tight cluster and fifty fill a row
   and wrap into the next one above it (wrap-reverse: the first row is the
   bottom one). The rows are as wide as the stage allows, not a fixed count,
   so a phone gets fewer per row than a laptop. */
.room-celebration-burst {
  bottom: 2.5rem;
  display: flex;
  flex-wrap: wrap-reverse;
  justify-content: center;
  position: absolute;
  width: 90%;
}

/* Each emoji leaves a beat after the last (--delay, set per emoji in the
   markup) and lands a little to one side (--x), so a row reads as a burst
   and the rows together as a shower rather than one sheet lifting off. */
.room-celebration-emoji {
  animation: room-celebration-float 3s ease-out forwards;
  animation-delay: var(--delay, 0ms);
  display: block;
  font-size: 2.5rem;
  line-height: 1;
  opacity: 0;
  text-shadow: 0 2px 8px rgb(0 0 0 / 40%);
}

.room-celebration-large .room-celebration-emoji {
  animation-duration: 4s;
  font-size: 3.5rem;
}

/* An uploaded face in the emoji's place (an image, a frame of a series, or
   a short video): the same flight, at the emoji's height, the width
   following the file. */
.room-celebration-asset {
  height: 2.5rem;
  object-fit: contain;
  width: auto;
}

.room-celebration-large .room-celebration-asset {
  height: 3.5rem;
}

/*
A full-screen gift: one video over everything, chat included, for the biggest
gifts. Fixed to the viewport rather than the stage on purpose, and it still
takes no clicks. Removed when the video ends, so the caption just sits.
*/
.room-celebration-fullscreen {
  align-items: center;
  display: flex;
  flex-direction: column;
  inset: 0;
  justify-content: flex-end;
  padding-bottom: 15%;
  pointer-events: none;
  position: fixed;
  z-index: var(--z-celebration);
}

.room-celebration-fullscreen-video {
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

.room-celebration-fullscreen .room-celebration-caption {
  animation: none;
  position: relative;
}

.room-celebration-caption {
  animation: room-celebration-caption calc(3.5s + var(--spread, 0ms)) ease-out forwards;
  background: rgb(0 0 0 / 55%);
  border-radius: var(--bs-border-radius-pill);
  bottom: 0;
  color: #fff;
  display: block;
  font-size: 0.875rem;
  padding: 0.25rem 0.75rem;
  text-align: center;
  white-space: nowrap;
}

@keyframes room-celebration-float {
  0% {
    opacity: 0;
    transform: translate(0, 0) scale(0.6);
  }

  12% {
    opacity: 1;
    transform: translate(0, -1rem) scale(1.1);
  }

  100% {
    opacity: 0;
    transform: translate(calc(var(--x, 0) * 2rem + var(--drift) * 6rem), -55vh) scale(1);
  }
}

@keyframes room-celebration-caption {
  0% {
    opacity: 0;
    transform: translateY(0.5rem);
  }

  10%,
  75% {
    opacity: 1;
    transform: translateY(0);
  }

  100% {
    opacity: 0;
    transform: translateY(0);
  }
}

/* Vestibular safety: no flight, just a fade in place. */
@media (prefers-reduced-motion: reduce) {
  .room-celebration-emoji {
    animation: room-celebration-caption 3s ease-out forwards;
  }
}

/*
A tip in the chat log. A tint on the row so it stands out when scrolling
back, on both the page colors and the immersive scrim; the tokens are bold
in the markup.
*/
.chat-tip {
  border-radius: var(--bs-border-radius);
  /* Indented on the left only: padding on the right would pull the delete
     control out of line with every other row's. */
  padding: 0.25rem 0 0.25rem 0.75rem;
}

/* A tip line lands with a tint and lets it go: the arrivals controller marks
   lines that arrive by broadcast, so the archive's old tips do not all flash
   on load, and takes the mark off again when the animation ends, so the
   line is an ordinary line afterward. The tokens stay bold, which is enough
   for scrolling back.

   The colors are an .alert-success's, background and text both, so a
   landing tip and the "Sent" alert on the Tips tab read as one event, and
   the text keeps its contrast on the green in either color mode. The text
   fades back to the log's own color (--chat-line-color, set per layout
   below), links included. */
.chat-tip.chat-arrived {
  animation: chat-tip-land 4s ease-out forwards;
}

/* Weighted to beat the immersive rule that paints every link in the log
   white, which would otherwise win over this one. */
.room-chat-log .chat-tip.chat-arrived a {
  color: inherit;
}

@keyframes chat-tip-land {
  0%,
  40% {
    background: var(--bs-success-bg-subtle);
    color: var(--bs-success-text-emphasis);
  }

  100% {
    background: transparent;
    color: var(--chat-line-color);
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat-tip.chat-arrived {
    animation: none;
    background: var(--bs-success-bg-subtle);
    color: var(--bs-success-text-emphasis);
  }
}

/*
The gift sits on the first line of text, centered on it: the box is exactly
one line tall (1.5em, the body line-height) and centers the icon in it, so
it lines up with the avatar and the x-height of the name rather than with
the descenders. The row itself aligns items to the top, so this stays on
the first line when a message wraps.
*/
.chat-tip-mark {
  align-items: center;
  display: flex;
  flex: 0 0 auto;
  height: 1.5em;
  /* The avatar sits a touch above the line's center (the icon helper's
     vertical-align pulls text down around it), so this follows it up. */
  margin-top: -0.125rem;
}

/*
Tipping the streamer. Takes clicks back from the chat layer, which passes them
through, and its balance line gets the same white-on-scrim treatment the rest
of the chat panel gets in immersive.
*/
/*
The streamer's wishlist as viewers see it: a total bar and one small bar per
line, on the Wishlist tab of the header's gift panel (rooms/_gifts_panel).
The owner's Wishlist tab holds the editor instead.
*/
.room-wishlist {
  font-size: 0.8125rem;
}

.room-wishlist-heading {
  align-items: baseline;
  display: flex;
  justify-content: space-between;
}

/* The tokens total is the streamer's business: viewers get the bars and the
   counts, not the money. The block is one broadcast for the whole room, so
   the owner's page is told apart here (data-owner on .room) rather than
   rendered differently. */
.room-wishlist-total {
  display: none;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

.room[data-owner] .room-wishlist-total {
  display: inline;
}

.room-wishlist-bar {
  height: 0.375rem;
  margin: 0.25rem 0 0.375rem;
}

/*
The streamer's goal, above their wishlist editor: one row of label, field,
and buttons, then the progress line and bar in the wishlist's clothes. A
hairline under it keeps it apart from the list, which is everyone's.
*/
.room-goal {
  border-bottom: 1px solid var(--bs-border-color);
  font-size: 0.8125rem;
  margin-bottom: 0.5rem;
  padding-bottom: 0.5rem;
}

.room-goal-form {
  align-items: center;
  display: flex;
  gap: 0.5rem;
}

.room-goal-field {
  align-items: center;
  display: flex;
  flex: 1 1 auto;
  gap: 0.5rem;
  margin: 0;
}

.room-goal-label {
  font-weight: 600;
}

.room-goal-total {
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

.room-wishlist-items {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.room-wishlist-item {
  align-items: center;
  display: grid;
  gap: 0.375rem;
  grid-template-columns: auto auto auto 1fr;
}

.room-wishlist-item-done {
  opacity: 0.7;
}

.room-wishlist-count {
  font-variant-numeric: tabular-nums;
}

/* One gift's line in the owner's editor: the label row, then its bar. */
.room-wishlist-edit {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

/* Stays a flex box like Bootstrap's .progress: the bar inside is sized by
   the flex row, and a block here left it with no height at all. */
.room-wishlist-item-bar {
  display: flex;
  height: 0.25rem;
}

/*
The gifts a room offers, on the Gifts tab of the header's gift panel: a grid
of tiles, three to a row, each a third of the panel's width. Each gift is
its own tiny form (see gifts/_menu), so the buttons lay out as the grid's
children. minmax(0, 1fr) so a long name cannot widen its column.
*/
.room-gifts {
  display: grid;
  gap: 0.375rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* A group's heading in the viewer's tab: "On the wishlist", "Everything
   else". Small and quiet, with air above the second one. */
.room-gifts-group {
  font-size: 0.75rem;
  margin: 0 0 0.25rem;
  opacity: 0.75;
}

.room-gifts + .room-gifts-group {
  margin-top: 0.75rem;
}

/* A wished-for gift's tile carries its wish: the count under the price and
   a thin bar across the tile's foot, in a span the room replaces by id as
   gifts land, which has to lay out like the button's other children. */
.room-gift-wish-state {
  align-items: center;
  display: flex;
  flex-direction: column;
  width: 100%;
}

.room-gift-wish {
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  margin-top: 0.125rem;
}

.room-gift-wish-bar {
  margin-top: 0.25rem;
  width: 100%;
}

.room-gift-button {
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 0;
  line-height: 1.1;
  padding: 0.25rem 0.5rem;
  width: 100%;
}

/* A name longer than its tile is cut, not wrapped: the row stays one line
   tall and the title attribute has the whole of it. */
.room-gift-name,
.room-gift-confirm {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.room-gift-emoji {
  font-size: 1.375rem;
}

.room-gift-name,
.room-gift-confirm {
  font-size: 0.75rem;
}

.room-gift-price {
  font-size: 0.6875rem;
  opacity: 0.75;
}

/* Armed by a first tap (gift-confirm): the name gives way to "Send now" and
   the button lights up, so it is plain which one is about to cost tokens. */
.room-gift-confirm {
  display: none;
}

.room-gift-button[data-armed] {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
  color: var(--bs-white);
}

.room-gift-button[data-armed] .room-gift-name {
  display: none;
}

.room-gift-button[data-armed] .room-gift-confirm {
  display: block;
}

/*
LAYER 40 — the composer, always on the floor of the chat panel however much
or little is above it. Takes clicks back from the chat layer, which passes
them through so the video stays reachable in immersive.
*/
.room-composer {
  flex: 0 0 auto;
  pointer-events: auto;
}

.room-composer-note {
  font-size: 0.875rem;
  margin: 0;
  padding: 0.5rem 0.75rem;
}

/*
One row by default, growing with the text where the browser supports it
(Chrome, Safari 26.2+, Firefox mid-2026) and simply scrolling where it does
not. Capped so a pasted essay cannot eat the panel. No Bootstrap equivalent —
there are no field-sizing or resize utilities.
*/
.chat-composer {
  field-sizing: content;
  max-height: 8rem;
  resize: none;
}

/*
Shift+Enter puts real newlines in a message, and HTML would collapse them.
Bootstrap has text-wrap and text-break but no white-space utility.
*/
.chat-content {
  white-space: pre-wrap;
}
