/* Chat page styles — moved unchanged out of index.php's <style> block (modular pass, 2026-10-18).
   Loaded right after the theme stylesheet, where the block was, so the cascade is the same. */
/* Holiday effects layer (z-index in CSS, not inline, so the open Live stage can
   sit ABOVE it — voice.js drops it to 0 under #chat-column.rcv-stage-open.
   The New Year ball drop still raises it inline for its finale.) 2026-09-30 */
#holiday-canvas { z-index: 2; }

/* ⚠️ HERE, not in a theme file.
   There are FIVE themes and this rule was added to ONE — so on any other theme
   the gift icon in a {gift: Name} token had no sizing at all and rendered at its
   natural 500px. Found 2026-09-11.

   Anything the CHAT emits, as opposed to anything a theme styles, belongs in
   this block.

   ⚠️ This block is served AFTER the theme stylesheet, so at equal specificity
   THIS rule wins and a theme cannot override it. That is deliberate here — the
   sizing is a correctness constraint, not a style choice — but it is worth
   knowing before putting anything else in this block that a theme might
   legitimately want to change. (Same cascade trap as .field / .field-rl on
   2026-09-05: the last declaration wins, whatever the intent.) */
/* ⚠️ TWO selectors, to beat `.msg-row img` / `.msg-content img` above, which set
   `height: auto` and are two classes deep. A single `.tok-gift-icon` loses on
   specificity however late it appears — the icon rendered at its natural 500px
   because of this, not because the rule was missing. Found 2026-09-11, and it
   is the THIRD cascade trap in this file (see .field / .field-rl). */
.msg-row img.tok-gift-icon,
.msg-content img.tok-gift-icon,
.tok-gift-icon {
  /* Sized in em so it tracks the message font, matched to the compact card
     icon (~22px). At 1.15em it was too small to recognise the gift, which is
     the point of showing it. */
  height: 1.6em;
  width: auto;
  vertical-align: -0.4em;
  margin-right: 5px;
  border-radius: 4px;
}

/* Mobile browsers auto-inflate ("boost") text to be readable, which makes the
   chat load looking zoomed-in on phones until you pinch it back. Pin text sizing
   to 100% so the page renders at its intended size on load. Theme-independent
   (lives here, not in the theme CSS) so it applies in every theme. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
/* Gift banners and the room welcome card must WRAP like normal chat text rather
   than run off both edges — an overflowing wide element is also what triggers
   iOS text autosizing in the first place. Theme-independent. */
/* .chat-card carries white-space/overflow-wrap for every card now; these are
   the gift card's inner text elements, which still need their own min-width:0
   so the icon's flex layout can't stop them shrinking. */
.card-headline, .card-text, .card-note {
  white-space: normal; overflow-wrap: anywhere; word-break: break-word; min-width: 0;
}
.live-gift-toast { max-width: 90vw !important; white-space: normal !important; text-align: center; }
.room-welcome-inner { max-width: min(480px, 92vw) !important; }
.room-welcome-title, .room-welcome-text { overflow-wrap: anywhere; word-break: break-word; }
@media (max-width: 600px) {
  .room-welcome-inner  { padding: 20px 22px !important; }
  .room-welcome-title  { font-size: 22px !important; }
}
/* Bottom-align chat messages with an auto-margin spacer instead of
   justify-content:flex-end. flex-end on a scrollable flex container mis-computes
   scroll height when children are removed — so pruning old messages (the DOM cap)
   left a phantom scroll gap where they used to be. margin-top:auto sticks
   messages to the bottom when few and scrolls cleanly when many. Overrides every
   theme (this <style> loads after the theme stylesheet). */
#messages { justify-content: flex-start; }
#messages > :first-child { margin-top: auto; }
/* Ticker relocated above the album art in the user list (phone drawer and
   tablet column — placeTicker(), modules/tiers.js) — force BOTH the box and the
   iframe to a small row height (an iframe defaults to ~150px if its height
   isn't pinned, which was pushing the album art way down). */
#user-list-panel #header-ticker {
  display: block !important; height: 32px !important; max-height: 32px !important;
  min-height: 0 !important; overflow: hidden !important;
  width: 100%; flex-shrink: 0; border-bottom: 1px solid var(--border);
}
#user-list-panel #header-ticker iframe,
#user-list-panel #ticker-iframe { height: 32px !important; max-height: 32px !important; display: block; }
@media (max-width: 600px) {
  /* Album art was rendering at full sidebar width (~240px) in the drawer,
     crushing the user list below it. Cap it so the list keeps room. */
  #user-list-panel #albumart-wrap { max-width: 150px !important; margin: 0 auto; }
  /* Only ONE album cover on mobile: hard-hide the header on-air image, and cap the
     drawer art image itself (not just its wrapper) so nothing renders full-width. */
  #onair-image-wrap, #onair-img { display: none !important; }
  #user-list-panel #albumArt { max-width: 150px !important; max-height: 150px !important; }
}
/* Live Goals header slot — sits between On Air image and the header
   action buttons. Takes available middle space; centers the widget
   horizontally. Stacks the slideshow widget on top of the dots row. */
