fOERbico/docs/foerbico-landing_draft3.html

2392 lines
83 KiB
HTML
Raw Normal View History

2026-05-05 18:24:45 +02:00
<!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);
}
2026-05-05 18:46:14 +02:00
.logo .oer { color: var(--orange); }
2026-05-05 18:24:45 +02:00
.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;
}
2026-05-05 18:46:14 +02:00
/* COMMUNITIES — Verbundpartner */
.communities { padding: 100px 0; border-top: 1px solid var(--gray-10); background: var(--paper); }
.partner-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.partner-card {
background: var(--white);
border: 1px solid var(--gray-10);
border-radius: var(--radius);
padding: 32px 28px;
transition: var(--ease);
display: flex; flex-direction: column;
text-decoration: none;
color: var(--ink);
}
.partner-card:hover {
border-color: var(--blue);
transform: translateY(-2px);
}
.partner-card .partner-logo {
height: 56px; margin-bottom: 24px;
display: flex; align-items: center;
}
.partner-card .partner-logo img {
max-height: 56px; max-width: 200px;
width: auto; height: auto;
object-fit: contain;
}
.partner-card .label {
color: var(--orange); margin-bottom: 12px;
font-size: 9px; letter-spacing: 0.2em; display: block;
}
.partner-card h4 {
font-size: 22px; font-weight: 500;
margin-bottom: 14px; color: var(--ink);
text-transform: none; letter-spacing: 0;
font-family: 'Roboto Condensed', sans-serif;
line-height: 1.2;
}
.partner-card .partner-desc {
font-size: 13px; line-height: 1.6;
color: var(--gray-60); font-weight: 300;
margin-bottom: 20px; flex: 1;
}
.partner-people {
border-top: 1px dashed var(--gray-10);
padding-top: 16px;
display: flex; flex-direction: column; gap: 6px;
}
.partner-people .label-people {
font-family: 'Roboto Condensed', sans-serif;
font-size: 9px; font-weight: 500;
letter-spacing: 0.22em; text-transform: uppercase;
color: var(--blue); margin-bottom: 6px;
}
.partner-person {
font-size: 13px; color: var(--ink);
line-height: 1.4;
}
.partner-person .role {
font-family: 'JetBrains Mono', monospace;
font-size: 10px; color: var(--gray-60);
display: block; margin-top: 1px;
}
.partner-link {
font-family: 'JetBrains Mono', monospace;
font-size: 10px; color: var(--blue);
margin-top: 16px;
overflow: hidden; text-overflow: ellipsis;
white-space: nowrap;
}
2026-05-05 18:24:45 +02:00
/* 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); }
2026-05-05 18:46:14 +02:00
.partner-grid { grid-template-columns: 1fr; }
2026-05-05 18:24:45 +02:00
.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">
2026-05-05 18:46:14 +02:00
<div class="logo">F<span class="oer">OER</span>BICO</div>
2026-05-05 18:24:45 +02:00
<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 · 20242027</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>
comeniusinstitut<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>
2026-05-05 18:46:14 +02:00
<!-- COMMUNITIES — Verbundpartner -->
2026-05-05 18:24:45 +02:00
<section class="communities section-wrap" id="communities">
2026-05-05 18:46:14 +02:00
<span class="corner-micro-tl">04 — verbundpartner</span>
<span class="corner-micro-tr">bmbfsfj-verbund · 20242027</span>
2026-05-05 18:24:45 +02:00
<div class="container">
<div class="section-head">
<div>
<div class="label" style="margin-bottom: 24px;">04 — Im Verbund</div>
2026-05-05 18:46:14 +02:00
<h2>Wer FOERBICO <span class="accent">trägt</span>.</h2>
2026-05-05 18:24:45 +02:00
</div>
<p class="section-head-right">
2026-05-05 18:46:14 +02:00
FOERBICO wird als Verbund von drei Institutionen umgesetzt: dem Comenius-Institut als Projektträger
und zwei religionspädagogischen Lehrstühlen für die wissenschaftliche Begleitung. Mehr zu den Personen
gibt es auf <a href="https://oer.community/unser-team/" target="_blank" rel="noopener" style="color: var(--blue); text-decoration: underline;">der Team-Seite</a>.
2026-05-05 18:24:45 +02:00
</p>
</div>
2026-05-05 18:46:14 +02:00
<div class="partner-grid">
<a href="https://comenius.de/" target="_blank" rel="noopener" class="partner-card">
<div class="partner-logo">
<img src="https://oer.community/hello-world/comenius-institut-logo.png" alt="Comenius-Institut" />
</div>
<span class="label">Projektträger</span>
2026-05-05 18:24:45 +02:00
<h4>Comenius-Institut</h4>
2026-05-05 18:46:14 +02:00
<p class="partner-desc">
Evangelische Arbeitsstätte für Erziehungswissenschaft. Verantwortet FOERBICO,
koordiniert Community-Aufbau und technische Umsetzung.
</p>
<div class="partner-people">
<span class="label-people">Team</span>
<span class="partner-person">Dr. Jens Dechow<span class="role">Direktor · Projektleitung</span></span>
<span class="partner-person">Jörg Lohrer<span class="role">Projektkoordination</span></span>
<span class="partner-person">Gina Buchwald-Chassée<span class="role">Community · Öffentlichkeitsarbeit</span></span>
<span class="partner-person">Ludger Sicking<span class="role">Technische Umsetzung</span></span>
</div>
<span class="partner-link">comenius.de</span>
2026-05-05 18:24:45 +02:00
</a>
2026-05-05 18:46:14 +02:00
<a href="https://www.uni-frankfurt.de/78330411/Professur_f%C3%BCr_Religionsp%C3%A4dagogik_und_Mediendidaktik" target="_blank" rel="noopener" class="partner-card">
<div class="partner-logo">
<img src="https://oer.community/hello-world/Goethe-Universitaet_Frankfurt_Logo.png" alt="Goethe-Universität Frankfurt" />
</div>
<span class="label">Verbundpartner</span>
<h4>Goethe-Universität Frankfurt</h4>
<p class="partner-desc">
Professur für Religionspädagogik und Mediendidaktik. Forschung zu religiöser Bildung
und Mediendidaktik, Vernetzung in die Wissenschaft.
</p>
<div class="partner-people">
<span class="label-people">Team</span>
<span class="partner-person">Prof. Dr. Viera Pirker<span class="role">Lehrstuhlinhaberin</span></span>
<span class="partner-person">Dr. Laura Mößle<span class="role">Wiss. Mitarbeiterin</span></span>
<span class="partner-person">Paula Paschke<span class="role">Wiss. Mitarbeiterin (Vertretung)</span></span>
</div>
<span class="partner-link">uni-frankfurt.de · Professur RPMD</span>
2026-05-05 18:24:45 +02:00
</a>
2026-05-05 18:46:14 +02:00
<a href="https://www.evrel.phil.fau.de/" target="_blank" rel="noopener" class="partner-card">
<div class="partner-logo">
<img src="https://oer.community/hello-world/Friedrich-Alexander-Universitaet_Erlangen-Nuernberg_Logo.png" alt="FAU Erlangen-Nürnberg" />
</div>
<span class="label">Verbundpartner</span>
<h4>FAU Erlangen-Nürnberg</h4>
<p class="partner-desc">
Lehrstuhl für Religionspädagogik und Didaktik des Evangelischen Religionsunterrichts.
Empirische Begleitforschung des Projekts.
</p>
<div class="partner-people">
<span class="label-people">Team</span>
<span class="partner-person">Prof. Dr. Manfred Pirner<span class="role">Lehrstuhlinhaber</span></span>
<span class="partner-person">Phillip Angelina<span class="role">Empirische Begleitforschung</span></span>
</div>
<span class="partner-link">evrel.phil.fau.de</span>
2026-05-05 18:24:45 +02:00
</a>
2026-05-05 18:46:14 +02:00
2026-05-05 18:24:45 +02:00
</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">
2026-05-05 18:46:14 +02:00
<div class="logo">F<span class="oer">OER</span>BICO</div>
<p>Community of Communities — Vernetzung religionsbezogener Communities für offene Bildungsmaterialien. Verbundprojekt von Comenius-Institut, Goethe-Universität Frankfurt und FAU Erlangen-Nürnberg.</p>
2026-05-05 18:24:45 +02:00
</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>
2026-05-05 18:46:14 +02:00
<li><a href="https://oer.community/blog/" target="_blank" rel="noopener">Blog</a></li>
2026-05-05 18:24:45 +02:00
</ul>
</div>
<div class="footer-col">
2026-05-05 18:46:14 +02:00
<h5>Verbund</h5>
2026-05-05 18:24:45 +02:00
<ul>
2026-05-05 18:46:14 +02:00
<li><a href="https://comenius.de" target="_blank" rel="noopener">Comenius-Institut</a></li>
<li><a href="https://www.uni-frankfurt.de/78330411/Professur_f%C3%BCr_Religionsp%C3%A4dagogik_und_Mediendidaktik" target="_blank" rel="noopener">Goethe-Uni Frankfurt</a></li>
<li><a href="https://www.evrel.phil.fau.de/" target="_blank" rel="noopener">FAU Erlangen-Nürnberg</a></li>
2026-05-05 18:24:45 +02:00
</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>
2026-05-05 18:46:14 +02:00
<li><a href="https://oer.community/impressum/" target="_blank" rel="noopener">Impressum</a></li>
<li><a href="https://oer.community/datenschutz/" target="_blank" rel="noopener">Datenschutz</a></li>
2026-05-05 18:24:45 +02:00
</ul>
</div>
</div>
<div class="footer-bottom">
2026-05-05 18:46:14 +02:00
<span class="micro">cc by 4.0 · 20242026 foerbico · soweit nicht anders angegeben</span>
2026-05-05 18:24:45 +02:00
<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
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>