🎉 initial commit

This commit is contained in:
Lukas Harat
2026-08-15 12:50:22 +02:00
commit cd04ebe254
31 changed files with 5423 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
import { Link } from '@tanstack/react-router'
export function SiteFooter() {
return (
<footer className="site-footer">
<span className="wordmark">
Steakhaus <span>bei Mirko</span>
</span>
Meppener Straße 8 · 49716 Meppen · 05931 - 12750
<nav className="footer-nav">
<Link to="/">Startseite</Link>
<Link to="/impressum">Impressum</Link>
</nav>
</footer>
)
}
+30
View File
@@ -0,0 +1,30 @@
import { Link } from '@tanstack/react-router'
export function SiteHeader() {
return (
<header className="site-header">
<Link className="wordmark" to="/">
Steakhaus <span>bei Mirko</span>
</Link>
<nav>
<ul className="site-nav">
<li>
<Link to="/" hash="reservieren">
Reservieren
</Link>
</li>
<li>
<Link to="/" hash="oeffnungszeiten">
Öffnungszeiten
</Link>
</li>
<li>
<Link to="/" hash="kontakt">
Kontakt
</Link>
</li>
</ul>
</nav>
</header>
)
}
+14
View File
@@ -0,0 +1,14 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
const connectionString = process.env.DATABASE_URL
if (!connectionString) {
throw new Error('DATABASE_URL environment variable is not set')
}
const client = postgres(connectionString)
export const db = drizzle(client, { schema })
+27
View File
@@ -0,0 +1,27 @@
import { integer, pgTable, serial, text, timestamp, unique } from 'drizzle-orm/pg-core'
export const restaurantTables = pgTable('restaurant_tables', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
seats: integer('seats').notNull(),
})
export const reservations = pgTable(
'reservations',
{
id: serial('id').primaryKey(),
tableId: integer('table_id')
.notNull()
.references(() => restaurantTables.id, { onDelete: 'cascade' }),
customerName: text('customer_name').notNull(),
customerEmail: text('customer_email').notNull(),
partySize: integer('party_size').notNull(),
reservationTime: timestamp('reservation_time', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [unique().on(table.tableId, table.reservationTime)],
)
export type RestaurantTable = typeof restaurantTables.$inferSelect
export type Reservation = typeof reservations.$inferSelect
export type NewReservation = typeof reservations.$inferInsert
+26
View File
@@ -0,0 +1,26 @@
import 'dotenv/config'
import { db } from './index'
import { restaurantTables } from './schema'
async function seed() {
const tables = await db
.insert(restaurantTables)
.values([
{ name: 'Tisch 1', seats: 2 },
{ name: 'Tisch 2', seats: 2 },
{ name: 'Tisch 3', seats: 4 },
{ name: 'Tisch 4', seats: 4 },
{ name: 'Tisch 5', seats: 6 },
{ name: 'Tisch 6', seats: 8 },
])
.returning()
console.log(`Seeded ${tables.length} tables.`)
process.exit(0)
}
seed().catch((error) => {
console.error(error)
process.exit(1)
})
+86
View File
@@ -0,0 +1,86 @@
import { createServerFn } from '@tanstack/react-start'
import { and, eq } from 'drizzle-orm'
import { z } from 'zod'
import { db } from '#/db'
import { reservations, restaurantTables } from '#/db/schema'
export const getTables = createServerFn({ method: 'GET' }).handler(async () => {
return db.select().from(restaurantTables).orderBy(restaurantTables.name)
})
export const getReservations = createServerFn({ method: 'GET' }).handler(async () => {
return db
.select({
id: reservations.id,
tableId: reservations.tableId,
tableName: restaurantTables.name,
customerName: reservations.customerName,
customerEmail: reservations.customerEmail,
partySize: reservations.partySize,
reservationTime: reservations.reservationTime,
})
.from(reservations)
.innerJoin(restaurantTables, eq(reservations.tableId, restaurantTables.id))
.orderBy(reservations.reservationTime)
})
const createReservationSchema = z.object({
tableId: z.number().int().positive(),
customerName: z.string().trim().min(1, 'Name is required').max(120),
customerEmail: z.string().trim().email('Enter a valid email'),
partySize: z.number().int().min(1, 'At least 1 guest').max(20),
reservationTime: z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
message: 'Enter a valid date and time',
}),
})
export const createReservation = createServerFn({ method: 'POST' })
.validator(createReservationSchema)
.handler(async ({ data }) => {
const reservationTime = new Date(data.reservationTime)
if (reservationTime.getTime() < Date.now()) {
throw new Error('Reservation time must be in the future')
}
const [table] = await db
.select()
.from(restaurantTables)
.where(eq(restaurantTables.id, data.tableId))
if (!table) {
throw new Error('Selected table does not exist')
}
if (data.partySize > table.seats) {
throw new Error(`${table.name} only seats up to ${table.seats} guests`)
}
const [conflict] = await db
.select()
.from(reservations)
.where(
and(
eq(reservations.tableId, data.tableId),
eq(reservations.reservationTime, reservationTime),
),
)
if (conflict) {
throw new Error('This table is already booked for that time')
}
const [reservation] = await db
.insert(reservations)
.values({
tableId: data.tableId,
customerName: data.customerName,
customerEmail: data.customerEmail,
partySize: data.partySize,
reservationTime,
})
.returning()
return reservation
})
+86
View File
@@ -0,0 +1,86 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ImpressumRouteImport } from './routes/impressum'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ImpressumRoute = ImpressumRouteImport.update({
id: '/impressum',
path: '/impressum',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/impressum': typeof ImpressumRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/impressum': typeof ImpressumRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/impressum': typeof ImpressumRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/impressum'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/impressum'
id: '__root__' | '/' | '/impressum'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ImpressumRoute: typeof ImpressumRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/impressum': {
id: '/impressum'
path: '/impressum'
fullPath: '/impressum'
preLoaderRoute: typeof ImpressumRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ImpressumRoute: ImpressumRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
+19
View File
@@ -0,0 +1,19 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
+47
View File
@@ -0,0 +1,47 @@
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import appCss from '../styles.css?url'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'Steakhaus bei Mirko | Meppen',
},
{
name: 'description',
content:
'Steakhaus bei Mirko in Meppen – Steaks vom Grill, gemütliche Atmosphäre. Reservieren Sie Ihren Tisch online.',
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
+81
View File
@@ -0,0 +1,81 @@
import { createFileRoute } from '@tanstack/react-router'
import { SiteFooter } from '#/components/site-footer'
import { SiteHeader } from '#/components/site-header'
export const Route = createFileRoute('/impressum')({
component: Impressum,
})
function Impressum() {
return (
<>
<SiteHeader />
<main className="page legal-page">
<h1>Impressum</h1>
<h2>Angaben gemäß § 5 TMG</h2>
<p>
Drenica &amp; Mousa GbR
<br />
Meppener Str. 8
<br />
49716 Meppen
</p>
<h2>Kontakt</h2>
<p>
Telefon: <a href="tel:+49593112750">+49 (5931) 12750</a>
<br />
E-Mail: <a href="mailto:info@steakhaus-meppen.de">info@steakhaus-meppen.de</a>
</p>
<h2>Haftungsausschluss</h2>
<h3>Haftung für Inhalte</h3>
<p>
Die Inhalte dieser Seite wurden mit Sorgfalt erstellt. Für die Richtigkeit,
Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als
Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den allgemeinen
Gesetzen verantwortlich, nicht jedoch dazu verpflichtet, übermittelte oder gespeicherte
fremde Informationen zu überwachen. Bei Bekanntwerden von Rechtsverletzungen entfernen wir
die betroffenen Inhalte umgehend.
</p>
<h3>Haftung für Links</h3>
<p>
Diese Seite kann Links zu externen Webseiten Dritter enthalten, auf deren Inhalte wir
keinen Einfluss haben. Für diese fremden Inhalte können wir daher keine Gewähr
übernehmen; verantwortlich ist stets der jeweilige Anbieter der verlinkten Seite. Bei
Bekanntwerden von Rechtsverletzungen werden entsprechende Links umgehend entfernt.
</p>
<h3>Urheberrecht</h3>
<p>
Die durch den Seitenbetreiber erstellten Inhalte unterliegen dem deutschen Urheberrecht.
Vervielfältigung, Bearbeitung und Verbreitung außerhalb der Grenzen des Urheberrechts
bedürfen der schriftlichen Zustimmung des jeweiligen Erstellers.
</p>
<h2>Datenschutz</h2>
<p>
Verantwortlich für die Verarbeitung personenbezogener Daten auf dieser Website ist die
oben genannte Drenica &amp; Mousa GbR.
</p>
<p>
Wenn Sie über das Reservierungsformular einen Tisch buchen, speichern wir die von Ihnen
angegebenen Daten (Name, E-Mail-Adresse, Personenzahl, Datum und Uhrzeit) ausschließlich
zur Bearbeitung und Bestätigung Ihrer Reservierung. Eine Weitergabe an Dritte findet
nicht statt.
</p>
<p>
Diese Seite bindet Schriftarten von Google Fonts ein. Dabei kann eine Verbindung Ihres
Browsers zu Servern von Google hergestellt und Ihre IP-Adresse übermittelt werden. Es
werden keine Analyse- oder Tracking-Cookies eingesetzt.
</p>
</main>
<SiteFooter />
</>
)
}
+249
View File
@@ -0,0 +1,249 @@
import { useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { SiteFooter } from '#/components/site-footer'
import { SiteHeader } from '#/components/site-header'
import { createReservation, getTables } from '#/lib/reservations'
export const Route = createFileRoute('/')({
component: Home,
loader: async () => {
const tables = await getTables()
return { tables }
},
})
const OPENING_HOURS = [
{ day: 'Montag', hours: null },
{ day: 'Dienstag', hours: ['17:00 – 21:30'] },
{ day: 'Mittwoch', hours: ['17:00 – 21:30'] },
{ day: 'Donnerstag', hours: ['17:00 – 21:30'] },
{ day: 'Freitag', hours: ['17:00 – 22:00'] },
{ day: 'Samstag', hours: ['17:00 – 22:00'] },
{ day: 'Sonntag', hours: ['11:30 – 13:30', '17:00 – 22:00'] },
]
function formatDateTimeLocal(date: Date) {
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
}
function FlameIcon() {
return (
<svg className="hero-flame" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 2c1 3-3 4-3 7.5A3.5 3.5 0 0 0 12 13a3.5 3.5 0 0 0 3-5.3c1.5 1 2.5 3 2.5 5a5.5 5.5 0 1 1-11 0C6.5 8.5 9.5 6 12 2Z"
stroke="currentColor"
strokeWidth="1.4"
strokeLinejoin="round"
/>
</svg>
)
}
function Home() {
const router = useRouter()
const { tables } = Route.useLoaderData()
const [tableId, setTableId] = useState<number | string>(tables[0]?.id ?? '')
const [customerName, setCustomerName] = useState('')
const [customerEmail, setCustomerEmail] = useState('')
const [partySize, setPartySize] = useState(2)
const [reservationTime, setReservationTime] = useState(() =>
formatDateTimeLocal(new Date(Date.now() + 60 * 60 * 1000)),
)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const todayName = new Date().toLocaleDateString('de-DE', { weekday: 'long' })
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
setError(null)
setSuccess(null)
if (!tableId) {
setError('Bitte wählen Sie einen Tisch aus')
return
}
setIsSubmitting(true)
try {
await createReservation({
data: {
tableId: Number(tableId),
customerName,
customerEmail,
partySize: Number(partySize),
reservationTime: new Date(reservationTime).toISOString(),
},
})
setSuccess('Vielen Dank! Ihre Reservierung ist bestätigt.')
setCustomerName('')
setCustomerEmail('')
setPartySize(2)
await router.invalidate()
} catch (err) {
setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen')
} finally {
setIsSubmitting(false)
}
}
return (
<>
<SiteHeader />
<section className="hero" id="top">
<FlameIcon />
<h1>
Steakhaus <span>bei Mirko</span>
</h1>
<p>
Steaks vom Grill, gute Gesellschaft und ein gemütlicher Abend mitten in Meppen. Reservieren
Sie Ihren Tisch – wir freuen uns auf Sie.
</p>
<div className="hero-actions">
<a className="btn btn-primary" href="#reservieren">
Tisch reservieren
</a>
<a className="btn btn-secondary" href="#oeffnungszeiten">
Öffnungszeiten
</a>
</div>
</section>
<main className="page">
<section className="section" id="oeffnungszeiten">
<div className="section-grid">
<div>
<span className="section-kicker">Wann wir für Sie da sind</span>
<h2 className="section-heading">Öffnungszeiten</h2>
</div>
<ul className="hours-list">
{OPENING_HOURS.map(({ day, hours }) => (
<li key={day} className={day === todayName ? 'today' : undefined}>
<span className="day">{day}</span>
{hours ? (
<span>{hours.join(' · ')}</span>
) : (
<span className="hours-closed">geschlossen</span>
)}
</li>
))}
</ul>
</div>
</section>
<section className="section" id="reservieren">
<div className="section-grid">
<div>
<span className="section-kicker">Wir freuen uns auf Sie</span>
<h2 className="section-heading">Tisch reservieren</h2>
</div>
<form className="reservation-form" onSubmit={handleSubmit}>
<label>
Tisch
<select value={tableId} onChange={(e) => setTableId(e.target.value)} required>
<option value="" disabled>
Tisch auswählen
</option>
{tables.map((table) => (
<option key={table.id} value={table.id}>
{table.name} ({table.seats} Plätze)
</option>
))}
</select>
</label>
<div className="form-row">
<label>
Name
<input
type="text"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
required
/>
</label>
<label>
E-Mail
<input
type="email"
value={customerEmail}
onChange={(e) => setCustomerEmail(e.target.value)}
required
/>
</label>
</div>
<div className="form-row">
<label>
Anzahl Personen
<input
type="number"
min={1}
max={20}
value={partySize}
onChange={(e) => setPartySize(Number(e.target.value))}
required
/>
</label>
<label>
Datum &amp; Uhrzeit
<input
type="datetime-local"
value={reservationTime}
onChange={(e) => setReservationTime(e.target.value)}
required
/>
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Wird gebucht …' : 'Jetzt reservieren'}
</button>
{error && <p className="message message-error">{error}</p>}
{success && <p className="message message-success">{success}</p>}
</form>
</div>
</section>
<section className="section" id="kontakt">
<div className="section-grid">
<div>
<span className="section-kicker">Hier finden Sie uns</span>
<h2 className="section-heading">Kontakt</h2>
</div>
<ul className="contact-list">
<li>
<span className="label">Adresse</span>
<span>
Meppener Straße 8
<br />
49716 Meppen
</span>
</li>
<li>
<span className="label">Telefon</span>
<a href="tel:+49593112750">05931 - 12750</a>
</li>
<li>
<span className="label">E-Mail</span>
<a href="mailto:info@steakhaus-meppen.de">info@steakhaus-meppen.de</a>
</li>
</ul>
</div>
</section>
</main>
<SiteFooter />
</>
)
}
+436
View File
@@ -0,0 +1,436 @@
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600&display=swap');
:root {
--bg: #17120f;
--bg-raised: #1e1712;
--line: #3a2f26;
--text: #f3ead9;
--text-muted: #c9baa3;
--gold: #c9a15a;
--gold-bright: #e0bb78;
--ember: #a8402c;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
html,
body {
overflow-x: hidden;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.5;
}
h1,
h2,
h3 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600;
letter-spacing: 0.01em;
margin: 0;
}
a {
color: var(--gold-bright);
}
.page {
max-width: 68rem;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Header */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
row-gap: 0.5rem;
gap: 1rem;
padding: 1.25rem 1.5rem;
max-width: 68rem;
margin: 0 auto;
}
@media (max-width: 30rem) {
.site-header {
justify-content: center;
text-align: center;
}
}
.wordmark {
font-family: 'Fraunces', Georgia, serif;
font-size: 1.15rem;
letter-spacing: 0.03em;
color: var(--text);
text-decoration: none;
}
.wordmark span {
color: var(--gold-bright);
}
.site-nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem 1.25rem;
list-style: none;
margin: 0;
padding: 0;
font-size: 0.9rem;
}
.site-nav a {
color: var(--text-muted);
text-decoration: none;
}
.site-nav a:hover {
color: var(--gold-bright);
}
/* Hero */
.hero {
position: relative;
padding: 4.5rem 1.5rem 5rem;
text-align: center;
background:
radial-gradient(ellipse at top, rgba(200, 90, 50, 0.2), transparent 60%),
linear-gradient(rgba(23, 18, 15, 0.55), rgba(23, 18, 15, 0.93)),
url('/hero-interior.jpg');
background-size: auto, auto, cover;
background-position: center, center, center 55%;
background-repeat: no-repeat;
border-bottom: 1px solid var(--line);
overflow: hidden;
}
.hero-flame {
width: 2.5rem;
height: 2.5rem;
margin: 0 auto 1.5rem;
color: var(--ember);
}
.hero h1 {
font-size: clamp(2.25rem, 5vw, 3.5rem);
color: var(--text);
}
.hero h1 span {
color: var(--gold-bright);
}
.hero p {
max-width: 34rem;
margin: 1rem auto 0;
color: var(--text-muted);
font-size: 1.05rem;
}
.hero-actions {
margin-top: 2rem;
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-block;
font: inherit;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 0.25rem;
text-decoration: none;
cursor: pointer;
border: 1px solid transparent;
}
.btn-primary {
background: var(--gold);
color: #201709;
}
.btn-primary:hover {
background: var(--gold-bright);
}
.btn-secondary {
background: transparent;
border-color: var(--line);
color: var(--text);
}
.btn-secondary:hover {
border-color: var(--gold);
color: var(--gold-bright);
}
/* Sections */
.section {
padding: 4rem 0;
border-bottom: 1px solid var(--line);
}
.section:last-of-type {
border-bottom: none;
}
.section-heading {
font-size: 1.75rem;
margin-bottom: 0.5rem;
}
.section-kicker {
display: block;
font-size: 0.8rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--gold);
margin-bottom: 0.75rem;
}
.section-grid {
display: grid;
grid-template-columns: 1fr 1.3fr;
gap: 3rem;
}
.section-grid > * {
min-width: 0;
}
@media (max-width: 46rem) {
.section-grid {
grid-template-columns: 1fr;
}
}
/* Opening hours */
.hours-list {
list-style: none;
margin: 1.5rem 0 0;
padding: 0;
border-top: 1px solid var(--line);
}
.hours-list li {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 0;
border-bottom: 1px solid var(--line);
color: var(--text-muted);
}
.hours-list li.today {
color: var(--text);
font-weight: 600;
}
.hours-list .day {
font-family: 'Fraunces', Georgia, serif;
}
.hours-closed {
font-style: italic;
}
/* Reservation form */
.reservation-form {
display: flex;
flex-direction: column;
gap: 1.1rem;
background: var(--bg-raised);
border: 1px solid var(--line);
border-radius: 0.5rem;
padding: 1.75rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.1rem;
}
.form-row > * {
min-width: 0;
}
@media (max-width: 30rem) {
.form-row {
grid-template-columns: 1fr;
}
}
.reservation-form label {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
font-weight: 500;
font-size: 0.9rem;
color: var(--text-muted);
}
.reservation-form input,
.reservation-form select {
width: 100%;
min-width: 0;
font: inherit;
padding: 0.6rem 0.7rem;
border: 1px solid var(--line);
border-radius: 0.3rem;
background: var(--bg);
color: var(--text);
}
.reservation-form input:focus,
.reservation-form select:focus {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 2px rgba(201, 161, 90, 0.25);
}
.reservation-form button {
align-self: flex-start;
font: inherit;
font-weight: 600;
padding: 0.7rem 1.5rem;
border: none;
border-radius: 0.3rem;
background: var(--gold);
color: #201709;
cursor: pointer;
}
.reservation-form button:hover:not(:disabled) {
background: var(--gold-bright);
}
.reservation-form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.message {
margin: 0;
padding: 0.65rem 0.9rem;
border-radius: 0.3rem;
font-size: 0.9rem;
}
.message-error {
background: #3a1414;
color: #f3a9a9;
border: 1px solid #5c1f1f;
}
.message-success {
background: #17301c;
color: #a9d9b4;
border: 1px solid #23502c;
}
/* Contact */
.contact-list {
list-style: none;
margin: 1.5rem 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.contact-list li {
display: flex;
gap: 0.75rem;
color: var(--text-muted);
}
.contact-list .label {
color: var(--gold);
min-width: 6rem;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding-top: 0.15rem;
}
/* Footer */
.site-footer {
padding: 2.5rem 1.5rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
.site-footer .wordmark {
display: block;
margin-bottom: 0.5rem;
}
.footer-nav {
display: flex;
gap: 1.25rem;
justify-content: center;
margin-top: 0.75rem;
}
.footer-nav a {
color: var(--text-muted);
text-decoration: none;
}
.footer-nav a:hover {
color: var(--gold-bright);
}
/* Legal page */
.legal-page {
padding: 3.5rem 0 4rem;
}
.legal-page h1 {
font-size: 2.25rem;
margin-bottom: 2rem;
}
.legal-page h2 {
font-size: 1.25rem;
color: var(--gold-bright);
margin: 2.25rem 0 0.75rem;
}
.legal-page h3 {
font-size: 1rem;
margin: 1.5rem 0 0.5rem;
}
.legal-page p {
color: var(--text-muted);
max-width: 42rem;
}