#leaderboard-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 3px 10px; background: var(--bg-panel, #16181f);
  border-bottom: 1px solid var(--border, #262b35);
  font-size: 12px; white-space: nowrap; overflow: hidden;
}
#leaderboard-row .lb-section { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; overflow: hidden; }
#leaderboard-row .lb-gifters { justify-content: flex-end; }
#leaderboard-row .lb-icon { font-size: 14px; flex-shrink: 0; }
#leaderboard-row .lb-list { display: flex; align-items: center; gap: 6px; overflow: hidden; }
#leaderboard-row .lb-entry { display: inline-flex; align-items: center; gap: 2px; max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#leaderboard-row .lb-entry .lb-name { overflow: hidden; text-overflow: ellipsis; }
#leaderboard-row .lb-total {
  display: flex; align-items: center; gap: 4px; flex-shrink: 0;
  font-weight: 700; color: #ff6b8a; padding: 0 10px;
}
#live-goals-header {
  flex: 1;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 0 12px;
  max-width: 720px;
  margin: 0 auto;
}
/* Each slideshow is a column: the widget cell on top, its dots below.
   Hot Rooms sizes to its (compact) content; the Live Goal takes the
   remaining width — the goal row has far more content (icon, name,
   count, bar, %, room + contributor chips) and was getting clipped
   when the row was split evenly. A slot with an empty container is
   hidden by JS (#hot-rooms-slot) so the goal takes the full width. */
#hot-rooms-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  flex: 0 1 auto;
}
#live-goals-slot {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  flex: 1 1 auto;
}
/* During a battle the bar takes the Live Goal slot. Camera stage is never involved. */
#live-goals-header.battle-active .rcv-battle-bar { display: flex; }
/* Live Goals + Hot Rooms containers — slideshow grid: all widgets share one
   cell; only .lgw-active is visible. Container collapses to zero
   when empty so the header doesn't show an empty slot. */
#live-goals-container, #hot-rooms-container {
  display: grid;
  grid-template-columns: 1fr;
  width: 100%;
  position: relative;
}
#live-goals-container:empty, #hot-rooms-container:empty { display: none; }
#live-goals-container .live-goal-widget, #hot-rooms-container .live-goal-widget {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
}
#live-goals-container .live-goal-widget.lgw-active, #hot-rooms-container .live-goal-widget.lgw-active {
  opacity: 1;
  pointer-events: auto;
}
/* Portrait phone: no horizontal room for two slideshows side by side, so stack
   them (Hot Rooms on top, Live Goal below). Landscape phones have the width and
   keep the side-by-side row — the orientation guard is what preserves that.
   (Full mobile layout is still deferred to the PWA pass; this is a sane default.) */
/* ⚠️ On a portrait phone AND on tablet widths the two widgets LEAVE the header
   (moved by placeHeaderWidgets() in JS; the class is only set there) into their
   own row just above the top likes/gifts strip, side by side 50/50. The header
   then holds only the logo and the buttons. Phone: owner, 2026-09-16 (clears
   the camera notch, gives the height back to the stage). Tablet: owner,
   2026-10-03 (full width to read them). */
#live-goals-header.rc-widget-row {
  flex-direction: row; gap: 6px; max-width: 100%; width: 100%; margin: 0;
  padding: 3px 6px; background: var(--bg-panel); border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}
#live-goals-header.rc-widget-row #hot-rooms-slot,
#live-goals-header.rc-widget-row #live-goals-slot { flex: 1 1 50%; width: 50%; min-width: 0; }
/* Inline avatar inside chat message prefix — sits between role
   emoji and personal emoji per the chat layout spec. Sized to
   match the emoji baseline so the line height doesn't bump up.
   Round shape for the standard "person" reading. */
/* Chat text must wrap. Without this, one long unbroken token (a pasted URL,
   a run of spammed characters) widens the row and the whole chat scrolls
   sideways — worst on mobile, where the column is narrow.
   - overflow-wrap:anywhere breaks a long token only when it can't otherwise fit
   - min-width:0 lets flex/grid children actually shrink below their content
   - the scroller itself never scrolls sideways */
#chat-main {
  overflow-x: hidden;   /* never scroll sideways */
  overflow-y: auto;     /* explicit: setting only overflow-x would compute y to auto anyway */
}
/* Kill double-tap zoom across the whole chat, not just its container. touch-action
   is NOT inherited, so taps landing on message rows/text (children) would still
   zoom unless we set it on them too. manipulation keeps scroll + pinch. */
