diff --git a/README.md b/README.md index 4e4fd5e..976021d 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,11 @@ Anderen Port wählen: `PORT=8080 npm start`. ## Seiten -- **Start** (`/`) – Startseite mit Platzhaltertext, Links zu Kalender und Fahrtplaner, nächste Spieltage. -- **Kalender** (`/kalender`) – Mitglieder wählen „Ich bin …“, tragen Spieltage ein und sagen zu oder ab. +- **Start** (`/`) – Startseite mit Flagge, die beim Scrollen heranzoomt, nächstem Spiel samt Countdown, + Links zu allen Bereichen, neuesten Patches, nächsten Terminen und „Pile of Shame“. +- **Kalender** (`/kalender`) – Mitglieder wählen „Ich bin …“, tragen Termine ein (optional mit Beginn und Ende), bearbeiten sie und sagen zu oder ab. + Pro Termin wird angekreuzt, ob es ein **Spiel-Event** ist – nur diese zählen in der Statistik (Treffen, Grillen usw. nicht). + Einzelne Termine oder alle kommenden lassen sich als `.ics`-Datei für Google, Apple oder Outlook herunterladen. Über „Fahrt planen“ geht es direkt in den Fahrtplaner, alle Zusagen sind dann schon als „kommt mit“ ausgewählt. - **Fahrtplaner** (`/fahrt`) – Treffpunkt, Abfahrt (Standard 07:00) und Treffzeit (15 min vorher) festlegen – nach der Berechnung pro Auto änderbar, Mitfahrer auswählen und pro Fahrt angeben, wer sein Auto dabei hat. Wer immer mit eigenem Auto kommt, fährt immer selbst und bekommt zuerst Mitfahrer. @@ -30,17 +33,22 @@ Anderen Port wählen: `PORT=8080 npm start`. Unter „Feste Zuteilung“ lässt sich vorab festlegen, wer diesmal sicher bei wem mitfährt, Gäste nur für diese Fahrt hinzufügen, Fahrgemeinschaften berechnen, Ergebnis kopieren oder drucken. „Eingabe zurücksetzen“ leert alles für die nächste Planung. - **Statistik** (`/statistik`, öffentlich) – wie oft jedes Mitglied (ohne Externe) im laufenden Jahr gespielt hat: - Spieltage mit Zusage aus dem Kalender plus manuelle Korrektur (− / +, nur angemeldet), z. B. für spontane Fahrten. + Spiel-Events mit Zusage aus dem Kalender plus manuelle Korrektur (− / +, nur angemeldet), z. B. für spontane Fahrten. Zu Neujahr wird der Endstand des Vorjahres archiviert (im Jahresmenü abrufbar) und die Zählung beginnt bei 0. Die Startseite zeigt als „Pile of Shame“, wer im laufenden Jahr noch nicht spielen war. Der Jahreswechsel richtet sich nach der Zeitzone des Servers (im systemd-Dienst `Environment=TZ=Europe/Berlin` setzen). +- **Patches** (`/patches`, öffentlich) – Galerie der Gruppen- und Event-Patches, getrennt nach „Gibt es schon“ und „In Planung“. + Angemeldet: Patch hochladen (PNG, JPEG oder WebP – wird im Browser auf max. 1600 px verkleinert – oder SVG bis 2 MB), + bearbeiten, löschen. SVGs mit Skripten, Ereignis-Attributen oder Verweisen auf fremde Dateien werden abgelehnt + und zusätzlich mit einer Content-Security-Policy ausgeliefert. + Die Bilder liegen in `data/patches/` und sind damit im Docker-Volume und im Backup enthalten. - **Gruppe** (`/gruppe`) – feste Mitglieder (Vorname, extern ja/nein, Auto ja/nein, freie Plätze ohne Fahrer, „kommt immer mit eigenem Auto“, Standard-Treffpunkt als Fahrer, „fährt am liebsten mit bei …“) und Treffpunkte (Name, Adresse) verwalten, Export/Import der Mitglieder als JSON. ## Login -Öffentlich sind nur die Startseite (inkl. der nächsten 3 Spieltage: Datum, Titel, Anzahl Zusagen) und die Statistik -(Namen und Anzahl gespielter Spieltage). +Öffentlich sind nur die Startseite (inkl. der nächsten 3 Termine: Datum, Uhrzeit, Titel, Anzahl Zusagen), die Statistik +(Namen und Anzahl gespielter Spieltage) und die Patches. Kalender, Fahrtplaner, Gruppe und die komplette API sind nur nach dem Anmelden erreichbar. Benutzer werden auf dem Server per Kommandozeile verwaltet: @@ -63,11 +71,11 @@ Sicherheit: ## Aufbau ``` -server.js Express-Server + REST-API (/api/members, /api/events, /api/meeting-points) +server.js Express-Server + REST-API (/api/members, /api/events, /api/meeting-points, /api/patches, .ics-Export) auth.js Login, Passwort-Hashing, Sitzungen scripts/user.js Benutzerverwaltung (npm run user) -data/ db.json, users.json, sessions.json (automatisch angelegt, nicht in Git) -public/ öffentlich: Startseite, Login, CSS, Bilder, common.js +data/ db.json, users.json, sessions.json, patches/ (automatisch angelegt, nicht in Git) +public/ öffentlich: Startseite, Statistik, Patches, Login, CSS, Bilder, Schriften, common.js private/ nur angemeldet: Kalender, Fahrtplaner, Gruppe ``` @@ -76,3 +84,8 @@ private/ nur angemeldet: Kalender, Fahrtplaner, Gruppe Per Docker hinter dem Nginx Proxy Manager: `docker compose up -d --build` (Daten liegen in `./data`, die Seite läuft auf Port 3000, änderbar per `HCRB_PORT` in `.env`). Schritt-für-Schritt-Anleitung inkl. Domain, HTTPS, Updates und Backups: [DEPLOY.md](DEPLOY.md). Skripte für Updates und Backups liegen in `deploy/`. + +## Schriften + +Die Schriften liegen in `public/fonts/` und werden vom eigenen Server ausgeliefert (keine Anfrage an Google, DSGVO): +Saira Stencil One (Schriftzug „HCRB“), Barlow und Barlow Condensed. Alle unter der SIL Open Font License 1.1. diff --git a/private/fahrt.html b/private/fahrt.html index 0a65ee2..3eb0425 100644 --- a/private/fahrt.html +++ b/private/fahrt.html @@ -7,9 +7,19 @@ -
+
+
+
+ Fahrgemeinschaften +

Fahrtplaner

+

Wer fährt, wer steigt wo zu, wann geht es los.

+
+
+ +
+

Fahrt

@@ -36,19 +46,6 @@
- -
-
-

Wer kommt mit?

