Compare commits

..

4 Commits

5 changed files with 1488 additions and 60 deletions

View File

@@ -19,15 +19,39 @@ function normalizeUser(user) {
}
return {
salutation: user.salutation || "",
firstName: user.firstName || "",
lastName: user.lastName || "",
birthday: user.birthday || "",
street: user.street || "",
houseNumber: user.houseNumber || "",
zip: user.zip || "",
city: user.city || "",
country: user.country || "Deutschland",
email: user.email || "",
password: user.password || "",
passwordHash: user.passwordHash || (user.password ? hashPassword(user.password) : ""),
orders: Array.isArray(user.orders) ? user.orders : [],
paymentMethods: Array.isArray(user.paymentMethods) ? user.paymentMethods : []
};
}
function hashPassword(password) {
let hashA = 0xdeadbeef;
let hashB = 0x41c6ce57;
const text = String(password || "");
for (let index = 0; index < text.length; index += 1) {
const charCode = text.charCodeAt(index);
hashA = Math.imul(hashA ^ charCode, 2654435761);
hashB = Math.imul(hashB ^ charCode, 1597334677);
}
hashA = Math.imul(hashA ^ (hashA >>> 16), 2246822507) ^ Math.imul(hashB ^ (hashB >>> 13), 3266489909);
hashB = Math.imul(hashB ^ (hashB >>> 16), 2246822507) ^ Math.imul(hashA ^ (hashA >>> 13), 3266489909);
return `hash$${(hashB >>> 0).toString(16).padStart(8, "0")}${(hashA >>> 0).toString(16).padStart(8, "0")}`;
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
@@ -58,45 +82,56 @@ if (!Array.isArray(users)) {
users = [];
}
users = users.map(normalizeUser).filter(Boolean);
persistUsers();
let currentUser = normalizeUser(readStorageJson("currentUser", null));
if (currentUser && currentUser.email) {
const storedMatch = users.find((user) => user.email === currentUser.email);
if (storedMatch) {
currentUser = storedMatch;
persistCurrentUser();
} else {
users.push(currentUser);
persistUsers();
persistCurrentUser();
}
}
function registerUser() {
if (!validateRegisterForm()) {
return;
}
const salutation = document.getElementById("reg-salutation")?.value.trim() || "";
const firstName = document.getElementById("reg-firstname")?.value.trim() || "";
const lastName = document.getElementById("reg-lastname")?.value.trim() || "";
const birthday = document.getElementById("reg-birthday")?.value.trim() || "";
const street = document.getElementById("reg-street")?.value.trim() || "";
const houseNumber = document.getElementById("reg-house-number")?.value.trim() || "";
const zip = document.getElementById("reg-zip")?.value.trim() || "";
const city = document.getElementById("reg-city")?.value.trim() || "";
const country = document.getElementById("reg-country")?.value.trim() || "";
const email = (document.getElementById("reg-email")?.value.trim() || "").toLowerCase();
const password = document.getElementById("reg-password")?.value || "";
if (!firstName || !lastName || !email || !password) {
alert("Bitte fuelle alle Felder aus.");
return;
}
if (!email.includes("@")) {
alert("Bitte gib eine gueltige E-Mail-Adresse ein.");
return;
}
const existingUser = users.find((user) => user.email.toLowerCase() === email);
if (existingUser) {
alert("E-Mail bereits registriert");
setFieldError(document.getElementById("reg-email"), "Diese E-Mail ist bereits registriert.");
return;
}
const newUser = {
salutation,
firstName,
lastName,
birthday,
street,
houseNumber,
zip,
city,
country,
email,
password,
passwordHash: hashPassword(password),
orders: [],
paymentMethods: []
};
@@ -107,22 +142,28 @@ function registerUser() {
persistUsers();
persistCurrentUser();
alert("Registrierung erfolgreich");
document.getElementById("register-modal")?.classList.add("hidden");
openAccountDashboard();
}
function loginUser() {
if (!validateLoginForm()) {
return;
}
const email = (document.getElementById("login-email")?.value.trim() || "").toLowerCase();
const password = document.getElementById("login-password")?.value || "";
const passwordHash = hashPassword(password);
const user = users.find(
(entry) => entry.email.toLowerCase() === email && entry.password === password
(entry) => entry.email.toLowerCase() === email && entry.passwordHash === passwordHash
);
if (!user) {
document.getElementById("login-error")?.classList.remove("hidden");
setFieldError(document.getElementById("login-email"), "E-Mail oder Passwort stimmen nicht.");
setFieldError(document.getElementById("login-password"), "E-Mail oder Passwort stimmen nicht.");
return;
}
@@ -146,8 +187,16 @@ function openAccountDashboard() {
<div class="account-panel">
<div class="account-panel-header">
<h2>Mein Konto</h2>
<div class="account-header-actions">
<button class="account-edit-btn" type="button" onclick="renderPersonalInfo(true)" aria-label="Persönliche Daten bearbeiten" title="Daten bearbeiten">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
</svg>
</button>
<button class="account-logout-btn" onclick="logoutUser()">Abmelden</button>
</div>
</div>
<div class="account-tabs">
<button class="account-tab-btn" onclick="renderPersonalInfo()">Persönliche Daten</button>
@@ -162,19 +211,280 @@ function openAccountDashboard() {
renderPersonalInfo();
}
function renderPersonalInfo() {
function renderPersonalInfo(isEditing = false) {
const target = document.getElementById("account-tab-content");
if (!target || !currentUser) {
return;
}
if (isEditing) {
target.innerHTML = `
<div class="account-card">
<p><strong>Vorname:</strong> ${currentUser.firstName || "-"}</p>
<p><strong>Nachname:</strong> ${currentUser.lastName || "-"}</p>
<p><strong>E-Mail:</strong> ${currentUser.email || "-"}</p>
<div class="account-card account-personal-card">
<div class="account-card-title-row">
<h3>Persönliche Daten bearbeiten</h3>
<span>E-Mail bleibt unverändert</span>
</div>
<div class="account-edit-form">
<label class="auth-field">
<span>Anrede</span>
<select id="account-edit-salutation">
<option value="">Bitte wählen</option>
<option value="Frau" ${currentUser.salutation === "Frau" ? "selected" : ""}>Frau</option>
<option value="Herr" ${currentUser.salutation === "Herr" ? "selected" : ""}>Herr</option>
<option value="Divers" ${currentUser.salutation === "Divers" ? "selected" : ""}>Divers</option>
</select>
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Vorname</span>
<input type="text" id="account-edit-firstname" value="${escapeHtml(currentUser.firstName)}" required data-required-message="Vorname ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Nachname</span>
<input type="text" id="account-edit-lastname" value="${escapeHtml(currentUser.lastName)}" required data-required-message="Nachname ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Geburtstag</span>
<input type="date" id="account-edit-birthday" value="${escapeHtml(currentUser.birthday)}">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Straße</span>
<input type="text" id="account-edit-street" value="${escapeHtml(currentUser.street)}">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Nr.</span>
<input type="text" id="account-edit-house-number" value="${escapeHtml(currentUser.houseNumber)}">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>PLZ</span>
<input type="text" id="account-edit-zip" value="${escapeHtml(currentUser.zip)}">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Ort</span>
<input type="text" id="account-edit-city" value="${escapeHtml(currentUser.city)}">
<span class="field-error"></span>
</label>
<label class="auth-field">
<span>Land</span>
<select id="account-edit-country">
<option value="Deutschland" ${currentUser.country === "Deutschland" ? "selected" : ""}>Deutschland</option>
<option value="Österreich" ${currentUser.country === "Österreich" ? "selected" : ""}>Österreich</option>
<option value="Schweiz" ${currentUser.country === "Schweiz" ? "selected" : ""}>Schweiz</option>
<option value="Luxemburg" ${currentUser.country === "Luxemburg" ? "selected" : ""}>Luxemburg</option>
</select>
<span class="field-error"></span>
</label>
<label class="auth-field account-email-locked">
<span>E-Mail</span>
<input type="email" value="${escapeHtml(currentUser.email)}" disabled>
<small>Die E-Mail-Adresse ist fest mit deinem Konto verbunden.</small>
</label>
</div>
<div class="account-edit-actions">
<button type="button" class="account-secondary-btn" onclick="renderPersonalInfo()">Abbrechen</button>
<button type="button" class="account-save-btn" onclick="savePersonalInfo()">Speichern</button>
</div>
</div>
`;
return;
}
target.innerHTML = `
<div class="account-card account-personal-card">
<h3>Persönliche Daten</h3>
<div class="account-personal-grid">
<p><span>Anrede</span><strong>${escapeHtml(currentUser.salutation || "-")}</strong></p>
<p><span>Vorname</span><strong>${escapeHtml(currentUser.firstName || "-")}</strong></p>
<p><span>Nachname</span><strong>${escapeHtml(currentUser.lastName || "-")}</strong></p>
<p><span>Geburtstag</span><strong>${escapeHtml(currentUser.birthday || "-")}</strong></p>
<p><span>Straße</span><strong>${escapeHtml(currentUser.street || "-")}</strong></p>
<p><span>Nr.</span><strong>${escapeHtml(currentUser.houseNumber || "-")}</strong></p>
<p><span>PLZ</span><strong>${escapeHtml(currentUser.zip || "-")}</strong></p>
<p><span>Ort</span><strong>${escapeHtml(currentUser.city || "-")}</strong></p>
<p><span>Land</span><strong>${escapeHtml(currentUser.country || "-")}</strong></p>
<p><span>E-Mail</span><strong>${escapeHtml(currentUser.email || "-")}</strong></p>
</div>
</div>
`;
}
function savePersonalInfo() {
if (!currentUser) {
return;
}
const requiredFields = ["account-edit-firstname", "account-edit-lastname"];
const isValid = requiredFields
.map((id) => validateAccountField(document.getElementById(id)))
.every(Boolean);
if (!isValid) {
return;
}
currentUser.salutation = document.getElementById("account-edit-salutation")?.value || "";
currentUser.firstName = document.getElementById("account-edit-firstname")?.value.trim() || "";
currentUser.lastName = document.getElementById("account-edit-lastname")?.value.trim() || "";
currentUser.birthday = document.getElementById("account-edit-birthday")?.value || "";
currentUser.street = document.getElementById("account-edit-street")?.value.trim() || "";
currentUser.houseNumber = document.getElementById("account-edit-house-number")?.value.trim() || "";
currentUser.zip = document.getElementById("account-edit-zip")?.value.trim() || "";
currentUser.city = document.getElementById("account-edit-city")?.value.trim() || "";
currentUser.country = document.getElementById("account-edit-country")?.value || "Deutschland";
const userIndex = users.findIndex((user) => user.email === currentUser.email);
if (userIndex >= 0) {
users[userIndex] = currentUser;
}
persistUsers();
persistCurrentUser();
renderPersonalInfo();
}
function getFieldWrapper(input) {
if (!input) {
return null;
}
return input.closest(".auth-field") || document.querySelector(`[data-field-for="${input.id}"]`);
}
function setFieldError(input, message) {
const wrapper = getFieldWrapper(input);
if (!wrapper) {
return;
}
input.dataset.touched = "true";
wrapper.classList.add("is-invalid");
const error = wrapper.querySelector(".field-error");
if (error) {
error.textContent = message;
}
}
function clearFieldError(input) {
const wrapper = getFieldWrapper(input);
if (!wrapper) {
return;
}
wrapper.classList.remove("is-invalid");
const error = wrapper.querySelector(".field-error");
if (error) {
error.textContent = "";
}
}
function validateAccountField(input) {
if (!input) {
return true;
}
const value = (input.value || "").trim();
if (input.required && !value) {
setFieldError(input, input.dataset.requiredMessage || "Dieses Feld ist ein Pflichtfeld.");
return false;
}
if (input.type === "email" && value && !value.includes("@")) {
setFieldError(input, input.dataset.emailMessage || "Bitte gib eine gültige E-Mail-Adresse ein.");
return false;
}
if (input.type === "password" && value && (value.length < 8 || value.length > 32)) {
setFieldError(input, input.dataset.passwordMessage || "Passwort muss 8 bis 32 Zeichen haben.");
return false;
}
clearFieldError(input);
return true;
}
function validateRegisterForm() {
const fieldIds = [
"reg-salutation",
"reg-firstname",
"reg-lastname",
"reg-birthday",
"reg-street",
"reg-house-number",
"reg-zip",
"reg-city",
"reg-country",
"reg-email",
"reg-password"
];
return fieldIds
.map((id) => validateAccountField(document.getElementById(id)))
.every(Boolean);
}
function validateLoginForm() {
return ["login-email", "login-password"]
.map((id) => validateAccountField(document.getElementById(id)))
.every(Boolean);
}
function initAccountFieldValidation() {
const fieldSelector = [
"#login-email",
"#login-password",
"#reg-salutation",
"#reg-firstname",
"#reg-lastname",
"#reg-birthday",
"#reg-street",
"#reg-house-number",
"#reg-zip",
"#reg-city",
"#reg-country",
"#reg-email",
"#reg-password"
].join(",");
document.querySelectorAll(fieldSelector).forEach((input) => {
input.addEventListener("blur", () => {
input.dataset.touched = "true";
validateAccountField(input);
});
input.addEventListener("input", () => {
input.dataset.touched = "true";
if (input.type !== "hidden") {
validateAccountField(input);
}
});
input.addEventListener("change", () => {
input.dataset.touched = "true";
validateAccountField(input);
});
});
document.querySelectorAll(".gender-selector button").forEach((button) => {
button.addEventListener("click", () => {
const salutationInput = document.getElementById("reg-salutation");
if (!salutationInput) {
return;
}
salutationInput.value = button.dataset.gender || "";
salutationInput.dataset.touched = "true";
button.closest(".gender-selector")?.querySelectorAll("button").forEach((item) => {
item.classList.toggle("active", item === button);
});
validateAccountField(salutationInput);
});
});
}
function renderOrders() {
@@ -220,7 +530,7 @@ function renderOrders() {
target.innerHTML = `
<div class="account-orders-shell">
<h3>Meine Bestellungen</h3>
<p class="account-payments-note">Klicke auf eine Bestellung, um dein Ticket-Detail zu sehen.</p>
<p class="account-payments-note">Klicke auf eine Bestellung, um genauere Informationen zu sehen.</p>
<div class="account-orders-grid">${orderHtml}</div>
<div id="order-ticket-details" class="order-ticket-details hidden"></div>
</div>
@@ -293,7 +603,7 @@ function renderPayments() {
target.innerHTML = `
<div class="account-card">
<h3>Zahlungsmethoden</h3>
<p class="account-payments-note">Platzhalter zum Hinterlegen deiner Logos oder Anbieter-Informationen.</p>
<p class="account-payments-note">Hier kannst du deine bevorzugten Zahlungsmethoden verwalten.</p>
<div class="account-payment-grid">
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-card">
<div class="payment-logo-slot">
@@ -314,14 +624,14 @@ function renderPayments() {
<img src="img/applepay.png" alt="Apple Pay">
</div>
<h4>Apple Pay</h4>
<p>Geraet freischalten</p>
<p>Gerät freischalten</p>
</button>
<button type="button" class="account-payment-card account-pay-trigger" data-pay-modal="pay-modal-google">
<div class="payment-logo-slot">
<img src="img/googlepay.png" alt="Google Pay">
</div>
<h4>Google Pay</h4>
<p>Wallet verknuepfen</p>
<p>Wallet verknüpfen</p>
</button>
</div>
</div>

BIN
img/youtube-logo.png LFS Normal file

Binary file not shown.

View File

@@ -305,7 +305,7 @@
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Zoomania 2</h3>
<p class="snack-card-note">Sammlerbecher mit Popcornfüllung, wahlweise süß oder salzig.</p>
<p class="snack-card-note">Metall Sammelbecher im Zoomania 2 Design.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
@@ -343,7 +343,7 @@
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Scream VII</h3>
<p class="snack-card-note">Hallo Sydney! Ghostface in der Tür.</p>
<p class="snack-card-note">Hallo Sydney! Ghostface ist an der Tür.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
@@ -427,7 +427,7 @@
<div class="snack-info">
<span class="badge">Metallbecher</span>
<h3>Limitierter Metallbecher - Hoppers</h3>
<p class="snack-card-note">Stabiler Becher mit Popcornfüllung.</p>
<p class="snack-card-note">Metall Sammelbecher im HOPPERS Design.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
@@ -442,7 +442,7 @@
<div class="snack-info">
<span class="badge">Collector</span>
<h3>Limitierter Sammelbecher - Hoppers</h3>
<p class="snack-card-note">Sammlerbecher mit warmem Popcornmoment.</p>
<p class="snack-card-note">Limitierter Sammelbecher als Biberdamm - Mit verteckter Eidechse.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
@@ -480,7 +480,7 @@
<div class="snack-info">
<span class="badge">Yoshi Cup</span>
<h3>Limitierter Sammelbecher - Yoshi Becher</h3>
<p class="snack-card-note">Verspielter Sammlerbecher für Mario-Fans.</p>
<p class="snack-card-note">Limitierter Sammelbecher in der Form von Yoshi.</p>
<div class="option-group">
<button class="opt-btn active">Süß</button>
<button class="opt-btn">Salzig</button>
@@ -841,6 +841,7 @@
<div class="snack-card highlight">
<div class="snack-img"><img src="img/nachokombigross.png" alt="Nacho Kombi Groß"></div>
<div class="snack-info">
<span class="badge">Bestseller</span>
<h3>Nacho Menü Groß</h3>
<p class="snack-card-note">Nachos groß + 1 Dip + 1x 0,33L Getränk</p>
<div class="option-group">
@@ -985,8 +986,14 @@
<div class="account-login-box">
<h2>Mein Konto</h2>
<input type="email" id="login-email" placeholder="E-Mail">
<input type="password" id="login-password" placeholder="Passwort">
<label class="auth-field">
<input type="email" id="login-email" placeholder="E-Mail" required data-required-message="E-Mail ist ein Pflichtfeld." data-email-message="Bitte gib eine gültige E-Mail-Adresse ein.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<input type="password" id="login-password" placeholder="Passwort" required data-required-message="Passwort ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<button id="btn-forgot-password" type="button">Passwort vergessen?</button>
<div id="login-error" class="hidden">
@@ -1001,14 +1008,80 @@
<div id="register-modal" class="modal hidden">
<div class="modal-content account-auth-modal">
<div class="modal-content account-auth-modal register-auth-modal">
<button id="btn-close-register" class="modal-close-btn" type="button" aria-label="Registrierung schliessen">&times;</button>
<h2>Registrieren</h2>
<p class="auth-modal-subtitle">Erstelle dein Konto für schnellere Buchungen.</p>
<input type="text" id="reg-firstname" placeholder="Vorname">
<input type="text" id="reg-lastname" placeholder="Nachname">
<input type="email" id="reg-email" placeholder="E-Mail">
<input type="password" id="reg-password" placeholder="Passwort">
<div class="register-form-grid">
<section class="register-form-section">
<h3>Meine Daten</h3>
<div class="gender-field auth-field" data-field-for="reg-salutation">
<input type="hidden" id="reg-salutation" required data-required-message="Anrede ist ein Pflichtfeld.">
<div class="gender-selector" role="group" aria-label="Anrede">
<button type="button" data-gender="Frau">Frau</button>
<button type="button" data-gender="Herr">Herr</button>
<button type="button" data-gender="Divers">Divers</button>
</div>
<span class="field-error"></span>
</div>
<label class="auth-field">
<input type="text" id="reg-firstname" placeholder="Vorname" required data-required-message="Vorname ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<input type="text" id="reg-lastname" placeholder="Nachname" required data-required-message="Nachname ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<input type="date" id="reg-birthday" placeholder="Geburtstag" required data-required-message="Geburtstag ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<div class="register-row street-row">
<label class="auth-field">
<input type="text" id="reg-street" placeholder="Straße" required data-required-message="Straße ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<input type="text" id="reg-house-number" placeholder="Nr." required data-required-message="Hausnummer ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
</div>
<div class="register-row">
<label class="auth-field">
<input type="text" id="reg-zip" placeholder="PLZ" required data-required-message="PLZ ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<input type="text" id="reg-city" placeholder="Ort" required data-required-message="Ort ist ein Pflichtfeld.">
<span class="field-error"></span>
</label>
</div>
<label class="auth-field">
<select id="reg-country" required data-required-message="Land ist ein Pflichtfeld.">
<option value="Deutschland">Deutschland</option>
<option value="Österreich">Österreich</option>
<option value="Schweiz">Schweiz</option>
<option value="Luxemburg">Luxemburg</option>
</select>
<span class="field-error"></span>
</label>
</section>
<section class="register-form-section">
<h3>Anmeldeinformationen</h3>
<label class="auth-field">
<input type="email" id="reg-email" placeholder="E-Mail" required data-required-message="E-Mail ist ein Pflichtfeld." data-email-message="Bitte gib eine gültige E-Mail-Adresse ein.">
<span class="field-error"></span>
</label>
<label class="auth-field">
<input type="password" id="reg-password" placeholder="Passwort" required minlength="8" maxlength="32" data-required-message="Passwort ist ein Pflichtfeld." data-password-message="Passwort muss 8 bis 32 Zeichen haben.">
<span class="field-error"></span>
</label>
<p class="register-warning">Achte auf eine korrekte E-Mail-Adresse. An diese Adresse wird deine Buchungsbestätigung gesendet.</p>
<p class="register-warning">Passwort mit 8 bis 32 Zeichen.</p>
</section>
</div>
<button id="btn-register-save" class="auth-submit-btn" type="button">Konto erstellen</button>
</div>
</div>

361
main.js
View File

@@ -65,7 +65,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "6",
description: "In Walt Disney Animation Studios \"Zoomania 2\" geraten die tierischen Detektive Judy Hopps und Nick Wilde auf die rätselhafte Spur eines geheimnisvollen Reptils, das in Zoomania auftaucht und die Metropole völlig auf den Kopf stellt: Gary DeSnake! ",
poster: "img/Zoomania-2.jpg",
backdrop: "img/Zoomania-2.jpg"
backdrop: "img/Zoomania-2.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=RPE_6KbMgYU&pp=ygUZem9vbWFuaWEgMiB0cmFpbGVyIGdlcm1hbg%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Shelter",
@@ -74,7 +76,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "16",
description: "Michael Mason, ein untergetauchter Elite-Agent, lebt auf einer abgelegenen Insel in Schottland. Als er in einem schweren Sturm ein Mädchen vor dem Ertrinken rettet, setzt er damit eine Kette von Ereignissen in Gang, die sein Versteck enttarnen und ihn zurück in die Welt zwingen. Dort muss er nicht nur um das Überleben der Teenagerin kämpfen, sondern sich auch den Dämonen seiner Vergangenheit stellen…",
poster: "img/shelter.jpg",
backdrop: "img/shelter.jpg"
backdrop: "img/shelter.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=_DgEucbPMZI&pp=ygUWc2hlbHRlciB0cmFpbGVyIGdlcm1hbg%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Mutiny",
@@ -83,7 +87,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "?",
description: "Als sein milliardenschwerer Boss vor seinen Augen einem Mordkomplott zum Opfer fällt, wird Cole Reed (Jason Statham) für das Verbrechen verantwortlich gemacht. Im Bestreben dessen Tod zu rächen und die Täter zu überführen, gelangt Reed an Bord eines Frachters und stößt dabei auf eine internationale Verschwörung. Im Alleingang startet Cole eine gnadenlose Racheaktion auf hoher See…",
poster: "img/mutiny.jpg",
backdrop: "img/mutiny.jpg"
backdrop: "img/mutiny.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=DU2cBNrvHA4&pp=ygUObXV0aW55IHRyYWlsZXI%3D",
actors: ["Name 1", "Name 2", "Name 3"],
},
{
title: "Der Austronaut - Project Hail Mary",
@@ -92,7 +98,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "12",
description: "Der Naturwissenschaftslehrer Ryland Grace wacht eines Tages auf einem Raumschiff auf - Lichtjahre von zu Hause entfernt und ohne Erinnerung daran, wer er ist oder wie er dorthin gekommen ist.",
poster: "img/derAustronaut.jpg",
backdrop: "img/derAustronaut.jpg"
backdrop: "img/derAustronaut.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=-KXQttVDPyk&pp=ygUdZGVyIGFzdHJvbmF1dCB0cmFpbGVyIGRldXRzY2g%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Der Super Mario Galaxy Film",
@@ -101,7 +109,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "6",
description: "''Der Super Mario Galaxy Film'' ist der neue Animationsfilm, basierend auf der Welt von „Super Mario Bros.“!",
poster: "img/mariogalaxy.jpg",
backdrop: "img/mariogalaxy.jpg"
backdrop: "img/mariogalaxy.jpg",
trailerYoutubeId:"https://www.youtube.com/watch?v=RtoRPSh0jvM&pp=ygUrZGVyIHN1cGVyIG1hcmlvIGdhbGF4eSBmaWxtIHRyYWlsZXIgZGV1dHNjaA%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Hoppers",
@@ -110,7 +120,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "6",
description: "In Disney Pixars ''Hoppers'' hoppt Tierfreundin Mabel mithilfe neuester Technologie in einen Roboterbiber und macht die Tierwelt (un)sicher! ",
poster: "img/hoppers.jpg",
backdrop: "img/hoppers.jpg"
backdrop: "img/hoppers.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=WqTZZcPe5R0&pp=ygUPaG9wcGVycyB0cmFpbGVy",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Solo Mio",
@@ -119,7 +131,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "12",
description: "Nachdem er in Rom am Altar stehen gelassen wurde, beschließt ein Mann, allein in die Flitterwochen zu fahren.",
poster: "img/solomio.png",
backdrop: "img/solomio.png"
backdrop: "img/solomio.png",
trailerYoutubeId: "https://www.youtube.com/watch?v=tYtiinTvE3s&pp=ygUYc29sZSBtaW8gdHJhaWxlciBkZXV0c2No",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "The Mandalorian and Grogu",
@@ -128,7 +142,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "?",
description: "Während die Neue Republik daran arbeitet, alles zu schützen, wofür die Rebellion gekämpft hat, sichern sie sich die Unterstützung des legendären mandalorianischen Kopfgeldjägers Din Djarin und seines jungen Lehrlings Grogu. ",
poster: "img/mandalorian.jpeg",
backdrop: "img/mandalorian.jpeg"
backdrop: "img/mandalorian.jpeg",
trailerYoutubeId: "https://www.youtube.com/watch?v=cMJMtGccVfs&pp=ygUlbWFuZGFsb3JpYW4gYW5kIGdyb2d1IHRyYWlsZXIgZGV1dHNjaA%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Glennkill - Ein Schafskrimi",
@@ -137,7 +153,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "?",
description: "Schäfer George liest seinen geliebten Schafen jeden Abend Krimis vor und nimmt an, dass sie kein Wort davon verstehen. Doch als ein mysteriöser Vorfall das friedliche Leben auf der Farm durchbricht, wird den Schafen bewusst, dass sie selbst nun die Ermittlungen übernehmen müssen. Also folgen sie der Spur der Beweise und machen menschliche Verdächtige ausfindig.",
poster: "img/glennkill.jpg",
backdrop: "img/glennkill.jpg"
backdrop: "img/glennkill.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=Ylu6xVTHAlk&pp=ygUZZ2xlbm5raWxsIGVpbiBzY2hhZnNrcmltaQ%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Masters of the Universe",
@@ -146,7 +164,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "?",
description: "Nach 15 Jahren der Trennung führt das Schwert der Macht Prinz Adam zurück nach Eternia und er entdeckt, dass seine Heimat unter der Herrschaft von Skeletor in Trümmern liegt.",
poster: "img/masteruniverse.jpg",
backdrop: "img/masteruniverse.jpg"
backdrop: "img/masteruniverse.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=COJU2b4XZ8U&pp=ygUfbWFzdGVycyBvZiB0aGUgdW5pdmVyc2UgdHJhaWxlcg%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Minions and Monsters",
@@ -155,7 +175,9 @@ document.addEventListener("DOMContentLoaded", () => {
fsk: "?",
description: "MINIONS & MONSTER erzählt die abgefahrene, aberwitzige und natürlich absolut wahre Geschichte von den Minions und wie sie Hollywood erobern, Filmstars werden, alles verlieren, Monster auf die Welt loslassen und sich dann heldenhaft daranmachen, den Planeten vor genau dem Chaos zu retten, das sie selbst angerichtet haben. ",
poster: "img/minionsmonsters.jpg",
backdrop: "img/minionsmonsters.jpg"
backdrop: "img/minionsmonsters.jpg",
trailerYoutubeId: "https://www.youtube.com/watch?v=a31E7qNxwtI&pp=ygUcbWluaW9ucyBhbmQgbW9uc3RlcnMgdHJhaWxlctIHCQkECwGHKiGM7w%3D%3D",
actors: ["Name 1", "Name 2", "Name 3"]
},
{
title: "Spider Man - Brand New Day",
@@ -334,6 +356,10 @@ document.addEventListener("DOMContentLoaded", () => {
let heroItems = [];
let heroIndex = 0;
let heroTimer = null;
let activeMovieDetail = {
index: null,
sourceCard: null
};
const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
@@ -437,6 +463,29 @@ document.addEventListener("DOMContentLoaded", () => {
`;
};
const getMovieDurationLabel = (movie) => {
const duration = movie?.duration ?? "";
return Number.isFinite(Number(duration)) ? `${duration} Min.` : escapeHtml(duration || "tba");
};
const getYoutubeVideoId = (input) => {
const value = String(input || "").trim();
if (!value) {
return "";
}
const watchMatch = value.match(/[?&]v=([^&]+)/);
const shortMatch = value.match(/youtu\.be\/([^?&/]+)/);
const embedMatch = value.match(/embed\/([^?&/]+)/);
return decodeURIComponent(watchMatch?.[1] || shortMatch?.[1] || embedMatch?.[1] || value);
};
const getYoutubeThumbnailUrl = (videoId) => {
const id = getYoutubeVideoId(videoId);
return id ? `https://i.ytimg.com/vi/${encodeURIComponent(id)}/hqdefault.jpg` : "";
};
const formatDateShort = (dateObj) => {
const day = String(dateObj.getDate()).padStart(2, "0");
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
@@ -499,6 +548,9 @@ document.addEventListener("DOMContentLoaded", () => {
const buildMovieProgram = () => {
movieProgram = movieCatalog.map((movie, movieIndex) => ({
...movie,
trailerYoutubeId: getYoutubeVideoId(movie.trailerYoutubeId || movie.trailerUrl || ""),
trailerThumbnail: getYoutubeThumbnailUrl(movie.trailerYoutubeId || movie.trailerUrl || "") || movie.backdrop || movie.poster,
actors: Array.isArray(movie.actors) ? movie.actors : [],
schedule: buildScheduleForMovie(movieIndex)
}));
heroItems = movieProgram.slice(0, 5);
@@ -575,7 +627,7 @@ document.addEventListener("DOMContentLoaded", () => {
}
ui.nowRunningRow.innerHTML = movieProgram.map((movie, index) => `
<article class="running-poster">
<article class="running-poster" data-program-index="${index}" tabindex="0" role="button" aria-label="Details zu ${escapeHtml(movie.title)} öffnen">
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
<div class="running-meta">
<h4>${escapeHtml(movie.title)}</h4>
@@ -637,7 +689,7 @@ document.addEventListener("DOMContentLoaded", () => {
<div class="card-right">
<div class="card-header">
<h2>${escapeHtml(movie.title)}</h2>
<span class="duration movie-meta-line">${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)}</span>
<span class="duration movie-meta-line">${getMovieDurationLabel(movie)} | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)}</span>
</div>
<div class="program-halls-summary">
<strong>Kinosaal</strong>
@@ -663,6 +715,169 @@ document.addEventListener("DOMContentLoaded", () => {
});
};
const ensureMovieDetailOverlay = () => {
let overlay = document.getElementById("movie-detail-overlay");
if (overlay) {
return overlay;
}
overlay = document.createElement("div");
overlay.id = "movie-detail-overlay";
overlay.className = "movie-detail-overlay hidden";
document.body.appendChild(overlay);
return overlay;
};
const renderMovieDetailScheduleRows = (programIndex, dayIndex) => {
const movie = movieProgram[programIndex];
const day = movie?.schedule?.[dayIndex];
const body = document.getElementById("movie-detail-schedule-body");
if (!movie || !day || !body) {
return;
}
body.innerHTML = day.showings.map((showing) => `
<button class="schedule-row time-chip program-time-row movie-detail-time-row" data-movie="${escapeHtml(movie.title)}" data-hall="${escapeHtml(showing.hall)}" data-time="${escapeHtml(showing.time)}">
<span>${escapeHtml(day.long)}</span>
<span class="hall-pill">${escapeHtml(showing.hall)}</span>
<span class="time-btn">${escapeHtml(showing.time)}</span>
</button>
`).join("");
};
const buildMovieDetailContent = (movie, programIndex) => {
const trailerId = getYoutubeVideoId(movie.trailerYoutubeId);
const trailerThumb = getYoutubeThumbnailUrl(trailerId) || movie.backdrop || movie.poster;
const actorMarkup = movie.actors.length
? movie.actors.map((actor) => `<span>${escapeHtml(actor)}</span>`).join("")
: `<p>Noch keine Schauspieler hinterlegt.</p>`;
const dayTabs = movie.schedule.map((day, dayIndex) => `
<button type="button" class="program-day-tab movie-detail-day-tab ${dayIndex === 0 ? "active" : ""}" data-program-index="${programIndex}" data-day-index="${dayIndex}">
<span>${escapeHtml(day.short)}</span>
<small>${escapeHtml(formatDateShort(day.date))}</small>
</button>
`).join("");
return `
<div class="movie-detail-stage">
<button type="button" class="movie-detail-close" aria-label="Filmdetails schließen">&times;</button>
<div class="movie-detail-backdrop" style="background-image: linear-gradient(180deg, rgba(4, 6, 10, 0.28), #05070a 78%), url('${escapeHtml(movie.backdrop || movie.poster)}');"></div>
<div class="movie-detail-content">
<section class="movie-detail-hero-block">
<div class="movie-detail-poster">
<img src="${escapeHtml(movie.poster)}" alt="${escapeHtml(movie.title)}">
</div>
<div class="movie-detail-copy">
<p class="movie-detail-kicker">Aktueller Film</p>
<h2>${escapeHtml(movie.title)}</h2>
<div class="movie-detail-meta movie-meta-line">
<span>${getMovieDurationLabel(movie)}</span>
<span>${escapeHtml(movie.genre)}</span>
${renderFskMetaLogo(movie)}
</div>
<p>${escapeHtml(movie.description)}</p>
</div>
</section>
<section class="movie-detail-section">
<div class="movie-detail-section-head movie-detail-trailer-head">
<h3>Trailer</h3>
<span class="movie-detail-youtube-logo"><img src="img/youtube-logo.png" alt="YouTube Logo"></span>
</div>
<div class="movie-trailer-frame">
<button type="button" class="movie-trailer-preview" data-trailer-id="${escapeHtml(trailerId)}" style="background-image: linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.42)), url('${escapeHtml(trailerThumb)}');">
<span class="movie-trailer-play" aria-hidden="true"></span>
<strong>Trailer ansehen</strong>
</button>
</div>
</section>
<section class="movie-detail-section">
<div class="movie-detail-section-head">
<h3>Schauspieler</h3>
<span>Cast</span>
</div>
<div class="movie-actor-list">${actorMarkup}</div>
</section>
<section class="movie-detail-section movie-detail-schedule">
<div class="movie-detail-section-head">
<h3>Spielzeiten</h3>
<span>Nur für diesen Film</span>
</div>
<div class="program-day-tabs">${dayTabs}</div>
<div class="schedule-container program-schedule-shell">
<div class="schedule-header">
<span>Tag</span><span>Kinosaal</span><span>Uhrzeit</span>
</div>
<div id="movie-detail-schedule-body" class="program-schedule-body"></div>
</div>
</section>
</div>
</div>
`;
};
const closeMovieDetail = () => {
const overlay = document.getElementById("movie-detail-overlay");
if (!overlay || overlay.classList.contains("hidden")) {
return;
}
const targetCard = activeMovieDetail.sourceCard?.isConnected ? activeMovieDetail.sourceCard : null;
const rect = targetCard?.getBoundingClientRect();
overlay.classList.remove("is-open");
overlay.classList.add("is-closing");
if (rect) {
overlay.style.left = `${rect.left}px`;
overlay.style.top = `${rect.top}px`;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;
overlay.style.borderRadius = getComputedStyle(targetCard).borderRadius || "20px";
}
setTimeout(() => {
overlay.className = "movie-detail-overlay hidden";
overlay.removeAttribute("style");
overlay.innerHTML = "";
document.body.style.overflow = "auto";
activeMovieDetail = { index: null, sourceCard: null };
}, 430);
};
const openMovieDetail = (programIndex, sourceCard) => {
const movie = movieProgram[programIndex];
if (!movie || !sourceCard) {
return;
}
const overlay = ensureMovieDetailOverlay();
const rect = sourceCard.getBoundingClientRect();
activeMovieDetail = { index: programIndex, sourceCard };
overlay.innerHTML = buildMovieDetailContent(movie, programIndex);
overlay.className = "movie-detail-overlay";
overlay.style.left = `${rect.left}px`;
overlay.style.top = `${rect.top}px`;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;
overlay.style.borderRadius = getComputedStyle(sourceCard).borderRadius || "20px";
document.body.style.overflow = "hidden";
renderMovieDetailScheduleRows(programIndex, 0);
requestAnimationFrame(() => {
overlay.classList.add("is-open");
overlay.style.left = "0px";
overlay.style.top = "0px";
overlay.style.width = "100vw";
overlay.style.height = "100vh";
overlay.style.borderRadius = "0px";
});
};
const initRevealAnimations = () => {
const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll"));
if (!revealElements.length) {
@@ -749,20 +964,41 @@ document.addEventListener("DOMContentLoaded", () => {
const bindProgramActions = () => {
views.moviesGrid?.addEventListener("click", (event) => {
const trigger = event.target.closest(".open-program-btn");
if (!trigger) {
if (trigger) {
const programIndex = Number(trigger.dataset.programIndex || 0);
showMovieList(programIndex);
return;
}
const programIndex = Number(trigger.dataset.programIndex || 0);
showMovieList(programIndex);
const runningCard = event.target.closest(".running-poster");
if (runningCard) {
const programIndex = Number(runningCard.dataset.programIndex || 0);
openMovieDetail(programIndex, runningCard);
}
});
views.moviesGrid?.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
if (event.target.closest(".open-program-btn")) {
return;
}
const runningCard = event.target.closest(".running-poster");
if (!runningCard) {
return;
}
event.preventDefault();
const programIndex = Number(runningCard.dataset.programIndex || 0);
openMovieDetail(programIndex, runningCard);
});
ui.movieProgramList?.addEventListener("click", (event) => {
const dayButton = event.target.closest(".program-day-tab");
if (!dayButton) {
return;
}
if (dayButton) {
const programIndex = Number(dayButton.dataset.programIndex || 0);
const dayIndex = Number(dayButton.dataset.dayIndex || 0);
@@ -771,6 +1007,64 @@ document.addEventListener("DOMContentLoaded", () => {
dayButton.classList.add("active");
renderScheduleRows(programIndex, dayIndex);
return;
}
if (event.target.closest("button, a, input, select, textarea")) {
return;
}
const movieCard = event.target.closest(".program-card");
if (!movieCard) {
return;
}
const programIndex = Number(movieCard.dataset.programIndex || 0);
openMovieDetail(programIndex, movieCard);
});
};
const bindMovieDetailOverlay = () => {
document.addEventListener("click", (event) => {
const overlay = event.target.closest("#movie-detail-overlay");
if (!overlay) {
return;
}
if (event.target.closest(".movie-detail-close")) {
closeMovieDetail();
return;
}
const dayButton = event.target.closest(".movie-detail-day-tab");
if (dayButton) {
const dayIndex = Number(dayButton.dataset.dayIndex || 0);
overlay.querySelectorAll(".movie-detail-day-tab").forEach((tab) => tab.classList.remove("active"));
dayButton.classList.add("active");
renderMovieDetailScheduleRows(activeMovieDetail.index, dayIndex);
return;
}
const trailerPreview = event.target.closest(".movie-trailer-preview");
if (trailerPreview) {
const trailerId = getYoutubeVideoId(trailerPreview.dataset.trailerId);
const frame = trailerPreview.closest(".movie-trailer-frame");
if (!trailerId || !frame) {
trailerPreview.classList.add("is-missing");
trailerPreview.querySelector("strong").textContent = "Trailer-ID in main.js hinterlegen";
return;
}
frame.innerHTML = `
<iframe src="https://www.youtube-nocookie.com/embed/${encodeURIComponent(trailerId)}?autoplay=1&rel=0" title="YouTube Trailer" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
`;
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeMovieDetail();
}
});
};
@@ -872,6 +1166,10 @@ document.addEventListener("DOMContentLoaded", () => {
const loginEmailInput = document.getElementById("login-email");
const loginPasswordInput = document.getElementById("login-password");
if (typeof initAccountFieldValidation === "function") {
initAccountFieldValidation();
}
const openModal = (modal) => modal?.classList.remove("hidden");
const closeModal = (modal) => modal?.classList.add("hidden");
const triggerLogin = () => {
@@ -882,6 +1180,24 @@ document.addEventListener("DOMContentLoaded", () => {
};
document.getElementById("btn-open-register")?.addEventListener("click", () => {
registerModal?.querySelectorAll("input, select").forEach((input) => {
if (input.type !== "hidden" && input.tagName !== "SELECT") {
input.value = "";
}
if (input.tagName === "SELECT") {
input.value = "Deutschland";
}
if (input.id === "reg-salutation") {
input.value = "";
}
input.dataset.touched = "";
if (typeof clearFieldError === "function") {
clearFieldError(input);
}
});
registerModal?.querySelectorAll(".gender-selector button").forEach((button) => {
button.classList.remove("active");
});
openModal(registerModal);
});
@@ -898,6 +1214,10 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("btn-login-account")?.addEventListener("click", triggerLogin);
[loginEmailInput, loginPasswordInput].forEach((input) => {
input?.addEventListener("input", () => {
loginError?.classList.add("hidden");
});
input?.addEventListener("keydown", (event) => {
if (event.key !== "Enter") {
return;
@@ -1134,6 +1454,7 @@ document.addEventListener("DOMContentLoaded", () => {
initThemeToggle();
bindNavigation();
bindProgramActions();
bindMovieDetailOverlay();
bindHomeInfoNavigation();
bindAccountActions();
bindGlobalDocumentClicks();

721
style.css
View File

@@ -4844,3 +4844,724 @@ body.theme-light .limited-special-card.wide-special .snack-img img {
min-height: 176px;
}
}
/* --- Account registration form refresh --- */
.auth-field {
display: block;
width: 100%;
margin-bottom: 12px;
}
.auth-field input,
.auth-field select {
width: 100%;
margin-bottom: 0;
}
.auth-field .field-error {
min-height: 16px;
display: block;
margin-top: 5px;
color: #ff6d7d;
font-size: 0.76rem;
line-height: 1.25;
}
.auth-field.is-invalid input,
.auth-field.is-invalid select,
.auth-field.is-invalid .gender-selector {
border-color: rgba(255, 84, 105, 0.88) !important;
box-shadow: 0 0 0 3px rgba(255, 84, 105, 0.12);
}
#login-error {
margin: 0 0 8px;
padding: 9px 11px;
border: 1px solid rgba(255, 84, 105, 0.35);
border-radius: 10px;
background: rgba(255, 84, 105, 0.1);
color: #ff8795;
font-size: 0.82rem;
}
.register-auth-modal {
width: min(1180px, 94vw);
max-width: 1180px;
max-height: 88vh;
overflow-y: auto;
padding: 28px 30px 24px;
}
.register-form-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 22px;
margin-top: 16px;
}
.register-form-section {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.035);
border-radius: 14px;
padding: 16px;
}
.register-form-section h3 {
margin-bottom: 14px;
font-size: 1rem;
color: #f7faff;
}
.register-form-section input,
.register-form-section select {
min-height: 48px;
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
padding: 10px 2px;
color: #f5f7fb;
}
.register-form-section select {
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, #9aa7ba 50%), linear-gradient(135deg, #9aa7ba 50%, transparent 50%);
background-position: calc(100% - 18px) 21px, calc(100% - 12px) 21px;
background-size: 6px 6px, 6px 6px;
background-repeat: no-repeat;
}
.register-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 18px;
}
.street-row {
grid-template-columns: minmax(0, 1fr) 110px;
}
.gender-selector {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 12px;
overflow: hidden;
background: rgba(255, 255, 255, 0.04);
}
.gender-selector button {
min-height: 52px;
border: 0;
border-right: 1px solid rgba(255, 255, 255, 0.16);
background: transparent;
color: #f8fbff;
font-weight: 700;
cursor: pointer;
}
.gender-selector button:last-child {
border-right: 0;
}
.gender-selector button.active {
background: rgba(0, 113, 227, 0.28);
color: #ffffff;
}
.register-warning {
color: #8f98a7;
font-size: 0.84rem;
line-height: 1.45;
margin-top: 10px;
}
.account-personal-card h3 {
margin-bottom: 12px;
}
.account-personal-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
}
.account-personal-grid p {
margin: 0;
padding: 11px 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
background: rgba(255, 255, 255, 0.035);
}
.account-personal-grid span {
display: block;
margin-bottom: 4px;
color: #91a0b4;
font-size: 0.72rem;
}
.account-personal-grid strong {
color: #f6f9ff;
font-size: 0.92rem;
}
body.theme-light .auth-field.is-invalid input,
body.theme-light .auth-field.is-invalid select,
body.theme-light .auth-field.is-invalid .gender-selector {
border-color: rgba(216, 42, 66, 0.82) !important;
box-shadow: 0 0 0 3px rgba(216, 42, 66, 0.1);
}
body.theme-light .register-form-section {
background: #f7faff;
border-color: rgba(28, 56, 94, 0.14);
}
body.theme-light .register-form-section h3 {
color: #10213a;
}
body.theme-light .register-form-section input,
body.theme-light .register-form-section select {
background: transparent !important;
color: #10213a !important;
border-color: rgba(33, 55, 91, 0.22) !important;
}
body.theme-light .gender-selector {
background: #eef4ff;
border-color: rgba(28, 56, 94, 0.16);
}
body.theme-light .gender-selector button {
color: #1b2f4d;
border-color: rgba(28, 56, 94, 0.16);
}
body.theme-light .gender-selector button.active {
background: #dcecff;
color: #0057ae;
}
body.theme-light .register-warning {
color: #5a6c86;
}
body.theme-light .account-personal-grid p {
background: #f7faff;
border-color: rgba(28, 56, 94, 0.12);
}
body.theme-light .account-personal-grid span {
color: #52647f;
}
body.theme-light .account-personal-grid strong {
color: #10213a;
}
body.theme-light .auth-field.is-invalid input,
body.theme-light .auth-field.is-invalid select,
body.theme-light .auth-field.is-invalid .gender-selector,
body.theme-light .register-form-section .auth-field.is-invalid input,
body.theme-light .register-form-section .auth-field.is-invalid select {
border-color: rgba(216, 42, 66, 0.82) !important;
box-shadow: 0 0 0 3px rgba(216, 42, 66, 0.1);
}
@media (max-width: 860px) {
.register-form-grid {
grid-template-columns: 1fr;
}
.register-auth-modal {
padding: 24px 18px 20px;
}
}
@media (max-width: 560px) {
.register-row,
.street-row {
grid-template-columns: 1fr;
gap: 0;
}
}
/* --- Account edit and movie detail overlay --- */
.account-header-actions {
display: inline-flex;
align-items: center;
gap: 10px;
}
.account-edit-btn {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
color: #ffffff;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.account-edit-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 113, 227, 0.75);
background: rgba(0, 113, 227, 0.2);
}
.account-edit-btn svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.account-card-title-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.account-card-title-row h3 {
margin: 0;
}
.account-card-title-row span {
color: #8fa0b6;
font-size: 0.82rem;
}
.account-edit-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 12px 16px;
}
.account-edit-form .auth-field {
margin-bottom: 0;
}
.account-edit-form .auth-field > span:first-child,
.account-email-locked > span:first-child {
display: block;
margin-bottom: 6px;
color: #91a0b4;
font-size: 0.78rem;
font-weight: 700;
}
.account-edit-form input,
.account-edit-form select {
min-height: 46px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
color: #f6f9ff;
padding: 10px 12px;
}
.account-email-locked input:disabled {
opacity: 0.72;
cursor: not-allowed;
}
.account-email-locked small {
display: block;
margin-top: 6px;
color: #7e8da1;
font-size: 0.74rem;
}
.account-edit-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 18px;
}
.account-save-btn,
.account-secondary-btn {
border: 0;
border-radius: 999px;
padding: 10px 16px;
font-weight: 800;
cursor: pointer;
}
.account-save-btn {
background: #0071e3;
color: #ffffff;
}
.account-secondary-btn {
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
}
.program-card,
.running-poster {
cursor: pointer;
}
.program-card:focus-within,
.running-poster:focus-visible {
outline: 2px solid rgba(0, 113, 227, 0.8);
outline-offset: 4px;
}
.movie-detail-overlay {
position: fixed;
z-index: 10000;
overflow: hidden;
background: #05070a;
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.5);
transition: left 0.46s cubic-bezier(0.2, 0.82, 0.2, 1),
top 0.46s cubic-bezier(0.2, 0.82, 0.2, 1),
width 0.46s cubic-bezier(0.2, 0.82, 0.2, 1),
height 0.46s cubic-bezier(0.2, 0.82, 0.2, 1),
border-radius 0.46s cubic-bezier(0.2, 0.82, 0.2, 1),
opacity 0.28s ease;
}
.movie-detail-overlay.hidden {
display: none;
}
.movie-detail-overlay.is-closing {
opacity: 0.2;
}
.movie-detail-stage {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
opacity: 0;
transform: scale(0.985);
transition: opacity 0.25s ease 0.12s, transform 0.32s ease 0.1s;
}
.movie-detail-overlay.is-open .movie-detail-stage {
opacity: 1;
transform: scale(1);
}
.movie-detail-backdrop {
position: absolute;
inset: 0 0 auto;
height: min(620px, 76vh);
background-size: cover;
background-position: center;
filter: saturate(1.05);
}
.movie-detail-content {
position: relative;
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 112px 0 70px;
}
.movie-detail-close {
position: fixed;
top: 22px;
right: 24px;
z-index: 2;
width: 42px;
height: 42px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%;
background: rgba(12, 14, 18, 0.76);
color: #ffffff;
font-size: 1.6rem;
line-height: 1;
cursor: pointer;
backdrop-filter: blur(18px);
}
.movie-detail-hero-block {
min-height: 520px;
display: grid;
grid-template-columns: minmax(190px, 300px) minmax(0, 1fr);
align-items: end;
gap: clamp(24px, 5vw, 58px);
}
.movie-detail-poster img {
width: 100%;
aspect-ratio: 2 / 3;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 28px 70px rgba(0, 0, 0, 0.55);
}
.movie-detail-copy {
padding-bottom: 22px;
}
.movie-detail-kicker {
margin-bottom: 8px;
color: #9bc8ff;
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
}
.movie-detail-copy h2 {
max-width: 820px;
margin: 0 0 14px;
font-size: clamp(2.1rem, 5vw, 5.2rem);
line-height: 0.96;
}
.movie-detail-meta {
gap: 10px;
margin-bottom: 18px;
color: #dce7f6;
font-weight: 800;
}
.movie-detail-copy > p:last-child {
max-width: 760px;
color: #d5dbe5;
font-size: 1rem;
line-height: 1.65;
}
.movie-detail-section {
margin-top: 28px;
padding-top: 28px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.movie-detail-section-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.movie-detail-section-head h3 {
margin: 0;
font-size: 1.35rem;
}
.movie-detail-section-head span {
color: #8d98a8;
font-size: 0.82rem;
font-weight: 800;
text-transform: uppercase;
}
.movie-detail-youtube-logo {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.movie-detail-youtube-logo img {
width: 34px;
max-height: 18px;
object-fit: contain;
display: block;
}
.movie-trailer-frame {
aspect-ratio: 16 / 9;
width: min(920px, 100%);
background: #0b0d11;
border-radius: 8px;
overflow: hidden;
}
.movie-trailer-preview {
width: 100%;
height: 100%;
border: 0;
display: grid;
place-items: center;
align-content: center;
gap: 14px;
background-size: cover;
background-position: center;
color: #ffffff;
cursor: pointer;
}
.movie-trailer-preview strong {
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.78);
}
.movie-trailer-preview.is-missing {
filter: grayscale(0.4);
}
.movie-trailer-play {
width: 74px;
height: 74px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.92);
position: relative;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
}
.movie-trailer-play::after {
content: "";
position: absolute;
left: 30px;
top: 23px;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: 20px solid #0a0d12;
}
.movie-trailer-frame iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
}
.movie-actor-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.movie-actor-list span {
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.06);
color: #eef4ff;
font-weight: 700;
}
.movie-actor-list p {
margin: 0;
color: #8f9caf;
}
.movie-detail-schedule {
padding-bottom: 20px;
}
.movie-detail-time-row:hover {
transform: none;
}
body.theme-light .account-edit-btn,
body.theme-light .account-secondary-btn {
background: #eef4ff;
border-color: rgba(28, 56, 94, 0.16);
color: #10213a;
}
body.theme-light .account-card-title-row span,
body.theme-light .account-edit-form .auth-field > span:first-child,
body.theme-light .account-email-locked > span:first-child,
body.theme-light .account-email-locked small {
color: #52647f;
}
body.theme-light .account-edit-form input,
body.theme-light .account-edit-form select {
background: #ffffff;
border-color: rgba(28, 56, 94, 0.16);
color: #10213a;
}
body.theme-light .movie-detail-overlay {
background: #f4f7fb;
}
body.theme-light .movie-detail-backdrop {
filter: saturate(1.05) brightness(1.08);
}
body.theme-light .movie-detail-copy > p:last-child,
body.theme-light .movie-detail-meta,
body.theme-light .movie-actor-list span {
color: #10213a;
}
body.theme-light .movie-detail-copy h2,
body.theme-light .movie-detail-copy > p:last-child,
body.theme-light .movie-detail-meta {
color: #ffffff;
}
body.theme-light .movie-detail-section {
border-color: rgba(28, 56, 94, 0.14);
}
body.theme-light .movie-actor-list span {
background: #ffffff;
border-color: rgba(28, 56, 94, 0.14);
}
@media (max-width: 760px) {
.account-panel-header,
.account-card-title-row {
align-items: flex-start;
flex-direction: column;
}
.account-edit-actions {
justify-content: stretch;
}
.account-save-btn,
.account-secondary-btn {
flex: 1;
}
.movie-detail-content {
width: min(100% - 24px, 560px);
padding-top: 78px;
}
.movie-detail-hero-block {
min-height: auto;
grid-template-columns: 1fr;
gap: 18px;
}
.movie-detail-poster {
width: min(210px, 62vw);
}
.movie-detail-copy {
padding-bottom: 0;
}
.movie-detail-copy h2 {
font-size: clamp(2rem, 12vw, 3.8rem);
}
.movie-detail-section-head {
align-items: flex-start;
flex-direction: column;
}
.movie-detail-trailer-head {
align-items: center;
flex-direction: row;
}
.movie-trailer-frame {
width: 100%;
}
}