Vogelalbum 1.0.0
Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine Sammlung, die sich von allein fuellt. Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert — ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten, gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte, Stufen und 15 Abzeichen. Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto, keine Telemetrie; die Bilder verlassen das eigene Netz nicht. Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und braucht deshalb weder eine erreichbare Adresse noch einen offenen Port — er laeuft hinter einem gewoehnlichen Router. Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs. AGPL-3.0.
This commit is contained in:
commit
c11fe9ab04
189 files changed
+29806
No files matched your search
@@ -0,0 +1,781 @@
|
||||
/* Vogelalbum — eigenständiges Stylesheet, kein Build-Schritt.
|
||||
*
|
||||
* Zwei Modi über Farbtokens: jede Farbe existiert einmal hell und einmal
|
||||
* dunkel, umgeschaltet wird nur die Zuordnung. Deshalb steht unten kein
|
||||
* einziger Farbwert mehr direkt in einer Regel.
|
||||
*
|
||||
* Die Akzentfarbe kommt vom Typ der Art, nicht global — dadurch fühlt sich
|
||||
* jede Artseite anders an. Die typabhängigen Töne werden in
|
||||
* dex/species_meta.py vorberechnet und als Inline-Variablen mitgegeben, in
|
||||
* beiden Modi (Endung -l und -d). Die Indirektion weiter unten wählt aus.
|
||||
*
|
||||
* Keine modernen Funktionen ohne Not: kein color-mix (Safari ab 16.2), kein
|
||||
* aspect-ratio (Safari ab 15). Beides fällt auf älteren Geräten ersatzlos weg
|
||||
* und reißt das Layout auseinander — Bildverhältnisse laufen über padding-top.
|
||||
*/
|
||||
|
||||
/* Die Palette kommt aus der Marke: tiefes Grün und Creme. Vorher war das
|
||||
* Grün der Oberfläche ein anderes als das des Zeichens — nebeneinander sah
|
||||
* man den Unterschied sofort, und die Seite wirkte kühl und beliebig.
|
||||
* Papierweiß statt Blauweiß, damit Fotos von Vögeln warm wirken. */
|
||||
:root {
|
||||
--bg: #fbf8ef;
|
||||
--bg-grad: #f4eddd;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f3ece0;
|
||||
--line: #e7dfcc;
|
||||
--text: #14281c;
|
||||
--text-2: #5c6b60;
|
||||
--muted: #7a8a7f;
|
||||
|
||||
--accent: #0f5f3c;
|
||||
--accent-2: #0a3f28;
|
||||
--accent-wash: #e6f0e9;
|
||||
--accent-on: #fcf2d3;
|
||||
|
||||
/* Der Kopf ist dunkel, damit die Seite nicht als weißes Blatt beginnt. */
|
||||
--kopf-bg: #0a3f28;
|
||||
--kopf-text: #fcf2d3;
|
||||
--kopf-text-2: #a8ceb8;
|
||||
--kopf-an: rgba(255,255,255,.15);
|
||||
--kopf-linie: rgba(255,255,255,.22);
|
||||
--gold: #cf9a1c;
|
||||
--gold-wash: #fbf3dc;
|
||||
--gold-line: #ecd9a0;
|
||||
|
||||
--shadow: 0 1px 2px rgba(22,33,26,.05), 0 8px 20px -12px rgba(22,33,26,.18);
|
||||
--shadow-lift: 0 2px 4px rgba(22,33,26,.07), 0 16px 32px -16px rgba(22,33,26,.26);
|
||||
--nr-bg: rgba(255,255,255,.92);
|
||||
--head-bg: rgba(255,255,255,.86);
|
||||
--locked-opacity: .30;
|
||||
|
||||
--r-sm: 10px;
|
||||
--r: 16px;
|
||||
--r-lg: 24px;
|
||||
--font: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||
|
||||
--typ: #7f9b6a;
|
||||
--typ-ink: #ffffff;
|
||||
}
|
||||
|
||||
/* Typabhängige Töne werden je Modus direkt am Verwendungsort gewählt.
|
||||
Der naheliegende Umweg über eine Variable auf :root funktioniert nicht:
|
||||
var() in einer Custom-Property-Deklaration wird auf dem deklarierenden
|
||||
Element ersetzt, und dort gibt es die -l/-d-Werte der Karte nicht. */
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Kühles Blaugrau statt Dunkelgrün: neutral genug, dass 16 Typfarben
|
||||
darauf nebeneinander funktionieren, ohne sich mit dem Grund zu beißen. */
|
||||
--bg: #10151b;
|
||||
--bg-grad: #141b23;
|
||||
--surface: #1a2129;
|
||||
--surface-2: #222b35;
|
||||
--line: #2e3945;
|
||||
--text: #e8eef4;
|
||||
--text-2: #b3c1cd;
|
||||
--muted: #7e8f9e;
|
||||
|
||||
--accent: #45c98a;
|
||||
--accent-2: #2f9d68;
|
||||
--accent-wash: #163024;
|
||||
--accent-on: #08150d;
|
||||
--gold: #e8be5a;
|
||||
--gold-wash: #2a2415;
|
||||
--gold-line: #4a4026;
|
||||
|
||||
/* Auf dunklem Grund trägt nicht der Schatten, sondern die Kante. */
|
||||
--shadow: 0 1px 2px rgba(0,0,0,.4);
|
||||
--shadow-lift: 0 10px 30px -14px rgba(0,0,0,.75);
|
||||
--nr-bg: rgba(10,14,19,.82);
|
||||
--head-bg: rgba(16,21,27,.86);
|
||||
--locked-opacity: .26;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font);
|
||||
color: var(--text);
|
||||
background: linear-gradient(180deg, var(--bg-grad) 0%, var(--bg) 420px);
|
||||
background-attachment: fixed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
img { max-width: 100%; }
|
||||
|
||||
/* ⚠ padding-inline, nicht die Kurzform ``padding``. Der Kopf traegt
|
||||
``class="wrap top-inner"``, und ``.top-inner`` setzt den senkrechten
|
||||
Abstand (12px). Eine Kurzform hier setzt den auf 0 zurueck — genau das
|
||||
ist auf Telefonen passiert: Marke und Navigation klebten oben und unten
|
||||
an der Kante der gruenen Leiste. */
|
||||
.wrap { max-width: 1120px; margin: 0 auto; padding-inline: 20px; }
|
||||
|
||||
/* ---------- Kopf ---------- */
|
||||
|
||||
/* 🔑 Der Kopf ist dunkel, nicht durchscheinend hell. Die Seite begann vorher
|
||||
als weisses Blatt und wirkte dadurch unfertig; eine dunkle Leiste gibt ihr
|
||||
oben einen Halt und laesst die Fotos darunter waermer wirken. */
|
||||
header.top {
|
||||
position: sticky; top: 0; z-index: 30;
|
||||
background: var(--kopf-bg);
|
||||
color: var(--kopf-text);
|
||||
}
|
||||
|
||||
.top-inner { display: flex; align-items: center; gap: 14px; padding: 12px 0; flex-wrap: wrap; }
|
||||
|
||||
.brand { font-weight: 800; font-size: 21px; letter-spacing: -0.03em; display: flex; align-items: center; gap: 10px; color: var(--kopf-text); }
|
||||
/* 🔑 Die Wortmarke ist EIN Flex-Kind, nicht zwei.
|
||||
Vorher standen "Vogel" und "album" getrennt im Flex-Container und bekamen
|
||||
dessen gap von 10 px zwischen sich — bei "Bird dex" war das gewollt, bei
|
||||
einem deutschen Kompositum liest es sich wie ein Tippfehler. Jetzt trennt
|
||||
der Abstand nur noch Zeichen und Wort; die Farbe wechselt mitten im Wort. */
|
||||
.brand .wort { color: var(--kopf-text); }
|
||||
.brand .wort span { color: #7fd3a4; }
|
||||
|
||||
/* ⚠ Das Zeichen traegt dunkelgruene Flaechen. Direkt auf der dunklen Leiste
|
||||
verschwinden die, und uebrig bleibt ein cremefarbener Klecks — die Marke
|
||||
war an der wichtigsten Stelle nicht mehr zu erkennen. Deshalb eine helle
|
||||
Scheibe darunter: das Zeichen selbst bleibt unveraendert, es bekommt nur
|
||||
einen Traeger. Nebenbei nimmt die runde Form die Abzeichen-Pins auf. */
|
||||
.brand .scheibe {
|
||||
width: 40px; height: 40px; flex: 0 0 40px;
|
||||
border-radius: 50%; background: var(--kopf-text);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.brand img { display: block; width: 31px; height: 31px; }
|
||||
|
||||
nav.main { display: flex; gap: 3px; margin-left: auto; flex-wrap: wrap; }
|
||||
nav.main a {
|
||||
padding: 8px 15px; border-radius: 999px;
|
||||
font-size: 14px; font-weight: 600; color: var(--text-2);
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
nav.main a:hover { background: var(--surface-2); color: var(--text); }
|
||||
nav.main a.active { background: var(--accent); color: var(--accent-on); }
|
||||
|
||||
/* ---------- Bedienelemente auf der dunklen Leiste ----------
|
||||
Alles im Kopf steht jetzt auf Dunkelgruen. Die Regeln davor rechneten mit
|
||||
hellem Grund; ohne diesen Block waere die Navigation dunkelgrau auf
|
||||
dunkelgruen, also praktisch unsichtbar. Gesammelt an einer Stelle, damit
|
||||
beim naechsten Farbwechsel klar ist, was zusammengehoert. */
|
||||
header.top nav.main a { color: var(--kopf-text-2); }
|
||||
header.top nav.main a:hover { background: rgba(255,255,255,.10); color: var(--kopf-text); }
|
||||
header.top nav.main a.active { background: var(--kopf-an); color: var(--kopf-text); }
|
||||
|
||||
header.top .live-knopf {
|
||||
background: transparent; border-color: var(--kopf-linie); color: var(--kopf-text-2);
|
||||
}
|
||||
header.top .live-knopf:hover { color: var(--kopf-text); border-color: var(--kopf-text); }
|
||||
|
||||
header.top .lang-switch { background: transparent; border-color: var(--kopf-linie); }
|
||||
header.top .lang-switch button { color: var(--kopf-text-2); }
|
||||
header.top .lang-switch button:hover { color: var(--kopf-text); }
|
||||
header.top .lang-switch button.on { background: var(--kopf-text); color: var(--kopf-bg); }
|
||||
|
||||
header.top .theme-toggle, header.top .gear {
|
||||
background: transparent; border-color: var(--kopf-linie); color: var(--kopf-text-2);
|
||||
}
|
||||
header.top .theme-toggle:hover, header.top .gear:hover {
|
||||
border-color: var(--kopf-text); color: var(--kopf-text);
|
||||
}
|
||||
|
||||
/* Der Live-Knopf. Kein Navigationspunkt, sondern ein Hinweis, dass gerade
|
||||
etwas zu sehen ist — der Punkt pulst, solange die Seite offen ist. Rot ist
|
||||
hier die richtige Farbe: sie heisst "laeuft jetzt", nicht "Fehler". */
|
||||
.live-knopf {
|
||||
flex: none; display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 700; text-decoration: none;
|
||||
color: var(--text-2); background: var(--surface);
|
||||
border: 1px solid var(--line); border-radius: 999px; padding: 6px 12px 6px 10px;
|
||||
transition: background .15s, color .15s, border-color .15s;
|
||||
}
|
||||
.live-knopf:hover { color: var(--text); border-color: var(--text-2); }
|
||||
.live-knopf.on { background: #d6303a; border-color: #d6303a; color: #fff; }
|
||||
.live-knopf .punkt {
|
||||
width: 8px; height: 8px; border-radius: 50%; background: #d6303a;
|
||||
animation: live-puls 2s ease-in-out infinite;
|
||||
}
|
||||
.live-knopf.on .punkt { background: #fff; }
|
||||
|
||||
@keyframes live-puls {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .35; }
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 38px; height: 38px; flex-shrink: 0;
|
||||
border: 1px solid var(--line); border-radius: 50%;
|
||||
background: var(--surface); color: var(--text-2);
|
||||
font-size: 17px; line-height: 1; cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
transition: border-color .15s, color .15s;
|
||||
}
|
||||
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.theme-toggle .icon-dark { display: none; }
|
||||
[data-theme="dark"] .theme-toggle .icon-dark { display: block; }
|
||||
[data-theme="dark"] .theme-toggle .icon-light { display: none; }
|
||||
|
||||
/* ---------- Fortschritt ---------- */
|
||||
|
||||
.progress-bar {
|
||||
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
|
||||
padding: 18px 22px; margin: 22px 0;
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
width: 60px; height: 60px; border-radius: 50%;
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
font-weight: 800; font-size: 24px; color: var(--accent-on);
|
||||
background: linear-gradient(145deg, var(--accent), var(--accent-2));
|
||||
box-shadow: 0 0 0 5px var(--accent-wash);
|
||||
}
|
||||
|
||||
.xp-track { height: 11px; border-radius: 999px; overflow: hidden; background: var(--surface-2); }
|
||||
.xp-fill {
|
||||
height: 100%; border-radius: 999px;
|
||||
background: linear-gradient(90deg, var(--accent-2), var(--accent));
|
||||
transition: width .6s cubic-bezier(.2,.8,.3,1);
|
||||
}
|
||||
|
||||
.rank { font-weight: 750; font-size: 18px; letter-spacing: -0.015em; }
|
||||
.sub { color: var(--muted); font-size: 13px; }
|
||||
|
||||
/* ---------- Kennzahlen ---------- */
|
||||
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px,1fr)); gap: 12px; }
|
||||
.stat {
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: var(--r); padding: 17px 19px; box-shadow: var(--shadow);
|
||||
}
|
||||
.stat .n { font-size: 30px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; color: var(--accent-2); }
|
||||
.stat .l { color: var(--muted); font-size: 13px; margin-top: 6px; }
|
||||
|
||||
h2 {
|
||||
font-size: 21px; margin: 36px 0 15px; letter-spacing: -0.025em;
|
||||
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
|
||||
}
|
||||
h2 .count { color: var(--muted); font-weight: 500; font-size: 14px; letter-spacing: 0; }
|
||||
|
||||
/* ---------- Sammelkarten ---------- */
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px,1fr)); gap: 16px; }
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
border-radius: var(--r); overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform .18s cubic-bezier(.2,.8,.3,1), box-shadow .18s, border-color .18s;
|
||||
}
|
||||
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); border-color: var(--typ-line-l, var(--line)); }
|
||||
[data-theme="dark"] .card:hover { border-color: var(--typ-line-d, var(--line)); }
|
||||
|
||||
/* Farbband oben — die Karte bekennt ihren Typ, bevor man den Namen liest. */
|
||||
.card::before {
|
||||
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
|
||||
background: var(--typ); z-index: 3;
|
||||
}
|
||||
|
||||
.card .nr {
|
||||
position: absolute; top: 12px; left: 11px; z-index: 2;
|
||||
font-size: 11px; font-weight: 800; letter-spacing: .05em;
|
||||
padding: 3px 8px; border-radius: 999px;
|
||||
background: var(--nr-bg); color: var(--text-2);
|
||||
}
|
||||
|
||||
.card .thumb {
|
||||
position: relative; width: 100%;
|
||||
padding-top: 75%; /* 4:3, funktioniert ohne aspect-ratio */
|
||||
background: var(--typ-wash-l, var(--surface-2));
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-theme="dark"] .card .thumb { background: var(--typ-wash-d, var(--surface-2)); }
|
||||
.card .thumb img {
|
||||
position: absolute; inset: 0;
|
||||
width: 100%; height: 100%; object-fit: cover; display: block;
|
||||
}
|
||||
|
||||
/* Nicht gesammelt: entfärbt und blass. Die Form ist zu ahnen, die
|
||||
Federzeichnung nicht — der Reiz des Freischaltens bleibt, ohne dass die
|
||||
Karte tot wirkt. */
|
||||
/* ⚠ Hier stand der Typton der Art als Grund. Gemeint war ein Hauch Farbe,
|
||||
wirksam war ein Flickenteppich: nebeneinander lagen graue, sepiafarbene,
|
||||
rosa und violette Kacheln, und das sah nach Zufall aus statt nach System.
|
||||
Verschlossen ist ein Zustand, kein Thema — also fuer alle derselbe
|
||||
neutrale Grund. Die Typfarbe bleibt sichtbar, wo sie etwas bedeutet:
|
||||
im Band oben und in der Pille unter dem Namen. */
|
||||
.card.locked .thumb { background: var(--surface-2); }
|
||||
[data-theme="dark"] .card.locked .thumb { background: var(--surface-2); }
|
||||
.card.locked .thumb img { filter: grayscale(1) contrast(.85); opacity: var(--locked-opacity); }
|
||||
.card.locked .thumb::after {
|
||||
content: "?"; position: absolute;
|
||||
right: 12px; bottom: 6px;
|
||||
font-size: 34px; font-weight: 800; line-height: 1;
|
||||
color: var(--typ); opacity: .6;
|
||||
}
|
||||
.card.locked .name { color: var(--text-2); }
|
||||
|
||||
.card .body { padding: 12px 14px 14px; }
|
||||
.card .name { font-weight: 700; font-size: 15.5px; line-height: 1.25; letter-spacing: -0.015em; }
|
||||
.card .meta { display: flex; align-items: center; gap: 6px; margin-top: 9px; flex-wrap: wrap; }
|
||||
.card .times { margin-left: auto; color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
|
||||
/* ---------- Badges ---------- */
|
||||
|
||||
.pill {
|
||||
font-size: 11px; font-weight: 800; letter-spacing: .03em;
|
||||
padding: 4px 10px; border-radius: 999px;
|
||||
color: var(--typ-ink); background: var(--typ);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pill.ghost { background: var(--surface-2); color: var(--text-2); border: 1px solid var(--line); }
|
||||
.pill.rar { color: #fff; }
|
||||
|
||||
.tag-new {
|
||||
background: var(--gold); color: #fff;
|
||||
font-size: 10px; font-weight: 800; letter-spacing: .06em;
|
||||
padding: 3px 8px; border-radius: 999px; text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ---------- Artseite ---------- */
|
||||
|
||||
.species-hero {
|
||||
position: relative; overflow: hidden;
|
||||
border-radius: var(--r-lg); margin: 18px 0 8px;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
/* Kräftiges Typband am oberen Rand statt einer flächigen Einfärbung — das
|
||||
gibt der Seite ihre Identität, ohne den Text zu bedrängen. */
|
||||
.species-hero::before {
|
||||
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 6px;
|
||||
background: var(--typ);
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid; grid-template-columns: minmax(0,1.05fr) minmax(0,1fr);
|
||||
gap: 26px; padding: 28px 26px 26px;
|
||||
}
|
||||
@media (max-width: 950px) { .hero-grid { grid-template-columns: 1fr; padding: 22px 18px 18px; } }
|
||||
|
||||
.hero-photo {
|
||||
position: relative; width: 100%; padding-top: 75%;
|
||||
border-radius: var(--r); overflow: hidden;
|
||||
background: var(--typ-wash-l, var(--surface-2)); border: 1px solid var(--typ-line-l, var(--line));
|
||||
}
|
||||
[data-theme="dark"] .hero-photo { background: var(--typ-wash-d, var(--surface-2)); border-color: var(--typ-line-d, var(--line)); }
|
||||
.hero-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
.dexnr { font-size: 13px; font-weight: 800; letter-spacing: .12em; color: var(--typ-text-l, var(--muted)); }
|
||||
[data-theme="dark"] .dexnr { color: var(--typ-text-d, var(--muted)); }
|
||||
.species-hero h1 { margin: 4px 0 2px; font-size: clamp(30px, 5vw, 44px); letter-spacing: -0.04em; line-height: 1.02; }
|
||||
.kategorie { font-size: 16px; font-weight: 700; color: var(--typ-text-l, var(--text-2)); margin-bottom: 5px; }
|
||||
[data-theme="dark"] .kategorie { color: var(--typ-text-d, var(--text-2)); }
|
||||
.sci { color: var(--muted); font-style: italic; font-size: 14px; }
|
||||
|
||||
.badge-row { display: flex; gap: 7px; flex-wrap: wrap; margin: 16px 0; }
|
||||
|
||||
/* Statuswerte als Balken — die Geste jeder Sammelkarte. */
|
||||
.werte { display: grid; gap: 9px; margin-top: 18px; }
|
||||
.wert { display: grid; grid-template-columns: 84px 1fr auto; align-items: center; gap: 11px; }
|
||||
.wert .k { font-size: 12px; font-weight: 700; color: var(--muted); }
|
||||
.wert .track { height: 9px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
|
||||
.wert .fill { height: 100%; border-radius: 999px; background: var(--typ); }
|
||||
.wert .v { font-size: 12.5px; font-weight: 750; font-variant-numeric: tabular-nums; min-width: 62px; text-align: right; }
|
||||
|
||||
/* ---------- Fließtext und Steckbrief ---------- */
|
||||
|
||||
.prose { max-width: 68ch; }
|
||||
.prose p { line-height: 1.7; margin: 0 0 16px; font-size: 16px; color: var(--text-2); }
|
||||
.prose p:first-of-type::first-letter {
|
||||
font-size: 3.1em; float: left; line-height: .82;
|
||||
padding: 5px 10px 0 0; font-weight: 800; color: var(--typ-text-l, var(--text-2));
|
||||
}
|
||||
[data-theme="dark"] .prose p:first-of-type::first-letter { color: var(--typ-text-d, var(--text-2)); }
|
||||
|
||||
.fakten { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap: 12px; margin: 18px 0; }
|
||||
.fakt {
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--typ);
|
||||
border-radius: var(--r); padding: 14px 16px; box-shadow: var(--shadow);
|
||||
}
|
||||
.fakt .k { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
||||
.fakt .v { margin-top: 5px; line-height: 1.5; font-size: 14.5px; color: var(--text-2); }
|
||||
|
||||
.verwechslung { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.vw {
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: 999px; padding: 6px 18px 6px 6px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform .15s, box-shadow .15s;
|
||||
}
|
||||
.vw:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
|
||||
.vw img { width: 42px; height: 42px; border-radius: 50%; object-fit: cover; background: var(--surface-2); }
|
||||
.vw .n { font-weight: 650; font-size: 14.5px; }
|
||||
|
||||
/* ---------- Sichtungen ---------- */
|
||||
|
||||
.rows { display: flex; flex-direction: column; gap: 9px; }
|
||||
.row {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: var(--r); padding: 10px 16px 10px 10px;
|
||||
box-shadow: var(--shadow); transition: box-shadow .15s;
|
||||
}
|
||||
.row:hover { box-shadow: var(--shadow-lift); }
|
||||
.row img.shot, .row .shot {
|
||||
width: 74px; height: 56px; object-fit: cover;
|
||||
border-radius: var(--r-sm); background: var(--surface-2); flex-shrink: 0;
|
||||
}
|
||||
.row .who { font-weight: 680; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.row .when { color: var(--muted); font-size: 13px; margin-top: 2px; }
|
||||
.row .right { margin-left: auto; text-align: right; flex-shrink: 0; }
|
||||
.row .conf { font-variant-numeric: tabular-nums; font-weight: 800; font-size: 15px; }
|
||||
.row .xp { color: var(--accent-2); font-size: 13px; font-weight: 800; }
|
||||
|
||||
/* ---------- Abzeichen ---------- */
|
||||
|
||||
.badges { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px,1fr)); gap: 13px; }
|
||||
.badge {
|
||||
display: flex; gap: 14px; align-items: flex-start;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: var(--r); padding: 16px 18px; box-shadow: var(--shadow);
|
||||
}
|
||||
.badge.locked { opacity: .55; box-shadow: none; background: var(--surface-2); }
|
||||
/* Der Pin ist ein Bild, das Emoji ein Zeichen — beide landen in .em, deshalb
|
||||
gilt die Graustufe fuer beides. Fehlt der Pin, faellt die Vorlage aufs
|
||||
Emoji zurueck und die Karte sieht trotzdem richtig aus. */
|
||||
.badge .em { font-size: 30px; line-height: 1; filter: grayscale(1); }
|
||||
.badge img.em {
|
||||
width: 64px; height: 64px; flex: 0 0 64px; object-fit: contain;
|
||||
/* blass, solange es nicht verdient ist: man soll sehen, was es zu holen
|
||||
gibt, ohne dass es wie eingeloest aussieht. */
|
||||
opacity: .45;
|
||||
}
|
||||
.badge:not(.locked) .em { filter: none; }
|
||||
.badge:not(.locked) img.em {
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 3px 6px rgba(20,40,28,.28));
|
||||
}
|
||||
.badge:not(.locked) { border-color: var(--gold-line); background: var(--gold-wash); }
|
||||
.badge .n { font-weight: 750; font-size: 15.5px; }
|
||||
.badge .d { color: var(--muted); font-size: 13.5px; margin-top: 4px; line-height: 1.45; }
|
||||
|
||||
/* ---------- Aktivität über den Tag ---------- */
|
||||
|
||||
.hours { display: flex; align-items: flex-end; gap: 3px; height: 74px; margin-top: 12px; }
|
||||
.hours div { flex: 1; min-height: 3px; border-radius: 3px 3px 0 0; background: var(--typ); }
|
||||
.hours-axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 11px; margin-top: 6px; }
|
||||
|
||||
/* ---------- Kleinteile ---------- */
|
||||
|
||||
.empty {
|
||||
border: 1px dashed var(--line); border-radius: var(--r-lg);
|
||||
padding: 42px 24px; text-align: center; color: var(--muted);
|
||||
line-height: 1.6; background: var(--surface);
|
||||
}
|
||||
|
||||
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.filters a {
|
||||
padding: 7px 14px; border-radius: 999px; font-size: 13px; font-weight: 650;
|
||||
border: 1px solid var(--line); background: var(--surface); color: var(--text-2);
|
||||
transition: background .15s, color .15s, border-color .15s;
|
||||
}
|
||||
.filters a:hover { border-color: var(--muted); }
|
||||
.filters a.on { background: var(--accent); color: var(--accent-on); border-color: var(--accent); }
|
||||
|
||||
.pager { display: flex; gap: 10px; justify-content: center; margin: 28px 0; }
|
||||
.pager a {
|
||||
padding: 9px 20px; border-radius: 999px; border: 1px solid var(--line);
|
||||
background: var(--surface); font-size: 14px; font-weight: 650; color: var(--text-2);
|
||||
}
|
||||
.pager a:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
.blaettern { display: flex; justify-content: space-between; gap: 12px; margin: 20px 0 4px; }
|
||||
.blaettern a { color: var(--muted); font-size: 14px; font-weight: 650; }
|
||||
.blaettern a:hover { color: var(--text); }
|
||||
|
||||
footer.bottom {
|
||||
border-top: 1px solid var(--line); margin-top: 60px; padding: 24px 0 32px;
|
||||
color: var(--muted); font-size: 13px; line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Der Werbeblock im Fuss.
|
||||
Hier unten ist der richtige Ort: Wer so weit gescrollt hat, hat fremde
|
||||
Vogelfotos gesehen und verstanden, was das Ding tut. Erst dann traegt der
|
||||
Satz "kannst du auch haben". Deshalb ein Angebot mit Knopf statt einer
|
||||
Fussnote — die leise Fassung stand hier einen Commit lang und war genau
|
||||
das Gegenteil dessen, was die Seite leisten soll. */
|
||||
footer.bottom .werbung {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 28px; flex-wrap: wrap;
|
||||
background: var(--accent-wash); border: 1px solid var(--line);
|
||||
border-radius: 16px; padding: 22px 26px;
|
||||
/* Kein margin-bottom mehr: Unter dem Block kommt nichts, seit die zweite
|
||||
Fusszeile weg ist. Der Abstand nach unten gehoert dem footer selbst. */
|
||||
}
|
||||
footer.bottom .werbung-text { flex: 1 1 340px; }
|
||||
footer.bottom .werbung-haken {
|
||||
margin: 0 0 4px; font-size: 19px; font-weight: 800;
|
||||
letter-spacing: -0.02em; color: var(--text);
|
||||
}
|
||||
footer.bottom .werbung-satz { margin: 0; font-size: 14px; color: var(--text-2); max-width: 54ch; }
|
||||
footer.bottom .werbung-knopf {
|
||||
flex: none; display: inline-flex; align-items: center; gap: 8px;
|
||||
background: var(--accent); color: var(--accent-on);
|
||||
font-size: 15px; font-weight: 700; text-decoration: none;
|
||||
border-radius: 999px; padding: 12px 20px;
|
||||
transition: background .15s, transform .15s;
|
||||
}
|
||||
footer.bottom .werbung-knopf:hover { background: var(--accent-2); transform: translateY(-1px); }
|
||||
footer.bottom .werbung-knopf svg { transition: transform .15s; }
|
||||
footer.bottom .werbung-knopf:hover svg { transform: translateX(3px); }
|
||||
|
||||
/* Auf dem Telefon untereinander, der Knopf ueber die volle Breite — ein
|
||||
Knopf, den man mit dem Daumen trifft, ist mehr wert als einer, der
|
||||
huebsch rechts sitzt. */
|
||||
@media (max-width: 560px) {
|
||||
footer.bottom .werbung { padding: 20px; gap: 18px; }
|
||||
footer.bottom .werbung-knopf { width: 100%; justify-content: center; }
|
||||
}
|
||||
|
||||
/* Das Zahnrad zum Admin — gleiche Form wie der Modus-Umschalter, weil es
|
||||
dieselbe Sorte Knopf ist: eine Einstellung, kein Navigationspunkt. Oben bei
|
||||
den Bedienelementen versteht man es ohne Beschriftung; im Footer neben
|
||||
einem Fliesstext nicht, das war der erste Versuch. */
|
||||
.gear {
|
||||
width: 38px; height: 38px; flex-shrink: 0;
|
||||
border: 1px solid var(--line); border-radius: 50%;
|
||||
background: var(--surface); color: var(--text-2);
|
||||
font-size: 17px; line-height: 1;
|
||||
display: grid; place-items: center;
|
||||
transition: border-color .15s, color .15s;
|
||||
}
|
||||
.gear:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||||
|
||||
/* ---------- Schmale Geräte ---------- */
|
||||
|
||||
/* Der Kopf bricht um, sobald Marke, Live-Knopf, Navigation, Sprache und
|
||||
Umschalter nicht mehr in eine Zeile passen. Gemessen, nicht geschätzt: bei
|
||||
810 px (iPad hochkant) rutschte sonst der Umschalter allein in Zeile zwei —
|
||||
einer der Gründe, warum Layoutarbeit in vier Breiten angesehen wird.
|
||||
Marke und die kleinen Knöpfe bleiben oben, die Navigation nimmt die Zeile
|
||||
darunter. */
|
||||
@media (max-width: 1000px) {
|
||||
.brand { order: 0; flex: 1 1 auto; }
|
||||
.live-knopf, .lang-switch, .theme-toggle, .gear { order: 1; }
|
||||
nav.main { order: 2; flex: 1 0 100%; margin-left: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
/* Fünf Punkte passen auf einem Telefon nicht mehr in eine Zeile, und einer
|
||||
allein in der zweiten sieht nach Versehen aus. Also seitlich wischen
|
||||
statt umbrechen — die Zeile bleibt eine Zeile, egal wie viele Punkte
|
||||
dazukommen oder wie lang die Sprache sie macht ("Sightings" gegen
|
||||
"Sichtungen"). Die Leiste wird bündig am Rand gescrollt, damit man ihr
|
||||
ansieht, dass da noch etwas kommt. */
|
||||
nav.main {
|
||||
order: 2; flex: 1 0 100%; margin-left: 0;
|
||||
flex-wrap: nowrap; justify-content: flex-start;
|
||||
overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* Der Wisch-Rand gehört in die Leiste, nicht neben sie. */
|
||||
margin-inline: -14px; padding-inline: 14px;
|
||||
}
|
||||
nav.main::-webkit-scrollbar { display: none; }
|
||||
nav.main { gap: 1px; }
|
||||
nav.main a { padding: 7px 8px; font-size: 12.5px; white-space: nowrap; flex: none; }
|
||||
|
||||
.wrap { padding-inline: 14px; }
|
||||
.grid { grid-template-columns: repeat(auto-fill, minmax(150px,1fr)); gap: 12px; }
|
||||
|
||||
/* Auf 360-px-Geräten ergeben Marke, Live, Sprache und Umschalter zusammen
|
||||
380 px und der Umschalter faellt in eine dritte Zeile. Gemessen, nicht
|
||||
geraten: 154 + 68 + 90 + 38 plus Abstaende gegen 332 px Platz, und das
|
||||
Zahnrad kommt noch dazu. Also alles eine Stufe kompakter — und vom
|
||||
Sprachumschalter bleibt nur die Sprache, in die man wechseln kann: welche
|
||||
gerade aktiv ist, sieht man am Text der Seite. */
|
||||
.top-inner { gap: 8px; }
|
||||
.brand { font-size: 19px; }
|
||||
.live-knopf { font-size: 12px; padding: 5px 10px 5px 8px; gap: 5px; }
|
||||
.theme-toggle, .gear { width: 34px; height: 34px; font-size: 15px; }
|
||||
.lang-switch button.on { display: none; }
|
||||
.progress-bar { padding: 16px; gap: 14px; }
|
||||
.species-hero h1 { font-size: 30px; }
|
||||
.wert { grid-template-columns: 74px 1fr auto; gap: 8px; }
|
||||
.fakten { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ---------- Sprachumschalter ---------- */
|
||||
|
||||
.lang-switch {
|
||||
display: flex; gap: 2px; flex-shrink: 0;
|
||||
border: 1px solid var(--line); border-radius: 999px;
|
||||
background: var(--surface); padding: 2px;
|
||||
}
|
||||
.lang-switch button {
|
||||
border: 0; background: transparent; cursor: pointer;
|
||||
padding: 5px 11px; border-radius: 999px;
|
||||
font: inherit; font-size: 12px; font-weight: 800; letter-spacing: .04em;
|
||||
color: var(--muted);
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.lang-switch button:hover { color: var(--text); }
|
||||
.lang-switch button.on { background: var(--accent); color: var(--accent-on); }
|
||||
|
||||
/* ---------- Filterleiste ---------- */
|
||||
|
||||
.filterleiste {
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
padding: 14px 16px; margin-bottom: 18px;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: var(--r); box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.filtergruppe { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
|
||||
.filterlabel {
|
||||
font-size: 11px; font-weight: 800; letter-spacing: .08em;
|
||||
text-transform: uppercase; color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filterleiste .pills { display: flex; gap: 7px; flex-wrap: wrap; }
|
||||
.filterleiste .pills a {
|
||||
padding: 6px 13px; border-radius: 999px;
|
||||
font-size: 13px; font-weight: 650;
|
||||
border: 1px solid var(--line); background: var(--surface); color: var(--text-2);
|
||||
transition: border-color .15s, background .15s, color .15s;
|
||||
}
|
||||
.filterleiste .pills a b { font-weight: 800; opacity: .55; margin-left: 2px; }
|
||||
.filterleiste .pills a:hover { border-color: var(--muted); }
|
||||
.filterleiste .pills a.on {
|
||||
background: var(--accent); color: var(--accent-on); border-color: var(--accent);
|
||||
}
|
||||
.filterleiste .pills a.on b { opacity: .8; }
|
||||
|
||||
/* Die Verfeinerung sitzt optisch unter der Hauptachse, nicht daneben. */
|
||||
.verfeinern { border-top: 1px solid var(--line-soft, var(--line)); padding-top: 12px; }
|
||||
.verfeinern label { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.verfeinern select {
|
||||
font: inherit; font-size: 13.5px; font-weight: 600;
|
||||
padding: 7px 30px 7px 12px; border-radius: 999px;
|
||||
border: 1px solid var(--line); background: var(--surface); color: var(--text);
|
||||
cursor: pointer;
|
||||
/* Eigener Pfeil: das native Dreieck sieht in jedem Browser anders aus. */
|
||||
-webkit-appearance: none; -moz-appearance: none; appearance: none;
|
||||
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
||||
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
||||
background-position: calc(100% - 16px) 52%, calc(100% - 11px) 52%;
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.verfeinern select:hover { border-color: var(--muted); }
|
||||
.verfeinern select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
|
||||
.verfeinern button {
|
||||
font: inherit; font-size: 13.5px; font-weight: 650;
|
||||
padding: 7px 16px; border-radius: 999px; cursor: pointer;
|
||||
border: 1px solid var(--accent); background: var(--accent); color: var(--accent-on);
|
||||
}
|
||||
|
||||
.zuruecksetzen {
|
||||
font-size: 13px; font-weight: 650; color: var(--muted);
|
||||
text-decoration: underline; text-underline-offset: 3px;
|
||||
}
|
||||
.zuruecksetzen:hover { color: var(--text); }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.filterleiste { padding: 12px; gap: 10px; }
|
||||
.filtergruppe { gap: 8px; }
|
||||
/* Label über die Zeile, damit die Pillen die volle Breite bekommen. */
|
||||
.filtergruppe > .filterlabel { flex: 1 0 100%; }
|
||||
.filterleiste .pills a { padding: 6px 11px; font-size: 12.5px; }
|
||||
.verfeinern { flex-wrap: wrap; }
|
||||
.verfeinern label { flex: 1 1 46%; }
|
||||
.verfeinern label .filterlabel { display: none; }
|
||||
.verfeinern select { width: 100%; }
|
||||
}
|
||||
|
||||
/* ---------- Blickfang der Startseite ----------
|
||||
Die Seite begann mit vier Kacheln, von denen an einem ruhigen Tag zwei auf
|
||||
null standen. Jetzt steht der letzte Gast gross oben; die Zahlen sind
|
||||
dorthin gewandert, wo sie etwas erklaeren. */
|
||||
|
||||
.held {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
|
||||
align-items: stretch; margin: 22px 0 8px;
|
||||
}
|
||||
.held-text h1 {
|
||||
font-size: 38px; line-height: 1.08; letter-spacing: -0.03em;
|
||||
font-weight: 800; margin: 0 0 10px;
|
||||
}
|
||||
.held-text p { margin: 0 0 20px; color: var(--text-2); font-size: 16px; max-width: 44ch; }
|
||||
.held .progress-bar { margin: 0; }
|
||||
|
||||
.held-bild {
|
||||
position: relative; border-radius: var(--r-lg); overflow: hidden;
|
||||
box-shadow: var(--shadow); min-height: 280px; background: var(--surface-2);
|
||||
display: block;
|
||||
}
|
||||
.held-bild img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.held-bild .schild {
|
||||
position: absolute; left: 0; right: 0; bottom: 0; padding: 40px 20px 16px;
|
||||
color: #fff;
|
||||
/* Fester Farbwert statt Token: der Verlauf liegt ueber einem Foto, nicht
|
||||
ueber dem Seitenhintergrund — er muss in beiden Modi gleich dunkeln. */
|
||||
background: linear-gradient(transparent, rgba(8,24,16,.88));
|
||||
}
|
||||
.held-bild .schild b { display: block; font-size: 22px; font-weight: 800; }
|
||||
.held-bild .schild small { opacity: .9; font-size: 13px; }
|
||||
.frisch {
|
||||
position: absolute; top: 14px; left: 14px;
|
||||
background: var(--kopf-text); color: var(--kopf-bg);
|
||||
padding: 5px 11px; border-radius: 999px;
|
||||
font-size: 11px; font-weight: 800; letter-spacing: .07em;
|
||||
}
|
||||
.held-leer {
|
||||
display: grid; place-items: center; text-align: center; padding: 30px;
|
||||
border: 1px dashed var(--line); border-radius: var(--r-lg);
|
||||
color: var(--muted); background: var(--surface);
|
||||
}
|
||||
|
||||
/* ---------- Besuche als Bildkarten ---------- */
|
||||
|
||||
.besuche { display: grid; grid-template-columns: repeat(4, 1fr); gap: 13px; }
|
||||
.besuch {
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: var(--r); overflow: hidden; box-shadow: var(--shadow);
|
||||
transition: transform .15s, box-shadow .15s;
|
||||
}
|
||||
.besuch:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
|
||||
/* ⚠ Feste Hoehe statt aspect-ratio: das gibt es erst ab Safari 15 und faellt
|
||||
davor ersatzlos weg, womit die Karten unterschiedlich hoch waeren. */
|
||||
.besuch img, .besuch .leer { width: 100%; height: 118px; object-fit: cover; display: block; }
|
||||
.besuch .leer { background: var(--surface-2); }
|
||||
.besuch .f { padding: 10px 12px; }
|
||||
.besuch b { font-weight: 750; font-size: 14.5px; }
|
||||
.besuch small { display: block; color: var(--muted); font-size: 12.5px; margin-top: 2px; }
|
||||
.besuch .xp { float: right; color: var(--accent); font-weight: 800; font-size: 12.5px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.held { grid-template-columns: 1fr; }
|
||||
.held-bild { min-height: 230px; order: -1; }
|
||||
.held-text h1 { font-size: 30px; }
|
||||
.besuche { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
.held-text h1 { font-size: 26px; }
|
||||
.besuche { grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
}
|
||||
Reference in new issue
Block a user