.room-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.5rem;
  border-bottom: 1px solid var(--tilt-border);
  flex-wrap: wrap;
}

.room-header h1 {
  font-family: var(--tilt-font-display);
}

.room-meta {
  color: var(--tilt-text-muted);
  font-size: 0.875rem;
}

.room-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.room-layout {
  display: grid;
  grid-template-columns: 1fr 20rem;
  gap: 1.5rem;
  align-items: start;
  padding: 1.5rem;
  max-width: 72rem;
  margin: 0 auto;
}

/* Cinema rooms (js/ui/room-page.js toggles this class + physically moves
   #now-playing-panel into #cinema-video-slot — grid-area only rearranges
   direct children, and the player panel normally lives nested inside the
   sidebar's queue section, not at this level): video large and on the
   left, chat compact and top-right, members/queue/events below it —
   "chat box smaller and to the right" without losing the room's other
   panels. Not capped at 72rem — the video benefits from the extra width,
   especially with the personal sidebar now reclaimed as an overlay
   (css/shell.css) instead of a permanent column. */
.room-layout--cinema {
  max-width: 100%;
  grid-template-columns: 1fr 22rem;
  grid-template-rows: auto 1fr;
  grid-template-areas: "video chat" "video sidebar";
}

.room-layout--cinema .cinema-video-slot {
  grid-area: video;
}

.room-layout--cinema .room-chat {
  grid-area: chat;
  height: 16rem;
}

.room-layout--cinema .room-sidebar {
  grid-area: sidebar;
}

/* Video size toggle (cinema rooms only, js/ui/room-page.js) — three
   discrete sizes rather than a free-drag resizer: simpler to build, and
   a drag handle isn't meaningfully more useful here than three presets.
   Persisted per-viewer in localStorage, not synced to other members —
   this is a personal display preference like a player's own size/theater
   toggle, not shared room state. Medium is the plain .room-layout--cinema
   rule above; small/large below are the only two that need overrides. */
.cinema-size-toggle:not([hidden]) {
  display: flex;
  gap: 0.4rem;
  padding: 0 1.5rem 1rem;
}

.cinema-size-toggle button {
  padding: 0.35rem 0.75rem;
  font-size: 0.7rem;
}

.cinema-size-toggle button[aria-pressed="true"] {
  background: var(--tilt-accent);
  color: var(--tilt-accent-contrast);
}

/* Sizes only change the video's HEIGHT — the grid (chat pinned to the
   right column) stays identical across all three, no column/area
   overrides here. Overriding aspect-ratio with an explicit height:
   YouTube's iframe letterboxes internally when given a non-16:9 box (no
   distortion), and the uploaded-video path gets object-fit: contain for
   the same reason — see the shared rule below. */
.room-layout--cinema.cinema-size-small .cinema-video-slot:not([hidden]) {
  min-height: 12rem;
}

.room-layout--cinema.cinema-size-small .now-playing-player iframe,
.room-layout--cinema.cinema-size-small .now-playing-player video {
  aspect-ratio: auto;
  height: 12rem;
}

.room-layout--cinema.cinema-size-large .cinema-video-slot:not([hidden]) {
  min-height: 34rem;
}

.room-layout--cinema.cinema-size-large .now-playing-player iframe,
.room-layout--cinema.cinema-size-large .now-playing-player video {
  aspect-ratio: auto;
  height: 34rem;
}

.now-playing-player video {
  object-fit: contain;
}

/* DJ rooms: the big chat sits up top next to a slim members+queue column,
   with the full-width mixing desk (js/ui/room-page.js physically moves
   #mixing-desk into #dj-desk-slot, a direct grid child — same
   move-a-descendant-into-a-named-area trick as cinema's video slot above)
   spanning the width underneath it. The sidebar column runs the full
   height of both rows (named in both grid-template-areas rows), so
   members/queue stay visible next to the desk too, not just next to
   chat. */