-
- - -
-
- -
    -
    -

    Weitere Personen (nur diese Fahrt)

    @@ -60,7 +57,6 @@
      -

      Feste Zuteilung (nur diese Fahrt)

      Wer hier eingetragen ist, fährt sicher bei diesem Fahrer mit – der Fahrer fährt dann auf jeden Fall. @@ -73,8 +69,23 @@

        +
        +
        +
        +
        +

        Wer kommt mit?

        +
        + + +
        +
        + +
          +
          +
          +
          -
          +

          Einteilung

          +
          + + +
          diff --git a/private/js/kalender.js b/private/js/kalender.js index 4a7d5b5..e7da696 100644 --- a/private/js/kalender.js +++ b/private/js/kalender.js @@ -1,6 +1,6 @@ import { $, api, esc, sortByName, showError, renderHeader, - toIso, fromIso, formatDate, getMe, setMe, + toIso, fromIso, formatDate, formatTime, getMe, setMe, } from './common.js'; renderHeader('kalender'); @@ -8,6 +8,7 @@ renderHeader('kalender'); let members = []; let events = []; let me = getMe(); +let editingId = null; // Termin, der gerade im Formular bearbeitet wird const todayIso = toIso(new Date()); let selectedDay = todayIso; @@ -23,7 +24,7 @@ function renderMe() { sortByName(members).map((m) => ``).join(''); $('#me-hint').hidden = !!me; - $('#event-form button[type=submit]').disabled = !me; + $('#event-submit').disabled = !me && !editingId; } $('#me-select').addEventListener('change', (e) => { @@ -53,14 +54,15 @@ function renderCalendar() { const iso = toIso(day); const classes = ['cal-day']; if (day.getMonth() !== viewMonth.getMonth()) classes.push('other'); + if (day.getDay() === 0 || day.getDay() === 6) classes.push('weekend'); if (iso === todayIso) classes.push('today'); if (iso === selectedDay) classes.push('selected'); const dayEvents = byDate.get(iso) || []; html += ` `; day.setDate(day.getDate() + 1); } @@ -70,34 +72,43 @@ function renderCalendar() { $('#calendar').addEventListener('click', (e) => { const cell = e.target.closest('[data-day]'); if (!cell) return; - selectedDay = cell.dataset.day; - const d = fromIso(selectedDay); - if (d.getMonth() !== viewMonth.getMonth()) viewMonth = new Date(d.getFullYear(), d.getMonth(), 1); - renderAll(); - $('#day-title').scrollIntoView({ behavior: 'smooth', block: 'start' }); + selectDay(cell.dataset.day); + // Auf schmalen Bildschirmen stehen die Details unter dem Kalender + if (matchMedia('(max-width: 999px)').matches) $('#day-title').scrollIntoView({ behavior: 'smooth', block: 'start' }); }); +function selectDay(iso) { + selectedDay = iso; + const d = fromIso(iso); + if (d.getMonth() !== viewMonth.getMonth()) viewMonth = new Date(d.getFullYear(), d.getMonth(), 1); + if (!editingId) $('#event-date').value = iso; + renderAll(); +} + function shiftMonth(delta) { viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + delta, 1); renderCalendar(); } $('#prev-month').addEventListener('click', () => shiftMonth(-1)); $('#next-month').addEventListener('click', () => shiftMonth(1)); -$('#today-btn').addEventListener('click', () => { - selectedDay = todayIso; - viewMonth = fromIso(todayIso); - viewMonth.setDate(1); - renderAll(); -}); +$('#today-btn').addEventListener('click', () => selectDay(todayIso)); // ---------- Termine ---------- +const downloadIcon = ''; + function eventHtml(ev) { const going = ev.attendees.includes(me); const attendees = sortByName(ev.attendees.map((id) => ({ id, name: memberName(id) }))); + const when = [formatDate(ev.date, true), formatTime(ev)].filter(Boolean).join(' · '); return ` -
          -

          ${esc(ev.title)}

          -
          ${formatDate(ev.date, true)}${ev.createdBy ? ` · eingetragen von ${esc(memberName(ev.createdBy))}` : ''}
          +
          +
          +

          ${esc(ev.title)}

          + ${ev.isGame + ? 'Spiel' + : 'Kein Spiel'} +
          +
          ${when}${ev.createdBy ? ` · eingetragen von ${esc(memberName(ev.createdBy))}` : ''}
          ${ev.note ? `
          ${esc(ev.note)}
          ` : ''}
          ${ev.attendees.length} zugesagt:
          @@ -110,6 +121,8 @@ function eventHtml(ev) { ? `` : ``) : ''} Fahrt planen + ${downloadIcon}Kalender +
          `; @@ -122,14 +135,14 @@ function renderDay() { const dayEvents = events.filter((e) => e.date === selectedDay); $('#day-events').innerHTML = dayEvents.length ? dayEvents.map(eventHtml).join('') - : '

          An diesem Tag ist noch kein Spieltag eingetragen.

          '; + : '

          An diesem Tag ist noch nichts eingetragen.

          '; } function renderUpcoming() { const upcoming = events.filter((e) => e.date >= todayIso); $('#upcoming').innerHTML = upcoming.length ? upcoming.map(eventHtml).join('') - : '

          Keine kommenden Spieltage.

          '; + : '

          Keine kommenden Termine.

          '; } document.addEventListener('click', async (e) => { @@ -137,12 +150,14 @@ document.addEventListener('click', async (e) => { if (!btn) return; const { action, id } = btn.dataset; try { + if (action === 'edit') return startEdit(id); if (action === 'join') await api('PUT', `/events/${id}/attendees/${me}`); if (action === 'leave') await api('DELETE', `/events/${id}/attendees/${me}`); if (action === 'delete') { const ev = events.find((x) => x.id === id); - if (!confirm(`Spieltag „${ev.title}“ am ${formatDate(ev.date)} wirklich löschen?`)) return; + if (!confirm(`Termin „${ev.title}“ am ${formatDate(ev.date)} wirklich löschen?`)) return; await api('DELETE', `/events/${id}`); + if (editingId === id) stopEdit(); } await load(); } catch (err) { @@ -150,17 +165,57 @@ document.addEventListener('click', async (e) => { } }); +// ---------- Formular: eintragen und bearbeiten ---------- +function startEdit(id) { + const ev = events.find((x) => x.id === id); + if (!ev) return; + editingId = id; + $('#event-form-title').textContent = 'Termin bearbeiten'; + $('#event-submit').textContent = 'Speichern'; + $('#event-cancel').hidden = false; + $('#event-title').value = ev.title; + $('#event-game').checked = ev.isGame; + $('#event-date').value = ev.date; + $('#event-time').value = ev.time || ''; + $('#event-end').value = ev.endTime || ''; + $('#event-note').value = ev.note || ''; + renderMe(); + $('#event-form').scrollIntoView({ behavior: 'smooth', block: 'center' }); + $('#event-title').focus({ preventScroll: true }); +} + +function stopEdit() { + editingId = null; + $('#event-form').reset(); + $('#event-form-title').textContent = 'Termin an diesem Tag eintragen'; + $('#event-submit').textContent = 'Eintragen'; + $('#event-cancel').hidden = true; + $('#event-date').value = selectedDay; + renderMe(); +} + +$('#event-cancel').addEventListener('click', stopEdit); + $('#event-form').addEventListener('submit', async (e) => { e.preventDefault(); + const body = { + date: $('#event-date').value, + title: $('#event-title').value, + note: $('#event-note').value, + isGame: $('#event-game').checked, + time: $('#event-time').value, + endTime: $('#event-end').value, + }; try { - await api('POST', '/events', { - date: selectedDay, - title: $('#event-title').value, - note: $('#event-note').value, - createdBy: me, - }); - e.target.reset(); + if (editingId) { + await api('PUT', `/events/${editingId}`, body); + } else { + await api('POST', '/events', { ...body, createdBy: me }); + } + const date = body.date; + stopEdit(); await load(); + if (date && date !== selectedDay) selectDay(date); } catch (err) { showError(err); } @@ -179,4 +234,5 @@ async function load() { renderAll(); } +$('#event-date').value = selectedDay; load().catch(showError); diff --git a/private/kalender.html b/private/kalender.html index aa6cbdf..2499c10 100644 --- a/private/kalender.html +++ b/private/kalender.html @@ -8,42 +8,92 @@
          -
          - - -
          - -
          -
          - -

          - +
          +
          + Spieltage & Termine +

          Kalender

          +

          Nur Termine mit Häkchen bei „Spiel-Event“ zählen für die Statistik.

          -
          -
          -
          + + -
          -

          -
          - -

          Spieltag an diesem Tag eintragen

          - - - - -
          +
          +
          +
          +
          + +

          + + +
          +
          +
          + Spiel-Event + Termin ohne Spiel + Du hast zugesagt +
          +
          +
          -
          -

          Kommende Spieltage

          -
          +
          +
          + + +
          + +
          +

          +
          +
          +

          Termin an diesem Tag eintragen

          + + +
          + +
          + + +
          +
          + +
          + + +
          +
          +
          +
          + +
          +
          +

          Kommende Termine

          +
          +
          +
          diff --git a/public/css/style.css b/public/css/style.css index fd8841b..15347e3 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,58 +1,97 @@ -/* Farben aus der Gruppenflagge: Marineblau #354173, Gold #e4b84a, Stahlgrau #cacaca */ +/* HCRB – Farben aus der Gruppenflagge: Marineblau #354173, Gold #e4b84a, Stahlgrau #cacaca + Layout: volle Breite mit flüssigem Seitenrand, mehrspaltig ab Tablet, abgeschrägte "taktische" Ecken statt Rundungen. */ + +@font-face { font-family: "Saira Stencil One"; src: url("/fonts/saira-stencil-one-latin-400-normal.woff2") format("woff2"); font-weight: 400; font-display: swap; } +@font-face { font-family: "Barlow"; src: url("/fonts/barlow-latin-400-normal.woff2") format("woff2"); font-weight: 400; font-display: swap; } +@font-face { font-family: "Barlow"; src: url("/fonts/barlow-latin-500-normal.woff2") format("woff2"); font-weight: 500; font-display: swap; } +@font-face { font-family: "Barlow"; src: url("/fonts/barlow-latin-600-normal.woff2") format("woff2"); font-weight: 600; font-display: swap; } +@font-face { font-family: "Barlow Condensed"; src: url("/fonts/barlow-condensed-latin-500-normal.woff2") format("woff2"); font-weight: 500; font-display: swap; } +@font-face { font-family: "Barlow Condensed"; src: url("/fonts/barlow-condensed-latin-700-normal.woff2") format("woff2"); font-weight: 700; font-display: swap; } + :root { color-scheme: dark; --flag: #354173; - --bg: #1b2140; - --bg-deep: #141932; - --card: #232a4d; - --card-2: #2b335a; - --text: #e9eaf0; - --muted: #a1a7c2; - --border: #3d4775; + --bg: #121730; + --bg-deep: #0d1126; + --card: #1a2142; + --card-2: #232b52; + --text: #eceef6; + --muted: #9ba2c3; + --border: #323c6c; --accent: #e4b84a; - --accent-text: #141932; - --accent-soft: rgba(228, 184, 74, .14); + --accent-text: #121730; + --accent-soft: rgba(228, 184, 74, .13); --steel: #cacaca; + --steel-soft: rgba(202, 202, 202, .1); --warn: #ec7f6a; - --chip: #333c66; - --radius: 2px; - --font-head: "Bahnschrift", "DIN Alternate", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif; + --chip: #2c3563; + --grid-line: rgba(160, 172, 230, .045); + --radius: 0; + --cut: 14px; + --gutter: clamp(16px, 3.2vw, 56px); + --font-stencil: "Saira Stencil One", "Bahnschrift", "Arial Narrow", system-ui, sans-serif; + --font-head: "Barlow Condensed", "Bahnschrift", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif; + --font-body: "Barlow", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --ease-out: cubic-bezier(.16, 1, .3, 1); } * { box-sizing: border-box; } [hidden] { display: none !important; } +html { scroll-padding-top: 80px; } + body { margin: 0; - font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - background: var(--bg); + font-family: var(--font-body); + font-size: 1rem; + background-color: var(--bg); + /* feines Kartenraster wie auf einer Lagekarte */ + background-image: + linear-gradient(var(--grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); + background-size: 56px 56px; color: var(--text); - line-height: 1.45; - /* Fußzeile bleibt auch auf kurzen Seiten unten */ + line-height: 1.5; min-height: 100vh; display: flex; flex-direction: column; } -a { color: var(--accent); } +a { color: var(--accent); text-underline-offset: 3px; } +:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -h1, h2, h3, .brand, .site-header nav a, button, .button, .badge, .cal-grid .dow { +h1, h2, h3, .brand, .site-header nav a, .site-header nav button, button, .button, .badge, .cal-grid .dow, .eyebrow { font-family: var(--font-head); } -h1, h2, h3 { text-transform: uppercase; letter-spacing: .08em; } +h1, h2, h3 { text-transform: uppercase; letter-spacing: .06em; text-wrap: balance; line-height: 1.1; } /* ---------- Kopfzeile ---------- */ .site-header { - background: var(--bg-deep); - border-bottom: 2px solid var(--accent); - padding: 10px 16px; + background: rgba(13, 17, 38, .86); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); + padding: 10px var(--gutter); position: sticky; top: 0; - z-index: 10; + z-index: 20; + transition: background-color .3s, border-color .3s; } +.site-header::after { + /* goldene Linie, die links beginnt und ausläuft */ + content: ""; + position: absolute; + left: 0; right: 0; bottom: -1px; + height: 2px; + background: linear-gradient(90deg, var(--accent) 0, var(--accent) 120px, transparent 60%); +} +/* Startseite: Kopfzeile liegt über der Flagge und wird beim Scrollen fest */ +.site-header.overlay { position: fixed; left: 0; right: 0; background: transparent; border-color: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; } +.site-header.overlay::after { opacity: 0; transition: opacity .3s; } +.site-header.overlay.scrolled { background: rgba(13, 17, 38, .9); border-color: var(--border); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } +.site-header.overlay.scrolled::after { opacity: 1; } + .site-header .inner { - max-width: 960px; - margin: 0 auto; display: flex; align-items: center; gap: 16px; @@ -61,339 +100,812 @@ h1, h2, h3 { text-transform: uppercase; letter-spacing: .08em; } .brand { display: inline-flex; align-items: center; - font-weight: 700; - font-size: 1.3rem; - letter-spacing: .18em; + font-family: var(--font-stencil); + font-size: 1.45rem; + letter-spacing: .12em; color: var(--accent); text-decoration: none; } -.brand img { width: 40px; height: 40px; margin-right: 10px; flex: none; } +.brand img { width: 40px; height: 40px; margin-right: 12px; flex: none; transition: transform .6s var(--ease-out); } +.brand:hover img { transform: rotate(30deg); } .brand small { - font-size: .72rem; - font-weight: 400; + font-family: var(--font-head); + font-size: .78rem; + font-weight: 500; color: var(--steel); - letter-spacing: .14em; + letter-spacing: .2em; text-transform: uppercase; - margin-left: 10px; + margin-left: 12px; + padding-left: 12px; + border-left: 1px solid var(--border); } -.site-header nav { display: flex; gap: 2px; flex-wrap: wrap; margin-left: auto; } +.site-header nav { display: flex; gap: 4px; flex-wrap: wrap; margin-left: auto; } .site-header nav a, .site-header nav button { + position: relative; text-decoration: none; color: var(--steel); - padding: 6px 12px; + padding: 8px 12px; text-transform: uppercase; - letter-spacing: .1em; - font-size: .85rem; - border-bottom: 2px solid transparent; + letter-spacing: .14em; + font-size: .92rem; + font-weight: 500; + background: none; + border: none; + border-radius: 0; +} +.site-header nav a::after, .site-header nav button::after { + content: ""; + position: absolute; + left: 12px; right: 12px; bottom: 2px; + height: 2px; + background: var(--accent); + transform: scaleX(0); + transform-origin: left; + transition: transform .3s var(--ease-out); } -.site-header nav button { background: none; border-width: 0 0 2px; border-radius: 0; } .site-header nav a:hover, .site-header nav button:hover { color: var(--text); } -.site-header nav a.active { color: var(--accent); border-bottom-color: var(--accent); } +.site-header nav a:hover::after, .site-header nav button:hover::after { transform: scaleX(.4); } +.site-header nav a.active { color: var(--accent); } +.site-header nav a.active::after { transform: scaleX(1); } -main { max-width: 960px; width: 100%; margin: 0 auto; padding: 16px; flex: 1 0 auto; } -main.narrow { max-width: 760px; } -main.login { max-width: 420px; padding-top: 48px; } +main { width: 100%; padding: 28px var(--gutter) 48px; flex: 1 0 auto; } +main.login { max-width: 460px; margin: 0 auto; padding-top: 8vh; } + +/* ---------- Seitenkopf ---------- */ +.page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px 24px; + flex-wrap: wrap; + margin-bottom: 24px; + padding-bottom: 18px; + border-bottom: 1px solid var(--border); +} +.page-head h1 { + margin: 0; + font-size: clamp(2.2rem, 5vw, 3.6rem); + font-weight: 700; + letter-spacing: .04em; +} +.page-head p { margin: 6px 0 0; color: var(--muted); max-width: 62ch; } +.page-head .actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } +.page-head .actions select { width: auto; margin: 0; } +.eyebrow { + display: block; + font-size: .82rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: .28em; + color: var(--accent); + margin-bottom: 6px; +} + +/* ---------- Spalten ---------- */ +.split { display: grid; gap: 20px; align-items: start; } +.split > .col { display: grid; gap: 20px; min-width: 0; } +.split > .col > .card { margin: 0; } +@media (min-width: 1000px) { + .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } + .split.wide-left { grid-template-columns: minmax(0, 1.65fr) minmax(340px, 1fr); } + .split.wide-right { grid-template-columns: minmax(340px, 1fr) minmax(0, 1.4fr); } +} +.span-all { grid-column: 1 / -1; } /* ---------- Bausteine ---------- */ .card { + position: relative; background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; - margin-bottom: 16px; + padding: 20px; + margin-bottom: 20px; + clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, var(--cut) 100%, 0 calc(100% - var(--cut))); + box-shadow: inset 0 0 0 1px var(--border); +} +.card::before { + /* Eckwinkel oben links */ + content: ""; + position: absolute; + top: 0; left: 0; + width: 18px; height: 18px; + border-top: 2px solid var(--accent); + border-left: 2px solid var(--accent); + pointer-events: none; } .card h2 { - font-size: .95rem; - margin: 0 0 14px; - padding-left: 10px; - border-left: 3px solid var(--accent); + font-size: 1.15rem; + font-weight: 700; + margin: 0 0 16px; + letter-spacing: .1em; } -.card h3 { font-size: .9rem; } -.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; } +.card h3 { font-size: 1rem; font-weight: 700; letter-spacing: .08em; margin: 18px 0 10px; } +.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; } .card-head h2 { margin: 0; } .card-head select { width: auto; margin-top: 0; } -label { display: block; margin-bottom: 10px; font-size: .92rem; color: var(--muted); } -label.inline { display: inline-flex; align-items: center; gap: 6px; color: var(--text); } -input[type=checkbox] { accent-color: var(--accent); width: 16px; height: 16px; } +label { display: block; margin-bottom: 12px; font-size: .94rem; color: var(--muted); } +label.inline { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; color: var(--text); } +input[type=checkbox] { accent-color: var(--accent); width: 17px; height: 17px; } input[type=text], input[type=password], input[type=number], input[type=date], input[type=time], select, textarea { display: block; width: 100%; - margin-top: 4px; - padding: 8px 10px; + margin-top: 5px; + padding: 9px 11px; font: inherit; color: var(--text); background: var(--bg-deep); border: 1px solid var(--border); border-radius: var(--radius); + transition: border-color .15s; } +input:hover, select:hover, textarea:hover { border-color: #4a568e; } input:focus, select:focus, textarea:focus { outline: 1px solid var(--accent); border-color: var(--accent); } -textarea { resize: vertical; min-height: 60px; } +textarea { resize: vertical; min-height: 70px; } .row input[type=text], .row input[type=number], .row select { width: auto; flex: 1 1 120px; margin-top: 0; } .row input[type=number] { flex: 0 0 80px; } +/* Schalter mit Erklärung, z. B. "Spiel-Event" */ +.toggle-field { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 12px 14px; + margin-bottom: 12px; + background: var(--accent-soft); + border-left: 3px solid var(--accent); + color: var(--text); + cursor: pointer; +} +.toggle-field input { margin-top: 3px; flex: none; } +.toggle-field strong { display: block; font-family: var(--font-head); text-transform: uppercase; letter-spacing: .1em; } +.toggle-field span { color: var(--muted); font-size: .9rem; } +.toggle-field:has(input:not(:checked)) { background: var(--steel-soft); border-left-color: var(--steel); } + button, .button { - font-size: .85rem; + font-size: .92rem; + font-weight: 500; text-transform: uppercase; - letter-spacing: .08em; - padding: 8px 14px; + letter-spacing: .1em; + padding: 8px 16px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--card-2); color: var(--text); cursor: pointer; - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; margin: 0; text-decoration: none; + transition: border-color .15s, background-color .15s, color .15s, transform .15s; } button:hover, .button:hover { border-color: var(--steel); } +button:active, .button:active { transform: translateY(1px); } button:disabled { opacity: .45; cursor: default; } -button.primary, .button.primary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); font-weight: 700; } -button.primary:hover { filter: brightness(1.08); } +button.primary, .button.primary { + background: var(--accent); + color: var(--accent-text); + border-color: var(--accent); + font-weight: 700; + clip-path: polygon(0 0, calc(100% - 9px) 0, 100% 9px, 100% 100%, 9px 100%, 0 calc(100% - 9px)); +} +button.primary:hover, .button.primary:hover { background: #f0c75c; border-color: #f0c75c; } button.danger { color: var(--warn); } button.link { background: none; border: none; padding: 4px 6px; color: var(--muted); } button.link:hover { color: var(--text); } -button.small, .button.small { padding: 4px 10px; font-size: .78rem; } +button.link.danger { color: var(--warn); opacity: .8; } +button.link.danger:hover { opacity: 1; } +button.small, .button.small { padding: 5px 11px; font-size: .82rem; } +.button svg, button svg { width: 16px; height: 16px; flex: none; } -.row { display: flex; gap: 8px; align-items: center; margin: 8px 0; } +.row { display: flex; gap: 8px; align-items: center; margin: 10px 0; } .row.wrap { flex-wrap: wrap; } .row.end { justify-content: flex-end; } .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } -@media (max-width: 560px) { .grid2 { grid-template-columns: 1fr; } } +@media (max-width: 560px) { .grid2:not(.keep) { grid-template-columns: 1fr; } } -.muted { color: var(--muted); font-size: .9rem; } +.muted { color: var(--muted); font-size: .94rem; } .list { list-style: none; padding: 0; margin: 0; } .list li { display: flex; align-items: center; gap: 10px; - padding: 8px 0; + padding: 10px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; } .list li:last-child { border-bottom: none; } -.list .name { flex: 1; } +/* Name behält Platz; reicht die Breite nicht, rutschen die Aktionen in die nächste Zeile */ +.list .name { flex: 1 1 10rem; min-width: 0; } /* Aktionen rücken nach rechts und brechen bei Platzmangel gemeinsam in die nächste Zeile um */ .list .actions { margin-left: auto; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; } .list li > .link:last-child { margin-left: auto; } -.list .meta { color: var(--muted); font-size: .85rem; } +.list .meta { color: var(--muted); font-size: .9rem; } .list label { margin: 0; } .badge { - font-size: .72rem; + font-size: .78rem; + font-weight: 500; text-transform: uppercase; - letter-spacing: .08em; - padding: 2px 8px; - border-radius: var(--radius); + letter-spacing: .1em; + padding: 1px 8px; border: 1px solid var(--border); color: var(--steel); white-space: nowrap; } +.badge.game { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); } +.badge.nogame { color: var(--steel); border-color: #5b6488; background: var(--steel-soft); } .chips { display: flex; flex-wrap: wrap; gap: 6px; } -.chip { font-size: .85rem; padding: 3px 10px; border-radius: var(--radius); background: var(--chip); } -.chip.me { background: var(--accent); color: var(--accent-text); } +.chip { font-size: .9rem; padding: 3px 10px; background: var(--chip); } +.chip.me { background: var(--accent); color: var(--accent-text); font-weight: 600; } .notice { - border: 1px solid var(--accent); - border-left-width: 4px; + border-left: 4px solid var(--accent); background: var(--accent-soft); - border-radius: var(--radius); - padding: 10px 12px; - margin-bottom: 16px; + padding: 12px 14px; + margin-bottom: 20px; } .warn { color: var(--warn); - border: 1px solid var(--warn); - border-left-width: 4px; - border-radius: var(--radius); + border-left: 4px solid var(--warn); + background: rgba(236, 127, 106, .08); padding: 10px 12px; margin-top: 12px; } -/* ---------- Startseite ---------- */ -.hero { - position: relative; - height: max(380px, 72vh); +/* ---------- Startseite: Hero mit Zoom beim Scrollen ---------- */ +.hero { position: relative; height: 175vh; background: var(--flag); } +.hero-stage { + position: sticky; + top: 0; + height: 100vh; + height: 100svh; + overflow: hidden; + display: grid; + place-items: center; + --p: 0; /* Scrollfortschritt 0 … 1, setzt home.js */ +} +.hero-flag { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + /* Zahnrad sitzt bei 41 % Höhe, oberhalb des Titels; beim Scrollen geht es bis auf den Bären heran */ + transform: translateY(-9%) scale(calc(.72 + var(--p) * 2)); + transform-origin: 50% 50%; + will-change: transform; +} +/* Zahnrad: gleiche Lage und gleicher Zoom wie die Flagge, zusätzlich Drehung um die eigene Mitte */ +.hero-gear { transform: translateY(-9%) scale(calc(.72 + var(--p) * 2)) rotate(calc(var(--p) * 150deg)); } +/* Vignette: Flaggenblau läuft an den Rändern in den Seitenhintergrund aus */ +.hero-stage::after { + content: ""; + position: absolute; + inset: 0; background: - linear-gradient(to bottom, transparent 55%, var(--bg) 100%), - var(--flag) url("/img/flagge.jpg") center 35% / contain no-repeat; - display: flex; - align-items: flex-end; - justify-content: center; - text-align: center; - padding: 0 16px 24px; + radial-gradient(ellipse 75% 70% at 50% 41%, transparent 55%, rgba(13, 17, 38, .55) 100%), + linear-gradient(to bottom, transparent 70%, var(--bg) 100%); + pointer-events: none; } -.hero h1 { - font-size: clamp(2.4rem, 7vw, 4rem); - margin: 0; - letter-spacing: .3em; - margin-right: -.3em; /* gleicht letter-spacing am Ende aus */ - color: var(--accent); - text-shadow: 0 2px 12px rgba(0, 0, 0, .5); +.hero-shade { + /* dunkelt beim Heranzoomen ab, damit der Übergang zum Inhalt weich ist */ + position: absolute; + inset: 0; + background: var(--bg); + opacity: calc(var(--p) * var(--p) * .85); + z-index: 2; + pointer-events: none; } -.hero .subtitle { +.hud { + position: absolute; + inset: 84px var(--gutter) 32px; + pointer-events: none; + z-index: 3; font-family: var(--font-head); text-transform: uppercase; - letter-spacing: .35em; + letter-spacing: .22em; + font-size: .8rem; color: var(--steel); - font-size: clamp(.8rem, 2.4vw, 1.05rem); - border-top: 1px solid var(--accent); - padding-top: 8px; - margin-top: 4px; + font-variant-numeric: tabular-nums; +} +.hud span { position: absolute; } +.hud .tl { top: 0; left: 0; } +.hud .tr { top: 0; right: 0; text-align: right; } +.hud .corner { width: 26px; height: 26px; border-color: var(--accent); border-style: solid; border-width: 0; } +.hud .corner.c1 { top: 22px; left: 0; border-top-width: 2px; border-left-width: 2px; } +.hud .corner.c2 { top: 22px; right: 0; border-top-width: 2px; border-right-width: 2px; } +.hud .corner.c3 { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; } +.hud .corner.c4 { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; } + +.hero-title { + position: absolute; + left: var(--gutter); + right: var(--gutter); + bottom: 6vh; + z-index: 4; + text-align: center; + transform: translateY(calc(var(--p) * -14vh)); + opacity: calc(1 - var(--p) * 1.7); +} +.hero-title h1 { + font-family: var(--font-stencil); + font-weight: 400; + font-size: clamp(3.4rem, 12vw, 7.5rem); + line-height: .9; + margin: 0; + letter-spacing: .14em; + margin-right: -.14em; /* gleicht letter-spacing am Ende aus */ + color: var(--accent); + text-shadow: 0 6px 30px rgba(10, 13, 30, .55); +} +.hero-title .subtitle { + display: inline-flex; + gap: 14px; + align-items: center; + font-family: var(--font-head); + font-weight: 500; + text-transform: uppercase; + letter-spacing: .4em; + color: var(--text); + font-size: clamp(.85rem, 2.2vw, 1.25rem); + margin-top: 12px; + padding: 6px 4px 6px 18px; + background: rgba(13, 17, 38, .55); } -@media (max-width: 560px) { - /* Hochformat: Flagge füllt die Fläche, damit das Zahnrad groß bleibt */ - .hero { height: min(78vh, 480px); background-size: auto, auto 70%; background-position: center, center 20%; } +/* Hochformat: Flagge größer, damit das Zahnrad die Breite füllt */ +@media (max-aspect-ratio: 1/1) { + .hero-flag { transform: translateY(-8%) scale(calc(1.25 + var(--p) * 2.6)); } + .hero-gear { transform: translateY(-8%) scale(calc(1.25 + var(--p) * 2.6)) rotate(calc(var(--p) * 150deg)); } } -.intro { max-width: 680px; margin: 8px auto 0; text-align: center; color: var(--muted); } -.notice.wip { max-width: 680px; margin: 16px auto 0; text-align: center; } +/* Einstiegsanimation der Titelzeile */ +@keyframes rise { from { transform: translateY(40px); opacity: 0; } to { transform: none; opacity: 1; } } +@keyframes wipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } } +.hero-title h1 { animation: rise 1s var(--ease-out) both; } +.hero-title .subtitle { animation: wipe .9s .35s var(--ease-out) both; } -.social { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 4px; } -.social .button { display: inline-flex; align-items: center; gap: 8px; } -.social svg { width: 18px; height: 18px; flex: none; } -.footer-links { margin-top: 8px; display: flex; gap: 16px; justify-content: center; } -.footer-links a { color: var(--muted); } -.footer-links a:hover { color: var(--accent); } +/* Inhalt der Startseite schiebt sich über das Ende des Heros */ +.home main { position: relative; z-index: 5; margin-top: -30vh; padding-top: 32px; } -.tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 24px 0; } -@media (max-width: 560px) { .tiles { grid-template-columns: 1fr; } } -.tile { +/* Nächster Einsatz */ +.next-op { + display: grid; + gap: 24px; + align-items: end; + margin-bottom: 48px; +} +@media (min-width: 900px) { .next-op { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); } } +.next-op h2 { + margin: 0; + font-size: clamp(2.2rem, 6vw, 4.8rem); + font-weight: 700; + letter-spacing: .02em; +} +.next-op .when { font-family: var(--font-head); font-size: clamp(1.1rem, 2.4vw, 1.5rem); text-transform: uppercase; letter-spacing: .14em; color: var(--steel); margin-top: 10px; } +.countdown { display: flex; + align-items: baseline; + gap: 14px; + justify-content: flex-start; + padding: 18px 22px; + background: var(--card); + box-shadow: inset 0 0 0 1px var(--border); + border-left: 4px solid var(--accent); +} +.countdown .big { + font-family: var(--font-head); + font-weight: 700; + font-size: clamp(3rem, 8vw, 5.5rem); + line-height: .9; + color: var(--accent); + font-variant-numeric: tabular-nums; +} +.countdown .unit { font-family: var(--font-head); text-transform: uppercase; letter-spacing: .14em; font-size: 1.05rem; } +.countdown .unit small { display: block; color: var(--muted); font-family: var(--font-body); text-transform: none; letter-spacing: 0; font-size: .92rem; } + +/* Bereiche: eine große Kachel, drei kleine */ +.areas { + display: grid; gap: 16px; - align-items: flex-start; + margin-bottom: 48px; + grid-template-columns: 1fr; +} +@media (min-width: 700px) { .areas { grid-template-columns: 1fr 1fr; } } +@media (min-width: 1150px) { + .areas { grid-template-columns: 1.5fr 1fr 1fr; grid-template-rows: auto auto; } + .areas .tile.big { grid-row: span 2; } +} +.tile { + position: relative; + display: flex; + flex-direction: column; + gap: 10px; text-decoration: none; color: var(--text); background: var(--card); - border: 1px solid var(--border); - border-top: 3px solid var(--accent); - border-radius: var(--radius); - padding: 20px; - transition: border-color .15s, background .15s; + box-shadow: inset 0 0 0 1px var(--border); + padding: 22px 24px; + overflow: hidden; + clip-path: polygon(0 0, calc(100% - 22px) 0, 100% 22px, 100% 100%, 0 100%); + transition: background-color .2s; } -.tile:hover { background: var(--card-2); border-color: var(--accent); } -.tile svg { flex: 0 0 36px; width: 36px; height: 36px; color: var(--accent); } -.tile h2 { margin: 0 0 4px; font-size: 1.1rem; } +.tile::after { + content: ""; + position: absolute; + left: 0; bottom: 0; + height: 3px; width: 100%; + background: var(--accent); + transform: scaleX(.12); + transform-origin: left; + transition: transform .45s var(--ease-out); +} +.tile:hover { background: var(--card-2); } +.tile:hover::after { transform: scaleX(1); } +.tile svg { width: 34px; height: 34px; color: var(--accent); transition: transform .45s var(--ease-out); } +.tile:hover svg { transform: translateX(4px); } +.tile h2 { margin: 0; font-size: 1.5rem; font-weight: 700; letter-spacing: .08em; } .tile p { margin: 0; color: var(--muted); } +.tile .go { margin-top: auto; font-family: var(--font-head); text-transform: uppercase; letter-spacing: .16em; font-size: .85rem; color: var(--accent); } +.tile.big { justify-content: flex-end; min-height: 280px; background: linear-gradient(160deg, var(--flag) 0%, var(--card) 70%); } +.tile.big svg { width: 54px; height: 54px; } +.tile.big h2 { font-size: clamp(2rem, 4vw, 3rem); } +.tile.big .bg-gear { + position: absolute; + right: -60px; top: -60px; + width: 300px; height: 300px; + opacity: .1; + transition: transform 1.2s var(--ease-out); +} +.tile.big:hover .bg-gear { transform: rotate(45deg); } +.tile .lock { position: absolute; top: 18px; right: 30px; color: var(--muted); font-family: var(--font-head); text-transform: uppercase; letter-spacing: .14em; font-size: .75rem; } + +/* Handy: kleine Schaltflächen, zwei pro Reihe, Kalender über die volle Breite */ +@media (max-width: 699px) { + .areas { grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 36px; } + .areas .tile.big { grid-column: span 2; } + .tile, .tile.big { + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 10px; + min-height: 0; + padding: 14px; + clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%); + } + .tile p, .tile .go, .tile.big .bg-gear { display: none; } + .tile svg, .tile.big svg { width: 22px; height: 22px; flex: none; } + .tile h2, .tile.big h2 { font-size: 1.05rem; letter-spacing: .06em; } + /* Patch-Vorschau nur ab Tablet */ + #patch-section { display: none !important; } +} + +.section-title { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin: 0 0 16px; +} +.section-title h2 { margin: 0; font-size: clamp(1.6rem, 3.4vw, 2.4rem); font-weight: 700; } + +/* Patch-Leiste auf der Startseite: waagerecht wischen */ +.patch-rail { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(180px, 220px); + gap: 16px; + overflow-x: auto; + scroll-snap-type: x mandatory; + padding-bottom: 12px; + margin-bottom: 48px; + scrollbar-color: var(--border) transparent; +} +.patch-rail a { scroll-snap-align: start; text-decoration: none; color: var(--text); } +.patch-rail figure { margin: 0; } +.patch-rail .patch-img { aspect-ratio: 1; } +.patch-rail figcaption { margin-top: 8px; font-family: var(--font-head); text-transform: uppercase; letter-spacing: .1em; font-weight: 700; } + +.home-grid { display: grid; gap: 20px; margin-bottom: 48px; align-items: start; } +@media (min-width: 1000px) { .home-grid { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); } } +.home-grid .card { margin: 0; } + +.about { + display: grid; + gap: 32px; + align-items: center; + padding: clamp(24px, 4vw, 56px); + background: linear-gradient(120deg, var(--card) 0%, var(--bg-deep) 100%); + box-shadow: inset 0 0 0 1px var(--border); + margin-bottom: 8px; +} +@media (min-width: 900px) { .about { grid-template-columns: auto minmax(0, 1fr); } } +.about img { width: clamp(120px, 16vw, 200px); height: auto; image-rendering: auto; } +.about h2 { margin: 0 0 10px; font-size: clamp(1.6rem, 3.4vw, 2.4rem); font-weight: 700; } +.about p { max-width: 65ch; color: var(--muted); } +.about p strong { color: var(--text); } + +.social { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; } +.social svg { width: 18px; height: 18px; flex: none; } + +/* Elemente gleiten beim ersten Sichtbarwerden herein (nur wenn home.js sie markiert) */ +.reveal-pending { opacity: 0; transform: translateY(28px); } +.reveal-in { opacity: 1; transform: none; transition: opacity .8s var(--ease-out), transform .8s var(--ease-out); } /* ---------- Kalender ---------- */ -.cal-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; } -.cal-nav h2 { margin: 0; font-size: 1rem; text-align: center; flex: 1; border: none; padding: 0; } +.cal-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; } +.cal-nav h2 { margin: 0; font-size: clamp(1.1rem, 4.2vw, 1.5rem); text-align: center; flex: 1; } +.cal-nav button { font-size: 1.2rem; padding: 4px 14px; } .cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); - gap: 3px; + gap: 4px; } .cal-grid .dow { text-align: center; - font-size: .75rem; + font-size: .85rem; + font-weight: 500; color: var(--muted); padding: 4px 0; text-transform: uppercase; - letter-spacing: .1em; + letter-spacing: .14em; } .cal-day { - min-height: 72px; + min-height: clamp(64px, 8.5vw, 124px); border: 1px solid var(--border); - border-radius: var(--radius); - padding: 4px; + padding: 6px; background: var(--bg-deep); cursor: pointer; text-align: left; display: flex; flex-direction: column; - gap: 2px; - font-family: inherit; - font-size: .85rem; + justify-content: flex-start; + align-items: stretch; + gap: 3px; + font-family: var(--font-body); + font-size: .9rem; text-transform: none; letter-spacing: 0; overflow: hidden; } +.cal-day:hover { border-color: #56629c; background: #10152e; } .cal-day.other { opacity: .35; } +.cal-day.weekend { background: #0f1430; } .cal-day.today .num { background: var(--steel); color: var(--bg-deep); padding: 0 6px; } .cal-day.selected { outline: 2px solid var(--accent); outline-offset: -1px; } -.cal-day .num { align-self: flex-start; font-weight: 600; } +.cal-day .num { align-self: flex-start; font-weight: 600; font-variant-numeric: tabular-nums; } .cal-day .ev { - font-size: .72rem; + font-size: .78rem; background: var(--accent-soft); border-left: 3px solid var(--accent); - padding: 1px 4px; + padding: 2px 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.cal-day .ev.mine { background: var(--accent); color: var(--accent-text); } +.cal-day .ev.nogame { background: var(--steel-soft); border-left-color: var(--steel); color: var(--steel); } +.cal-day .ev.mine { background: var(--accent); color: var(--accent-text); font-weight: 600; } +.cal-day .ev.nogame.mine { background: var(--steel); color: var(--bg-deep); } +.cal-legend { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 12px; font-size: .88rem; color: var(--muted); } +.cal-legend span { display: inline-flex; align-items: center; gap: 6px; } +.cal-legend i { width: 14px; height: 10px; display: inline-block; } +.cal-legend .l-game { background: var(--accent-soft); border-left: 3px solid var(--accent); } +.cal-legend .l-nogame { background: var(--steel-soft); border-left: 3px solid var(--steel); } +.cal-legend .l-mine { background: var(--accent); } @media (max-width: 560px) { - .cal-day { min-height: 52px; padding: 2px; } + .cal-day { min-height: 52px; padding: 3px; } .cal-day .ev { font-size: 0; padding: 0; height: 6px; } } +.events-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); align-items: start; } .event { + position: relative; border: 1px solid var(--border); border-left: 3px solid var(--accent); - border-radius: var(--radius); - padding: 12px; - margin-bottom: 10px; + padding: 14px; + margin-bottom: 12px; background: var(--bg-deep); } -.event h3 { margin: 0; font-size: .95rem; } -.event .when { color: var(--muted); font-size: .85rem; margin-bottom: 6px; } -.event .note { margin: 6px 0; white-space: pre-wrap; } +.events-grid .event { margin: 0; } +.event.nogame { border-left-color: var(--steel); } +.event-top { display: flex; gap: 10px; align-items: flex-start; justify-content: space-between; } +.event h3 { margin: 0; font-size: 1.15rem; } +.event .when { color: var(--muted); font-size: .92rem; margin: 4px 0 8px; } +.event .note { margin: 8px 0; white-space: pre-wrap; overflow-wrap: anywhere; } /* ---------- Statistik ---------- */ -.stats .rank { color: var(--muted); min-width: 3ch; font-family: var(--font-head); } +.stats li { flex-wrap: nowrap; } +.stats .rank { color: var(--muted); min-width: 3ch; font-family: var(--font-head); font-size: 1.1rem; font-variant-numeric: tabular-nums; } +.stats li.top .rank { color: var(--accent); } +.stats .who { flex: 1; min-width: 0; display: grid; gap: 4px; } +.stats .who .name { font-weight: 600; } +.stat-bar { height: 8px; background: var(--steel-soft); position: relative; overflow: hidden; } +.stat-bar i { + position: absolute; + inset: 0 auto 0 0; + background: var(--accent); + border-radius: 0 4px 4px 0; + transform-origin: left; + animation: grow .9s var(--ease-out) both; +} +@keyframes grow { from { transform: scaleX(0); } } .stat-total { min-width: 3ch; text-align: right; font-family: var(--font-head); - font-size: 1.2rem; + font-size: 1.6rem; font-weight: 700; - color: var(--accent); + color: var(--text); + font-variant-numeric: tabular-nums; } +.stats .actions { flex-wrap: nowrap; } + +.facts { margin: 0; padding-left: 1.2em; display: grid; gap: 10px; color: var(--muted); } +.facts strong { color: var(--text); } +.facts li::marker { color: var(--accent); } + +/* ---------- Patches ---------- */ +.patch-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr)); +} +.patch { + position: relative; + display: flex; + flex-direction: column; + background: var(--card); + box-shadow: inset 0 0 0 1px var(--border); + clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px)); +} +.patch-img { + display: grid; + place-items: center; + aspect-ratio: 1; + max-width: 100%; + padding: 18px; + background: + radial-gradient(circle at 50% 45%, #2a3466 0%, var(--bg-deep) 75%); + border: none; + width: 100%; + cursor: zoom-in; + overflow: hidden; +} +.patch-img img { + width: 100%; + height: 100%; + object-fit: contain; + filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .45)); + transition: transform .5s var(--ease-out); +} +.patch:hover .patch-img img, .patch-rail a:hover .patch-img img { transform: scale(1.06) rotate(-2deg); } +.patch-body { padding: 14px 16px 16px; display: grid; gap: 6px; } +.patch-body h3 { margin: 0; font-size: 1.15rem; } +.patch-body p { margin: 0; color: var(--muted); font-size: .94rem; white-space: pre-wrap; overflow-wrap: anywhere; } +.patch-body .actions { display: flex; gap: 4px; margin-top: 4px; } + +.patch-group + .patch-group { margin-top: 40px; } +.patch-body .badge { justify-self: start; } +/* Geplante Patches: gestrichelter Rahmen, Bild leicht entsättigt */ +.patch.planned { box-shadow: inset 0 0 0 1px #5b6488; } +.patch.planned .patch-img { background: repeating-linear-gradient(135deg, var(--bg-deep) 0 12px, #121834 12px 24px); } +.patch.planned .patch-img img { filter: saturate(.55) drop-shadow(0 10px 18px rgba(0, 0, 0, .45)); opacity: .85; } + +.segmented { display: flex; gap: 0; border: none; padding: 0; margin: 0 0 12px; flex-wrap: wrap; } +.segmented legend { font-size: .94rem; color: var(--muted); margin-bottom: 5px; padding: 0; } +.segmented label { margin: 0; cursor: pointer; } +.segmented input { position: absolute; opacity: 0; pointer-events: none; } +.segmented label { + display: inline-flex; + align-items: center; + padding: 8px 16px; + border: 1px solid var(--border); + background: var(--bg-deep); + color: var(--muted); + font-family: var(--font-head); + text-transform: uppercase; + letter-spacing: .1em; +} +.segmented label + label { border-left: none; } +.segmented label:has(input:checked) { background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 700; } +.segmented label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; } + +.dropzone { + display: grid; + place-items: center; + gap: 8px; + text-align: center; + min-height: 190px; + padding: 18px; + margin-bottom: 14px; + border: 2px dashed var(--border); + background: var(--bg-deep); + color: var(--muted); + cursor: pointer; + transition: border-color .15s, background-color .15s; +} +.dropzone:hover, .dropzone.over { border-color: var(--accent); background: #10152e; } +.dropzone img { max-height: 220px; max-width: 100%; object-fit: contain; } +.dropzone strong { color: var(--text); font-family: var(--font-head); text-transform: uppercase; letter-spacing: .12em; } + +.lightbox { + border: none; + padding: 0; + background: transparent; + max-width: min(92vw, 900px); + max-height: 92vh; + color: var(--text); +} +.lightbox::backdrop { background: rgba(8, 10, 24, .88); backdrop-filter: blur(4px); } +.lightbox img { display: block; max-width: 100%; max-height: 80vh; margin: 0 auto; object-fit: contain; } +.lightbox figcaption { text-align: center; margin-top: 12px; } +.lightbox figcaption strong { display: block; font-family: var(--font-head); text-transform: uppercase; letter-spacing: .12em; font-size: 1.3rem; } +.lightbox .close { position: absolute; top: 0; right: 0; } + +.empty-state { + display: grid; + place-items: center; + gap: 8px; + text-align: center; + padding: 48px 16px; + color: var(--muted); + border: 1px dashed var(--border); +} +.empty-state svg { width: 56px; height: 56px; color: var(--border); } /* ---------- Fahrtplaner ---------- */ -.cars { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; margin-top: 12px; } +.cars { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 14px; margin-top: 14px; } .car-meet { margin: 10px 0 4px; padding-top: 10px; border-top: 1px solid var(--border); } -.car-meet label { font-size: .8rem; margin-bottom: 6px; } +.car-meet label { font-size: .85rem; margin-bottom: 6px; } .car-meet .grid2 { gap: 8px; grid-template-columns: 1fr 1fr; } .car { border: 1px solid var(--border); border-top: 3px solid var(--steel); - border-radius: var(--radius); - padding: 12px; + padding: 14px; background: var(--bg-deep); } -.car h3 { margin: 0 0 4px; font-size: .9rem; } +.car h3 { margin: 0 0 4px; font-size: 1.05rem; } .car ol { margin: 6px 0 0; padding-left: 20px; } .meeting { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; margin: 8px 0 4px; } .meeting dt { color: var(--muted); } .meeting dd { margin: 0; font-weight: 600; } .meeting dd small { display: block; font-weight: 400; color: var(--muted); } +/* ---------- Fußzeile ---------- */ footer { - text-align: center; + display: flex; + gap: 16px 32px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; color: var(--muted); font-family: var(--font-head); text-transform: uppercase; letter-spacing: .2em; - font-size: .75rem; - padding: 24px 16px; + font-size: .82rem; + padding: 28px var(--gutter); border-top: 1px solid var(--border); + background: var(--bg-deep); } +footer .f-brand { display: inline-flex; align-items: center; gap: 12px; } +footer .f-brand img { width: 32px; height: 32px; } +.footer-links { display: flex; gap: 20px; flex-wrap: wrap; } +.footer-links a { color: var(--muted); text-decoration: none; } +.footer-links a:hover { color: var(--accent); } /* ---------- Responsiv ---------- */ .nav-toggle { display: none; } -@media (max-width: 900px) { - .site-header { padding: 8px 16px; } +@media (max-width: 960px) { + .site-header { padding: 8px var(--gutter); } .site-header .inner { gap: 8px; } .nav-toggle { display: inline-flex; - align-items: center; - justify-content: center; margin-left: auto; width: 44px; height: 44px; @@ -401,40 +913,58 @@ footer { background: none; color: var(--steel); } + .site-header.open { background: rgba(13, 17, 38, .97); } .site-header.open .nav-toggle { color: var(--accent); border-color: var(--accent); } .site-header nav { display: none; width: 100%; flex-direction: column; margin: 4px 0 0; gap: 0; } .site-header.open nav { display: flex; } .site-header nav a, .site-header nav button { - padding: 12px 4px; + padding: 13px 4px; text-align: left; + justify-content: flex-start; border-bottom: 1px solid var(--border); } + .site-header nav a::after, .site-header nav button::after { display: none; } .site-header nav a:last-child, .site-header nav button:last-child { border-bottom: none; } - .site-header nav a.active { border-bottom-color: var(--border); border-left: 3px solid var(--accent); padding-left: 10px; } + .site-header nav a.active { border-left: 3px solid var(--accent); padding-left: 10px; } } -@media (max-width: 380px) { +@media (max-width: 420px) { .brand small { display: none; } } @media (max-width: 760px) { - main { padding: 12px; } - .card { padding: 14px; } + main { padding-top: 20px; } + .card { padding: 16px; } /* iOS zoomt bei Schrift unter 16px ins Feld */ input[type=text], input[type=password], input[type=number], input[type=date], input[type=time], select, textarea { font-size: 16px; } - .stats .meta { flex-basis: 100%; order: 10; padding-left: calc(3ch + 10px); margin-top: -6px; } + .hero { height: 150vh; } + .hud .tr { display: none; } + /* Handy: Inhalt beginnt erst unter dem Logo-Bereich, keine Überlappung */ + .home main { margin-top: 0; } } /* Touch-Geräte: ausreichend große Tippflächen */ @media (pointer: coarse) { - button.small, .button.small { min-height: 36px; min-width: 36px; } + button.small, .button.small { min-height: 38px; min-width: 38px; } button.link { padding: 8px 8px; } input[type=checkbox] { width: 20px; height: 20px; } } +/* Weniger Bewegung: kein Zoom, nichts bleibt unsichtbar */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation: none !important; transition-duration: .01ms !important; } + .hero { height: auto; } + .hero-stage { position: relative; height: min(80vh, 720px); --p: 0 !important; } + .home main { margin-top: 0; padding-top: 28px; } + .reveal-pending { opacity: 1; transform: none; } +} + @media print { .site-header, footer, .no-print { display: none !important; } :root { --text: #000; --muted: #444; --border: #999; --bg-deep: #fff; --card: #fff; } body { background: #fff; color: #000; } - .card { border: none; } + .card { box-shadow: none; clip-path: none; padding: 0; } + .card::before { display: none; } + .split { display: block; } + main { padding: 0; } } diff --git a/public/fonts/barlow-condensed-latin-500-normal.woff2 b/public/fonts/barlow-condensed-latin-500-normal.woff2 new file mode 100644 index 0000000..1c0b873 Binary files /dev/null and b/public/fonts/barlow-condensed-latin-500-normal.woff2 differ diff --git a/public/fonts/barlow-condensed-latin-700-normal.woff2 b/public/fonts/barlow-condensed-latin-700-normal.woff2 new file mode 100644 index 0000000..8b27b54 Binary files /dev/null and b/public/fonts/barlow-condensed-latin-700-normal.woff2 differ diff --git a/public/fonts/barlow-latin-400-normal.woff2 b/public/fonts/barlow-latin-400-normal.woff2 new file mode 100644 index 0000000..44aafda Binary files /dev/null and b/public/fonts/barlow-latin-400-normal.woff2 differ diff --git a/public/fonts/barlow-latin-500-normal.woff2 b/public/fonts/barlow-latin-500-normal.woff2 new file mode 100644 index 0000000..bd02c3d Binary files /dev/null and b/public/fonts/barlow-latin-500-normal.woff2 differ diff --git a/public/fonts/barlow-latin-600-normal.woff2 b/public/fonts/barlow-latin-600-normal.woff2 new file mode 100644 index 0000000..0656293 Binary files /dev/null and b/public/fonts/barlow-latin-600-normal.woff2 differ diff --git a/public/fonts/saira-stencil-one-latin-400-normal.woff2 b/public/fonts/saira-stencil-one-latin-400-normal.woff2 new file mode 100644 index 0000000..6f38d4c Binary files /dev/null and b/public/fonts/saira-stencil-one-latin-400-normal.woff2 differ diff --git a/public/img/flagge-grund.jpg b/public/img/flagge-grund.jpg new file mode 100644 index 0000000..328cd29 Binary files /dev/null and b/public/img/flagge-grund.jpg differ diff --git a/public/img/zahnrad.webp b/public/img/zahnrad.webp new file mode 100644 index 0000000..de397d4 Binary files /dev/null and b/public/img/zahnrad.webp differ diff --git a/public/index.html b/public/index.html index 741a820..904db79 100644 --- a/public/index.html +++ b/public/index.html @@ -4,118 +4,132 @@ HCRB – Highcap Rasselbande + + + - -
          -
          -

          HCRB

          -
          Highcap Rasselbande
          + +
          +
          + + Flagge der HCRB: Berliner Bär in einem Zahnrad + +
          + +
          +

          HCRB

          +
          Highcap Rasselbande
          +
          -

          - Wir sind die Highcap Rasselbande – eine Airsoft-Gruppe aus Berlin. - Am häufigsten findet ihr uns auf der Darkzone. -

          +
          +
          + Nächster Einsatz +

          Lädt …

          +
          +
          + +
          -

          - Diese Seite ist noch in Arbeit. Es kommen noch Änderungen und neue Funktionen dazu. -

          - - + + +

          Patches

          +

          Alle Patches der Bande.

          +
          + + +

          Statistik

          +

          Wer war wie oft spielen?

          +
          + + +

          Gruppe

          +

          Mitglieder und Treffpunkte.

          +
          + -
          -
          -

          Nächste Spieltage

          - Zum Kalender +
          +
          +
          -
          -
          -

          Pile of Shame

          - Zur Statistik +
          +
          +
          +

          Nächste Termine

          + Zum Kalender +
          +

          Lädt …

          -

          Lädt …

          -
          -
          -

          Über uns

          -

          - Wir sind die Highcap Rasselbande (HCRB), eine Airsoft-Gruppe aus Berlin. - Gespielt wird bei uns vor allem auf der Darkzone – dort sind wir regelmäßig unterwegs. -

          -

          - Hier planen wir unsere Spieltage und Fahrgemeinschaften. Videos und Bilder von unseren Spielen - gibt es auf YouTube und Instagram: -

          - + +
          + +
          + Über uns +

          Highcap Rasselbande

          +

          + Wir sind die Highcap Rasselbande (HCRB), eine Airsoft-Gruppe aus Berlin. + Gespielt wird bei uns vor allem auf der Darkzone, dort sind wir regelmäßig unterwegs. +

          +

          + Hier planen wir unsere Spieltage und Fahrgemeinschaften. Videos und Bilder von unseren Spielen + gibt es auf YouTube und Instagram. +

          + +
          +
          - - + diff --git a/public/js/common.js b/public/js/common.js index 5630acb..2dab0d2 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -80,11 +80,13 @@ const NAV = [ { href: '/fahrt', key: 'fahrt', label: 'Fahrtplaner' }, { href: '/gruppe', key: 'gruppe', label: 'Gruppe' }, { href: '/statistik', key: 'statistik', label: 'Statistik' }, + { href: '/patches', key: 'patches', label: 'Patches' }, ]; -export function renderHeader(active) { +// overlay: Kopfzeile liegt durchsichtig über dem Hero und wird erst beim Scrollen fest (Startseite) +export function renderHeader(active, { overlay = false } = {}) { const header = document.createElement('header'); - header.className = 'site-header'; + header.className = overlay ? 'site-header overlay' : 'site-header'; header.innerHTML = `
          HCRBHighcap Rasselbande @@ -98,6 +100,11 @@ export function renderHeader(active) {
          `; document.body.prepend(header); + if (overlay) { + const update = () => header.classList.toggle('scrolled', scrollY > 40); + addEventListener('scroll', update, { passive: true }); + update(); + } // Auf schmalen Bildschirmen klappt das Menü über die Schaltfläche auf const toggle = header.querySelector('.nav-toggle'); toggle.addEventListener('click', () => { @@ -114,7 +121,7 @@ export function renderHeader(active) { export function renderFooter() { const footer = document.createElement('footer'); footer.innerHTML = ` - HCRB · Highcap Rasselbande · Berlin + HCRB · Highcap Rasselbande · Berlin