hotel_pi/frontend/src/components/HomeScreen.svelte
2026-04-16 23:36:13 -04:00

910 lines
26 KiB
Svelte

<script>
import { selectedIndex, pushScreen } from '../lib/store.js';
import {
heroWelcomeText,
heroGuestText,
welcomePrefix,
welcomeSuffix,
resortName,
roomNumber,
heroStatusBarBg,
heroHeaderBg,
heroAccentLineColor,
heroWelcomePrefixColor,
heroWelcomeTextColor,
heroGuestTextColor,
enableHeaderBarOpacity,
borderTrimColor,
navElementsColor,
heroBgStart,
heroBgMid,
heroBgEnd,
navBarStart,
navBarMid,
navBarEnd,
} from '../lib/configStore.js';
import { onMount } from 'svelte';
let currentTime = new Date();
let weather = null;
let weatherError = false;
let inactivityTimeout;
let hasInteracted = false;
let currentNavIndex = -1;
// Subscribe to selectedIndex changes to keep currentNavIndex in sync
const unsubscribe = selectedIndex.subscribe(val => {
currentNavIndex = val;
});
onMount(() => {
// Update time every second
const interval = setInterval(() => {
currentTime = new Date();
}, 1000);
// Fetch weather data
fetchWeather();
// Add document-level keyboard listener for arrow keys
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
// Cycle forward through items
const newIndex = (currentNavIndex + 1) % navItems.length;
selectedIndex.set(newIndex);
handleNavHover();
} else if (e.key === 'Enter') {
e.preventDefault();
if (currentNavIndex >= 0 && currentNavIndex < navItems.length) {
handleSelect(navItems[currentNavIndex]);
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
clearInterval(interval);
document.removeEventListener('keydown', handleKeyDown);
unsubscribe();
};
});
function resetInactivityTimeout() {
if (inactivityTimeout) clearTimeout(inactivityTimeout);
inactivityTimeout = setTimeout(() => {
selectedIndex.set(-1);
}, 20000); // 20 seconds
}
function handleNavHover() {
hasInteracted = true;
resetInactivityTimeout();
}
async function fetchWeather() {
try {
// Try to get user's location and fetch weather
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
console.log('Location:', latitude, longitude);
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weather_code&temperature_unit=fahrenheit&timezone=auto`
);
const data = await response.json();
console.log('API Response:', data);
if (data.current) {
weather = {
temp: Math.round(data.current.temperature_2m),
code: data.current.weather_code,
};
console.log('Weather Code:', data.current.weather_code, 'Temp:', data.current.temperature_2m + '°F');
}
},
(error) => {
console.log('Geolocation error:', error);
weatherError = true;
}
);
} else {
console.log('Geolocation not supported');
}
} catch (e) {
console.log('Fetch error:', e);
weatherError = true;
}
}
function getWeatherIcon(code) {
// Returns weather icon type for SVG rendering
if (code === 0) return 'sun';
if (code === 1 || code === 2) return 'cloud-sun';
if (code === 3) return 'cloud';
if (code === 45 || code === 48) return 'cloud-fog';
if (code === 51 || code === 53 || code === 55) return 'cloud-rain';
if (code === 61 || code === 63 || code === 65) return 'cloud-rain';
if (code === 71 || code === 73 || code === 75) return 'cloud-snow';
return 'cloud-sun';
}
function formatTime(date) {
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
});
}
function getOrdinalSuffix(day) {
if (day > 3 && day < 21) return 'TH';
switch (day % 10) {
case 1: return 'ST';
case 2: return 'ND';
case 3: return 'RD';
default: return 'TH';
}
}
function formatDate(date) {
const weekday = date.toLocaleDateString('en-US', { weekday: 'long' });
const month = date.toLocaleDateString('en-US', { month: 'long' });
const day = date.getDate();
const year = date.getFullYear();
const suffix = getOrdinalSuffix(day);
return `${weekday}, ${month} ${day}${suffix} ${year}`;
}
const navItems = [
{ id: 'plex', label: 'Watch Plex', icon: 'play' },
{ id: 'youtube', label: 'Watch YouTube', icon: 'play-circle' },
{ id: 'restaurants', label: 'Restaurants', icon: 'utensils' },
{ id: 'attractions', label: 'Entertainment', icon: 'compass' },
];
function handleSelect(item) {
if (item.id === 'plex') {
// Launch Plex via control service
const ws = new WebSocket(import.meta.env.VITE_WS_URL || 'ws://localhost:3001');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'launch-plex' }));
ws.close();
};
} else if (item.id === 'youtube') {
// Launch YouTube via control service
const ws = new WebSocket(import.meta.env.VITE_WS_URL || 'ws://localhost:3001');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'launch-youtube' }));
ws.close();
};
} else if (item.id === 'restaurants' || item.id === 'attractions') {
pushScreen(item.id);
}
}
function handleNavigation(direction) {
const currentIdx = $selectedIndex === -1 ? -1 : $selectedIndex;
const newIndex =
direction === 'right' || direction === 'down'
? (currentIdx + 1) % navItems.length
: (currentIdx - 1 + navItems.length) % navItems.length;
selectedIndex.set(newIndex);
}
</script>
<div class="home-container">
<!-- Background Video is handled globally in App.svelte -->
<!-- Top Status Bar -->
<div class="status-bar" style="background-color: {$heroStatusBarBg};">
<div class="resort-name">{$resortName}</div>
<div class="room-number">{$roomNumber}</div>
</div>
<!-- Accent Line -->
<div class="accent-line" style="background-color: {$heroAccentLineColor};"></div>
<!-- Header Bar -->
<div class="header-bar" style="background: linear-gradient(135deg, {$heroHeaderBg} 0%, {$enableHeaderBarOpacity ? $heroHeaderBg + 'dd' : $heroHeaderBg} 50%, {$heroHeaderBg} 100%);">
<div class="header-left">
<div class="welcome" style="color: {$heroWelcomePrefixColor};">{$welcomePrefix}</div>
<div class="guest-name" style="color: {$heroGuestTextColor};">{$heroGuestText}</div>
</div>
<div class="header-right">
<div class="time-date">
<div class="time-line">
<div class="weather-info">
{#if weather}
<svg class="weather-icon" viewBox="0 0 24 24" fill="currentColor">
{#if getWeatherIcon(weather.code) === 'sun'}
<!-- Improved Sun Icon -->
<circle cx="12" cy="12" r="5"></circle>
<!-- Top ray -->
<line x1="12" y1="1" x2="12" y2="3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Bottom ray -->
<line x1="12" y1="20.5" x2="12" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Right ray -->
<line x1="20.5" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Left ray -->
<line x1="1" y1="12" x2="3.5" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Top-right ray -->
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Top-left ray -->
<line x1="6.34" y1="6.34" x2="4.22" y2="4.22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Bottom-right ray -->
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
<!-- Bottom-left ray -->
<line x1="6.34" y1="17.66" x2="4.22" y2="19.78" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"></line>
{:else if getWeatherIcon(weather.code) === 'cloud-sun'}
<!-- Sun rays -->
<circle cx="5" cy="6" r="3" stroke="currentColor" stroke-width="1.5" fill="none"></circle>
<line x1="5" y1="2" x2="5" y2="0.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line>
<line x1="5" y1="10" x2="5" y2="11.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line>
<line x1="1" y1="6" x2="-0.5" y2="6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line>
<line x1="9" y1="6" x2="10.5" y2="6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line>
<!-- Cloud -->
<path d="M12 15a4 4 0 0 1 4-4h1a5 5 0 0 1 5 5v4H8v-4a4 4 0 0 1 4-4z" stroke="currentColor" stroke-width="1.5" fill="none"></path>
{:else if getWeatherIcon(weather.code) === 'cloud'}
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
{:else if getWeatherIcon(weather.code) === 'cloud-fog'}
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
<line x1="2" y1="20" x2="18" y2="20" stroke-width="1.5"></line>
<line x1="2" y1="17" x2="16" y2="17" stroke-width="1.5"></line>
{:else if getWeatherIcon(weather.code) === 'cloud-rain'}
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
<line x1="8" y1="19" x2="8" y2="21" stroke-width="1.5"></line>
<line x1="8" y1="13" x2="8" y2="15" stroke-width="1.5"></line>
<line x1="14" y1="19" x2="14" y2="21" stroke-width="1.5"></line>
<line x1="14" y1="13" x2="14" y2="15" stroke-width="1.5"></line>
<line x1="20" y1="19" x2="20" y2="21" stroke-width="1.5"></line>
<line x1="20" y1="13" x2="20" y2="15" stroke-width="1.5"></line>
{:else if getWeatherIcon(weather.code) === 'cloud-snow'}
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
<g stroke-width="1.2">
<path d="M8 19L6 20m0-3l2 1m0 0l-2 1m2-1l2 1"></path>
<path d="M14 19l-2 1m0-3l2 1m0 0l-2 1m2-1l2 1"></path>
<path d="M20 19l-2 1m0-3l2 1m0 0l-2 1m2-1l2 1"></path>
</g>
{:else}
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
{/if}
</svg>
<span class="weather-temp">{weather.temp}°F</span>
{:else}
<!-- Default cloud icon while loading or on error -->
<svg class="weather-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
</svg>
<span class="weather-temp">--°F</span>
{/if}
</div>
<div class="time">{formatTime(currentTime)}</div>
</div>
<div class="date">{formatDate(currentTime)}</div>
</div>
</div>
</div>
<!-- Hero Image Section -->
<div class="hero-section">
<div class="hero-image" style="background: linear-gradient(135deg, {$heroBgStart} 0%, {$heroBgMid} 50%, {$heroBgEnd} 100%);"></div>
</div>
<!-- Border Above Nav -->
<div class="border-above-nav" style="background-color: {$borderTrimColor};"></div>
<!-- Bottom Navigation Bar -->
<div class="nav-bar" style="background: linear-gradient(to bottom, {$navBarStart} 0%, {$navBarMid} 50%, {$navBarEnd} 100%);">
{#each navItems as item, index (item.id)}
<button
class="nav-item"
class:active={hasInteracted && index === $selectedIndex}
on:click={() => handleSelect(item)}
on:mouseenter={() => {
handleNavHover();
selectedIndex.set(index);
}}
on:keydown={(e) => {
if (e.key === 'Enter') handleSelect(item);
}}
style="color: {$navElementsColor};"
>
<div class="nav-icon" style="color: {$navElementsColor};">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5">
{#if item.icon === 'play'}
<!-- Plex Logo -->
<rect x="2" y="3" width="6" height="18"></rect>
<polygon points="10 3 10 21 20 12"></polygon>
{:else if item.icon === 'play-circle'}
<!-- YouTube Play Button -->
<rect x="2" y="3" width="20" height="18" rx="3" ry="3" fill="currentColor"></rect>
<polygon points="9 8 9 16 16 12" fill="white"></polygon>
{:else if item.icon === 'utensils'}
<!-- Fork and Knife -->
<g stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 3v15M4 8h-1.5M4 11h-1.5M8 3v15M8 8h1.5M8 11h1.5M16 4l2 2M20 8l-6 12M16 8l2 2M18 13l2-2"></path>
</g>
{:else if item.icon === 'compass'}
<circle cx="12" cy="12" r="10" fill="none"></circle>
<path d="M12 6v6l4 2" fill="none"></path>
{/if}
</svg>
</div>
<div class="nav-label">{item.label}</div>
</button>
{/each}
</div>
<!-- Bottom Trim -->
<div class="bottom-trim" style="background-color: {$borderTrimColor};"></div>
<!-- Weather Icon Test Grid -->
<div class="weather-test">
<div class="weather-test-item">
<strong>Sun (0)</strong>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="2.81" y2="2.81"></line>
<line x1="19.78" y1="19.78" x2="21.19" y2="21.19"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="2.81" y2="21.19"></line>
<line x1="19.78" y1="4.22" x2="21.19" y2="2.81"></line>
</svg>
</div>
<div class="weather-test-item">
<strong>Cloud-Sun (1,2)</strong>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="5" cy="6" r="3"></circle>
<line x1="5" y1="2" x2="5" y2="0.5"></line>
<line x1="5" y1="10" x2="5" y2="11.5"></line>
<line x1="1" y1="6" x2="-0.5" y2="6"></line>
<line x1="9" y1="6" x2="10.5" y2="6"></line>
<path d="M12 15a4 4 0 0 1 4-4h1a5 5 0 0 1 5 5v4H8v-4a4 4 0 0 1 4-4z"></path>
</svg>
</div>
<div class="weather-test-item">
<strong>Cloud (3)</strong>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
</svg>
</div>
<div class="weather-test-item">
<strong>Cloud-Fog (45,48)</strong>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
<line x1="2" y1="20" x2="18" y2="20"></line>
<line x1="2" y1="17" x2="16" y2="17"></line>
</svg>
</div>
<div class="weather-test-item">
<strong>Rain (51-75)</strong>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
<line x1="8" y1="19" x2="8" y2="21"></line>
<line x1="14" y1="19" x2="14" y2="21"></line>
</svg>
</div>
<div class="weather-test-item">
<strong>Snow (71-75)</strong>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
<text x="8" y="22" font-size="10"></text>
<text x="14" y="22" font-size="10"></text>
</svg>
</div>
</div>
</div>
<style>
@font-face {
font-family: 'Waltograph';
src: url('/fonts/waltograph/Waltograph/waltograph42.ttf') format('truetype');
}
@font-face {
font-family: 'Mickey Mouse';
src: url('/fonts/mickey-mouse-font/MickeyMousePersonalUseRegular-mLRAG.otf') format('opentype');
}
.home-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #000;
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: white;
position: relative;
overflow: hidden;
}
.background-video {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
object-fit: cover;
z-index: 10;
pointer-events: none;
}
/* Top Status Bar */
.status-bar {
background: #1B4965;
height: 9.26vh;
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 0 3rem 1.2rem 3rem;
box-sizing: border-box;
position: relative;
z-index: 10;
}
.resort-name {
font-family: 'Mickey Mouse', serif;
font-size: 1.75rem;
font-weight: 700;
color: #FFFFFF;
letter-spacing: 0.15em;
}
.room-number {
font-size: 1.15rem;
font-weight: 700;
color: #FFFFFF;
letter-spacing: 0.05em;
}
/* Accent Line */
.accent-line {
height: 0.65vh;
background: #FF6F61;
position: relative;
z-index: 10;
}
/* Header Bar */
.header-bar {
background: linear-gradient(135deg, #1E8E9F 0%, #2EC4B6 50%, #1E8E9F 100%);
height: 14.35vh;
padding: 0 3rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
box-sizing: border-box;
position: relative;
z-index: 10;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.welcome {
font-size: 1rem;
color: #FF6F61;
letter-spacing: 0.12em;
margin-top: 0.2rem;
}
.guest-name {
font-size: 2.4rem;
font-weight: 700;
color: #fff;
letter-spacing: -0.01em;
margin-top: 0.2rem;
text-shadow: 0 2px 10px rgba(212, 175, 55, 0.2);
}
.header-right {
display: flex;
gap: 2.5rem;
align-items: flex-start;
text-align: right;
}
.weather-info {
display: flex;
align-items: center;
gap: 0.6rem;
}
.weather-icon {
width: 2.2rem;
height: 2.2rem;
color: #fff;
flex-shrink: 0;
}
.weather-temp {
font-weight: 700;
color: #fff;
font-size: 2.2rem;
letter-spacing: -0.01em;
font-variant-numeric: tabular-nums;
}
.time-date {
display: flex;
flex-direction: column;
gap: 0.3rem;
text-align: right;
}
.time-line {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1.5rem;
}
.time {
font-size: 2.2rem;
font-weight: 700;
color: #fff;
letter-spacing: -0.01em;
font-variant-numeric: tabular-nums;
}
.date {
font-size: 1.35rem;
color: rgba(255, 255, 255, 0.65);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* Hero Section */
.hero-section {
height: 60.1vh;
position: relative;
overflow: hidden;
background: linear-gradient(135deg, #1B6B7A 0%, #2EC4B6 50%, #0F4C5C 100%);
}
.hero-image {
width: 100%;
height: 100%;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080"><defs><linearGradient id="grad1" x1="0%25" y1="0%25" x2="100%25" y2="100%25"><stop offset="0%25" style="stop-color:%231a3a52;stop-opacity:1" /><stop offset="50%25" style="stop-color:%230f2540;stop-opacity:1" /><stop offset="100%25" style="stop-color:%23051018;stop-opacity:1" /></linearGradient></defs><rect width="1920" height="1080" fill="url(%23grad1)"/></svg>');
background-size: cover;
background-position: center;
animation: subtle-zoom 40s ease-in-out infinite;
position: relative;
}
.hero-image::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 20%, rgba(212, 175, 55, 0.05) 0%, transparent 50%);
pointer-events: none;
}
@keyframes subtle-zoom {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.015);
}
}
@keyframes gentle-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Weather Icon Test Grid */
.weather-test {
display: none;
position: fixed;
bottom: 160px;
left: 10px;
right: 10px;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
background: rgba(0, 0, 0, 0.9);
padding: 10px;
border: 2px solid #d4af37;
z-index: 100;
max-height: 120px;
overflow-y: auto;
}
.weather-test-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
border: 1px solid #d4af37;
border-radius: 4px;
font-size: 0.75rem;
color: white;
}
.weather-test-item svg {
width: 40px;
height: 40px;
margin: 5px 0;
color: #d4af37;
}
/* Border Above Nav */
.border-above-nav {
height: 0.93vh;
background: #1B4965;
position: relative;
z-index: 10;
}
/* Bottom Trim */
.bottom-trim {
height: 2.31vh;
background: #1B4965;
position: relative;
z-index: 10;
}
/* Bottom Navigation Bar */
.nav-bar {
background: linear-gradient(to bottom, #F4E1C1 0%, #EFDBAB 50%, #EAD5A0 100%);
height: 12.5vh;
padding: 1.75rem 3rem;
display: flex;
justify-content: space-around;
align-items: center;
gap: 1.5rem;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
position: relative;
z-index: 10;
}
.nav-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
padding: 0.9rem 1.2rem;
background: transparent;
border: none;
border-radius: 0.75rem;
color: #000;
cursor: pointer;
font-family: inherit;
font-size: 0.75rem;
font-weight: 700;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex: 0 1 auto;
position: relative;
transform: scale(1);
}
.nav-item:hover {
transform: scale(1.5);
}
.nav-item.active {
transform: scale(1.5);
}
.nav-icon {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
color: #1B4965;
flex-shrink: 0;
margin-top: 0.1rem;
}
.nav-icon svg {
width: 100%;
height: 100%;
stroke-linecap: round;
stroke-linejoin: round;
}
.nav-label {
font-size: 0.8rem;
text-align: left;
font-weight: 700;
letter-spacing: 0.02em;
max-width: 3.5rem;
line-height: 1.15;
white-space: normal;
color: #1B4965;
}
@keyframes bounce-in {
0% {
transform: scale(0.8);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
@media (max-width: 1400px) {
.status-bar {
padding: 0.9rem 2.5rem;
}
.header-bar {
padding: 1.5rem 2.5rem;
}
.header-right {
gap: 3rem;
}
.guest-name {
font-size: 1.7rem;
}
.nav-bar {
padding: 1.5rem 2.5rem;
}
.nav-item {
flex: 0 1 140px;
padding: 0.8rem 1.4rem;
}
}
@media (max-width: 1024px) {
.status-bar {
padding: 0.8rem 2rem;
font-size: 0.9rem;
}
.header-bar {
padding: 1.5rem 2rem;
}
.header-right {
gap: 2.5rem;
}
.guest-name {
font-size: 1.5rem;
}
.weather-info {
padding: 0.6rem 1.2rem;
}
.weather-icon {
font-size: 2rem;
}
.nav-bar {
padding: 1.25rem 2rem;
gap: 1rem;
}
.nav-item {
flex: 0 1 130px;
padding: 0.7rem 1.2rem;
}
.nav-icon {
font-size: 1.8rem;
}
.nav-label {
font-size: 0.8rem;
}
}
@media (max-width: 768px) {
.status-bar {
padding: 0.7rem 1.5rem;
border-bottom: 2px solid rgba(212, 175, 55, 0.3);
}
.resort-name {
font-size: 0.85rem;
}
.room-number {
font-size: 0.85rem;
padding: 0.3rem 0.8rem;
}
.header-bar {
padding: 1rem 1.5rem;
flex-direction: column;
gap: 1rem;
text-align: center;
}
.header-left {
gap: 0.2rem;
}
.guest-name {
font-size: 1.3rem;
}
.header-right {
width: 100%;
justify-content: space-around;
gap: 1.5rem;
}
.weather-info {
padding: 0.5rem 1rem;
gap: 0.6rem;
}
.weather-icon {
font-size: 1.6rem;
}
.weather-temp {
font-size: 1.1rem;
}
.time {
font-size: 1.4rem;
}
.nav-bar {
padding: 1rem 1.5rem;
gap: 0.5rem;
}
.nav-item {
flex: 0 1 auto;
padding: 0.6rem 0.9rem;
gap: 0.4rem;
}
.nav-icon {
font-size: 1.4rem;
}
.nav-label {
font-size: 0.7rem;
}
}
</style>