.room-layout--dj {
  max-width: 100%;
  grid-template-columns: 1fr 20rem;
  grid-template-rows: auto auto;
  grid-template-areas: "chat sidebar" "desk sidebar";
}

.room-layout--dj .room-chat {
  grid-area: chat;
  height: 30rem;
}

.room-layout--dj .dj-desk-slot {
  grid-area: desk;
}

.room-layout--dj .room-sidebar {
  grid-area: sidebar;
}

/* :not([hidden]) — same specificity fight as .deck-load-panel/.reply-preview
   /#chat-form/.stack-form elsewhere in this file. Found while adding
   #dj-desk-slot right next to this: without the guard, a plain
   .cinema-video-slot rule's display:flex beat the UA [hidden] rule and
   this showed as an empty 20rem-tall black box above the chat in every
   non-cinema room, hidden attribute notwithstanding. Confirmed via
   getComputedStyle before and after this fix, not just visual read. */
.cinema-video-slot:not([hidden]) {
  background: #000;
  border-radius: var(--tilt-radius);
  overflow: hidden;
  min-height: 20rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.cinema-video-slot .now-playing-panel {
  width: 100%;
  margin: 0;
  border: none;
  border-radius: 0;
  background: none;
}

.room-chat {
  display: flex;
  flex-direction: column;
  height: 32rem;
  background: var(--tilt-surface);
  border: 1px solid var(--tilt-border);
  border-radius: var(--tilt-radius);
  overflow: hidden;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.chat-message {
  max-width: 80%;
}

.chat-message .author {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--tilt-text-muted);
}

.chat-message .body {
  background: var(--tilt-bg);
  border-radius: calc(var(--tilt-radius) / 2);
  padding: 0.4rem 0.6rem;
  word-break: break-word;
}

.chat-message.is-pinned .body {
  border: 1px solid var(--tilt-accent);
}

/* A quoted preview of the message being replied to, shown above the
   reply's own body — same idiom Discord/Slack/iMessage use for threading
   without a full nested-tree UI. */
.chat-message .reply-context {
  font-size: 0.75rem;
  color: var(--tilt-text-muted);
  border-left: 2px solid var(--tilt-border);
  padding-left: 0.5rem;
  margin-bottom: 0.2rem;
  overflow-wrap: anywhere;
}

.chat-message-actions {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.2rem;
}

.chat-message-actions button {
  font-size: 0.7rem;
  padding: 0.15rem 0.4rem;
  text-transform: none;
  background: transparent;
  border: 1px solid var(--tilt-border);
  color: var(--tilt-text-muted);
  box-shadow: none;
}

.chat-message-actions button:hover {
  color: var(--tilt-accent);
  border-color: var(--tilt-accent);
  filter: none;
  transform: none;
}

/* The "hero board" — pinned messages, shown above the live chat log. A
   bounded, scrollable mini-list is the normal/expected pattern here (like
   Discord's own pinned-messages panel) — NOT the tile-content-clipping
   antipattern fixed on profile.html: this is a small, deliberately-scoped
   auxiliary list, not a tile whose main content got squeezed into too
   little space. */
.pinned-board {
  padding: 0.6rem 1rem;
  border-bottom: 1px solid var(--tilt-border);
  background: color-mix(in srgb, var(--tilt-accent) 8%, transparent);
}

.pinned-board-label {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--tilt-text-muted);
  margin: 0 0 0.4rem;
}

#pinned-list {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  max-height: 8rem;
  overflow-y: auto;
}

.pinned-item {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  font-size: 0.8rem;
}

.pinned-item .author {
  font-weight: 600;
  color: var(--tilt-text-muted);
  flex-shrink: 0;
}

.pinned-item .body {
  overflow-wrap: anywhere;
}

