plebsaber.stream/samples/MapCard.svelte
2025-08-09 00:16:28 -07:00

1258 lines
27 KiB
Svelte

<script>
import {createEventDispatcher, getContext, onMount} from 'svelte';
import {fade, fly, slide} from 'svelte/transition';
import Value from '../../Common/Value.svelte';
import Badge from '../../Common/Badge.svelte';
import Icons from '../../Song/Icons.svelte';
import {formatDiffStatus, DifficultyStatus, wrapBLStatus, getSongSortingValue} from '../../../utils/beatleader/format';
import {configStore} from '../../../stores/config';
import HashDisplay from '../../Common/HashDisplay.svelte';
import SongStatus from './SongStatus.svelte';
import MapperList from '../../Leaderboard/MapperList.svelte';
import LeaderboardStats from '../../Leaderboard/LeaderboardStats.svelte';
import ModesList from './ModesList.svelte';
import SongPlayer from './SongPlayer.svelte';
import {songPlayerStore} from '../../../stores/songPlayer';
import MapRequirements from './MapRequirements.svelte';
import Popover from '../../Common/Popover.svelte';
import {navigate} from 'svelte-routing';
import MapTypeDescription from './MapTypeDescription.svelte';
export let map;
export let sortBy = 'stars';
export let forcePlaceholder = false;
export let dateType = 'ranked';
const dispatch = createEventDispatcher();
const {open, close} = getContext('simple-modal');
function onSelectedGroupEntryChanged() {
dispatch('group-changed');
}
let status = null;
let requirements = null;
let isHovered = false;
let mapCardElement;
let mapCardWrapper;
let mapCardRect;
let bottomContainer;
let bottomContainerHeight = 0;
let bottomContainerObserver;
function calculateStatus(song) {
for (const status of [
DifficultyStatus.ranked,
DifficultyStatus.qualified,
DifficultyStatus.nominated,
DifficultyStatus.inevent,
DifficultyStatus.ost,
]) {
if (song.difficulties.find(difficulty => difficulty.status == status)) {
return status;
}
}
return song.difficulties.length > 0 ? song.difficulties[0].status : null;
}
function calculateRequirements(song) {
var result = 0;
for (const difficulty of song.difficulties) {
result |= difficulty.requirements;
}
return result;
}
let song = null;
let hash = null;
let name = null;
let sortValue = null;
let coverUrl = null;
let mapType = null;
function getSong(map) {
if (!map) return;
song = map;
if (song.placeholder) {
song.updateCallback = newMap => {
getSong(newMap);
};
} else {
if (song.difficulties) {
status = calculateStatus(song);
requirements = calculateRequirements(song);
mapType = song.difficulties?.filter(d => d.value && d.applicable).sort((a, b) => b.value - a.value)?.[0]?.type;
}
hash = song.hash;
name = song.name;
coverUrl = song.coverImage;
sortValue = getSongSortingValue(song, null, sortBy);
}
}
$: getSong(map);
let cinematicsCanvas;
let rootcinematicsCanvas;
let headerContainer;
let headerContainerHeight = 0;
function updateHeaderContainerHeight(headerContainer) {
headerContainerHeight = headerContainer?.getBoundingClientRect().height ?? 0;
}
$: headerContainer && updateHeaderContainerHeight(headerContainer);
let hoverTimeout;
let playingSong = false;
let mouseInside = false;
let modesListContainer;
let scrollPosition = 0;
let containerHeight = 0;
let contentHeight = 0;
let currentHeight = 0;
let currentTargetHeight = 0;
let currentAnimation = null;
let scheduledAnimation = null;
function lerp(start, end, t) {
return start * (1 - t) + end * t;
}
function animateHeight(targetHeight, callback) {
if (!modesListContainer) return;
// Cancel any ongoing animation
if (currentAnimation) {
cancelAnimationFrame(currentAnimation);
}
function update(currentTime) {
if (!modesListContainer) return;
currentHeight = lerp(currentHeight, targetHeight, targetHeight > 0 ? 0.3 : 0.6);
modesListContainer.style.height = `${currentHeight}px`;
containerHeight = modesListContainer.clientHeight;
contentHeight = modesListContainer.scrollHeight;
if (Math.abs(currentHeight - targetHeight) > 1) {
currentAnimation = requestAnimationFrame(update);
if (contentHeight > 350 && Math.abs(currentHeight - targetHeight) < 50) {
callback?.();
}
} else {
currentAnimation = null;
if (targetHeight > 0) {
currentHeight = modesListContainer.scrollHeight;
} else {
currentHeight = 0;
}
callback?.();
modesListContainer.style.height = 'auto';
}
}
currentTargetHeight = targetHeight;
currentAnimation = requestAnimationFrame(update);
}
var shouldInit = false;
function handleHover(hovering, userHovering = false) {
mouseInside = hovering && userHovering;
if (!hovering && playingSong) {
return;
}
clearTimeout(hoverTimeout);
hoverTimeout = setTimeout(() => {
if (isHovered == hovering) return;
clearTimeout(scheduledAnimation);
isHovered = hovering;
if (hovering) {
if (!mapCardWrapper) {
return;
}
mapCardRect = mapCardWrapper.getBoundingClientRect();
// Start observing bottom container when hovered
if (bottomContainer && bottomContainerObserver) {
bottomContainerObserver.observe(bottomContainer);
}
modesListContainer.style.height = `${currentHeight}px`;
scheduledAnimation = setTimeout(() => {
if (modesListContainer) {
shouldInit = true;
animateHeight(modesListContainer.scrollHeight, () => {
if (shouldInit) {
updateMaskImage(true);
shouldInit = false;
}
});
}
}, 0);
} else {
if (modesListContainer) {
let callback = () => {
// Stop observing when not hovered
if (bottomContainerObserver) {
bottomContainerObserver.disconnect();
}
};
if (currentHeight > 0) {
animateHeight(0, callback);
} else {
callback();
if (currentAnimation) {
cancelAnimationFrame(currentAnimation);
}
currentAnimation = null;
currentHeight = 0;
modesListContainer.style.height = 'auto';
}
}
}
}, 0);
}
onMount(() => {
bottomContainerObserver = new ResizeObserver(entries => {
for (const entry of entries) {
bottomContainerHeight = entry.contentRect.height;
}
});
return () => {
if (bottomContainerObserver) {
bottomContainerObserver.disconnect();
}
if (currentAnimation) {
cancelAnimationFrame(currentAnimation);
}
};
});
let currentGradient = {
transparent: 0,
whiteStart: 0,
whiteEnd: 345,
transparentEnd: 345,
};
// Define target gradient positions based on scroll
let targetGradient = currentGradient;
let needsUpdate = false;
function animateMask() {
if (!modesListContainer) return;
const easing = 0.15; // Controls animation speed (0-1)
// Interpolate each value
for (let key in currentGradient) {
if (Math.abs(currentGradient[key] - targetGradient[key]) > 0.1) {
currentGradient[key] = lerp(currentGradient[key], targetGradient[key], easing);
needsUpdate = true;
} else {
currentGradient[key] = targetGradient[key];
}
}
const maskImage = `linear-gradient(180deg,
transparent ${currentGradient.transparent}px,
white ${currentGradient.whiteStart}px,
white ${currentGradient.whiteEnd}px,
transparent ${currentGradient.transparentEnd}px)`;
modesListContainer.style.maskImage = maskImage;
modesListContainer.style.webkitMaskImage = maskImage;
if (needsUpdate) {
requestAnimationFrame(animateMask);
}
}
function updateMaskImage(initial = false) {
if (!modesListContainer) return;
if (Math.abs(contentHeight - containerHeight) < 5) {
targetGradient = {
transparent: 0,
whiteStart: 0,
whiteEnd: containerHeight + 5,
transparentEnd: containerHeight + 5,
};
} else {
const scrollPercentage = scrollPosition / (contentHeight - containerHeight);
if (scrollPosition === 0) {
targetGradient = {
transparent: 0,
whiteStart: 0,
whiteEnd: 320,
transparentEnd: containerHeight,
};
} else if (Math.abs(scrollPercentage - 1) <= 0.05) {
targetGradient = {
transparent: 0,
whiteStart: 20,
whiteEnd: containerHeight,
transparentEnd: containerHeight,
};
} else {
targetGradient = {
transparent: 0,
whiteStart: 20,
whiteEnd: 320,
transparentEnd: containerHeight,
};
}
}
if (initial) {
currentGradient = {...targetGradient};
const maskImage = `linear-gradient(180deg,
transparent ${currentGradient.transparent}px,
white ${currentGradient.whiteStart}px,
white ${currentGradient.whiteEnd}px,
transparent ${currentGradient.transparentEnd}px)`;
modesListContainer.style.maskImage = maskImage;
modesListContainer.style.webkitMaskImage = maskImage;
} else {
// Animate the gradient positions
if (!needsUpdate) {
requestAnimationFrame(animateMask);
}
}
}
function handleScroll(e) {
scrollPosition = e.target.scrollTop;
containerHeight = e.target.clientHeight;
contentHeight = e.target.scrollHeight;
updateMaskImage();
}
function handlePlay(currentHash, songHash) {
if (currentHash === songHash) {
if (!isHovered) {
handleHover(true);
}
playingSong = true;
} else {
playingSong = false;
if (isHovered && !mouseInside) {
handleHover(false);
}
}
}
let mapLink = null;
function getMapLink(song) {
if (song.difficulties) {
mapLink = `/leaderboard/global/${song.difficulties?.filter(d => d.value && d.applicable).sort((a, b) => b.value - a.value)?.[0]?.leaderboardId}`;
} else {
mapLink = `/leaderboard/global/${song.id}`;
}
}
$: getMapLink(song);
let titleTextElement;
let authorElement;
let mappersElement;
$: hash && handlePlay($songPlayerStore?.currentHash, hash);
</script>
{#if song}
{#if !forcePlaceholder && !song.placeholder}
<Popover triggerEvents={['hover', 'focus']} placement="top" referenceElement={titleTextElement} forOverflow={true} hoverDelay={200}>
<div class="title-tooltip">
{name}
{#if $configStore?.leaderboardPreferences?.showSubtitleInHeader && song.subName}
<span class="tooltip-subname">{song.subName}</span>
{/if}
</div>
</Popover>
<Popover triggerEvents={['hover', 'focus']} placement="top" referenceElement={authorElement} forOverflow={true} hoverDelay={200}>
<div class="title-tooltip">
<span class="tooltip-author">{song.author}</span>
</div>
</Popover>
<Popover
triggerEvents={['hover', 'focus']}
placement="top"
referenceElement={mappersElement}
forOverflow={true}
spaceAway={4}
hoverDelay={200}>
<div class="title-tooltip">
<MapperList {song} maxHeight="unset" maxWidth="25em" fontSize="0.9em" noArrow={true} tooltip={true} />
</div>
</Popover>
<div
class="map-card-wrapper"
class:transparent={song.transparent}
class:is-hovered={isHovered}
class:long={$configStore.mapCards.wideCards}
bind:this={mapCardWrapper}>
{#if isHovered}
<div
transition:fade={{duration: 150}}
class="cinematics root-cinematics"
style={isHovered ? `height: ${mapCardRect.height}px;` : ''}>
<div class="cinematics-canvas root-canvas">
<div
style="position: absolute; background-size: cover;
background-position: center; background-image: url({coverUrl}); width: 100%; height: 100%" />
</div>
</div>
{/if}
<div
class="map-card"
class:is-hovered={isHovered || currentAnimation}
class:expanding={currentAnimation && currentTargetHeight > 0}
class:player-hovered={isHovered && mouseInside}
style={isHovered || currentAnimation ? `position: absolute;` : ''}
bind:this={mapCardElement}
tabindex="-1"
role="button"
on:mouseover={() => handleHover(true, true)}
on:mouseout={() => handleHover(false, true)}
on:focus={() => handleHover(true, true)}
on:blur={() => handleHover(false, true)}>
<div class="cinematics">
<div class="cinematics-canvas">
{#if $configStore.mapCards.cinematics}
<div
style="position: absolute; background-size: cover; background-position: center; background-image: url({coverUrl}); width: 100%; height: 100%" />
{:else}
<div
style="position: absolute; background-size: cover; background-position: center; background-color: #777777; width: 100%; height: 100%" />
{/if}
</div>
</div>
<a on:click|preventDefault|stopPropagation={() => navigate(mapLink)} class="header-link" href={mapLink}></a>
<div class="header" style="height: {headerContainerHeight < 150 ? '100%' : 'unset'};" class:is-hovered={isHovered}>
<div
class="map-cover"
style={coverUrl
? `background: url(${coverUrl}); background-repeat: no-repeat; background-size: cover; background-position: center;`
: ''}>
<div class="sort-value-background" class:with-value={sortValue} class:is-hovered={sortValue && isHovered}></div>
</div>
{#if requirements && isHovered && $configStore.mapCards.requirements}
<div transition:fly|local={{x: -40, duration: 300, y: 0}} class="requirements-icons">
<MapRequirements type={requirements} />
</div>
{/if}
<a on:click|preventDefault|stopPropagation={() => navigate(mapLink)} class="main-container" href={mapLink}>
<div class="header-container" bind:this={headerContainer}>
<div class="header-top-part">
<h1 class="song-title">
<div class="title-text" bind:this={titleTextElement}>
<span class="name">{name}</span>
{#if $configStore?.leaderboardPreferences?.showSubtitleInHeader && song.subName}
<span class="subname">{song.subName}</span>
{/if}
</div>
</h1>
<div class="title-container">
<span class="author" bind:this={authorElement}>{song.author}</span>
</div>
</div>
<div class="mapper-container">
<MapperList {song} maxHeight="2.2em" fontSize="0.9em" noArrow={true} bind:rootElement={mappersElement} />
</div>
<div class="status-container" class:is-hovered={isHovered}>
{#if status && status != DifficultyStatus.unranked && status != DifficultyStatus.unrankable}
<SongStatus songStatus={wrapBLStatus(status)} />
{/if}
{#if song.externalStatuses}
{#each song.externalStatuses as songStatus}
<SongStatus {songStatus} />
{/each}
{/if}
{#if mapType && $configStore.mapCards.mapType}
<MapTypeDescription type={mapType} />
{/if}
</div>
</div>
</a>
{#if status != DifficultyStatus.ost}
<div class="icons-container" class:is-hovered={isHovered}>
<Icons {song} icons={['preview', 'bsr', 'bs', 'oneclick']} />
</div>
{/if}
</div>
<div class="bottom-container-background" class:is-hovered={isHovered} style="--bottom-container-height: {bottomContainerHeight}px;">
</div>
<div
class="bottom-container"
class:has-sort-value={!!sortValue}
class:is-hovered={isHovered}
bind:this={bottomContainer}
style="--margin-top-value: -{headerContainerHeight < 150 ? 2.4 : 0}em;">
<div class="placeholder">
{#if sortValue}
<div class="sort-value" class:is-hovered={isHovered || currentAnimation}>
{sortValue}
</div>
{/if}
</div>
{#if isHovered || currentAnimation}
<div class="song-player">
<SongPlayer {song} />
</div>
<div
class="mobile-chevron-container hovered mobile-only"
on:click={() => handleHover(false, true)}
on:keydown={() => handleHover(false, true)}
tabindex="-1"
role="button">
<i class="fas fa-chevron-up"></i>
</div>
{/if}
<div
class="modes-list-container"
class:is-hovered={isHovered || currentAnimation}
on:scroll={handleScroll}
bind:this={modesListContainer}>
<ModesList {song} isHovered={isHovered || currentAnimation} {sortValue} {sortBy} {dateType} />
</div>
{#if !isHovered && !currentAnimation}
<div class="mobile-chevron-container mobile-only">
<i class="fas fa-chevron-down"></i>
</div>
{/if}
</div>
</div>
</div>
{:else}
<div class="map-card-wrapper" class:long={$configStore.mapCards.wideCards}>
<div class="map-card-placeholder">
<div class="map-card-loading">Loading...</div>
</div>
</div>
{/if}
{/if}
<style>
.map-card-wrapper {
position: relative;
width: 32em;
height: 10em;
margin-bottom: 1.2em;
overflow: visible;
background-color: #000000;
border-radius: 12px;
}
.map-card-wrapper.long {
width: 40em;
}
.map-card-wrapper.is-hovered {
border-radius: 12px 12px 16px 16px;
}
.map-card-wrapper.transparent {
opacity: 0;
pointer-events: none;
}
.map-card {
position: relative;
border-radius: 0.4em;
overflow: hidden;
width: 100%;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
transition: box-shadow 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
}
.map-card.is-hovered {
z-index: 3;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.7);
background-color: #141414;
height: unset;
border-radius: 12px 12px 16px 16px;
}
.map-card.player-hovered {
z-index: 5;
}
.map-card.expanding {
z-index: 4;
}
.map-card-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background-color: #1c1c1c;
border-radius: 12px;
}
.root-cinematics {
opacity: 0.9;
z-index: -1;
}
.song-player {
flex: 1;
}
.header {
padding: 0.5em;
color: var(--alternate);
display: flex;
align-items: flex-start;
justify-content: start !important;
gap: 0.8em;
width: 100%;
flex: 1;
}
.header.is-hovered {
height: unset;
}
.bottom-container {
display: flex;
flex-wrap: wrap;
padding: 0.3em;
z-index: 1;
position: relative;
background-color: #0000004f;
border-radius: 0 0 12px 12px;
margin-top: var(--margin-top-value);
align-items: center;
}
.bottom-container.has-sort-value {
z-index: 2;
}
.bottom-container.is-hovered {
background-color: transparent;
}
.bottom-container-background {
display: none;
z-index: -2;
height: calc(var(--bottom-container-height) + 9px);
}
.bottom-container-background.is-hovered {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: black;
}
.requirements-icons {
position: absolute;
top: 0em;
width: 2em;
display: flex;
flex-wrap: wrap;
justify-content: start;
align-items: center;
gap: 0.2em;
z-index: 2;
padding-top: 0.6em;
padding-left: 0.13em;
}
:global(.requirements-icons:has(> :nth-child(4))) {
width: 4em;
}
.sort-value {
z-index: 2;
font-weight: 600;
font-size: 0.9em;
margin-bottom: 0.4em;
}
.sort-value.is-hovered {
margin-bottom: 0.49em;
}
.sort-value-background {
height: 1.88em;
position: absolute;
display: block;
bottom: 0;
background-color: transparent;
width: 100%;
border-radius: 0 0 8px 8px;
}
.sort-value-background.is-hovered {
background-color: #00000066 !important;
}
.sort-value-background.with-value {
background-color: #0000004a;
}
.modes-list-container {
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
width: calc(100% - 10.5em);
}
.modes-list-container::-webkit-scrollbar {
display: none;
}
.modes-list-container.is-hovered {
max-height: 22em;
width: 100%;
margin-top: 0.1em;
}
.header-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
:global(.map-card-wrapper .icons-container .buttons-container.flat) {
flex-direction: column;
}
.header:before {
position: absolute;
content: ' ';
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.1;
background-repeat: no-repeat;
background-size: cover;
pointer-events: none;
}
.main-container {
display: flex;
justify-content: space-between;
flex: 1;
z-index: 1;
user-select: text;
-webkit-user-select: text;
-webkit-user-drag: none;
overflow: hidden;
}
.mobile-chevron-container {
cursor: pointer;
pointer-events: auto;
color: #ffffffa1;
}
.mobile-chevron-container.hovered {
margin-left: 1em;
}
.buttons-container {
position: absolute;
bottom: 0;
height: 2.7em;
margin-left: -0.6em;
width: 100%;
display: flex;
justify-content: space-between;
background-color: #0000004f;
padding: 0.6em;
border-radius: 0 0 12px 12px;
}
.icons-container {
width: fit-content;
margin-top: 0.25em;
transform: scale(1.1);
margin-right: -2em;
transition: margin-right 0.15s;
}
.icons-container.is-hovered {
margin-right: 0;
}
.version-selector-container {
transform: scale(1.15);
margin-bottom: -0.5em;
}
.header .song-title {
color: inherit !important;
margin-bottom: 0;
width: 100%;
}
.header .song-title .title-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
color: #ffffff93 !important;
}
.header h1 {
margin-bottom: 0.2em;
max-width: 100%;
overflow: hidden;
}
.header h1 span.name {
color: #ffffffcc !important;
font-size: 1.3em;
font-weight: 600;
margin-top: -0.2em;
}
.subname {
color: #ffffff93;
font-size: 0.8em;
}
.map-cover {
position: relative;
width: 9em;
aspect-ratio: 1;
border-radius: 8px;
z-index: 2;
pointer-events: none;
}
.placeholder {
width: 9.7em;
display: flex;
justify-content: center;
}
.author {
color: #ffffffa3;
font-size: 1em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.diff-status {
color: white;
}
.capture-status {
position: absolute;
top: 0;
left: 0;
height: 6em;
overflow: hidden;
z-index: 2;
}
.header h2.song-title {
font-size: 1em !important;
color: var(--increase, #42b129) !important;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.header-container {
display: flex;
justify-content: start;
flex-direction: column;
gap: 0.2em;
width: 100%;
overflow: hidden;
height: 7em;
}
.header-top-part {
display: flex;
flex-direction: column;
justify-content: flex-start;
z-index: 1;
width: 100%;
overflow: hidden;
}
:global(.map-card-wrapper.long) {
.header-top-part {
flex-direction: row;
gap: 0.5em;
}
.author {
margin-top: unset;
}
.title-container {
width: unset;
}
.song-title {
width: unset;
}
}
.header-bottom-part {
z-index: 1;
}
.title-container {
display: flex;
justify-content: center;
flex-direction: column;
grid-gap: 0.2em;
width: 100%;
overflow: hidden;
}
.mapper-container {
display: flex;
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
width: 100%;
margin-top: -0.1em;
mask-image: linear-gradient(90deg, white 0%, white 80%, transparent 100%);
}
.status-container {
display: flex;
gap: 0.3em;
font-size: 0.9em;
justify-content: flex-start;
margin-top: 0.1em;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
.title-tooltip {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.3em 0.5em;
border-radius: 0.6em;
pointer-events: none;
white-space: normal;
width: max-content;
max-width: 24vw;
z-index: 8;
}
.tooltip-subname {
opacity: 0.8;
font-size: 0.9em;
margin-left: 0.05em;
}
.tooltip-author {
color: #ffffffee;
}
.cinematics {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
}
.cinematics-canvas {
filter: blur(5em) opacity(0.5) saturate(250%);
left: 0;
pointer-events: none;
position: absolute;
top: 0;
transform: scale(1.1) translateZ(0);
width: 100%;
height: 100%;
}
.root-canvas {
transform: scale(0.9) translateZ(0);
}
.status-and-type {
display: flex;
gap: 0.6em;
overflow: hidden;
white-space: nowrap;
}
:global(.title-container .stats) {
justify-content: start !important;
color: #ffffffa3;
max-width: 35em;
}
.group-select {
height: fit-content;
padding: 0.175rem;
text-align: center;
text-align-last: center;
white-space: nowrap;
border: 0;
border-radius: 6px;
cursor: pointer;
color: #363636;
background-color: #dbdbdb;
box-shadow: none;
opacity: 0.35;
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
width: 100%;
margin-bottom: -0.6em;
}
.group-option {
color: black;
font-family: inherit;
}
.requirements {
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 0.2em;
row-gap: 0.5em;
padding-top: 0.7em;
padding-bottom: 0.7em;
overflow: hidden;
}
.header small {
font-size: 0.75em;
color: var(--ppColour);
}
.header .diff :global(.reversed) {
display: inline-block;
padding: 0.1em 0.25em 0.25em 0.25em;
margin-left: 0.5em;
margin-right: 0.5em;
border-radius: 0.25em;
}
:global(.voter-feedback-button) {
height: 1.8em;
}
:global(.battleroyalebtn) {
margin-left: 1em;
margin-bottom: 0.5em;
}
.title-and-buttons {
display: flex;
align-items: center;
align-self: stretch;
justify-content: center;
flex-wrap: nowrap;
flex-direction: column;
padding: 0.5em;
min-width: fit-content;
overflow: hidden;
}
.mobile-triangle {
position: absolute;
left: 8em;
top: 8.4em;
z-index: 2;
}
.mobile-only {
display: none;
}
:global(.voteButton) {
margin-top: 0 !important;
height: 1.8em;
}
@media screen and (max-width: 1024px) {
.header {
margin-inline: 0;
}
}
@media screen and (max-width: 767px) {
.map-card-wrapper {
width: 100%;
margin-bottom: 0.2em;
height: 7.2em;
}
.map-card-wrapper.long {
width: 100%;
}
.bottom-container {
padding: 0.2em;
margin-top: calc(var(--margin-top-value) + 0.8em);
}
.header {
margin-inline: 0;
padding: 0.4em 0.4em 0.15em 0.4em;
gap: 0.5em;
width: 100%;
}
.song-player {
width: calc(100vw - 9.7em);
flex: unset;
}
.sort-value {
font-size: 0.8em;
}
.desktop-only {
display: none;
}
.mobile-only {
display: flex;
}
.buttons-container {
position: relative;
margin-left: 0;
border-radius: 0;
justify-content: flex-start;
gap: 0.6em;
height: unset;
}
.cinematics-canvas {
transform: scaleY(1.2) translateZ(0);
}
:global(.player .clan-badges) {
display: none;
}
.main-container {
min-height: unset;
}
.header .song-title {
margin-left: unset;
}
.header {
border-radius: 0;
margin-bottom: 0;
}
.song-statuses {
flex-wrap: nowrap;
}
.author {
font-size: 0.9em;
margin-top: -0.2em;
}
.map-cover {
width: 6.4em;
}
.placeholder {
width: 6.8em;
height: 1em;
}
.icons-container {
transform: scale(0.75);
margin-top: -0.8em;
margin-right: -2.2em;
transition:
margin-right 0.15s,
margin-left 0.15s;
}
.icons-container.is-hovered {
margin-left: 0;
}
.main-container {
font-size: 0.8em;
max-width: calc(100vw - 9.5em);
}
.requirements-icons {
padding-top: 0.5em;
left: 0.4em;
}
.header h1 span.name {
font-size: 1.1em;
}
.bottom-container-background {
height: calc(var(--bottom-container-height) + 7px);
}
.modes-list-container {
width: calc(100% - 8.3em);
}
:global(.song-statuses .song-status) {
font-size: 0.6em;
}
:global(.leaderboard-header-box) {
margin: 0.6em 0 0 !important;
border-radius: 0 !important;
}
}
</style>