#chat-main, #chat-main * {
  touch-action: manipulation;
}
/* Heart Mode active: the ❤️ button glows/pulses so it's clear you're in it and
   tapping the chat sends hearts. Tap it again to exit. */
#like-btn.heart-mode-on {
  background: rgba(224, 48, 64, 0.9);
  box-shadow: 0 0 0 3px rgba(224,48,64,.35), 0 2px 12px rgba(224,48,64,.6);
  animation: heart-mode-pulse 1.1s ease-in-out infinite;
}
@keyframes heart-mode-pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(224,48,64,.35), 0 2px 12px rgba(224,48,64,.6); }
  50%      { box-shadow: 0 0 0 6px rgba(224,48,64,.18), 0 2px 16px rgba(224,48,64,.8); }
}
/* ── Mobile users drawer ─────────────────────────────────────────────
   On phones the online-users panel (with album art) slides in from the right
   instead of taking a side column, so the chat gets full width. Toggled by the
   👥 header button (RC.toggleMobileUsers → .mobile-open) with a tap-away backdrop
   (#users-overlay). This is structural, not thematic, so it lives here in
   index.php (loaded after the theme, so it overrides the per-theme mobile
   side-panel rules) and works across all themes. Desktop keeps the normal sidebar. */
@media (max-width: 600px) {
  #user-list-panel {
    position: fixed !important;
    top: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    left: auto !important;
    width: 80% !important;
    max-width: 320px !important;
    min-width: 0 !important;
    transform: translateX(100%) !important;   /* off-screen until opened */
    transition: transform .25s ease;
    z-index: 7500;   /* above chat content, BELOW modals (8000+) so invite/profile/etc. open on top */
    box-shadow: -6px 0 24px rgba(0,0,0,.55);
    border-left: 1px solid var(--border);
    overflow-y: auto !important;
  }
  #user-list-panel.mobile-open { transform: translateX(0) !important; }
  /* Direct one-tap access: show the 👥 button in the header on mobile (the theme
     hides it). Sits next to ☰ and 💬; opens the drawer on the right directly. */
  #btn-users-mobile { display: flex !important; }
  /* Album art + online count ride along in the drawer (the theme hides them for
     the old cramped side panel). */
  #albumart-wrap { display: block !important; max-width: 100% !important; }
  #albumArt      { max-width: 100% !important; max-height: 60vw !important; }
  #online-count  { display: block !important; }
}
/* User context menu must sit above the users drawer (7500) in EVERY theme. Only
   the default theme sets a z-index on .ctx-menu; the others left it unset, so the
   menu opened UNDER the drawer. Pin it here (theme-independent) above the drawer. */
.ctx-menu { z-index: 9000; }
/* Mobile size cap for the WAVE only. The wave lays out many icons across the
   width and was composed at a fixed 300px, so it crowds a phone — cap it to fit.
   Other styles (hero, explosion, snap) already size themselves responsively in
   vmin by tier (hero tier 5 = 90vmin, meant to fill the screen), so they must NOT
   be capped — a flat cap on every image is what made the hero tiny. Desktop
   (≥1000px) keeps the wave at 300px. */