/* :not([hidden]), not a plain element selector — a bare .reply-preview
   rule (specificity 0-1-0) would beat the UA stylesheet's [hidden] {
   display: none } (also 0-1-0: author-stylesheet rules always win ties
   over user-agent ones) and keep this visible even with the hidden
   attribute set. Same bug class already hit and fixed elsewhere this
   session (css/editor.css's form label, css/editor.css's
   .gradient-editor) — :not([hidden]) sidesteps the specificity fight. */
.reply-preview:not([hidden]) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.4rem 0.75rem;
  background: var(--tilt-bg);
  border-top: 1px solid var(--tilt-border);
  font-size: 0.8rem;
  color: var(--tilt-text-muted);
}

/* :not([hidden]) — same specificity fight as .reply-preview above:
   #chat-form is toggled via the hidden attribute (js/ui/room-page.js's
   setupChat()) for non-members, and a bare #chat-form rule (an ID
   selector, no less) would always beat the UA [hidden] rule regardless. */
/* #thread-form (js/ui/messages-page.js's DM composer) shares this exact
   look — same guard, same reason: a bare selector's display:flex would
   otherwise beat the UA [hidden] rule. */
#chat-form:not([hidden]),
#thread-form:not([hidden]) {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem;
  border-top: 1px solid var(--tilt-border);
}

#chat-form textarea,
#thread-form textarea {
  flex: 1;
  resize: none;
  padding: 0.5rem;
  border: 1px solid var(--tilt-border);
  border-radius: calc(var(--tilt-radius) / 2);
  background: var(--tilt-bg);
  color: var(--tilt-text);
}

.room-sidebar {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.room-sidebar section {
  background: var(--tilt-surface);
  border: 1px solid var(--tilt-border);
  border-radius: var(--tilt-radius);
  padding: 1rem;
}

/* :not([hidden]) — same specificity fight as #chat-form/.reply-preview
   above: #queue-form/#event-form (both .stack-form) are toggled via the
   hidden attribute based on room role (js/ui/room-page.js's
   renderQueue()/renderEvents()), and a bare .stack-form rule would always
   beat the UA [hidden] rule regardless of role. */
.stack-form:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.stack-form input,
.stack-form select {
  padding: 0.4rem;
  border: 1px solid var(--tilt-border);
  border-radius: calc(var(--tilt-radius) / 2);
  background: var(--tilt-bg);
  color: var(--tilt-text);
}

.now-playing-panel {
  background: var(--tilt-bg);
  border: 1px solid var(--tilt-border);
  border-radius: var(--tilt-radius);
  padding: 0.75rem;
  margin-bottom: 0.75rem;
}

.now-playing-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--tilt-text-muted);
}

#now-playing-title {
  font-weight: 600;
  margin: 0.25rem 0 0.5rem;
}

/* The actual embed (js/rooms/synced-player.js) — a YouTube iframe fills
   its own 16:9 box; SoundCloud's widget sets its own fixed height. Both
   get a consistent rounded frame regardless of which one's mounted. */
.now-playing-player {
  margin-bottom: 0.5rem;
}

.now-playing-player iframe,
.now-playing-player video {
  width: 100%;
  border: none;
  border-radius: calc(var(--tilt-radius) / 2);
  aspect-ratio: 16 / 9;
  background: #000;
}

/* SoundCloud's widget sets its own height attribute (166px) — fighting
   that with aspect-ratio would squash it, so only YouTube's iframe/the
   uploaded-video <video> element (neither has such an attribute) get the
   16:9 treatment above; this override keeps SoundCloud's own sizing
   intact. */
.now-playing-player iframe[height] {
  aspect-ratio: auto;
}

/* Spotify/"other" — no embed exists (a real API limitation, not a bug —
   see synced-player.js's file comment), so this is what "now playing"
   looks like for those instead. */
.synced-player-metadata-card {
  background: var(--tilt-bg);
  border: 1px dashed var(--tilt-border);
  border-radius: calc(var(--tilt-radius) / 2);
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.synced-player-provider-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--tilt-text-muted);
}

.synced-player-title {
  font-weight: 600;
}

