2294 lines
80 KiB
HTML
2294 lines
80 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="de">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|||
|
|
<title>FOERBICO — Community of Communities</title>
|
|||
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|||
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=Yanone+Kaffeesatz:wght@300;400;500;600;700&family=Roboto+Condensed:wght@300;400;500;700&family=Alumni+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
|||
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--blue: #203A8F;
|
|||
|
|
--blue-deep: #152560;
|
|||
|
|
--blue-soft: #2E4CA8;
|
|||
|
|
--orange: #FFA500;
|
|||
|
|
--white: #FFFFFF;
|
|||
|
|
--paper: #FAFAF8;
|
|||
|
|
--gray-05: #F2F3F5;
|
|||
|
|
--gray-10: #E6E8EC;
|
|||
|
|
--gray-30: #BFC3CC;
|
|||
|
|
--gray-60: #6B7280;
|
|||
|
|
--ink: #14171F;
|
|||
|
|
--radius: 4px;
|
|||
|
|
--ease: all 160ms ease-out;
|
|||
|
|
--max: 1240px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|||
|
|
html { scroll-behavior: smooth; }
|
|||
|
|
body {
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
font-weight: 400;
|
|||
|
|
color: var(--ink);
|
|||
|
|
background: var(--paper);
|
|||
|
|
line-height: 1.5;
|
|||
|
|
-webkit-font-smoothing: antialiased;
|
|||
|
|
overflow-x: hidden;
|
|||
|
|
}
|
|||
|
|
.container { max-width: var(--max); margin: 0 auto; padding: 0 40px; }
|
|||
|
|
|
|||
|
|
h1, h2, h3, h4 {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-weight: 500;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.06em;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.micro {
|
|||
|
|
font-family: 'Alumni Sans SC', sans-serif;
|
|||
|
|
font-size: 6px;
|
|||
|
|
letter-spacing: 0.25em;
|
|||
|
|
color: var(--gray-60);
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
font-weight: 500;
|
|||
|
|
font-size: 11px;
|
|||
|
|
letter-spacing: 0.22em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
color: var(--blue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* NAV */
|
|||
|
|
nav {
|
|||
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 50;
|
|||
|
|
background: rgba(250, 250, 248, 0.88);
|
|||
|
|
backdrop-filter: blur(12px);
|
|||
|
|
border-bottom: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: 68px; }
|
|||
|
|
.logo {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 26px; font-weight: 600;
|
|||
|
|
letter-spacing: 0.18em; color: var(--blue);
|
|||
|
|
}
|
|||
|
|
.logo span { color: var(--orange); }
|
|||
|
|
.nav-links { display: flex; gap: 36px; list-style: none; }
|
|||
|
|
.nav-links a {
|
|||
|
|
text-decoration: none; color: var(--ink);
|
|||
|
|
font-size: 12px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.22em; text-transform: uppercase;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.nav-links a:hover { color: var(--blue); }
|
|||
|
|
|
|||
|
|
.nav-status {
|
|||
|
|
display: flex; align-items: center; gap: 10px;
|
|||
|
|
padding: 8px 16px;
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: var(--white);
|
|||
|
|
}
|
|||
|
|
.status-dot {
|
|||
|
|
width: 6px; height: 6px; border-radius: 50%;
|
|||
|
|
background: var(--gray-30); transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.status-dot.connecting { background: var(--orange); animation: pulse 1.2s infinite; }
|
|||
|
|
.status-dot.connected { background: #38A169; }
|
|||
|
|
.status-dot.error { background: #E53E3E; }
|
|||
|
|
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
|
|||
|
|
.status-text {
|
|||
|
|
font-size: 10px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.22em; text-transform: uppercase;
|
|||
|
|
color: var(--gray-60);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* HERO */
|
|||
|
|
.hero { padding: 148px 0 120px; position: relative; }
|
|||
|
|
.hero-grid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 80px; align-items: center; }
|
|||
|
|
.hero-eyebrow { display: flex; align-items: center; gap: 14px; margin-bottom: 32px; }
|
|||
|
|
.hero-eyebrow::before { content: ''; width: 32px; height: 1px; background: var(--blue); }
|
|||
|
|
.hero h1 {
|
|||
|
|
font-size: clamp(72px, 9vw, 132px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
color: var(--ink); margin-bottom: 36px;
|
|||
|
|
}
|
|||
|
|
.hero h1 .accent { color: var(--blue); display: block; }
|
|||
|
|
.hero h1 .dash { color: var(--orange); }
|
|||
|
|
.hero-lead {
|
|||
|
|
font-size: 17px; line-height: 1.6;
|
|||
|
|
color: var(--gray-60); max-width: 460px;
|
|||
|
|
margin-bottom: 44px; font-weight: 300;
|
|||
|
|
}
|
|||
|
|
.hero-actions { display: flex; gap: 16px; align-items: center; }
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
padding: 14px 30px; border-radius: var(--radius);
|
|||
|
|
font-size: 12px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.24em; text-transform: uppercase;
|
|||
|
|
cursor: pointer; text-decoration: none;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
display: inline-flex; align-items: center; gap: 10px;
|
|||
|
|
background: transparent; font-family: inherit;
|
|||
|
|
}
|
|||
|
|
.btn-primary { background: var(--blue); color: var(--white); border-color: var(--blue); }
|
|||
|
|
.btn-primary:hover { background: var(--blue-deep); border-color: var(--blue-deep); }
|
|||
|
|
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--gray-30); }
|
|||
|
|
.btn-ghost:hover { border-color: var(--blue); color: var(--blue); }
|
|||
|
|
|
|||
|
|
.hero-visual {
|
|||
|
|
position: relative; aspect-ratio: 4 / 5;
|
|||
|
|
border-radius: var(--radius); overflow: hidden;
|
|||
|
|
background: var(--blue); border: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.hero-visual::before {
|
|||
|
|
content: ''; position: absolute; inset: 0;
|
|||
|
|
background:
|
|||
|
|
repeating-linear-gradient(0deg, transparent, transparent 48px, rgba(255,255,255,0.04) 48px, rgba(255,255,255,0.04) 49px),
|
|||
|
|
repeating-linear-gradient(90deg, transparent, transparent 48px, rgba(255,255,255,0.04) 48px, rgba(255,255,255,0.04) 49px),
|
|||
|
|
linear-gradient(135deg, var(--blue) 0%, var(--blue-deep) 100%);
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
.hero-visual::after {
|
|||
|
|
content: ''; position: absolute; inset: 0;
|
|||
|
|
background: radial-gradient(circle at 30% 20%, rgba(255,165,0,0.15), transparent 60%);
|
|||
|
|
z-index: 2;
|
|||
|
|
}
|
|||
|
|
.visual-content {
|
|||
|
|
position: absolute; inset: 0; z-index: 3;
|
|||
|
|
padding: 32px; display: flex; flex-direction: column;
|
|||
|
|
justify-content: space-between; color: var(--white);
|
|||
|
|
}
|
|||
|
|
.visual-top { display: flex; justify-content: space-between; align-items: flex-start; }
|
|||
|
|
.visual-badge {
|
|||
|
|
width: 44px; height: 44px;
|
|||
|
|
border: 1px solid rgba(255,255,255,0.3);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
display: grid; place-items: center;
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 22px; letter-spacing: 0.1em;
|
|||
|
|
color: var(--orange);
|
|||
|
|
}
|
|||
|
|
.visual-meta {
|
|||
|
|
text-align: right;
|
|||
|
|
font-family: 'Alumni Sans SC', sans-serif;
|
|||
|
|
font-size: 6px; letter-spacing: 0.25em;
|
|||
|
|
color: rgba(255,255,255,0.6); line-height: 1.8;
|
|||
|
|
}
|
|||
|
|
.visual-center {
|
|||
|
|
position: absolute; inset: 0;
|
|||
|
|
display: grid; place-items: center; z-index: 3; padding: 80px;
|
|||
|
|
}
|
|||
|
|
.visual-logo {
|
|||
|
|
max-width: 72%; max-height: 72%;
|
|||
|
|
object-fit: contain;
|
|||
|
|
filter: brightness(0) invert(1);
|
|||
|
|
opacity: 0.96; transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.hero-visual:hover .visual-logo { opacity: 1; transform: scale(1.02); }
|
|||
|
|
.visual-bottom {
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: flex-end; position: relative; z-index: 3;
|
|||
|
|
}
|
|||
|
|
.visual-number {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 14px; letter-spacing: 0.3em;
|
|||
|
|
color: rgba(255,255,255,0.7);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* FEATURED */
|
|||
|
|
.featured { padding: 100px 0 60px; border-top: 1px solid var(--gray-10); }
|
|||
|
|
.section-head {
|
|||
|
|
display: grid; grid-template-columns: 1fr 2fr;
|
|||
|
|
gap: 80px; margin-bottom: 72px; align-items: end;
|
|||
|
|
}
|
|||
|
|
.section-head h2 {
|
|||
|
|
font-size: clamp(44px, 5vw, 68px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
}
|
|||
|
|
.section-head h2 .accent { color: var(--blue); }
|
|||
|
|
.section-head-right {
|
|||
|
|
font-size: 15px; color: var(--gray-60);
|
|||
|
|
line-height: 1.6; max-width: 520px; font-weight: 300;
|
|||
|
|
}
|
|||
|
|
.inline-code {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 12px; background: var(--gray-05);
|
|||
|
|
padding: 2px 6px; border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.featured-card {
|
|||
|
|
display: grid; grid-template-columns: 1.2fr 1fr;
|
|||
|
|
gap: 0;
|
|||
|
|
background: var(--white); border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius); overflow: hidden;
|
|||
|
|
min-height: 440px; cursor: pointer;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.featured-card:hover { border-color: var(--blue); }
|
|||
|
|
.featured-image {
|
|||
|
|
position: relative; background: var(--blue);
|
|||
|
|
min-height: 440px; overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.featured-image::before {
|
|||
|
|
content: ''; position: absolute; inset: 0;
|
|||
|
|
background:
|
|||
|
|
repeating-linear-gradient(0deg, transparent, transparent 48px, rgba(255,255,255,0.04) 48px, rgba(255,255,255,0.04) 49px),
|
|||
|
|
repeating-linear-gradient(90deg, transparent, transparent 48px, rgba(255,255,255,0.04) 48px, rgba(255,255,255,0.04) 49px),
|
|||
|
|
linear-gradient(135deg, var(--blue) 0%, var(--blue-deep) 100%);
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
.featured-image img {
|
|||
|
|
position: absolute; inset: 0;
|
|||
|
|
width: 100%; height: 100%;
|
|||
|
|
object-fit: cover; z-index: 2;
|
|||
|
|
opacity: 0.85; mix-blend-mode: luminosity;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.featured-card:hover .featured-image img {
|
|||
|
|
opacity: 1; mix-blend-mode: normal;
|
|||
|
|
}
|
|||
|
|
.featured-image-overlay {
|
|||
|
|
position: absolute; inset: 0; z-index: 3;
|
|||
|
|
padding: 28px;
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
color: var(--white); pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.featured-image-overlay .micro { color: rgba(255,255,255,0.7); }
|
|||
|
|
.featured-body {
|
|||
|
|
padding: 48px;
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
.featured-meta { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
|
|||
|
|
.featured-meta .label-new {
|
|||
|
|
padding: 4px 10px; background: var(--orange); color: var(--ink);
|
|||
|
|
border-radius: var(--radius); font-size: 10px;
|
|||
|
|
font-weight: 500; letter-spacing: 0.22em; text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.featured-card h3 {
|
|||
|
|
font-size: clamp(36px, 4vw, 56px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
color: var(--ink); margin-bottom: 20px; line-height: 1.05;
|
|||
|
|
}
|
|||
|
|
.featured-card .summary {
|
|||
|
|
font-size: 16px; line-height: 1.6;
|
|||
|
|
color: var(--gray-60); font-weight: 300;
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
display: -webkit-box; -webkit-line-clamp: 4;
|
|||
|
|
-webkit-box-orient: vertical; overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.featured-footer {
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: center; padding-top: 20px;
|
|||
|
|
border-top: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ARTICLES GRID */
|
|||
|
|
.articles { padding: 60px 0 120px; }
|
|||
|
|
.articles-head {
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: end; margin-bottom: 48px;
|
|||
|
|
}
|
|||
|
|
.articles-head h2 {
|
|||
|
|
font-size: clamp(40px, 4.5vw, 60px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
}
|
|||
|
|
.articles-head h2 .accent { color: var(--blue); }
|
|||
|
|
.articles-count {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 72px; font-weight: 300;
|
|||
|
|
color: var(--orange); letter-spacing: 0.02em;
|
|||
|
|
line-height: 0.8; text-align: right;
|
|||
|
|
}
|
|||
|
|
.articles-count small {
|
|||
|
|
font-family: 'Alumni Sans SC', sans-serif;
|
|||
|
|
font-size: 8px; letter-spacing: 0.3em;
|
|||
|
|
color: var(--gray-60); display: block; margin-top: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-bar {
|
|||
|
|
display: flex; gap: 10px; flex-wrap: wrap;
|
|||
|
|
margin-bottom: 40px; padding-bottom: 24px;
|
|||
|
|
border-bottom: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.filter-chip {
|
|||
|
|
padding: 6px 14px; background: var(--white);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
font-size: 11px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.18em; text-transform: uppercase;
|
|||
|
|
color: var(--gray-60); cursor: pointer;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.filter-chip:hover { border-color: var(--blue); color: var(--blue); }
|
|||
|
|
.filter-chip.active { background: var(--blue); color: var(--white); border-color: var(--blue); }
|
|||
|
|
|
|||
|
|
.articles-grid {
|
|||
|
|
display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;
|
|||
|
|
}
|
|||
|
|
.article-card {
|
|||
|
|
background: var(--white); border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius); overflow: hidden;
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
cursor: pointer; transition: var(--ease);
|
|||
|
|
min-height: 420px;
|
|||
|
|
}
|
|||
|
|
.article-card:hover {
|
|||
|
|
border-color: var(--blue); transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
.article-image {
|
|||
|
|
position: relative; aspect-ratio: 16 / 10;
|
|||
|
|
background: var(--gray-05); overflow: hidden;
|
|||
|
|
border-bottom: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.article-image.placeholder {
|
|||
|
|
background:
|
|||
|
|
repeating-linear-gradient(0deg, transparent, transparent 32px, rgba(32,58,143,0.04) 32px, rgba(32,58,143,0.04) 33px),
|
|||
|
|
repeating-linear-gradient(90deg, transparent, transparent 32px, rgba(32,58,143,0.04) 32px, rgba(32,58,143,0.04) 33px),
|
|||
|
|
linear-gradient(135deg, var(--gray-05) 0%, #E8EAF0 100%);
|
|||
|
|
}
|
|||
|
|
.article-image img { width: 100%; height: 100%; object-fit: cover; }
|
|||
|
|
.article-image-overlay {
|
|||
|
|
position: absolute; inset: 0; padding: 14px;
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: flex-start; pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.article-num {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 13px; letter-spacing: 0.3em; color: var(--blue);
|
|||
|
|
}
|
|||
|
|
.article-body {
|
|||
|
|
padding: 24px 24px 20px;
|
|||
|
|
display: flex; flex-direction: column; flex: 1;
|
|||
|
|
}
|
|||
|
|
.article-date {
|
|||
|
|
font-size: 10px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.22em; text-transform: uppercase;
|
|||
|
|
color: var(--blue); margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
.article-card h4 {
|
|||
|
|
font-size: 24px; font-weight: 500;
|
|||
|
|
margin-bottom: 10px; color: var(--ink);
|
|||
|
|
line-height: 1.1;
|
|||
|
|
display: -webkit-box; -webkit-line-clamp: 2;
|
|||
|
|
-webkit-box-orient: vertical; overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.article-card p {
|
|||
|
|
font-size: 13px; line-height: 1.55;
|
|||
|
|
color: var(--gray-60); font-weight: 300;
|
|||
|
|
display: -webkit-box; -webkit-line-clamp: 3;
|
|||
|
|
-webkit-box-orient: vertical; overflow: hidden;
|
|||
|
|
flex: 1; margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.article-tags {
|
|||
|
|
display: flex; gap: 6px; flex-wrap: wrap;
|
|||
|
|
padding-top: 14px; border-top: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.tag {
|
|||
|
|
font-size: 9px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.18em; text-transform: uppercase;
|
|||
|
|
color: var(--gray-60); padding: 3px 8px;
|
|||
|
|
background: var(--gray-05); border-radius: var(--radius);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ABOUT — Community of Communities */
|
|||
|
|
.about { padding: 120px 0 100px; border-top: 1px solid var(--gray-10); background: var(--white); }
|
|||
|
|
.about-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1.4fr;
|
|||
|
|
gap: 100px;
|
|||
|
|
align-items: start;
|
|||
|
|
}
|
|||
|
|
.about-eyebrow {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 64px; font-weight: 400;
|
|||
|
|
line-height: 0.9; color: var(--orange);
|
|||
|
|
letter-spacing: 0.02em;
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
}
|
|||
|
|
.about-eyebrow span { color: var(--blue); display: block; }
|
|||
|
|
.about-side {
|
|||
|
|
border-left: 2px solid var(--blue);
|
|||
|
|
padding-left: 24px;
|
|||
|
|
}
|
|||
|
|
.about-side .label { margin-bottom: 12px; display: block; }
|
|||
|
|
.about-side p {
|
|||
|
|
font-size: 13px; color: var(--gray-60);
|
|||
|
|
line-height: 1.6; font-weight: 300;
|
|||
|
|
}
|
|||
|
|
.about-content h3 {
|
|||
|
|
font-size: clamp(32px, 3.5vw, 44px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
color: var(--ink); margin-bottom: 28px;
|
|||
|
|
line-height: 1.1;
|
|||
|
|
}
|
|||
|
|
.about-content h3 .accent { color: var(--blue); }
|
|||
|
|
.about-content p {
|
|||
|
|
font-size: 17px; line-height: 1.7;
|
|||
|
|
color: var(--ink); font-weight: 300;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
.about-content p.lead {
|
|||
|
|
font-size: 19px; color: var(--ink);
|
|||
|
|
font-weight: 400;
|
|||
|
|
}
|
|||
|
|
.about-content em {
|
|||
|
|
font-style: normal; color: var(--blue);
|
|||
|
|
font-weight: 500;
|
|||
|
|
border-bottom: 2px solid var(--orange);
|
|||
|
|
padding-bottom: 1px;
|
|||
|
|
}
|
|||
|
|
.about-content a {
|
|||
|
|
color: var(--blue); text-decoration: underline;
|
|||
|
|
text-decoration-color: var(--gray-30);
|
|||
|
|
text-underline-offset: 3px; transition: var(--ease);
|
|||
|
|
font-weight: 400;
|
|||
|
|
}
|
|||
|
|
.about-content a:hover { text-decoration-color: var(--blue); }
|
|||
|
|
|
|||
|
|
/* COMMUNITY — Mitmachen Block */
|
|||
|
|
.community {
|
|||
|
|
padding: 120px 0;
|
|||
|
|
background: var(--blue);
|
|||
|
|
color: var(--white);
|
|||
|
|
position: relative; overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.community::before {
|
|||
|
|
content: ''; position: absolute; inset: 0;
|
|||
|
|
background:
|
|||
|
|
repeating-linear-gradient(0deg, transparent, transparent 64px, rgba(255,255,255,0.03) 64px, rgba(255,255,255,0.03) 65px),
|
|||
|
|
repeating-linear-gradient(90deg, transparent, transparent 64px, rgba(255,255,255,0.03) 64px, rgba(255,255,255,0.03) 65px);
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.community .container { position: relative; z-index: 2; }
|
|||
|
|
.community-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1.2fr 1fr;
|
|||
|
|
gap: 80px;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
.community-text .label {
|
|||
|
|
color: var(--orange); margin-bottom: 24px; display: block;
|
|||
|
|
}
|
|||
|
|
.community-text h2 {
|
|||
|
|
font-size: clamp(48px, 6vw, 84px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
color: var(--white); margin-bottom: 32px;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
.community-text h2 .accent { color: var(--orange); display: block; }
|
|||
|
|
.community-text p {
|
|||
|
|
font-size: 17px; line-height: 1.6;
|
|||
|
|
color: rgba(255,255,255,0.85); font-weight: 300;
|
|||
|
|
margin-bottom: 32px; max-width: 540px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cta-card {
|
|||
|
|
background: var(--white);
|
|||
|
|
color: var(--ink);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 40px 36px;
|
|||
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|||
|
|
}
|
|||
|
|
.cta-card .label { color: var(--blue); margin-bottom: 16px; display: block; }
|
|||
|
|
.cta-card h3 {
|
|||
|
|
font-size: 32px; font-weight: 500;
|
|||
|
|
margin-bottom: 16px; color: var(--ink);
|
|||
|
|
text-transform: none; letter-spacing: 0.01em;
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
line-height: 1.2;
|
|||
|
|
}
|
|||
|
|
.cta-card p {
|
|||
|
|
font-size: 14px; line-height: 1.6;
|
|||
|
|
color: var(--gray-60); font-weight: 300;
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
}
|
|||
|
|
.cta-element {
|
|||
|
|
display: flex; align-items: center; gap: 14px;
|
|||
|
|
padding: 16px 20px;
|
|||
|
|
background: var(--paper);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
text-decoration: none;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
.cta-element:hover {
|
|||
|
|
border-color: var(--blue);
|
|||
|
|
background: var(--white);
|
|||
|
|
transform: translateX(2px);
|
|||
|
|
}
|
|||
|
|
.cta-element img {
|
|||
|
|
width: 32px; height: 32px;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
.cta-element-text {
|
|||
|
|
flex: 1; min-width: 0;
|
|||
|
|
}
|
|||
|
|
.cta-element-name {
|
|||
|
|
font-size: 15px; font-weight: 500;
|
|||
|
|
color: var(--ink); margin-bottom: 2px;
|
|||
|
|
}
|
|||
|
|
.cta-element-sub {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 10px; color: var(--gray-60);
|
|||
|
|
overflow: hidden; text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.cta-element-arrow {
|
|||
|
|
color: var(--blue); font-size: 18px;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
.cta-team-link {
|
|||
|
|
font-size: 13px; color: var(--gray-60);
|
|||
|
|
text-align: center; padding-top: 12px;
|
|||
|
|
border-top: 1px dashed var(--gray-10); margin-top: 12px;
|
|||
|
|
}
|
|||
|
|
.cta-team-link a {
|
|||
|
|
color: var(--blue); text-decoration: underline;
|
|||
|
|
text-decoration-color: var(--gray-30);
|
|||
|
|
text-underline-offset: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* COMMUNITIES — Beteiligte Communities */
|
|||
|
|
.communities { padding: 100px 0; border-top: 1px solid var(--gray-10); background: var(--paper); }
|
|||
|
|
.communities-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, 1fr);
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
.community-card {
|
|||
|
|
background: var(--white);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 28px 24px;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
min-height: 180px;
|
|||
|
|
text-decoration: none;
|
|||
|
|
color: var(--ink);
|
|||
|
|
}
|
|||
|
|
.community-card:hover {
|
|||
|
|
border-color: var(--blue);
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
.community-card .label {
|
|||
|
|
color: var(--orange); margin-bottom: 14px;
|
|||
|
|
font-size: 9px; letter-spacing: 0.2em;
|
|||
|
|
}
|
|||
|
|
.community-card h4 {
|
|||
|
|
font-size: 22px; font-weight: 500;
|
|||
|
|
margin-bottom: 10px; color: var(--ink);
|
|||
|
|
text-transform: none; letter-spacing: 0;
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
line-height: 1.2;
|
|||
|
|
}
|
|||
|
|
.community-card p {
|
|||
|
|
font-size: 13px; line-height: 1.55;
|
|||
|
|
color: var(--gray-60); font-weight: 300;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
.community-card .community-link {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 10px; color: var(--blue);
|
|||
|
|
margin-top: 14px; padding-top: 12px;
|
|||
|
|
border-top: 1px dashed var(--gray-10);
|
|||
|
|
overflow: hidden; text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* TECH — kollabierte Tech-Sektion */
|
|||
|
|
.tech-section {
|
|||
|
|
padding: 80px 0;
|
|||
|
|
border-top: 1px solid var(--gray-10);
|
|||
|
|
background: var(--paper);
|
|||
|
|
}
|
|||
|
|
.tech-toggle {
|
|||
|
|
display: flex; align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 24px;
|
|||
|
|
padding: 24px 32px;
|
|||
|
|
background: var(--white);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
user-select: none;
|
|||
|
|
}
|
|||
|
|
.tech-toggle:hover {
|
|||
|
|
border-color: var(--blue);
|
|||
|
|
}
|
|||
|
|
.tech-toggle-text { display: flex; flex-direction: column; gap: 6px; }
|
|||
|
|
.tech-toggle-text h3 {
|
|||
|
|
font-size: 28px; font-weight: 500;
|
|||
|
|
color: var(--ink); letter-spacing: 0.01em;
|
|||
|
|
text-transform: none;
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
line-height: 1.1;
|
|||
|
|
}
|
|||
|
|
.tech-toggle-text p {
|
|||
|
|
font-size: 13px; color: var(--gray-60);
|
|||
|
|
font-weight: 300;
|
|||
|
|
}
|
|||
|
|
.tech-toggle-icon {
|
|||
|
|
width: 40px; height: 40px;
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
display: grid; place-items: center;
|
|||
|
|
background: var(--paper);
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 22px; color: var(--blue);
|
|||
|
|
}
|
|||
|
|
.tech-toggle.expanded .tech-toggle-icon {
|
|||
|
|
background: var(--blue); color: var(--white);
|
|||
|
|
border-color: var(--blue);
|
|||
|
|
transform: rotate(45deg);
|
|||
|
|
}
|
|||
|
|
.tech-content {
|
|||
|
|
display: none;
|
|||
|
|
padding-top: 48px;
|
|||
|
|
}
|
|||
|
|
.tech-content.expanded { display: block; }
|
|||
|
|
.tech-content > section {
|
|||
|
|
padding: 0 0 60px;
|
|||
|
|
border: none; background: transparent;
|
|||
|
|
}
|
|||
|
|
.tech-content > section:last-child { padding-bottom: 0; }
|
|||
|
|
|
|||
|
|
/* FOOTER — Förderhinweis */
|
|||
|
|
.funding {
|
|||
|
|
padding: 64px 0;
|
|||
|
|
background: var(--white);
|
|||
|
|
border-top: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.funding-inner {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1.5fr;
|
|||
|
|
gap: 48px; align-items: center;
|
|||
|
|
}
|
|||
|
|
.funding-logos {
|
|||
|
|
display: flex; align-items: center; gap: 32px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
.funding-logos img {
|
|||
|
|
max-height: 64px; width: auto;
|
|||
|
|
}
|
|||
|
|
.funding-text p {
|
|||
|
|
font-size: 13px; line-height: 1.7;
|
|||
|
|
color: var(--gray-60); font-weight: 300;
|
|||
|
|
}
|
|||
|
|
.funding-text strong {
|
|||
|
|
color: var(--ink); font-weight: 500;
|
|||
|
|
}
|
|||
|
|
.funding-codes {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 11px; color: var(--blue);
|
|||
|
|
margin-top: 12px;
|
|||
|
|
letter-spacing: 0.04em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* INFRA — Pipeline */
|
|||
|
|
.infra { padding: 100px 0; border-top: 1px solid var(--gray-10); background: var(--white); }
|
|||
|
|
.pipeline {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(4, 1fr);
|
|||
|
|
gap: 0;
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: var(--paper);
|
|||
|
|
}
|
|||
|
|
.pipeline-step {
|
|||
|
|
padding: 36px 28px 32px;
|
|||
|
|
border-right: 1px solid var(--gray-10);
|
|||
|
|
position: relative;
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
min-height: 220px;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.pipeline-step:last-child { border-right: none; }
|
|||
|
|
.pipeline-step:hover { background: var(--white); }
|
|||
|
|
.pipeline-step::before {
|
|||
|
|
content: ''; position: absolute;
|
|||
|
|
top: 0; left: 0; right: 0; height: 2px;
|
|||
|
|
background: var(--blue);
|
|||
|
|
transform: scaleX(0); transform-origin: left;
|
|||
|
|
transition: transform 240ms ease-out;
|
|||
|
|
}
|
|||
|
|
.pipeline-step:hover::before { transform: scaleX(1); }
|
|||
|
|
.pipeline-arrow {
|
|||
|
|
position: absolute; right: -8px; top: 50%;
|
|||
|
|
transform: translateY(-50%);
|
|||
|
|
width: 16px; height: 16px;
|
|||
|
|
background: var(--paper);
|
|||
|
|
border-top: 1px solid var(--gray-10);
|
|||
|
|
border-right: 1px solid var(--gray-10);
|
|||
|
|
transform-origin: center;
|
|||
|
|
transform: translateY(-50%) rotate(45deg);
|
|||
|
|
z-index: 2;
|
|||
|
|
}
|
|||
|
|
.pipeline-step:last-child .pipeline-arrow { display: none; }
|
|||
|
|
.pipeline-num {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 13px; letter-spacing: 0.3em;
|
|||
|
|
color: var(--orange); margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.pipeline-step h4 {
|
|||
|
|
font-size: 28px; font-weight: 500;
|
|||
|
|
color: var(--ink); margin-bottom: 10px;
|
|||
|
|
letter-spacing: 0.02em;
|
|||
|
|
}
|
|||
|
|
.pipeline-step p {
|
|||
|
|
font-size: 13px; color: var(--gray-60);
|
|||
|
|
line-height: 1.55; font-weight: 300;
|
|||
|
|
margin-bottom: 16px; flex: 1;
|
|||
|
|
}
|
|||
|
|
.pipeline-tech {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 10px; color: var(--blue);
|
|||
|
|
letter-spacing: 0.04em;
|
|||
|
|
border-top: 1px dashed var(--gray-10);
|
|||
|
|
padding-top: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* KINDS — Schema-Karten */
|
|||
|
|
.kinds { padding: 100px 0; border-top: 1px solid var(--gray-10); }
|
|||
|
|
.kinds-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(4, 1fr);
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
.kind-card {
|
|||
|
|
background: var(--white);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 28px 24px;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
position: relative;
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
min-height: 200px;
|
|||
|
|
}
|
|||
|
|
.kind-card:hover {
|
|||
|
|
border-color: var(--blue);
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
.kind-card .kind-num {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 56px; font-weight: 500;
|
|||
|
|
line-height: 0.9; color: var(--blue);
|
|||
|
|
letter-spacing: 0.01em;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.kind-card h4 {
|
|||
|
|
font-size: 20px; font-weight: 500;
|
|||
|
|
color: var(--ink); margin-bottom: 8px;
|
|||
|
|
text-transform: none; letter-spacing: 0;
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
}
|
|||
|
|
.kind-card p {
|
|||
|
|
font-size: 12px; line-height: 1.55;
|
|||
|
|
color: var(--gray-60); font-weight: 300;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
.kind-card .kind-nip {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 9px; letter-spacing: 0.06em;
|
|||
|
|
color: var(--orange); text-transform: uppercase;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
padding-top: 10px;
|
|||
|
|
border-top: 1px dashed var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.kind-card.primary {
|
|||
|
|
background: var(--blue); border-color: var(--blue);
|
|||
|
|
}
|
|||
|
|
.kind-card.primary .kind-num { color: var(--orange); }
|
|||
|
|
.kind-card.primary h4 { color: var(--white); }
|
|||
|
|
.kind-card.primary p { color: rgba(255,255,255,0.75); }
|
|||
|
|
.kind-card.primary .kind-nip {
|
|||
|
|
color: var(--orange);
|
|||
|
|
border-top-color: rgba(255,255,255,0.18);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* STATS — Live counter / tag cloud */
|
|||
|
|
.stats { padding: 100px 0; border-top: 1px solid var(--gray-10); background: var(--white); }
|
|||
|
|
.stats-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 0;
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.stats-numbers {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
background: var(--paper);
|
|||
|
|
}
|
|||
|
|
.stat-cell {
|
|||
|
|
padding: 36px 32px;
|
|||
|
|
border-right: 1px solid var(--gray-10);
|
|||
|
|
border-bottom: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.stat-cell:nth-child(2n) { border-right: none; }
|
|||
|
|
.stat-cell:nth-child(n+3) { border-bottom: none; }
|
|||
|
|
.stat-value {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 64px; font-weight: 400;
|
|||
|
|
line-height: 0.9; letter-spacing: 0.02em;
|
|||
|
|
color: var(--blue); margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
.stat-value.orange { color: var(--orange); }
|
|||
|
|
.stat-label {
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
font-size: 11px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.22em; text-transform: uppercase;
|
|||
|
|
color: var(--gray-60);
|
|||
|
|
}
|
|||
|
|
.stat-sub {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 10px; color: var(--gray-60);
|
|||
|
|
margin-top: 6px;
|
|||
|
|
}
|
|||
|
|
.tag-cloud {
|
|||
|
|
padding: 36px 32px;
|
|||
|
|
border-left: 1px solid var(--gray-10);
|
|||
|
|
background: var(--paper);
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
}
|
|||
|
|
.tag-cloud .label { margin-bottom: 20px; }
|
|||
|
|
.tag-cloud-tags {
|
|||
|
|
display: flex; flex-wrap: wrap; gap: 8px;
|
|||
|
|
flex: 1; align-content: flex-start;
|
|||
|
|
}
|
|||
|
|
.tag-cloud-tag {
|
|||
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|||
|
|
font-weight: 500; letter-spacing: 0.04em;
|
|||
|
|
color: var(--blue); cursor: pointer;
|
|||
|
|
line-height: 1.2; transition: var(--ease);
|
|||
|
|
text-transform: lowercase;
|
|||
|
|
}
|
|||
|
|
.tag-cloud-tag:hover { color: var(--orange); }
|
|||
|
|
.tag-cloud-tag.size-1 { font-size: 13px; }
|
|||
|
|
.tag-cloud-tag.size-2 { font-size: 17px; }
|
|||
|
|
.tag-cloud-tag.size-3 { font-size: 22px; }
|
|||
|
|
.tag-cloud-tag.size-4 { font-size: 28px; }
|
|||
|
|
.tag-cloud-tag.size-5 { font-size: 36px; color: var(--orange); }
|
|||
|
|
|
|||
|
|
/* AMB — Events Liste */
|
|||
|
|
.amb { padding: 100px 0; border-top: 1px solid var(--gray-10); }
|
|||
|
|
.amb-list {
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: var(--white);
|
|||
|
|
}
|
|||
|
|
.amb-row {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 80px 1fr 200px 120px;
|
|||
|
|
gap: 24px;
|
|||
|
|
padding: 20px 28px;
|
|||
|
|
border-bottom: 1px solid var(--gray-10);
|
|||
|
|
align-items: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.amb-row:last-child { border-bottom: none; }
|
|||
|
|
.amb-row:hover { background: var(--gray-05); }
|
|||
|
|
.amb-row .amb-num {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-size: 18px; font-weight: 400;
|
|||
|
|
color: var(--orange); letter-spacing: 0.18em;
|
|||
|
|
}
|
|||
|
|
.amb-row .amb-title {
|
|||
|
|
font-size: 16px; font-weight: 500;
|
|||
|
|
color: var(--ink); line-height: 1.3;
|
|||
|
|
overflow: hidden; text-overflow: ellipsis;
|
|||
|
|
display: -webkit-box; -webkit-line-clamp: 2;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
}
|
|||
|
|
.amb-row .amb-meta {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 10px; color: var(--gray-60);
|
|||
|
|
overflow: hidden; text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.amb-row .amb-date {
|
|||
|
|
font-size: 11px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.12em; text-transform: uppercase;
|
|||
|
|
color: var(--blue); text-align: right;
|
|||
|
|
}
|
|||
|
|
.amb-row .amb-empty {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
text-align: center; padding: 40px 0;
|
|||
|
|
color: var(--gray-60); font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Loading / empty states */
|
|||
|
|
.loading-state { padding: 80px 0; text-align: center; }
|
|||
|
|
.loading-state .label { color: var(--gray-60); margin-bottom: 16px; display: inline-block; }
|
|||
|
|
.loading-state h3 {
|
|||
|
|
font-size: 32px; font-weight: 300;
|
|||
|
|
color: var(--ink); margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
.loading-state p {
|
|||
|
|
font-size: 14px; color: var(--gray-60);
|
|||
|
|
max-width: 400px; margin: 0 auto;
|
|||
|
|
}
|
|||
|
|
.loading-state code {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 12px; background: var(--gray-05);
|
|||
|
|
padding: 2px 6px; border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.skeleton {
|
|||
|
|
background: linear-gradient(90deg, var(--gray-05) 25%, var(--gray-10) 50%, var(--gray-05) 75%);
|
|||
|
|
background-size: 200% 100%;
|
|||
|
|
animation: shimmer 1.5s infinite;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
}
|
|||
|
|
@keyframes shimmer {
|
|||
|
|
0% { background-position: 200% 0; }
|
|||
|
|
100% { background-position: -200% 0; }
|
|||
|
|
}
|
|||
|
|
.skeleton-card {
|
|||
|
|
background: var(--white);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
overflow: hidden; min-height: 420px;
|
|||
|
|
}
|
|||
|
|
.skeleton-card .skeleton-img { aspect-ratio: 16 / 10; }
|
|||
|
|
.skeleton-card .skeleton-body { padding: 24px; }
|
|||
|
|
.skeleton-card .skeleton-line { height: 14px; margin-bottom: 10px; }
|
|||
|
|
.skeleton-card .skeleton-line.short { width: 40%; }
|
|||
|
|
.skeleton-card .skeleton-line.title { height: 22px; width: 90%; margin-bottom: 16px; }
|
|||
|
|
|
|||
|
|
/* MODAL */
|
|||
|
|
.modal {
|
|||
|
|
position: fixed; inset: 0; z-index: 100;
|
|||
|
|
background: rgba(20, 23, 31, 0.85);
|
|||
|
|
backdrop-filter: blur(8px);
|
|||
|
|
display: none; align-items: flex-start;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 40px 20px; overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
.modal.active { display: flex; }
|
|||
|
|
.modal-inner {
|
|||
|
|
max-width: 820px; width: 100%;
|
|||
|
|
background: var(--paper);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
position: relative;
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
margin-bottom: 40px;
|
|||
|
|
}
|
|||
|
|
.modal-close {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 16px; right: 16px;
|
|||
|
|
width: 36px; height: 36px;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: var(--white);
|
|||
|
|
border: 1px solid var(--gray-10);
|
|||
|
|
display: grid; place-items: center;
|
|||
|
|
cursor: pointer; z-index: 10;
|
|||
|
|
transition: var(--ease);
|
|||
|
|
color: var(--ink);
|
|||
|
|
font-family: inherit; font-size: 20px; line-height: 1;
|
|||
|
|
}
|
|||
|
|
.modal-close:hover {
|
|||
|
|
background: var(--blue); color: var(--white); border-color: var(--blue);
|
|||
|
|
}
|
|||
|
|
.modal-hero {
|
|||
|
|
position: relative; aspect-ratio: 2 / 1;
|
|||
|
|
background: var(--blue); overflow: hidden;
|
|||
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|||
|
|
}
|
|||
|
|
.modal-hero::before {
|
|||
|
|
content: ''; position: absolute; inset: 0;
|
|||
|
|
background:
|
|||
|
|
repeating-linear-gradient(0deg, transparent, transparent 48px, rgba(255,255,255,0.04) 48px, rgba(255,255,255,0.04) 49px),
|
|||
|
|
repeating-linear-gradient(90deg, transparent, transparent 48px, rgba(255,255,255,0.04) 48px, rgba(255,255,255,0.04) 49px),
|
|||
|
|
linear-gradient(135deg, var(--blue) 0%, var(--blue-deep) 100%);
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
.modal-hero img {
|
|||
|
|
position: absolute; inset: 0;
|
|||
|
|
width: 100%; height: 100%;
|
|||
|
|
object-fit: cover; z-index: 2;
|
|||
|
|
}
|
|||
|
|
.modal-hero-overlay {
|
|||
|
|
position: absolute; inset: 0; z-index: 3;
|
|||
|
|
padding: 32px;
|
|||
|
|
display: flex; flex-direction: column;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
background: linear-gradient(180deg, transparent 50%, rgba(20,23,31,0.8) 100%);
|
|||
|
|
}
|
|||
|
|
.modal-hero-overlay h2 {
|
|||
|
|
color: var(--white);
|
|||
|
|
font-size: clamp(32px, 4vw, 52px);
|
|||
|
|
font-weight: 500; letter-spacing: 0.02em;
|
|||
|
|
line-height: 1.05; max-width: 680px;
|
|||
|
|
}
|
|||
|
|
.modal-hero-overlay .label { color: var(--orange); margin-bottom: 12px; display: block; }
|
|||
|
|
|
|||
|
|
.modal-content { padding: 48px 56px 64px; }
|
|||
|
|
.modal-meta {
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding-bottom: 20px; border-bottom: 1px solid var(--gray-10);
|
|||
|
|
margin-bottom: 32px;
|
|||
|
|
flex-wrap: wrap; gap: 16px;
|
|||
|
|
}
|
|||
|
|
.modal-body {
|
|||
|
|
font-size: 16px; line-height: 1.75;
|
|||
|
|
color: var(--ink); font-weight: 400;
|
|||
|
|
}
|
|||
|
|
.modal-body h1, .modal-body h2, .modal-body h3, .modal-body h4 {
|
|||
|
|
font-family: 'Yanone Kaffeesatz', sans-serif;
|
|||
|
|
font-weight: 500; letter-spacing: 0.04em;
|
|||
|
|
color: var(--blue); margin: 36px 0 16px;
|
|||
|
|
text-transform: none; line-height: 1.1;
|
|||
|
|
}
|
|||
|
|
.modal-body h1 { font-size: 42px; }
|
|||
|
|
.modal-body h2 { font-size: 34px; }
|
|||
|
|
.modal-body h3 { font-size: 26px; }
|
|||
|
|
.modal-body h4 { font-size: 20px; }
|
|||
|
|
.modal-body p { margin-bottom: 20px; }
|
|||
|
|
.modal-body a {
|
|||
|
|
color: var(--blue); text-decoration: underline;
|
|||
|
|
text-decoration-color: var(--gray-30);
|
|||
|
|
text-underline-offset: 3px; transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.modal-body a:hover {
|
|||
|
|
text-decoration-color: var(--blue); color: var(--blue-deep);
|
|||
|
|
}
|
|||
|
|
.modal-body img {
|
|||
|
|
max-width: 100%; height: auto;
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
margin: 24px 0; border: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.modal-body blockquote {
|
|||
|
|
border-left: 3px solid var(--orange);
|
|||
|
|
padding: 8px 0 8px 20px; margin: 24px 0;
|
|||
|
|
color: var(--gray-60); font-style: italic;
|
|||
|
|
}
|
|||
|
|
.modal-body ul, .modal-body ol { margin: 0 0 20px 24px; }
|
|||
|
|
.modal-body li { margin-bottom: 8px; }
|
|||
|
|
.modal-body code {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 13px; background: var(--gray-05);
|
|||
|
|
padding: 2px 6px; border-radius: var(--radius);
|
|||
|
|
color: var(--blue-deep);
|
|||
|
|
}
|
|||
|
|
.modal-body pre {
|
|||
|
|
background: var(--ink); color: var(--paper);
|
|||
|
|
padding: 20px 24px; border-radius: var(--radius);
|
|||
|
|
overflow-x: auto; margin: 24px 0;
|
|||
|
|
}
|
|||
|
|
.modal-body pre code {
|
|||
|
|
background: transparent; color: inherit; padding: 0; font-size: 13px;
|
|||
|
|
}
|
|||
|
|
.modal-body hr {
|
|||
|
|
border: none; border-top: 1px solid var(--gray-10); margin: 40px 0;
|
|||
|
|
}
|
|||
|
|
.modal-footer {
|
|||
|
|
padding: 24px 56px;
|
|||
|
|
background: var(--white);
|
|||
|
|
border-top: 1px solid var(--gray-10);
|
|||
|
|
border-radius: 0 0 var(--radius) var(--radius);
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
flex-wrap: wrap; gap: 16px;
|
|||
|
|
}
|
|||
|
|
.event-id {
|
|||
|
|
font-family: 'JetBrains Mono', monospace;
|
|||
|
|
font-size: 10px; color: var(--gray-60);
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
max-width: 60%; overflow: hidden;
|
|||
|
|
text-overflow: ellipsis; white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* FOOTER */
|
|||
|
|
footer {
|
|||
|
|
border-top: 1px solid var(--gray-10);
|
|||
|
|
padding: 48px 0 32px;
|
|||
|
|
background: var(--paper);
|
|||
|
|
}
|
|||
|
|
.footer-inner {
|
|||
|
|
display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr;
|
|||
|
|
gap: 48px; margin-bottom: 48px;
|
|||
|
|
}
|
|||
|
|
.footer-brand p {
|
|||
|
|
font-size: 13px; color: var(--gray-60);
|
|||
|
|
font-weight: 300; margin-top: 14px;
|
|||
|
|
max-width: 280px; line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
.footer-col h5 {
|
|||
|
|
font-size: 10px; font-weight: 500;
|
|||
|
|
letter-spacing: 0.24em; text-transform: uppercase;
|
|||
|
|
color: var(--blue); margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.footer-col ul { list-style: none; }
|
|||
|
|
.footer-col li { font-size: 13px; margin-bottom: 8px; }
|
|||
|
|
.footer-col a {
|
|||
|
|
color: var(--ink); text-decoration: none; transition: var(--ease);
|
|||
|
|
}
|
|||
|
|
.footer-col a:hover { color: var(--blue); }
|
|||
|
|
.footer-bottom {
|
|||
|
|
display: flex; justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding-top: 24px; border-top: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.footer-bottom .micro { font-size: 7px; }
|
|||
|
|
|
|||
|
|
/* Utility */
|
|||
|
|
.corner-micro-tl, .corner-micro-tr, .corner-micro-bl, .corner-micro-br, .corner-micro-bc {
|
|||
|
|
position: absolute;
|
|||
|
|
font-family: 'Alumni Sans SC', sans-serif;
|
|||
|
|
font-size: 6px; letter-spacing: 0.3em;
|
|||
|
|
color: var(--gray-60); text-transform: uppercase;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.corner-micro-tl { top: 12px; left: 12px; }
|
|||
|
|
.corner-micro-tr { top: 12px; right: 12px; }
|
|||
|
|
.corner-micro-bl { bottom: 12px; left: 12px; }
|
|||
|
|
.corner-micro-br { bottom: 12px; right: 12px; }
|
|||
|
|
.corner-micro-bc { bottom: 12px; left: 50%; transform: translateX(-50%); }
|
|||
|
|
.section-wrap { position: relative; }
|
|||
|
|
|
|||
|
|
/* Responsive */
|
|||
|
|
@media (max-width: 1024px) {
|
|||
|
|
.hero-grid { grid-template-columns: 1fr; gap: 48px; }
|
|||
|
|
.section-head { grid-template-columns: 1fr; gap: 24px; }
|
|||
|
|
.featured-card { grid-template-columns: 1fr; }
|
|||
|
|
.featured-body { padding: 32px; }
|
|||
|
|
.articles-grid { grid-template-columns: repeat(2, 1fr); }
|
|||
|
|
.footer-inner { grid-template-columns: 1fr 1fr; }
|
|||
|
|
.modal-content { padding: 32px 28px; }
|
|||
|
|
.modal-footer { padding: 20px 28px; }
|
|||
|
|
.pipeline { grid-template-columns: repeat(2, 1fr); }
|
|||
|
|
.pipeline-step:nth-child(2) .pipeline-arrow { display: none; }
|
|||
|
|
.pipeline-step:nth-child(1), .pipeline-step:nth-child(2) {
|
|||
|
|
border-bottom: 1px solid var(--gray-10);
|
|||
|
|
}
|
|||
|
|
.kinds-grid { grid-template-columns: repeat(2, 1fr); }
|
|||
|
|
.stats-grid { grid-template-columns: 1fr; }
|
|||
|
|
.tag-cloud { border-left: none; border-top: 1px solid var(--gray-10); }
|
|||
|
|
.amb-row { grid-template-columns: 60px 1fr 100px; }
|
|||
|
|
.amb-row .amb-meta { display: none; }
|
|||
|
|
.about-grid { grid-template-columns: 1fr; gap: 48px; }
|
|||
|
|
.community-grid { grid-template-columns: 1fr; gap: 48px; }
|
|||
|
|
.communities-grid { grid-template-columns: repeat(2, 1fr); }
|
|||
|
|
.funding-inner { grid-template-columns: 1fr; gap: 32px; }
|
|||
|
|
}
|
|||
|
|
@media (max-width: 640px) {
|
|||
|
|
.container { padding: 0 24px; }
|
|||
|
|
.nav-links { display: none; }
|
|||
|
|
.nav-status { padding: 6px 12px; }
|
|||
|
|
.articles-grid { grid-template-columns: 1fr; }
|
|||
|
|
.footer-inner { grid-template-columns: 1fr; }
|
|||
|
|
.footer-bottom { flex-direction: column; gap: 12px; }
|
|||
|
|
.articles-head { flex-direction: column; align-items: flex-start; gap: 24px; }
|
|||
|
|
.pipeline { grid-template-columns: 1fr; }
|
|||
|
|
.pipeline-step { border-right: none; border-bottom: 1px solid var(--gray-10); }
|
|||
|
|
.pipeline-step:last-child { border-bottom: none; }
|
|||
|
|
.pipeline-arrow { display: none; }
|
|||
|
|
.kinds-grid { grid-template-columns: 1fr; }
|
|||
|
|
.stats-numbers { grid-template-columns: 1fr; }
|
|||
|
|
.stat-cell { border-right: none; border-bottom: 1px solid var(--gray-10); }
|
|||
|
|
.stat-cell:last-child { border-bottom: none; }
|
|||
|
|
.amb-row {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
.amb-row .amb-date { text-align: left; }
|
|||
|
|
.hero h1 { font-size: clamp(56px, 14vw, 80px); }
|
|||
|
|
.infra, .kinds, .stats, .amb, .about, .community, .communities { padding: 64px 0; }
|
|||
|
|
.communities-grid { grid-template-columns: 1fr; }
|
|||
|
|
.cta-card { padding: 28px 24px; }
|
|||
|
|
.tech-toggle { padding: 20px 24px; }
|
|||
|
|
.tech-toggle-text h3 { font-size: 22px; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
|
|||
|
|
<!-- NAV -->
|
|||
|
|
<nav>
|
|||
|
|
<div class="container nav-inner">
|
|||
|
|
<div class="logo">FOER<span>·</span>BICO</div>
|
|||
|
|
<ul class="nav-links">
|
|||
|
|
<li><a href="#about">Über</a></li>
|
|||
|
|
<li><a href="#community">Mitmachen</a></li>
|
|||
|
|
<li><a href="#featured">Beiträge</a></li>
|
|||
|
|
<li><a href="#tech">Tech</a></li>
|
|||
|
|
</ul>
|
|||
|
|
<div class="nav-status" title="Relay-Status">
|
|||
|
|
<span class="status-dot connecting" id="status-dot"></span>
|
|||
|
|
<span class="status-text" id="status-text">Verbinde</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<!-- HERO -->
|
|||
|
|
<section class="hero section-wrap">
|
|||
|
|
<span class="corner-micro-tl">index · 001 — community of communities</span>
|
|||
|
|
<span class="corner-micro-tr">religionsbezogene oer · 2024–2027</span>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="hero-grid">
|
|||
|
|
<div class="hero-text">
|
|||
|
|
<div class="hero-eyebrow">
|
|||
|
|
<span class="label">Community of Communities</span>
|
|||
|
|
</div>
|
|||
|
|
<h1>
|
|||
|
|
Offen.<br>
|
|||
|
|
<span class="accent">Vernetzt.</span>
|
|||
|
|
<span class="dash">—</span> Religionsbezogen.
|
|||
|
|
</h1>
|
|||
|
|
<p class="hero-lead">
|
|||
|
|
FOERBICO bringt religionsbezogene Communities zusammen, die offene Bildungsmaterialien (OER)
|
|||
|
|
erstellen, teilen und weiterentwickeln. Gemeinsame Qualitätskriterien, gemeinsame Metadaten —
|
|||
|
|
damit das, was in einer Community entsteht, auch in den anderen ankommt.
|
|||
|
|
</p>
|
|||
|
|
<div class="hero-actions">
|
|||
|
|
<a href="#community" class="btn btn-primary">Mitmachen →</a>
|
|||
|
|
<a href="#about" class="btn btn-ghost">Mehr erfahren</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="hero-visual">
|
|||
|
|
<div class="visual-content">
|
|||
|
|
<div class="visual-top">
|
|||
|
|
<div class="visual-badge">FB</div>
|
|||
|
|
<div class="visual-meta">
|
|||
|
|
lat 49.1508 · lon 9.1628<br>
|
|||
|
|
comenius–institut<br>
|
|||
|
|
amb · relay · oersi
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="visual-bottom">
|
|||
|
|
<div class="visual-number" id="visual-count">№ ···· / 30023</div>
|
|||
|
|
<div class="visual-meta">
|
|||
|
|
kind · 30023<br>
|
|||
|
|
kind · 30142<br>
|
|||
|
|
schema · foerbico
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="visual-center">
|
|||
|
|
<img src="https://oer.community/images/FOERBICO.png" alt="FOERBICO" class="visual-logo" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<span class="corner-micro-bc">scroll — was ist foerbico · mitmachen · beiträge</span>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- ABOUT — Was ist FOERBICO -->
|
|||
|
|
<section class="about section-wrap" id="about">
|
|||
|
|
<span class="corner-micro-tl">02 — was ist foerbico</span>
|
|||
|
|
<span class="corner-micro-tr">community of communities · religionsbezogen</span>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="about-grid">
|
|||
|
|
<div class="about-side">
|
|||
|
|
<span class="label">Förderung offener<br>Bildungspraktiken</span>
|
|||
|
|
<p>
|
|||
|
|
in religionsbezogenen Communities durch die Entwicklung
|
|||
|
|
eines koordinierten OER-Ökosystems.
|
|||
|
|
</p>
|
|||
|
|
<div class="about-eyebrow" style="margin-top: 32px;">
|
|||
|
|
Community<br>
|
|||
|
|
<span>of Communities.</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="about-content">
|
|||
|
|
<h3>Vernetzung statt <span class="accent">Vereinzelung</span>.</h3>
|
|||
|
|
<p class="lead">
|
|||
|
|
Du bist interessiert an offenen Materialien (<a href="https://oer.community/oer-und-oep/" target="_blank" rel="noopener">OER — Open Educational Resources</a>) für das Lernen und Lehren?
|
|||
|
|
Du hast sogar schon mal selbst Materialien erstellt oder arbeitest in einer Community oder einem
|
|||
|
|
Netzwerk an der Entwicklung von Bildungsmaterialien mit?
|
|||
|
|
</p>
|
|||
|
|
<p>
|
|||
|
|
Dann bist du bei FOERBICO genau richtig. Im Sinne einer <em>Community of Communities</em> zielt
|
|||
|
|
FOERBICO auf die Vernetzung und den Austausch von verschiedenen religionsbezogenen Communities,
|
|||
|
|
um gemeinsame Qualitätskriterien und Metadatenstandards zu entwickeln.
|
|||
|
|
</p>
|
|||
|
|
<p>
|
|||
|
|
So können die wertvoll erarbeiteten OER in verschiedene Materialpools eingepflegt und
|
|||
|
|
anschlussfähig gemacht werden — um weiter verbreitet, weiterentwickelt und von mehr Leuten
|
|||
|
|
verwendet zu werden.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- COMMUNITY — Mitmachen -->
|
|||
|
|
<section class="community section-wrap" id="community">
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="community-grid">
|
|||
|
|
<div class="community-text">
|
|||
|
|
<span class="label">03 — Mitmachen</span>
|
|||
|
|
<h2>
|
|||
|
|
Lust auf <span class="accent">OER</span>
|
|||
|
|
ins Gespräch?
|
|||
|
|
</h2>
|
|||
|
|
<p>
|
|||
|
|
Werde Teil der Community of Communities. Wir treffen uns in einem offenen Element-Space —
|
|||
|
|
niedrigschwellig, dezentral, ohne große Hürden. Komm dazu, hör rein, schreib mit.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="cta-card">
|
|||
|
|
<span class="label">Offener Austausch</span>
|
|||
|
|
<h3>OERcommunity Space</h3>
|
|||
|
|
<p>
|
|||
|
|
Unser Element/Matrix-Space ist offen für alle, die an offenen Bildungsmaterialien
|
|||
|
|
im religionsbezogenen Kontext interessiert sind.
|
|||
|
|
</p>
|
|||
|
|
<a href="https://matrix.to/#/%23oercommunity:rpi-virtuell.de" target="_blank" rel="noopener" class="cta-element">
|
|||
|
|
<img src="https://oer.community/images/element-logo.svg" alt="Element" />
|
|||
|
|
<div class="cta-element-text">
|
|||
|
|
<div class="cta-element-name">Space "OERcommunity"</div>
|
|||
|
|
<div class="cta-element-sub">#oercommunity:rpi-virtuell.de</div>
|
|||
|
|
</div>
|
|||
|
|
<span class="cta-element-arrow">→</span>
|
|||
|
|
</a>
|
|||
|
|
<div class="cta-team-link">
|
|||
|
|
Lieber direkt schreiben? Melde dich beim
|
|||
|
|
<a href="https://oer.community/unser-team/" target="_blank" rel="noopener">Team</a>.
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- COMMUNITIES — Beteiligte / Vernetzte Communities -->
|
|||
|
|
<section class="communities section-wrap" id="communities">
|
|||
|
|
<span class="corner-micro-tl">04 — beteiligte communities</span>
|
|||
|
|
<span class="corner-micro-tr">stand · mai 2026</span>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="section-head">
|
|||
|
|
<div>
|
|||
|
|
<div class="label" style="margin-bottom: 24px;">04 — Im Verbund</div>
|
|||
|
|
<h2>Wer <span class="accent">mitmacht</span>.</h2>
|
|||
|
|
</div>
|
|||
|
|
<p class="section-head-right">
|
|||
|
|
FOERBICO entsteht im Zusammenspiel mehrerer religionsbezogener Initiativen und Plattformen.
|
|||
|
|
Die Liste wächst — wenn deine Community fehlt, melde dich.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="communities-grid">
|
|||
|
|
<a href="https://rpi-virtuell.de" target="_blank" rel="noopener" class="community-card">
|
|||
|
|
<span class="label">Plattform</span>
|
|||
|
|
<h4>rpi-virtuell</h4>
|
|||
|
|
<p>Religionspädagogisches Portal mit Materialpool, Webinaren und Community-Räumen.</p>
|
|||
|
|
<span class="community-link">rpi-virtuell.de</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="https://relilab.org" target="_blank" rel="noopener" class="community-card">
|
|||
|
|
<span class="label">Netzwerk</span>
|
|||
|
|
<h4>relilab</h4>
|
|||
|
|
<p>Selbstorganisierte Lerncommunity rund um digitale religiöse Bildung.</p>
|
|||
|
|
<span class="community-link">relilab.org</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="https://oer.community" target="_blank" rel="noopener" class="community-card">
|
|||
|
|
<span class="label">Hub</span>
|
|||
|
|
<h4>oer.community</h4>
|
|||
|
|
<p>Zentrale Anlaufstelle für die OER-Community mit Blog, Veranstaltungen und Vernetzung.</p>
|
|||
|
|
<span class="community-link">oer.community</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="https://www.comenius.de" target="_blank" rel="noopener" class="community-card">
|
|||
|
|
<span class="label">Träger</span>
|
|||
|
|
<h4>Comenius-Institut</h4>
|
|||
|
|
<p>Evangelisches Forschungsinstitut für Bildung — fachliche Heimat von FOERBICO.</p>
|
|||
|
|
<span class="community-link">comenius.de</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="https://co-woerk.de" target="_blank" rel="noopener" class="community-card">
|
|||
|
|
<span class="label">Partner</span>
|
|||
|
|
<h4>CO-WOERK</h4>
|
|||
|
|
<p>Wissenstransfer und Begleitung in offenen Bildungspraktiken.</p>
|
|||
|
|
<span class="community-link">co-woerk.de</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="https://oersi.org" target="_blank" rel="noopener" class="community-card">
|
|||
|
|
<span class="label">Index</span>
|
|||
|
|
<h4>OERSI</h4>
|
|||
|
|
<p>Open Educational Resources Search Index — der zentrale Sucheinstieg, an den FOERBICO andockt.</p>
|
|||
|
|
<span class="community-link">oersi.org</span>
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- FEATURED -->
|
|||
|
|
<section class="featured section-wrap" id="featured">
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="section-head">
|
|||
|
|
<div>
|
|||
|
|
<div class="label" style="margin-bottom: 24px;">05 — Zuletzt veröffentlicht</div>
|
|||
|
|
<h2>Aktueller <span class="accent">Artikel</span>.</h2>
|
|||
|
|
</div>
|
|||
|
|
<p class="section-head-right">
|
|||
|
|
Live aus dem Relay-Netzwerk. Signiert via Nostr, verteilt über
|
|||
|
|
<span class="inline-code">relay-rpi.edufeed.org</span> und
|
|||
|
|
<span class="inline-code">relay.primal.net</span>.
|
|||
|
|
Die folgende Ressource ist die zuletzt veröffentlichte Longform-Note.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="featured-container">
|
|||
|
|
<div class="featured-card skeleton-card" style="grid-template-columns: 1.2fr 1fr;">
|
|||
|
|
<div class="skeleton" style="min-height: 440px;"></div>
|
|||
|
|
<div class="skeleton-body" style="padding: 48px;">
|
|||
|
|
<div class="skeleton skeleton-line short" style="margin-bottom: 24px;"></div>
|
|||
|
|
<div class="skeleton skeleton-line title"></div>
|
|||
|
|
<div class="skeleton skeleton-line title" style="width: 70%;"></div>
|
|||
|
|
<div style="margin-top: 24px;">
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line" style="width: 60%;"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- ARTICLES -->
|
|||
|
|
<section class="articles section-wrap" id="articles">
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="articles-head">
|
|||
|
|
<div>
|
|||
|
|
<div class="label" style="margin-bottom: 20px;">06 — Archiv</div>
|
|||
|
|
<h2>Alle <span class="accent">Ressourcen</span>.</h2>
|
|||
|
|
</div>
|
|||
|
|
<div class="articles-count">
|
|||
|
|
<span id="article-count">··</span>
|
|||
|
|
<small>kind · 30023</small>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="filter-bar" id="filter-bar"></div>
|
|||
|
|
|
|||
|
|
<div class="articles-grid" id="articles-grid">
|
|||
|
|
<div class="article-card skeleton-card">
|
|||
|
|
<div class="skeleton skeleton-img"></div>
|
|||
|
|
<div class="skeleton-body">
|
|||
|
|
<div class="skeleton skeleton-line short"></div>
|
|||
|
|
<div class="skeleton skeleton-line title"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line" style="width: 60%;"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="article-card skeleton-card">
|
|||
|
|
<div class="skeleton skeleton-img"></div>
|
|||
|
|
<div class="skeleton-body">
|
|||
|
|
<div class="skeleton skeleton-line short"></div>
|
|||
|
|
<div class="skeleton skeleton-line title"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line" style="width: 60%;"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="article-card skeleton-card">
|
|||
|
|
<div class="skeleton skeleton-img"></div>
|
|||
|
|
<div class="skeleton-body">
|
|||
|
|
<div class="skeleton skeleton-line short"></div>
|
|||
|
|
<div class="skeleton skeleton-line title"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line"></div>
|
|||
|
|
<div class="skeleton skeleton-line" style="width: 60%;"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- TECH — Aufklappbare Infrastruktur-Details -->
|
|||
|
|
<section class="tech-section section-wrap" id="tech">
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="tech-toggle" id="tech-toggle" onclick="toggleTech()" role="button" tabindex="0" aria-expanded="false">
|
|||
|
|
<div class="tech-toggle-text">
|
|||
|
|
<h3>Wie das technisch funktioniert</h3>
|
|||
|
|
<p>Pipeline · Event-Kinds · Live-Statistik aus dem Relay-Netzwerk · für die Neugierigen.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="tech-toggle-icon">+</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="tech-content" id="tech-content">
|
|||
|
|
<!-- INFRA — Pipeline -->
|
|||
|
|
<section class="infra section-wrap" id="infra">
|
|||
|
|
<span class="corner-micro-tl">t·01 — pipeline</span>
|
|||
|
|
<span class="corner-micro-tr">stack · sveltekit · nostr · oersi</span>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="section-head">
|
|||
|
|
<div>
|
|||
|
|
<div class="label" style="margin-bottom: 24px;">T·01 — Pipeline</div>
|
|||
|
|
<h2>Wie eine Ressource <span class="accent">verteilt</span> wird.</h2>
|
|||
|
|
</div>
|
|||
|
|
<p class="section-head-right">
|
|||
|
|
Markdown ist die Quelle. Nostr-Events sind das Transport-Format.
|
|||
|
|
Relays cachen, Clients rendern. Der Prozess ist deterministisch,
|
|||
|
|
signiert und reproduzierbar — keine Plattform dazwischen.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="pipeline">
|
|||
|
|
<div class="pipeline-step">
|
|||
|
|
<div class="pipeline-num">01</div>
|
|||
|
|
<h4>Verfassen</h4>
|
|||
|
|
<p>Markdown mit YAML-Frontmatter. Dual-Block-Schema für Inhalt & Statik. Gespeichert in Forgejo.</p>
|
|||
|
|
<div class="pipeline-tech">.md · #commonMetadata · #staticSiteGenerator</div>
|
|||
|
|
<span class="pipeline-arrow"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="pipeline-step">
|
|||
|
|
<div class="pipeline-num">02</div>
|
|||
|
|
<h4>Signieren</h4>
|
|||
|
|
<p>NIP-07-Browser-Extension oder <code class="inline-code">nak</code>-CLI. Schlüssel bleiben lokal, der Inhalt wird kryptographisch attestiert.</p>
|
|||
|
|
<div class="pipeline-tech">nak event sign · nip-07 · nip-46</div>
|
|||
|
|
<span class="pipeline-arrow"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="pipeline-step">
|
|||
|
|
<div class="pipeline-num">03</div>
|
|||
|
|
<h4>Verteilen</h4>
|
|||
|
|
<p>Publikation an mehrere Relays gleichzeitig. Kein Single-Point-of-Failure, jedes Relay ist eine vollständige Kopie.</p>
|
|||
|
|
<div class="pipeline-tech">wss://relay-rpi · wss://primal · wss://amb-relay</div>
|
|||
|
|
<span class="pipeline-arrow"></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="pipeline-step">
|
|||
|
|
<div class="pipeline-num">04</div>
|
|||
|
|
<h4>Indexieren</h4>
|
|||
|
|
<p>OERSI liest die <code class="inline-code">kind:30142</code>-AMB-Events ab. Diese Seite liest <code class="inline-code">kind:30023</code> live aus dem Relay.</p>
|
|||
|
|
<div class="pipeline-tech">oersi · njump · custom client</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- KINDS — Schema-Karten -->
|
|||
|
|
<section class="kinds section-wrap">
|
|||
|
|
<span class="corner-micro-tl">t·02 — event kinds</span>
|
|||
|
|
<span class="corner-micro-tr">schema · foerbico · 2026</span>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="section-head">
|
|||
|
|
<div>
|
|||
|
|
<div class="label" style="margin-bottom: 24px;">T·02 — Kinds</div>
|
|||
|
|
<h2>Was eine Ressource <span class="accent">ist</span>.</h2>
|
|||
|
|
</div>
|
|||
|
|
<p class="section-head-right">
|
|||
|
|
Jeder Inhalt bekommt eine maschinenlesbare Form. Vier Event-Kinds
|
|||
|
|
bilden das aktive FOERBICO-Vokabular ab — von der Longform-Note bis
|
|||
|
|
zum AMB-Metadatensatz.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="kinds-grid">
|
|||
|
|
<div class="kind-card primary">
|
|||
|
|
<div class="kind-num">30142</div>
|
|||
|
|
<h4>AMB Metadata</h4>
|
|||
|
|
<p>Allgemeines Metadatenschema für Bildungsressourcen. FOERBICO-Kernformat.</p>
|
|||
|
|
<div class="kind-nip">parameterized replaceable</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="kind-card">
|
|||
|
|
<div class="kind-num">30023</div>
|
|||
|
|
<h4>Longform</h4>
|
|||
|
|
<p>Markdown-Artikel mit Titel, Summary und Bild. Was du gerade liest.</p>
|
|||
|
|
<div class="kind-nip">nip-23 · longform content</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="kind-card">
|
|||
|
|
<div class="kind-num">30818</div>
|
|||
|
|
<h4>Wiki</h4>
|
|||
|
|
<p>Kollaborativ editierbare Begriffe und Glossarseiten als versionierter Stream.</p>
|
|||
|
|
<div class="kind-nip">nip-54 · wiki</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="kind-card">
|
|||
|
|
<div class="kind-num">31923</div>
|
|||
|
|
<h4>Termin</h4>
|
|||
|
|
<p>Zeitbasierte Veranstaltungen wie Webinare, Tagungen, Sprechstunden.</p>
|
|||
|
|
<div class="kind-nip">nip-52 · time-based event</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- STATS — Live counter / tag cloud -->
|
|||
|
|
<section class="stats section-wrap">
|
|||
|
|
<span class="corner-micro-tl">t·03 — live aus dem relay</span>
|
|||
|
|
<span class="corner-micro-tr">aggregiert · clientseitig · ohne tracking</span>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="section-head">
|
|||
|
|
<div>
|
|||
|
|
<div class="label" style="margin-bottom: 24px;">T·03 — Statistik</div>
|
|||
|
|
<h2>Was gerade <span class="accent">da</span> ist.</h2>
|
|||
|
|
</div>
|
|||
|
|
<p class="section-head-right">
|
|||
|
|
Die folgenden Zahlen werden direkt aus den geladenen Events berechnet
|
|||
|
|
— kein Server, keine Telemetrie. Wenn du die Seite neu lädst, kommen
|
|||
|
|
die Daten frisch aus dem Relay.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="stats-grid">
|
|||
|
|
<div class="stats-numbers">
|
|||
|
|
<div class="stat-cell">
|
|||
|
|
<div class="stat-value" id="stat-total">··</div>
|
|||
|
|
<div class="stat-label">Artikel · gesamt</div>
|
|||
|
|
<div class="stat-sub" id="stat-total-sub">kind 30023</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-cell">
|
|||
|
|
<div class="stat-value orange" id="stat-tags">··</div>
|
|||
|
|
<div class="stat-label">Tags · einzigartig</div>
|
|||
|
|
<div class="stat-sub">t-tag</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-cell">
|
|||
|
|
<div class="stat-value" id="stat-first">····</div>
|
|||
|
|
<div class="stat-label">Erstveröffentlichung</div>
|
|||
|
|
<div class="stat-sub" id="stat-first-sub">—</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-cell">
|
|||
|
|
<div class="stat-value orange" id="stat-latest">····</div>
|
|||
|
|
<div class="stat-label">Letzte Aktualisierung</div>
|
|||
|
|
<div class="stat-sub" id="stat-latest-sub">—</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="tag-cloud">
|
|||
|
|
<span class="label">Themen-Cloud</span>
|
|||
|
|
<div class="tag-cloud-tags" id="tag-cloud">
|
|||
|
|
<span class="tag-cloud-tag size-2">···</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- FUNDING — Förderhinweis -->
|
|||
|
|
<section class="funding section-wrap">
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="funding-inner">
|
|||
|
|
<div class="funding-logos">
|
|||
|
|
<img src="https://oer.community/images/gefoerdert_vom_bmbfsfj.png" alt="Gefördert vom BMBFSFJ" />
|
|||
|
|
<a href="https://www.oer-strategie.de/" target="_blank" rel="noopener">
|
|||
|
|
<img src="https://oer.community/images/OER_Strategie.png" alt="OER-Strategie des Bundes" />
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="funding-text">
|
|||
|
|
<p>
|
|||
|
|
<strong>FOERBICO</strong> wird von Mai 2024 bis Ende April 2027 vom
|
|||
|
|
<a href="https://www.bmfsfj.de/" target="_blank" rel="noopener" style="color: var(--blue);">Bundesministerium für Bildung, Familie, Senioren, Frauen und Jugend (BMBFSFJ)</a>
|
|||
|
|
im Rahmen der <a href="https://www.oer-strategie.de/" target="_blank" rel="noopener" style="color: var(--blue);">OER-Strategie</a> des Bundes gefördert.
|
|||
|
|
</p>
|
|||
|
|
<div class="funding-codes">FKZ · 01PO23012A · 01PO23012B · 01PO23012C</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- FOOTER -->
|
|||
|
|
<footer class="section-wrap">
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="footer-inner">
|
|||
|
|
<div class="footer-brand">
|
|||
|
|
<div class="logo">FOER<span>·</span>BICO</div>
|
|||
|
|
<p>Community of Communities — Vernetzung religionsbezogener Communities für offene Bildungsmaterialien. Ein Projekt des Comenius-Instituts mit rpi-virtuell und CO-WOERK.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="footer-col">
|
|||
|
|
<h5>Community</h5>
|
|||
|
|
<ul>
|
|||
|
|
<li><a href="https://oer.community" target="_blank" rel="noopener">oer.community</a></li>
|
|||
|
|
<li><a href="https://matrix.to/#/%23oercommunity:rpi-virtuell.de" target="_blank" rel="noopener">Matrix-Space</a></li>
|
|||
|
|
<li><a href="https://oer.community/unser-team/" target="_blank" rel="noopener">Team</a></li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
<div class="footer-col">
|
|||
|
|
<h5>Partner</h5>
|
|||
|
|
<ul>
|
|||
|
|
<li><a href="https://www.comenius.de" target="_blank" rel="noopener">Comenius-Institut</a></li>
|
|||
|
|
<li><a href="https://rpi-virtuell.de" target="_blank" rel="noopener">rpi-virtuell</a></li>
|
|||
|
|
<li><a href="https://co-woerk.de" target="_blank" rel="noopener">CO-WOERK</a></li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
<div class="footer-col">
|
|||
|
|
<h5>Kontakt</h5>
|
|||
|
|
<ul>
|
|||
|
|
<li><a href="https://oer.community/unser-team/" target="_blank" rel="noopener">Team</a></li>
|
|||
|
|
<li><a href="https://www.comenius.de/impressum/" target="_blank" rel="noopener">Impressum</a></li>
|
|||
|
|
<li><a href="https://www.comenius.de/datenschutzerklaerung/" target="_blank" rel="noopener">Datenschutz</a></li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="footer-bottom">
|
|||
|
|
<span class="micro">© 2026 comenius-institut · cc by-sa 4.0 · design system v1</span>
|
|||
|
|
<span class="micro">built with markdown · signed with nostr · indexed by oersi</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</footer>
|
|||
|
|
|
|||
|
|
<!-- MODAL -->
|
|||
|
|
<div class="modal" id="modal">
|
|||
|
|
<div class="modal-inner">
|
|||
|
|
<button class="modal-close" onclick="closeModal()" aria-label="Schließen">×</button>
|
|||
|
|
<div class="modal-hero" id="modal-hero">
|
|||
|
|
<div class="modal-hero-overlay">
|
|||
|
|
<span class="label">Longform · kind 30023</span>
|
|||
|
|
<h2 id="modal-title">—</h2>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-meta">
|
|||
|
|
<span class="label" id="modal-date">—</span>
|
|||
|
|
<div id="modal-tags-wrap" style="display: flex; gap: 6px; flex-wrap: wrap;"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body" id="modal-body"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<span class="event-id" id="modal-event-id">—</span>
|
|||
|
|
<a href="#" id="modal-external" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size: 10px; padding: 8px 16px;">Auf njump öffnen →</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
/* ========================================================================
|
|||
|
|
* FOERBICO Longform Loader
|
|||
|
|
* Liest alle kind:30023 Events eines npub
|
|||
|
|
* Relays: relay-rpi.edufeed.org + relay.primal.net
|
|||
|
|
* Ohne externe Nostr-Library — reines WebSocket + bech32 decode
|
|||
|
|
* ======================================================================== */
|
|||
|
|
|
|||
|
|
const CONFIG = {
|
|||
|
|
// Default npub — über ?npub=... in der URL überschreibbar
|
|||
|
|
defaultNpub: 'npub1tgftg8kptdrxxg0g3sm3hckuglv3j0uu3way4vylc5qyt0f44m0s3gun6e',
|
|||
|
|
relays: [
|
|||
|
|
'wss://relay-rpi.edufeed.org',
|
|||
|
|
'wss://relay.primal.net'
|
|||
|
|
],
|
|||
|
|
queryTimeoutMs: 7000
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|||
|
|
const activeNpub = urlParams.get('npub') || CONFIG.defaultNpub;
|
|||
|
|
|
|||
|
|
// ---- bech32 (NIP-19) ----
|
|||
|
|
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|||
|
|
|
|||
|
|
function bech32Polymod(values) {
|
|||
|
|
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|||
|
|
let chk = 1;
|
|||
|
|
for (let p = 0; p < values.length; p++) {
|
|||
|
|
const top = chk >> 25;
|
|||
|
|
chk = (chk & 0x1ffffff) << 5 ^ values[p];
|
|||
|
|
for (let i = 0; i < 5; i++) if ((top >> i) & 1) chk ^= GEN[i];
|
|||
|
|
}
|
|||
|
|
return chk;
|
|||
|
|
}
|
|||
|
|
function bech32HrpExpand(hrp) {
|
|||
|
|
const ret = [];
|
|||
|
|
for (let p = 0; p < hrp.length; p++) ret.push(hrp.charCodeAt(p) >> 5);
|
|||
|
|
ret.push(0);
|
|||
|
|
for (let p = 0; p < hrp.length; p++) ret.push(hrp.charCodeAt(p) & 31);
|
|||
|
|
return ret;
|
|||
|
|
}
|
|||
|
|
function bech32VerifyChecksum(hrp, data) {
|
|||
|
|
return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 1;
|
|||
|
|
}
|
|||
|
|
function bech32Decode(bechString) {
|
|||
|
|
const pos = bechString.lastIndexOf('1');
|
|||
|
|
if (pos < 1 || pos + 7 > bechString.length) throw new Error('Invalid bech32');
|
|||
|
|
const hrp = bechString.substring(0, pos).toLowerCase();
|
|||
|
|
const data = [];
|
|||
|
|
for (let p = pos + 1; p < bechString.length; p++) {
|
|||
|
|
const d = CHARSET.indexOf(bechString.charAt(p).toLowerCase());
|
|||
|
|
if (d === -1) throw new Error('Invalid char');
|
|||
|
|
data.push(d);
|
|||
|
|
}
|
|||
|
|
if (!bech32VerifyChecksum(hrp, data)) throw new Error('Invalid checksum');
|
|||
|
|
return { hrp, data: data.slice(0, data.length - 6) };
|
|||
|
|
}
|
|||
|
|
function bech32ConvertBits(data, fromBits, toBits, pad = true) {
|
|||
|
|
let acc = 0, bits = 0;
|
|||
|
|
const ret = [];
|
|||
|
|
const maxv = (1 << toBits) - 1;
|
|||
|
|
for (let p = 0; p < data.length; p++) {
|
|||
|
|
const value = data[p];
|
|||
|
|
if (value < 0 || value >> fromBits !== 0) throw new Error('Invalid value');
|
|||
|
|
acc = (acc << fromBits) | value;
|
|||
|
|
bits += fromBits;
|
|||
|
|
while (bits >= toBits) {
|
|||
|
|
bits -= toBits;
|
|||
|
|
ret.push((acc >> bits) & maxv);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (pad && bits > 0) ret.push((acc << (toBits - bits)) & maxv);
|
|||
|
|
return ret;
|
|||
|
|
}
|
|||
|
|
function bytesToHex(bytes) {
|
|||
|
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|||
|
|
}
|
|||
|
|
function npubToHex(npub) {
|
|||
|
|
const { hrp, data } = bech32Decode(npub);
|
|||
|
|
if (hrp !== 'npub') throw new Error('Not an npub');
|
|||
|
|
return bytesToHex(bech32ConvertBits(data, 5, 8, false));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- State ----
|
|||
|
|
const state = {
|
|||
|
|
events: new Map(),
|
|||
|
|
activeFilter: null
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const el = {
|
|||
|
|
statusDot: document.getElementById('status-dot'),
|
|||
|
|
statusText: document.getElementById('status-text'),
|
|||
|
|
featured: document.getElementById('featured-container'),
|
|||
|
|
grid: document.getElementById('articles-grid'),
|
|||
|
|
count: document.getElementById('article-count'),
|
|||
|
|
visualCount: document.getElementById('visual-count'),
|
|||
|
|
filterBar: document.getElementById('filter-bar'),
|
|||
|
|
modal: document.getElementById('modal'),
|
|||
|
|
modalTitle: document.getElementById('modal-title'),
|
|||
|
|
modalDate: document.getElementById('modal-date'),
|
|||
|
|
modalBody: document.getElementById('modal-body'),
|
|||
|
|
modalHero: document.getElementById('modal-hero'),
|
|||
|
|
modalTags: document.getElementById('modal-tags-wrap'),
|
|||
|
|
modalEventId: document.getElementById('modal-event-id'),
|
|||
|
|
modalExternal: document.getElementById('modal-external'),
|
|||
|
|
npubLink: document.getElementById('npub-link'),
|
|||
|
|
// Stats
|
|||
|
|
statTotal: document.getElementById('stat-total'),
|
|||
|
|
statTotalSub: document.getElementById('stat-total-sub'),
|
|||
|
|
statTags: document.getElementById('stat-tags'),
|
|||
|
|
statFirst: document.getElementById('stat-first'),
|
|||
|
|
statFirstSub: document.getElementById('stat-first-sub'),
|
|||
|
|
statLatest: document.getElementById('stat-latest'),
|
|||
|
|
statLatestSub: document.getElementById('stat-latest-sub'),
|
|||
|
|
tagCloud: document.getElementById('tag-cloud')
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function setStatus(kind, text) {
|
|||
|
|
el.statusDot.className = 'status-dot ' + kind;
|
|||
|
|
el.statusText.textContent = text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- Tech-Toggle ----
|
|||
|
|
function toggleTech() {
|
|||
|
|
const toggle = document.getElementById('tech-toggle');
|
|||
|
|
const content = document.getElementById('tech-content');
|
|||
|
|
const icon = toggle.querySelector('.tech-toggle-icon');
|
|||
|
|
const expanded = content.classList.toggle('expanded');
|
|||
|
|
toggle.classList.toggle('expanded', expanded);
|
|||
|
|
toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|||
|
|
icon.textContent = expanded ? '×' : '+';
|
|||
|
|
}
|
|||
|
|
// Keyboard support for tech toggle
|
|||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|||
|
|
const toggle = document.getElementById('tech-toggle');
|
|||
|
|
if (toggle) {
|
|||
|
|
toggle.addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
toggleTech();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---- Helpers ----
|
|||
|
|
function escapeHtml(str) {
|
|||
|
|
if (str === null || str === undefined) return '';
|
|||
|
|
return String(str)
|
|||
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|||
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|||
|
|
}
|
|||
|
|
function formatDate(timestamp) {
|
|||
|
|
const d = new Date(timestamp * 1000);
|
|||
|
|
return d.toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' });
|
|||
|
|
}
|
|||
|
|
function getTag(event, tagName) {
|
|||
|
|
const tag = event.tags.find(t => t[0] === tagName);
|
|||
|
|
return tag ? tag[1] : null;
|
|||
|
|
}
|
|||
|
|
function getAllTags(event, tagName) {
|
|||
|
|
return event.tags.filter(t => t[0] === tagName).map(t => t[1]).filter(Boolean);
|
|||
|
|
}
|
|||
|
|
function extractFirstImage(markdown) {
|
|||
|
|
if (!markdown) return null;
|
|||
|
|
const mdMatch = markdown.match(/!\[[^\]]*\]\(([^)]+)\)/);
|
|||
|
|
if (mdMatch) return mdMatch[1];
|
|||
|
|
const htmlMatch = markdown.match(/<img[^>]+src=["']([^"']+)["']/i);
|
|||
|
|
if (htmlMatch) return htmlMatch[1];
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
function extractSummary(event) {
|
|||
|
|
const s = getTag(event, 'summary');
|
|||
|
|
if (s && s.trim()) return s;
|
|||
|
|
const text = (event.content || '')
|
|||
|
|
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
|
|||
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|||
|
|
.replace(/[#*_`>]/g, '')
|
|||
|
|
.replace(/\n+/g, ' ').trim();
|
|||
|
|
return text.slice(0, 220) + (text.length > 220 ? '…' : '');
|
|||
|
|
}
|
|||
|
|
function extractTitle(event) {
|
|||
|
|
const t = getTag(event, 'title');
|
|||
|
|
if (t) return t;
|
|||
|
|
const h1 = (event.content || '').match(/^#\s+(.+)$/m);
|
|||
|
|
if (h1) return h1[1];
|
|||
|
|
const h2 = (event.content || '').match(/^##\s+(.+)$/m);
|
|||
|
|
if (h2) return h2[1];
|
|||
|
|
return 'Ohne Titel';
|
|||
|
|
}
|
|||
|
|
function getPublishTimestamp(event) {
|
|||
|
|
// NIP-23: published_at tag is authoritative; created_at is just the signature time
|
|||
|
|
const p = getTag(event, 'published_at');
|
|||
|
|
if (p) {
|
|||
|
|
const n = parseInt(p, 10);
|
|||
|
|
if (!isNaN(n) && n > 0) return n;
|
|||
|
|
}
|
|||
|
|
return event.created_at;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- Rendering ----
|
|||
|
|
function renderFeatured(event) {
|
|||
|
|
const title = extractTitle(event);
|
|||
|
|
const summary = extractSummary(event);
|
|||
|
|
const image = getTag(event, 'image') || extractFirstImage(event.content);
|
|||
|
|
const date = formatDate(getPublishTimestamp(event));
|
|||
|
|
const tags = getAllTags(event, 't').slice(0, 4);
|
|||
|
|
|
|||
|
|
el.featured.innerHTML = `
|
|||
|
|
<div class="featured-card" onclick="openArticle('${event.id}')">
|
|||
|
|
<div class="featured-image">
|
|||
|
|
${image ? `<img src="${escapeHtml(image)}" alt="${escapeHtml(title)}" onerror="this.style.display='none'"/>` : ''}
|
|||
|
|
<div class="featured-image-overlay">
|
|||
|
|
<span class="micro">featured · latest · signed</span>
|
|||
|
|
<span class="micro">event · ${escapeHtml(event.id.slice(0, 16))}···</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="featured-body">
|
|||
|
|
<div>
|
|||
|
|
<div class="featured-meta">
|
|||
|
|
<span class="label-new">Neu</span>
|
|||
|
|
<span class="label" style="color: var(--gray-60);">${date}</span>
|
|||
|
|
</div>
|
|||
|
|
<h3>${escapeHtml(title)}</h3>
|
|||
|
|
<p class="summary">${escapeHtml(summary)}</p>
|
|||
|
|
${tags.length ? `<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 16px;">${tags.map(t => `<span class="tag">#${escapeHtml(t)}</span>`).join('')}</div>` : ''}
|
|||
|
|
</div>
|
|||
|
|
<div class="featured-footer">
|
|||
|
|
<span class="label">Artikel öffnen →</span>
|
|||
|
|
<span class="micro">kind · 30023</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderGrid(events) {
|
|||
|
|
if (events.length === 0) {
|
|||
|
|
el.grid.innerHTML = `
|
|||
|
|
<div class="loading-state" style="grid-column: 1 / -1;">
|
|||
|
|
<span class="label">Leer</span>
|
|||
|
|
<h3 style="margin-top: 12px;">Keine Artikel in dieser Auswahl</h3>
|
|||
|
|
<p>Wechsle den Filter oder entferne ihn, um alle Ressourcen zu sehen.</p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
el.grid.innerHTML = events.map((event, i) => {
|
|||
|
|
const title = extractTitle(event);
|
|||
|
|
const summary = extractSummary(event);
|
|||
|
|
const image = getTag(event, 'image') || extractFirstImage(event.content);
|
|||
|
|
const date = formatDate(getPublishTimestamp(event));
|
|||
|
|
const tags = getAllTags(event, 't').slice(0, 3);
|
|||
|
|
const num = String(i + 1).padStart(3, '0');
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="article-card" onclick="openArticle('${event.id}')">
|
|||
|
|
<div class="article-image ${image ? '' : 'placeholder'}">
|
|||
|
|
${image ? `<img src="${escapeHtml(image)}" alt="${escapeHtml(title)}" onerror="this.parentElement.classList.add('placeholder'); this.remove();"/>` : ''}
|
|||
|
|
<div class="article-image-overlay">
|
|||
|
|
<span class="article-num">№ ${num}</span>
|
|||
|
|
<span class="micro" style="color: var(--blue);">30023</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="article-body">
|
|||
|
|
<div class="article-date">${date}</div>
|
|||
|
|
<h4>${escapeHtml(title)}</h4>
|
|||
|
|
<p>${escapeHtml(summary)}</p>
|
|||
|
|
${tags.length ? `<div class="article-tags">${tags.map(t => `<span class="tag">#${escapeHtml(t)}</span>`).join('')}</div>` : ''}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderFilters() {
|
|||
|
|
const tagCounts = {};
|
|||
|
|
for (const event of state.events.values()) {
|
|||
|
|
getAllTags(event, 't').forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; });
|
|||
|
|
}
|
|||
|
|
const sorted = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|||
|
|
|
|||
|
|
if (sorted.length === 0) {
|
|||
|
|
el.filterBar.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
el.filterBar.style.display = 'flex';
|
|||
|
|
|
|||
|
|
el.filterBar.innerHTML = `
|
|||
|
|
<div class="filter-chip ${state.activeFilter === null ? 'active' : ''}" onclick="setFilter(null)">
|
|||
|
|
Alle · ${state.events.size}
|
|||
|
|
</div>
|
|||
|
|
${sorted.map(([tag, count]) => `
|
|||
|
|
<div class="filter-chip ${state.activeFilter === tag ? 'active' : ''}" onclick="setFilter('${escapeHtml(tag)}')">
|
|||
|
|
#${escapeHtml(tag)} · ${count}
|
|||
|
|
</div>
|
|||
|
|
`).join('')}
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setFilter(tag) {
|
|||
|
|
state.activeFilter = tag;
|
|||
|
|
renderFilters();
|
|||
|
|
updateAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderStats() {
|
|||
|
|
const all = Array.from(state.events.values());
|
|||
|
|
const total = all.length;
|
|||
|
|
|
|||
|
|
// Total + sub
|
|||
|
|
el.statTotal.textContent = String(total).padStart(2, '0');
|
|||
|
|
el.statTotalSub.textContent = total === 1 ? '1 longform · kind 30023' : `${total} longforms · kind 30023`;
|
|||
|
|
|
|||
|
|
// Tags
|
|||
|
|
const tagCounts = {};
|
|||
|
|
for (const event of all) {
|
|||
|
|
getAllTags(event, 't').forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; });
|
|||
|
|
}
|
|||
|
|
const uniqueTags = Object.keys(tagCounts).length;
|
|||
|
|
el.statTags.textContent = String(uniqueTags).padStart(2, '0');
|
|||
|
|
|
|||
|
|
// First / Latest
|
|||
|
|
if (all.length > 0) {
|
|||
|
|
const sorted = all.slice().sort((a, b) => getPublishTimestamp(a) - getPublishTimestamp(b));
|
|||
|
|
const first = sorted[0];
|
|||
|
|
const latest = sorted[sorted.length - 1];
|
|||
|
|
const firstDate = new Date(getPublishTimestamp(first) * 1000);
|
|||
|
|
const latestDate = new Date(getPublishTimestamp(latest) * 1000);
|
|||
|
|
|
|||
|
|
el.statFirst.textContent = firstDate.getFullYear();
|
|||
|
|
el.statFirstSub.textContent = firstDate.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' });
|
|||
|
|
el.statLatest.textContent = latestDate.getFullYear();
|
|||
|
|
el.statLatestSub.textContent = latestDate.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Tag cloud
|
|||
|
|
if (uniqueTags === 0) {
|
|||
|
|
el.tagCloud.innerHTML = '<span class="tag-cloud-tag size-2" style="color: var(--gray-60);">noch keine tags</span>';
|
|||
|
|
} else {
|
|||
|
|
const sortedTags = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 30);
|
|||
|
|
const max = sortedTags[0][1];
|
|||
|
|
const min = sortedTags[sortedTags.length - 1][1];
|
|||
|
|
const range = Math.max(1, max - min);
|
|||
|
|
el.tagCloud.innerHTML = sortedTags.map(([tag, count]) => {
|
|||
|
|
const norm = (count - min) / range; // 0..1
|
|||
|
|
const size = Math.min(5, Math.max(1, Math.ceil(norm * 5))) || 1;
|
|||
|
|
return `<span class="tag-cloud-tag size-${size}" onclick="setFilter('${escapeHtml(tag)}'); document.getElementById('articles').scrollIntoView({behavior: 'smooth'});" title="${count} ressource${count !== 1 ? 'n' : ''}">#${escapeHtml(tag)}</span>`;
|
|||
|
|
}).join('');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateAll() {
|
|||
|
|
const all = Array.from(state.events.values())
|
|||
|
|
.sort((a, b) => getPublishTimestamp(b) - getPublishTimestamp(a));
|
|||
|
|
|
|||
|
|
if (all.length > 0) renderFeatured(all[0]);
|
|||
|
|
|
|||
|
|
const remaining = all.slice(1);
|
|||
|
|
const filtered = state.activeFilter
|
|||
|
|
? remaining.filter(e => getAllTags(e, 't').includes(state.activeFilter))
|
|||
|
|
: remaining;
|
|||
|
|
renderGrid(filtered);
|
|||
|
|
|
|||
|
|
el.count.textContent = String(all.length).padStart(2, '0');
|
|||
|
|
el.visualCount.textContent = `№ ${String(all.length).padStart(4, '0')} / 30023`;
|
|||
|
|
renderFilters();
|
|||
|
|
renderStats();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- Modal ----
|
|||
|
|
function openArticle(eventId) {
|
|||
|
|
const event = Array.from(state.events.values()).find(e => e.id === eventId);
|
|||
|
|
if (!event) return;
|
|||
|
|
|
|||
|
|
const title = extractTitle(event);
|
|||
|
|
const image = getTag(event, 'image') || extractFirstImage(event.content);
|
|||
|
|
const date = formatDate(getPublishTimestamp(event));
|
|||
|
|
const tags = getAllTags(event, 't');
|
|||
|
|
|
|||
|
|
el.modalTitle.textContent = title;
|
|||
|
|
el.modalDate.textContent = date;
|
|||
|
|
|
|||
|
|
el.modalHero.innerHTML = `
|
|||
|
|
${image ? `<img src="${escapeHtml(image)}" alt="${escapeHtml(title)}" onerror="this.remove();"/>` : ''}
|
|||
|
|
<div class="modal-hero-overlay">
|
|||
|
|
<span class="label">Longform · kind 30023</span>
|
|||
|
|
<h2>${escapeHtml(title)}</h2>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
el.modalTags.innerHTML = tags.slice(0, 8).map(t => `<span class="tag">#${escapeHtml(t)}</span>`).join('');
|
|||
|
|
|
|||
|
|
if (typeof marked !== 'undefined') {
|
|||
|
|
marked.setOptions({ breaks: true, gfm: true });
|
|||
|
|
el.modalBody.innerHTML = marked.parse(event.content || '');
|
|||
|
|
} else {
|
|||
|
|
el.modalBody.textContent = event.content || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
el.modalEventId.textContent = event.id;
|
|||
|
|
el.modalExternal.href = 'https://njump.me/' + event.id;
|
|||
|
|
|
|||
|
|
el.modal.classList.add('active');
|
|||
|
|
document.body.style.overflow = 'hidden';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
el.modal.classList.remove('active');
|
|||
|
|
document.body.style.overflow = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
el.modal.addEventListener('click', (e) => {
|
|||
|
|
if (e.target === el.modal) closeModal();
|
|||
|
|
});
|
|||
|
|
document.addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Escape') closeModal();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---- Relay Query ----
|
|||
|
|
function queryRelay(url, pubkeyHex) {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
let ws;
|
|||
|
|
const subId = 'foerbico-' + Math.random().toString(36).slice(2, 10);
|
|||
|
|
let settled = false;
|
|||
|
|
|
|||
|
|
const cleanup = () => {
|
|||
|
|
if (settled) return;
|
|||
|
|
settled = true;
|
|||
|
|
try { ws && ws.close(); } catch (e) {}
|
|||
|
|
resolve();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
try { ws = new WebSocket(url); }
|
|||
|
|
catch (e) { console.warn('Relay-Fehler:', url, e); return cleanup(); }
|
|||
|
|
|
|||
|
|
const timeout = setTimeout(cleanup, CONFIG.queryTimeoutMs);
|
|||
|
|
|
|||
|
|
ws.onopen = () => {
|
|||
|
|
ws.send(JSON.stringify(['REQ', subId, {
|
|||
|
|
authors: [pubkeyHex],
|
|||
|
|
kinds: [30023],
|
|||
|
|
limit: 100
|
|||
|
|
}]));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
ws.onmessage = (msg) => {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(msg.data);
|
|||
|
|
if (data[0] === 'EVENT' && data[1] === subId) {
|
|||
|
|
ingestEvent(data[2]);
|
|||
|
|
} else if (data[0] === 'EOSE' && data[1] === subId) {
|
|||
|
|
clearTimeout(timeout);
|
|||
|
|
cleanup();
|
|||
|
|
}
|
|||
|
|
} catch (e) { console.warn('Parse-Fehler:', e); }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
ws.onerror = () => { clearTimeout(timeout); cleanup(); };
|
|||
|
|
ws.onclose = () => { clearTimeout(timeout); cleanup(); };
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ingestEvent(event) {
|
|||
|
|
// Replaceable events (NIP-33): Key = kind:pubkey:dTag
|
|||
|
|
const dTag = getTag(event, 'd') || event.id;
|
|||
|
|
const key = `${event.kind}:${event.pubkey}:${dTag}`;
|
|||
|
|
const existing = state.events.get(key);
|
|||
|
|
if (!existing || event.created_at > existing.created_at) {
|
|||
|
|
state.events.set(key, event);
|
|||
|
|
if (window._renderTimeout) clearTimeout(window._renderTimeout);
|
|||
|
|
window._renderTimeout = setTimeout(updateAll, 150);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- Init ----
|
|||
|
|
async function init() {
|
|||
|
|
if (el.npubLink) el.npubLink.href = `https://njump.me/${activeNpub}`;
|
|||
|
|
|
|||
|
|
let pubkeyHex;
|
|||
|
|
try {
|
|||
|
|
pubkeyHex = npubToHex(activeNpub);
|
|||
|
|
} catch (e) {
|
|||
|
|
setStatus('error', 'Ungültiger npub');
|
|||
|
|
el.featured.innerHTML = `
|
|||
|
|
<div class="loading-state">
|
|||
|
|
<span class="label">Fehler</span>
|
|||
|
|
<h3 style="margin-top: 12px;">Ungültiger npub</h3>
|
|||
|
|
<p>Der angegebene npub <code>${escapeHtml(activeNpub)}</code> konnte nicht dekodiert werden. Nutze den URL-Parameter <code>?npub=npub1...</code></p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
el.grid.innerHTML = '';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setStatus('connecting', `Verbinde · ${CONFIG.relays.length} relays`);
|
|||
|
|
|
|||
|
|
await Promise.all(CONFIG.relays.map(url => queryRelay(url, pubkeyHex)));
|
|||
|
|
|
|||
|
|
if (state.events.size === 0) {
|
|||
|
|
setStatus('error', 'Keine Events');
|
|||
|
|
el.featured.innerHTML = `
|
|||
|
|
<div class="loading-state">
|
|||
|
|
<span class="label">Kein Inhalt gefunden</span>
|
|||
|
|
<h3 style="margin-top: 12px;">Keine Longform-Notes</h3>
|
|||
|
|
<p>Für den npub <code>${escapeHtml(activeNpub.slice(0, 20))}…</code> wurden auf den konfigurierten Relays keine Events vom Typ <code>kind:30023</code> gefunden.</p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
el.grid.innerHTML = '';
|
|||
|
|
el.count.textContent = '00';
|
|||
|
|
el.visualCount.textContent = '№ 0000 / 30023';
|
|||
|
|
} else {
|
|||
|
|
setStatus('connected', `${state.events.size} artikel · live`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init();
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
</body>
|
|||
|
|
</html>
|