@media (max-width: 1000px) {
  img.gift-wave-img {
    max-width: 18vmin !important;
    max-height: 18vmin !important;
  }
}
#messages,
.msg-row {
  max-width: 100%;
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.msg-content,
.msg-prefix {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Long links are the most common offender. */
.msg-content a {
  overflow-wrap: anywhere;
  word-break: break-all;
}
/* Embedded media should never exceed the column. */
.msg-row img,
.msg-row video,
.msg-content img {
  max-width: 100%;
  height: auto;
}
/* Preformatted/code blocks wrap rather than scroll the page. */
.msg-content pre,
.msg-content code {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Gift cards carry user-supplied text too: the sender/recipient names in the
   headline and the optional note. The card sits in a .msg-row so it inherits
   the row rules, but the inner text column needs min-width:0 or the icon's
   flex layout keeps it from shrinking, and the note is its own element. */
.card-text {
  min-width: 0;
  overflow-wrap: anywhere;
}
.card-headline,
.card-meta,
.card-note {
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.card-icon {
  max-width: 100%;
  flex-shrink: 0;
}

.msg-avatar {
  display: inline-block;
  width: 18px;
  height: 18px;
  object-fit: cover;
  border-radius: 50%;
  vertical-align: -3px;
  margin: 0 1px;
}

/* Subscription banner — flashing blue/purple takeover when a sub
   badge is purchased. Sits in the same slot as the live-goals
   widget; while visible, the live-goals-container is hidden via
   JS so they don't overlap. Animation alternates color + a subtle
   glow pulse on a tight cadence for that "look at me" feel. */
/* Slideshow position dots — small horizontal row under the widget,
   only shown when there's more than one active goal. */
.lgw-dots {
  display: flex;
  justify-content: center;
  gap: 5px;
  height: 6px;
}
.lgw-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255,255,255,0.25);
  transition: background 0.3s ease, transform 0.3s ease;
}
.lgw-dot.lgw-dot-active {
  background: var(--accent, #FFD700);
  transform: scale(1.4);
}

/* Live Goal widget — compact horizontal layout for header. Single
   row: icon, name+progress, mini bar, percent. Designed to fit in a
   ~32px-tall header slot without crowding the surrounding controls. */
.live-goal-widget {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  background: linear-gradient(135deg, rgba(255,215,0,0.10), rgba(255,255,255,0.04));
  border: 1px solid var(--accent, #FFD700);
  border-radius: 14px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.3), 0 0 8px rgba(255,215,0,0.12);
  font-size: 12px;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  min-width: 0;
  max-width: 100%;
}
.live-goal-widget.lgw-complete {
  border-color: #4caf50;
  box-shadow: 0 1px 4px rgba(0,0,0,0.3), 0 0 12px rgba(76,175,80,0.4);
  animation: lgw-complete-pulse 2s ease-in-out;
}
@keyframes lgw-complete-pulse {
  0%, 100% { box-shadow: 0 1px 4px rgba(0,0,0,0.3), 0 0 12px rgba(76,175,80,0.4); }
  50%      { box-shadow: 0 1px 4px rgba(0,0,0,0.3), 0 0 18px rgba(76,175,80,0.7); }
}
.live-goal-widget .lgw-icon {
  width: 22px; height: 22px;
  object-fit: contain;
  border-radius: 3px;
  flex-shrink: 0;
}
.live-goal-widget .lgw-label {
  font-weight: 700;
  color: var(--text-primary, #f0f0f4);
  flex-shrink: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.live-goal-widget .lgw-progress {
  color: var(--text-muted, #d0d0d8);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.live-goal-widget .lgw-percent {
  font-weight: 900;
  color: var(--accent, #FFD700);
  text-shadow: 0 0 4px rgba(255,215,0,0.4);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
/* Latest-contribution chips — sit at the right of the gift row and
   show where the most recent gift came from + who sent it. Pill
   styling matches the legacy .lgw-room-chip so it feels native.
   Each chip hides via display:none when there are no contributions
   yet on a fresh goal. */
.live-goal-widget .lgw-room {
  display: inline-block;
  padding: 2px 8px;
  font-size: 11px;
  color: var(--text-primary, #f0f0f4);
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,215,0,0.3);
  border-radius: 10px;
  white-space: nowrap;
  flex-shrink: 0;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.live-goal-widget .lgw-contributor {
  display: inline-block;
  padding: 2px 8px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary, #f0f0f4);
  background: rgba(76, 175, 80, 0.12);
  border: 1px solid rgba(76, 175, 80, 0.5);
  border-radius: 10px;
  white-space: nowrap;
  flex-shrink: 0;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.live-goal-widget .lgw-bar {
  flex: 1;
  min-width: 40px;
  max-width: 120px;
  height: 6px;
  background: rgba(255,255,255,0.10);
  border-radius: 3px;
  overflow: hidden;
}
.live-goal-widget .lgw-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent, #FFD700), #ffa500);
  border-radius: 3px;
  transition: width 0.5s ease-out;
  width: 0;
}
.live-goal-widget.lgw-complete .lgw-bar-fill {
  background: linear-gradient(90deg, #4caf50, #8bc34a);
}
/* Mobile: drop the bar to free up header room; keep label + numbers + %.
   The room and contributor chips also hide so the gift row stays
   compact on narrow viewports. The progress bar is the priority info;
   admins on desktop see the "who sent what from where" context. */
@media (max-width: 720px) {
  #live-goals-header { padding: 0 6px; }
  .live-goal-widget .lgw-bar { display: none; }
  .live-goal-widget .lgw-label { font-size: 11px; }
  .live-goal-widget .lgw-room { display: none !important; }
  .live-goal-widget .lgw-contributor { display: none !important; }
}

.lgw-hot-slide { display: flex; align-items: center; justify-content: center; }
.lgw-hot-slide .lgw-hot-inner { max-width: 100%; }

/* Breakdown slide — alternates with each goal's gift slide in the
   slideshow. Shows per-room contribution chips so viewers can see
   where activity is coming from across rooms. Same outer pill shape
   as the gift widget but the inner row is a flex of room chips. */
.live-goal-widget.lgw-breakdown-slide {
  padding: 4px 10px;
  justify-content: center;
}
.lgw-breakdown-chips {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-wrap: nowrap;
  overflow: hidden;
}
.lgw-room-chip {
  display: inline-block;
  padding: 2px 8px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary, #f0f0f4);
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,215,0,0.3);
  border-radius: 10px;
  white-space: nowrap;
}
/* Sender chips — same shape as room chips but tinted so admins
   can distinguish "where" from "who" at a glance during the show. */
.lgw-room-chip.lgw-sender-chip {
  border-color: rgba(76, 175, 80, 0.5);
  background: rgba(76, 175, 80, 0.12);
}
.lgw-senders-label {
  font-size: 13px;
  margin-right: 4px;
  flex-shrink: 0;
}
/* Breakdown dots are smaller / dimmer than gift-slide dots — visually
   secondary so the eye reads goal-slide dots first. */
.lgw-dot.lgw-dot-breakdown {
  width: 4px;
  height: 4px;
  opacity: 0.7;
}

/* ── Batch-gift combo feedback ─────────────────────────────
   Rapid-tapping a cheap gift fires a burst of individual sends.
   Instead of a raw count, the tapped tile shows a TikTok-style combo
   ring that drains over the ~3s batch window and refills on every
   tap, with an escalating praise word — COMBO → GOOD → GREAT →
   AWESOME → EPIC → INSANE — that pops and shifts color as each
   milestone is crossed. No number is shown. Cleared when the batch
   closes (3s idle, gift switch, or out of HTC). */
/* Touch handling for rapid taps: on a phone, two quick taps on the
   same tile are otherwise interpreted as double-tap-to-zoom, which
   swallows the follow-up taps (and can zoom the page so later taps
   miss the tile) — so the first gift registers but successive rapid
   taps don't. touch-action:manipulation disables double-tap zoom on
   the tiles so every tap fires its own immediate click. The
   transparent tap-highlight removes the grey flash on each tap, and
   user-select:none stops a fast burst from selecting the label text.
   Desktop (mouse clicks) was never affected by any of this. */
.gift-item {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}
@keyframes gift-tap-pulse {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 var(--accent); }
  35%  { transform: scale(0.93); box-shadow: 0 0 0 5px var(--accent); }
  100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(0,0,0,0); }
}
.gift-item.gift-tap-pulse {
  animation: gift-tap-pulse 220ms ease-out;
  border-color: var(--accent) !important;
}
.gift-combo {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 58px;
  height: 58px;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 4;
  --combo-color: var(--accent);
}
.gift-combo.gift-combo-out {
  opacity: 0;
  transition: opacity 0.35s ease;
}
.gift-combo-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
  /* An <svg> clips to its viewport by default, which squared off the
     ring's circular glow at the box edges. Let it bloom past the box. */
  overflow: visible;
}
.gift-combo-track {
  fill: rgba(0, 0, 0, 0.5);
  stroke: rgba(255, 255, 255, 0.15);
  stroke-width: 4;
}
.gift-combo-prog {
  fill: none;
  stroke: var(--combo-color);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-dasharray: 132;
  stroke-dashoffset: 0;
  filter: drop-shadow(0 0 3px var(--combo-color));
}
/* Ring drains full → empty over the batch window; restarted each tap. */
.gift-combo.gift-combo-draining .gift-combo-prog {
  animation: gift-combo-drain 3s linear forwards;
}
@keyframes gift-combo-drain {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: 132; }
}
.gift-combo-word {
  position: relative;
  z-index: 1;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0.03em;
  color: #fff;
  text-transform: uppercase;
  white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.75), 0 0 7px var(--combo-color);
}
/* Small bump on a normal tap. */
.gift-combo.gift-combo-bump { animation: gift-combo-bump 160ms ease-out; }
@keyframes gift-combo-bump {
  0%   { transform: translate(-50%, -50%) scale(1); }
  40%  { transform: translate(-50%, -50%) scale(1.18); }
  100% { transform: translate(-50%, -50%) scale(1); }
}
/* Bigger pop when crossing into a new praise tier (the milestone). */
.gift-combo.gift-combo-level { animation: gift-combo-level 380ms ease-out; }
@keyframes gift-combo-level {
  0%   { transform: translate(-50%, -50%) scale(1); }
  35%  { transform: translate(-50%, -50%) scale(1.55); }
  100% { transform: translate(-50%, -50%) scale(1); }
}
/* Tier colors drive the ring, glow, and word via --combo-color. */
.gift-combo.tier-0 { --combo-color: var(--accent); }
.gift-combo.tier-1 { --combo-color: #4caf50; }
.gift-combo.tier-2 { --combo-color: #2196f3; }
.gift-combo.tier-3 { --combo-color: #b061ff; }
.gift-combo.tier-4 { --combo-color: #ff9800; }
.gift-combo.tier-5 { --combo-color: #ff3b6b; }
.gift-combo.tier-6  { --combo-color: #ff1f4d; }
.gift-combo.tier-7  { --combo-color: #ff4d2e; }
.gift-combo.tier-8  { --combo-color: #ff7a18; }
.gift-combo.tier-9  { --combo-color: #ffa000; }
.gift-combo.tier-10 { --combo-color: #ffc83d; }
.gift-combo.tier-11 { --combo-color: #ffd700; }

/* ── Combo milestone effects — additive as the burst climbs ─────────
   sparks (SAVAGE/100) → electricity (GLORIOUS/500) → ring shake
   (MYTHIC/800) → gold fill (LEGENDARY/1000, stays for the rest of the
   burst). Effects layer on top of one another; the tier color keeps
   shifting underneath. prefers-reduced-motion drops the motion but keeps
   color/glow/gold. */

/* Sparks — twinkling points sweeping around the ring. */
.gift-combo.fx-sparks::before {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  pointer-events: none;
  background:
    radial-gradient(circle at 50% 1%,  #fff      0, transparent 2px),
    radial-gradient(circle at 99% 50%, #fff      0, transparent 2px),
    radial-gradient(circle at 50% 99%, #fff      0, transparent 2px),
    radial-gradient(circle at 1% 50%,  #fff      0, transparent 2px),
    radial-gradient(circle at 85% 15%, #ffe9a8   0, transparent 1.5px),
    radial-gradient(circle at 15% 85%, #ffe9a8   0, transparent 1.5px);
  animation: fx-spark-spin 1.4s linear infinite, fx-spark-blink 0.5s ease-in-out infinite alternate;
}
@keyframes fx-spark-spin  { to { transform: rotate(360deg); } }
@keyframes fx-spark-blink { from { opacity: 0.35; } to { opacity: 1; } }

/* Electricity — crackling stroke + cyan halo on the progress ring. */
.gift-combo.fx-electric .gift-combo-prog {
  filter: drop-shadow(0 0 3px var(--combo-color)) drop-shadow(0 0 5px #7df9ff);
}
.gift-combo.fx-electric.gift-combo-draining .gift-combo-prog {
  animation: gift-combo-drain 3s linear forwards, fx-electric-flicker 0.11s steps(2) infinite;
}
@keyframes fx-electric-flicker {
  0%, 100% { stroke: var(--combo-color); }
  50%      { stroke: #d6fbff; }
}

/* Ring vibration — applied to the ring (not .gift-combo) so it doesn't
   fight the per-tap pop animation. */
.gift-combo.fx-shake .gift-combo-ring { animation: fx-shake-ring 0.08s linear infinite; }
@keyframes fx-shake-ring {
  0%, 100% { transform: rotate(-90deg) translate(0, 0); }
  25%      { transform: rotate(-90deg) translate(0.7px, -0.5px); }
  50%      { transform: rotate(-90deg) translate(-0.6px, 0.6px); }
  75%      { transform: rotate(-90deg) translate(0.5px, 0.5px); }
}

/* Gold finale — fill + shine, persists for the rest of the burst. */
.gift-combo.fx-gold .gift-combo-track { fill: rgba(90, 66, 0, 0.7); stroke: rgba(255, 215, 0, 0.45); }
.gift-combo.fx-gold .gift-combo-prog  { stroke: #ffd700; filter: drop-shadow(0 0 4px #ffd700) drop-shadow(0 0 6px #ffcc00); }
.gift-combo.fx-gold .gift-combo-word  { color: #fff6cf; text-shadow: 0 1px 3px rgba(0,0,0,0.8), 0 0 10px #ffd700; }
.gift-combo.fx-gold::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  background: linear-gradient(115deg, transparent 35%, rgba(255,255,255,0.6) 50%, transparent 65%);
  background-size: 250% 250%;
  mix-blend-mode: screen;
  animation: fx-gold-shine 1.7s linear infinite;
}
@keyframes fx-gold-shine {
  from { background-position: 200% 0; }
  to   { background-position: -120% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .gift-combo.fx-sparks::before { animation: none; opacity: 0.55; }
  .gift-combo.fx-electric.gift-combo-draining .gift-combo-prog { animation: gift-combo-drain 3s linear forwards; }
  .gift-combo.fx-shake .gift-combo-ring { animation: none; }
  .gift-combo.fx-gold::after { animation: none; opacity: 0.25; }
}

/* ── Weekly Slot Machine modal (distinct neon look vs gift animations) ── */
#slot-modal{display:none;position:fixed;inset:0;z-index:9500;background:rgba(8,2,16,.86);align-items:center;justify-content:center;padding:16px}
.slot-cabinet{position:relative;width:min(440px,96vw);background:linear-gradient(160deg,#1b0b2e,#0e0618);border:2px solid #b06bff;border-radius:16px;padding:18px 18px 22px;box-shadow:0 0 0 2px rgba(176,107,255,.25),0 0 40px rgba(176,107,255,.35),inset 0 0 30px rgba(120,40,200,.25);animation:slot-frame 1.6s ease-in-out infinite}
@keyframes slot-frame{0%,100%{box-shadow:0 0 0 2px rgba(176,107,255,.28),0 0 34px rgba(176,107,255,.30),inset 0 0 30px rgba(120,40,200,.22)}50%{box-shadow:0 0 0 2px rgba(0,240,255,.38),0 0 46px rgba(0,240,255,.42),inset 0 0 34px rgba(120,40,200,.30)}}
.slot-title{text-align:center;font-weight:800;letter-spacing:3px;font-size:20px;color:#fff;text-shadow:0 0 8px #b06bff,0 0 16px #00f0ff;margin:2px 0 14px}
.slot-reels{display:flex;gap:10px;justify-content:center;padding:12px;background:#060010;border-radius:12px;border:1px solid #3a1f5c;position:relative}
.slot-reels::before{content:'';position:absolute;left:8px;right:8px;top:50%;height:72px;transform:translateY(-50%);border-top:2px solid rgba(255,210,60,.9);border-bottom:2px solid rgba(255,210,60,.9);box-shadow:0 0 12px rgba(255,210,60,.5);pointer-events:none;z-index:3}
.slot-reel{width:84px;height:216px;overflow:hidden;background:#0d0420;border-radius:8px;border:1px solid #2a1547;position:relative}
.slot-reel::after{content:'';position:absolute;inset:0;pointer-events:none;z-index:2;background:linear-gradient(180deg,rgba(6,0,16,.94) 0%,rgba(6,0,16,.62) 20%,rgba(6,0,16,0) 34%,rgba(6,0,16,0) 66%,rgba(6,0,16,.62) 80%,rgba(6,0,16,.94) 100%)}
.slot-strip{display:flex;flex-direction:column;will-change:transform}
.slot-cell{height:72px;display:flex;align-items:center;justify-content:center;flex:0 0 72px}
.slot-cell img{max-width:56px;max-height:56px;object-fit:contain}
.slot-emoji{font-size:40px;line-height:1}
.slot-coin{position:absolute;width:42px;height:42px;pointer-events:none;z-index:12;will-change:transform,opacity;filter:drop-shadow(0 0 5px rgba(255,210,60,.7))}
.slot-msg{text-align:center;min-height:22px;margin-top:14px;font-weight:700;font-size:15px;color:#ffe;text-shadow:0 0 8px rgba(255,210,60,.6)}
.slot-actions{display:flex;gap:10px;justify-content:center;margin-top:14px}
.slot-btn{background:linear-gradient(180deg,#8a3ffb,#5a1fb0);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:800;letter-spacing:1px;cursor:pointer;box-shadow:0 3px 0 #3a1080,0 0 14px rgba(138,63,251,.5)}
.slot-btn:disabled{opacity:.5;cursor:default}
.slot-btn.ghost{background:transparent;border:1px solid #5a3f8c;box-shadow:none}
#slot-modal.slot-win .slot-cabinet{animation:slot-win-flash .5s ease-in-out 3}
@keyframes slot-win-flash{0%,100%{box-shadow:0 0 0 2px rgba(255,210,60,.4),0 0 40px rgba(255,210,60,.5)}50%{box-shadow:0 0 0 3px rgba(255,210,60,.95),0 0 60px rgba(255,210,60,.85)}}
#slot-test-fab{background:#5a1fb0;color:#fff;border:1px solid #b06bff;border-radius:50%;width:26px;height:26px;padding:0;margin-left:4px;font-size:15px;line-height:1;cursor:pointer;box-shadow:0 0 8px rgba(176,107,255,.5);flex-shrink:0;vertical-align:middle}
@media (prefers-reduced-motion: reduce){.slot-cabinet{animation:none}#slot-modal.slot-win .slot-cabinet{animation:none}}

/* ── Cosmetics shop: mobile fit ────────────────────────────
   The shop window carries inline min-width:480px / min-height:400px
   and is positioned by makeModalMovable() (which sets inline left/top/
   width/height + resize:both). On a phone the 480px floor beats
   max-width:95vw, so the window is wider than the screen, overflows,
   and parts sit off-screen with no way to reach them. Here we pin it
   to the full viewport and clear the floors. !important is required to
   override the inline styles the drag/resize helper writes; it also
   effectively freezes drag/resize on mobile (inline left/top can't win
   over these rules), which is the behaviour we want — a fixed panel
   that fits, with #shop-content scrolling internally. dvh after vh so
   modern mobile browsers account for the address-bar chrome. */
@media (max-width: 600px) {
  #shop-window {
    left: 0 !important;
    top: 0 !important;
    width: 100vw !important;
    height: 100vh !important;
    height: 100dvh !important;
    max-width: 100vw !important;
    max-height: 100vh !important;
    max-height: 100dvh !important;
    min-width: 0 !important;
    min-height: 0 !important;
    border-radius: 0 !important;
    resize: none !important;
  }
  /* Header no longer advertises drag (window is pinned on mobile). */
  #shop-header { cursor: default !important; }
  /* Stack the cart under the catalog instead of a side-by-side
     sidebar, and let it span full width so it doesn't overflow when
     toggled open. Capped height keeps the catalog visible above it. */
  #shop-body { flex-direction: column !important; }
  #shop-cart-panel {
    width: 100% !important;
    min-width: 0 !important;
    border-left: none !important;
    border-top: 1px solid var(--border) !important;
    max-height: 45% !important;
  }
  /* Catalog: fixed 2-up grid on mobile. The default auto-fill grid
     uses a 180px column floor, which can't fit two columns on a phone
     and collapses to one full-width column. Pinning to two equal 1fr
     columns keeps the cards compact and side-by-side. */
  #shop-content .shop-grid {
    grid-template-columns: repeat(2, 1fr) !important;
  }
}

/* ── Gift picker: mobile bottom-sheet ──────────────────────
   makeModalMovable() centers the gift window, which on a phone parks
   it over the top-center of the screen — exactly where batch-gift
   animations play, so a rapid-tap sender can't watch what they're
   sending. Here we re-anchor it to the bottom of the viewport as a
   sheet and cap its height so the top ~40% stays clear for the
   animation. !important overrides the inline left/top/resize the drag
   helper writes (and freezes drag/resize on mobile, which is fine —
   the inner list scrolls). dvh after vh for mobile browser chrome. */
@media (max-width: 600px) {
  #gift-picker-window {
    left: 0 !important;
    top: auto !important;
    bottom: 0 !important;
    width: 100vw !important;
    max-width: 100vw !important;
    min-width: 0 !important;
    min-height: 0 !important;
    max-height: 85vh !important;
    max-height: 85dvh !important;
    border-radius: 14px 14px 0 0 !important;
    resize: none !important;
  }
  /* Handle no longer advertises drag (window is pinned on mobile). */
  #gift-picker-handle { cursor: default !important; padding: 9px 12px !important; }

  /* Mobile: the fixed top was eating the grid. Tighten every block above the
     gifts so the scroll area gets the space it needs for multitap. */
  #gift-picker-window .gp-body { padding: 10px 12px !important; }
  #gift-recipient-label      { margin-bottom: 6px !important; min-height: 0 !important; }
  #gift-recipient-picker     { margin-bottom: 8px !important; }
  #gift-recipient-picker label,
  #gift-message-wrap label   { margin-bottom: 2px !important; font-size: 11px !important; }
  #gift-recipient-select     { padding: 6px 8px !important; }
  #gift-message-wrap         { margin-bottom: 8px !important; }
  #gift-message-input        { padding: 6px 44px 6px 8px !important; }
  #gift-visibility-wrap      { margin-bottom: 8px !important; padding: 5px !important; }
  #gift-tabs-wrap            { margin-bottom: 6px !important; }
  #gift-grids                { margin-bottom: 6px !important; }
  #gift-result               { min-height: 0 !important; }
}

/* Radio player Off (Configure → Stream, owner 2026-10-24): no player, ticker,
   album art, on-air image or song history. The user list moves up to the top of
   the sidebar; the footer keeps the heart and the slot machine. */
body.rc-no-radio #albumart-wrap,
body.rc-no-radio #header-ticker,
body.rc-no-radio #user-list-panel #header-ticker,   /* phone/tablet: the ticker sits in the user list */
body.rc-no-radio #onair-image-wrap,
body.rc-no-radio #footer-inner > a,
body.rc-no-radio #play-btn,
body.rc-no-radio #player-label,
body.rc-no-radio #stream-dot,
body.rc-no-radio #station-select,
body.rc-no-radio #vol-slider,
body.rc-no-radio #mute-btn,
body.rc-no-radio #history-btn,
body.rc-no-radio #history-modal { display: none !important; }

/* ── Name plates (owner, 2026-10-30) — subscriber tier around the chat name.
   Super Fan: bronze border. VIP: silver border + 30% background. Elite: gold
   border + 30% background + glow. No animation. Off for a viewer who turned
   plates off in Settings (body.rc-no-plates). */
.name-plate{border:1px solid transparent;border-radius:6px;padding:0 5px;-webkit-box-decoration-break:clone;box-decoration-break:clone}
.name-plate.plate-superfan{border-color:#cd7f32}
.name-plate.plate-vip{border-color:#c0c0c0;background:rgba(192,192,192,.3)}
.name-plate.plate-elite{border-color:#ffd700;background:rgba(255,215,0,.3);box-shadow:0 0 6px rgba(255,215,0,.75)}
body.rc-no-plates .name-plate{border:0;background:none;box-shadow:none;padding:0}

/* Subscriber tier icon — a small copy of the tier's badge in place of the
   member icon (owner, 2026-10-30). Emoji-sized, like the avatar slot. */
.msg-tier-icon{display:inline-block;width:18px;height:18px;object-fit:contain;vertical-align:-3px;margin:0 1px}