.media-controls {
  display: flex;
  gap: 0.5rem;
}

.media-controls button {
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  font-size: 1rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Mixing desk (DJ rooms, js/ui/room-page.js) — a set of two decks either
   side of a crossfader, styled like actual hardware: a dark desk surface,
   each deck a raised panel with a spinning "vinyl" that only turns while
   that deck is actually playing. No audio is actually mixed server-side
   (see js/rooms/synced-player.js's file comment) — both decks' embeds
   play simultaneously in every listener's own browser, with the
   crossfader just setting each one's individual volume there. Lives
   full-width below the chat in DJ rooms (#dj-desk-slot, a direct grid
   child — see .room-layout--dj above), so this can be roomy rather than
   squeezed into a sidebar. */
/* :not([hidden]) — same specificity fight as .cinema-video-slot above:
   #mixing-desk is only ever shown for DJ rooms (renderQueue() toggles its
   hidden attribute), and a bare .mixing-desk rule's display:grid would
   otherwise beat the UA [hidden] rule and show it in every room type. */
.mixing-desk:not([hidden]) {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 1rem;
  align-items: start;
  padding: 1.25rem;
  background: linear-gradient(180deg, #1a1420, #0f0c14);
  border: 1px solid var(--tilt-border);
  border-radius: var(--tilt-radius);
}

.deck {
  background: var(--tilt-bg);
  border: 1px solid var(--tilt-border);
  border-radius: var(--tilt-radius);
  padding: 0.85rem;
}

.deck-label {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--tilt-accent);
}

/* The clickable "face" of the deck — vinyl + title — is how you load or
   swap a track on it (js/ui/room-page.js's wireDeckFace()). A plain
   <button>, not a div-with-onclick, so it's keyboard-reachable and
   properly announced; :disabled (non-DJ members) still shows the same
   content, just without the pointer/hover affordance. */
.deck-face {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.4rem;
  margin: 0.4rem 0;
  background: transparent;
  border: none;
  box-shadow: none;
  text-align: left;
  color: inherit;
  cursor: pointer;
}

.deck-face:disabled {
  cursor: default;
}

.deck-face:not(:disabled):hover {
  filter: none;
  transform: none;
  background: color-mix(in srgb, var(--tilt-accent) 8%, transparent);
  border-radius: calc(var(--tilt-radius) / 2);
}

@keyframes room-deck-spin {
  to {
    transform: rotate(360deg);
  }
}

.deck-vinyl {
  flex-shrink: 0;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: radial-gradient(circle at center, var(--tilt-bg) 0 18%, #111 19% 30%, #000 31% 100%);
  border: 2px solid var(--tilt-border);
  animation: room-deck-spin 2.4s linear infinite;
  animation-play-state: paused;
}

/* Only spins while the deck actually has a track loaded (the container
   only carries an embed/player once syncDeck() loads one) — a plain
   sibling-combinator check, no extra JS state needed just for this. */
.deck:has(.now-playing-player:not(:empty)) .deck-vinyl {
  animation-play-state: running;
}

.deck-title {
  font-weight: 600;
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}

/* :not([hidden]) — same specificity fight already fixed elsewhere in this
   file (.reply-preview, #chat-form, .stack-form): a bare .deck-load-panel
   rule (0-1-0) ties the UA stylesheet's [hidden] { display: none } (also
   0-1-0), and author rules win ties — so without this guard the panel
   would stay visible despite the hidden attribute wireDeckFace() toggles. */
.deck-load-panel:not([hidden]) {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--tilt-border);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.deck-load-panel label {
  font-size: 0.75rem;
  color: var(--tilt-text-muted);
}

.deck-load-panel select,
.deck-load-panel input[type="file"] {
  padding: 0.4rem;
  border: 1px solid var(--tilt-border);
  border-radius: calc(var(--tilt-radius) / 2);
  background: var(--tilt-bg);
  color: var(--tilt-text);
}

.deck-load-divider {
  align-self: center;
  font-size: 0.7rem;
  color: var(--tilt-text-muted);
}

.crossfader-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  align-self: center;
  padding-top: 2rem;
  min-width: 6rem;
}

.crossfader-wrap label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--tilt-text-muted);
  text-align: center;
}

.crossfader-wrap input[type="range"] {
  width: 100%;
}

.crossfader-labels {
  display: flex;
  justify-content: space-between;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--tilt-text-muted);
}

/* Per deck gain/EQ/filter/pitch (js/ui/room-page.js, migration 056).
   Plain range inputs, matching the crossfader above, not a custom knob
   widget (this app has none anywhere). Always rendered, even for a
   provider that can't actually support a given control. See the
   :disabled rule below, which is what makes an unsupported knob read as
   "not available for this track" rather than the panel looking broken or
   incomplete. */
.deck-fx {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--tilt-border);
}

.deck-fx-row {
  display: grid;
  grid-template-columns: 3rem 1fr;
  align-items: center;
  gap: 0.5rem;
}

.deck-fx-row label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--tilt-text-muted);
}

.deck-fx-row input[type="range"]:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

@media (max-width: 40rem) {
  .mixing-desk {
    grid-template-columns: 1fr;
  }

  .crossfader-wrap {
    padding-top: 0;
  }
}

/* Consumes the --page-* custom properties set by js/theme/apply-theme.js,
   the same variable contract css/themes.css defines for profile pages —
   rooms just have their own selectors here since the markup is different
   (chat/sidebar, not tiles). See DECISIONS.md for why rooms.theme is one
   jsonb column (config + customCss together) rather than a separate table
   like profile_themes. */

.tilt-page.room-content,
#main.tilt-page {
  background-color: var(--page-bg-color, var(--tilt-bg));
  background-image: var(--page-bg-image, none);
  background-size: var(--page-bg-size, auto);
  background-repeat: var(--page-bg-repeat, no-repeat);
  background-position: var(--page-bg-position, center);
  color: var(--page-text, var(--tilt-text));
  font-family: var(--page-font-body, var(--tilt-font-body));
}

.tilt-page .room-chat,
.tilt-page .room-sidebar section,
.tilt-page .chat-message .body,
.tilt-page .room-theme-preview {
  background: color-mix(in srgb, var(--page-surface, var(--tilt-surface)) calc(var(--page-tile-alpha, 1) * 100%), transparent);
  border-color: var(--page-border, var(--tilt-border));
  border-radius: var(--page-tile-radius, var(--tilt-radius));
  box-shadow: var(--page-tile-shadow, none);
}

.tilt-page .room-header h1,
.tilt-page .room-sidebar h2 {
  font-family: var(--page-font-display, var(--tilt-font-display));
}

.tilt-page a {
  color: var(--page-accent, var(--tilt-accent));
}

.room-theme-preview {
  padding: 1.5rem;
  border: 1px dashed var(--tilt-border);
  border-radius: var(--tilt-radius);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

@media (max-width: 60rem) {
  .room-layout {
    grid-template-columns: 1fr;
  }

  .room-chat {
    height: 24rem;
  }

  /* Cinema/DJ modes each set their own grid-template-areas (see the rules
     above), which still forces a two-column layout here even once
     grid-template-columns above is down to a single track. An areas
     string implying two columns extends the explicit column count to
     match, so without overriding it here too the video/desk and chat/
     sidebar panels stay squeezed side by side on a phone screen instead
     of actually stacking. */
  .room-layout--cinema {
    grid-template-columns: 1fr;
    grid-template-areas: "video" "chat" "sidebar";
  }

  .room-layout--cinema .room-chat {
    height: 24rem;
  }

  .room-layout--dj {
    grid-template-columns: 1fr;
    grid-template-areas: "chat" "desk" "sidebar";
  }

  .room-layout--dj .room-chat {
    height: 24rem;
  }
}
