559 lines
21 KiB
HTML
559 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<link rel="icon" href="images/generated_two.png">
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Portfolio</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=Instrument+Serif:ital@0;1&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root {
|
||
--bg: #fafaf8;
|
||
--surface: #f3f3f0;
|
||
--border: #e5e5e0;
|
||
--text: #1a1a18;
|
||
--muted: #999990;
|
||
--serif: 'Instrument Serif', Georgia, serif;
|
||
--sans: 'DM Sans', system-ui, sans-serif;
|
||
--nav-w: 230px;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root { --bg:#141412; --surface:#1c1c19; --border:#2a2a26; --text:#f0f0ec; --muted:#686860; }
|
||
}
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html { scroll-behavior: smooth; }
|
||
body {
|
||
background: var(--bg); color: var(--text);
|
||
font-family: var(--sans); font-size: 15px;
|
||
font-weight: 300; line-height: 1.7;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* ── SIDEBAR ─────────────────────────────────────── */
|
||
.sidebar {
|
||
position: fixed; top: 0; left: 0;
|
||
width: var(--nav-w); height: 100vh;
|
||
border-right: 1px solid var(--border);
|
||
display: flex; flex-direction: column;
|
||
z-index: 100; background: var(--bg);
|
||
}
|
||
.sidebar-top {
|
||
padding: 36px 32px 28px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.sidebar-name {
|
||
font-family: var(--serif); font-size: 1.1rem;
|
||
color: var(--text); text-decoration: none; display: block;
|
||
margin-bottom: 4px;
|
||
}
|
||
.sidebar-sub {
|
||
font-size: 0.75rem; color: var(--muted);
|
||
}
|
||
.sidebar-nav {
|
||
flex: 1; overflow-y: auto; padding: 20px 0;
|
||
}
|
||
.nav-group {
|
||
margin-bottom: 4px;
|
||
}
|
||
.nav-group-label {
|
||
font-size: 0.65rem; font-weight: 500;
|
||
letter-spacing: 0.12em; text-transform: uppercase;
|
||
color: var(--muted); padding: 16px 32px 6px;
|
||
display: block;
|
||
}
|
||
.nav-link {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 6px 32px; font-size: 0.84rem; font-weight: 400;
|
||
color: var(--muted); text-decoration: none;
|
||
transition: color 0.15s; position: relative;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.nav-link::before {
|
||
content: ''; position: absolute; left: 0; top: 50%;
|
||
transform: translateY(-50%); width: 2px; height: 0;
|
||
background: var(--text); border-radius: 0 2px 2px 0;
|
||
transition: height 0.18s;
|
||
}
|
||
.nav-link:hover { color: var(--text); }
|
||
.nav-link.active { color: var(--text); font-weight: 500; }
|
||
.nav-link.active::before { height: 14px; }
|
||
.nav-link .idx {
|
||
font-size: 0.68rem; color: var(--muted);
|
||
min-width: 18px; flex-shrink: 0;
|
||
}
|
||
.sidebar-bottom {
|
||
padding: 20px 32px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
}
|
||
.sidebar-bottom a {
|
||
font-size: 0.76rem; color: var(--muted);
|
||
text-decoration: none; transition: color 0.15s;
|
||
}
|
||
.sidebar-bottom a:hover { color: var(--text); }
|
||
|
||
/* ── MAIN ────────────────────────────────────────── */
|
||
.main { margin-left: var(--nav-w); }
|
||
|
||
/* ── EACH PROJECT SECTION ────────────────────────── */
|
||
.project-section {
|
||
padding: 96px 80px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
/* Section header */
|
||
.section-head {
|
||
margin-bottom: 56px;
|
||
}
|
||
.section-kicker {
|
||
font-size: 0.7rem; font-weight: 500;
|
||
letter-spacing: 0.12em; text-transform: uppercase;
|
||
color: var(--muted); margin-bottom: 14px;
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.section-kicker::before {
|
||
content: ''; display: inline-block;
|
||
width: 24px; height: 1px; background: var(--muted);
|
||
}
|
||
.section-title {
|
||
font-family: var(--serif);
|
||
font-size: clamp(2rem, 3.5vw, 3rem);
|
||
font-weight: 400; letter-spacing: -0.02em; line-height: 1.12;
|
||
margin-bottom: 18px;
|
||
}
|
||
.section-desc {
|
||
max-width: 560px; color: var(--muted);
|
||
font-size: 0.94rem; line-height: 1.8;
|
||
}
|
||
|
||
/* Tags + meta row */
|
||
.meta-row {
|
||
display: flex; align-items: center;
|
||
gap: 20px; flex-wrap: wrap; margin-top: 20px;
|
||
}
|
||
.tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.tag {
|
||
font-size: 0.71rem; color: var(--muted);
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
padding: 3px 10px; border-radius: 3px;
|
||
}
|
||
.stat {
|
||
font-size: 0.78rem; color: var(--muted);
|
||
}
|
||
.status-dot {
|
||
display: inline-block; width: 6px; height: 6px;
|
||
border-radius: 50%; background: currentColor;
|
||
vertical-align: middle; margin-right: 4px;
|
||
}
|
||
.status-dot.active { color: #3a7d44; }
|
||
.status-dot.archived { color: var(--muted); opacity: .5; }
|
||
|
||
/* Content blocks (alternating) */
|
||
.content-blocks {
|
||
display: flex; flex-direction: column; gap: 0;
|
||
}
|
||
|
||
.content-block {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
/* even blocks: flip columns */
|
||
.content-block.flip {
|
||
direction: rtl;
|
||
}
|
||
.content-block.flip > * { direction: ltr; }
|
||
|
||
.block-image {
|
||
aspect-ratio: 4/3;
|
||
background: var(--surface);
|
||
border-right: 1px solid var(--border);
|
||
display: flex; align-items: center; justify-content: center;
|
||
overflow: hidden; position: relative;
|
||
}
|
||
.content-block.flip .block-image {
|
||
border-right: none;
|
||
border-left: 1px solid var(--border);
|
||
}
|
||
|
||
/* Placeholder visuals (replace with real <img> tags) */
|
||
.block-image img {
|
||
width: 100%; height: 100%; object-fit: cover; display: block;
|
||
}
|
||
.placeholder {
|
||
width: 100%; height: 100%;
|
||
display: flex; flex-direction: column;
|
||
align-items: center; justify-content: center;
|
||
gap: 10px; color: var(--muted);
|
||
}
|
||
.placeholder-icon { font-size: 2.4rem; line-height: 1; }
|
||
.placeholder-label { font-size: 0.72rem; letter-spacing: 0.06em; }
|
||
|
||
.block-text {
|
||
padding: 44px 48px;
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
}
|
||
.block-label {
|
||
font-size: 0.68rem; font-weight: 500;
|
||
letter-spacing: 0.1em; text-transform: uppercase;
|
||
color: var(--muted); margin-bottom: 16px;
|
||
}
|
||
.block-text h3 {
|
||
font-family: var(--serif); font-size: 1.25rem;
|
||
font-weight: 400; letter-spacing: -0.01em;
|
||
line-height: 1.3; margin-bottom: 14px;
|
||
}
|
||
.block-text p {
|
||
font-size: 0.88rem; color: var(--muted);
|
||
line-height: 1.8;
|
||
}
|
||
|
||
/* Links row */
|
||
.links-row {
|
||
display: flex; gap: 20px; margin-top: 40px;
|
||
}
|
||
.text-link {
|
||
font-size: 0.84rem; font-weight: 500; color: var(--text);
|
||
text-decoration: none; border-bottom: 1px solid var(--border);
|
||
padding-bottom: 2px; transition: border-color 0.15s;
|
||
}
|
||
.text-link:hover { border-color: var(--text); }
|
||
.text-link.ghost { color: var(--muted); }
|
||
|
||
/* ── HERO ────────────────────────────────────────── */
|
||
.hero-section {
|
||
padding: 100px 80px 80px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.hero-eyebrow {
|
||
font-size: 0.7rem; font-weight: 500;
|
||
letter-spacing: 0.12em; text-transform: uppercase;
|
||
color: var(--muted); margin-bottom: 24px;
|
||
}
|
||
.hero-section h1 {
|
||
font-family: var(--serif);
|
||
font-size: clamp(2.8rem, 5vw, 4.2rem);
|
||
font-weight: 400; letter-spacing: -0.025em;
|
||
line-height: 1.1; margin-bottom: 28px; max-width: 640px;
|
||
}
|
||
.hero-section h1 em { font-style: italic; color: var(--muted); }
|
||
.hero-desc {
|
||
max-width: 460px; color: var(--muted);
|
||
font-size: 0.94rem; line-height: 1.8; margin-bottom: 40px;
|
||
}
|
||
.hero-links { display: flex; gap: 24px; flex-wrap: wrap; }
|
||
|
||
/* ── MOBILE ──────────────────────────────────────── */
|
||
.mobile-bar {
|
||
display: none; position: sticky; top: 0; z-index: 100;
|
||
background: var(--bg); border-bottom: 1px solid var(--border);
|
||
padding: 14px 20px; align-items: center; justify-content: space-between;
|
||
}
|
||
.mobile-logo { font-family: var(--serif); font-size: 1rem; color: var(--text); text-decoration: none; }
|
||
.mobile-btn { background: none; border: none; cursor: pointer; color: var(--muted); font-family: var(--sans); font-size: 0.85rem; padding: 4px; }
|
||
.mobile-drawer {
|
||
display: none; position: fixed; inset: 0; z-index: 200;
|
||
background: var(--bg); overflow-y: auto;
|
||
padding: 24px 0; flex-direction: column;
|
||
}
|
||
.mobile-drawer.open { display: flex; }
|
||
.mobile-drawer-head {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 0 24px 24px; border-bottom: 1px solid var(--border); margin-bottom: 16px;
|
||
}
|
||
.mobile-drawer-head span { font-family: var(--serif); font-size: 1rem; }
|
||
.mobile-drawer a {
|
||
display: block; padding: 10px 24px;
|
||
font-size: 0.9rem; color: var(--muted); text-decoration: none;
|
||
}
|
||
.mobile-drawer a:hover { color: var(--text); }
|
||
|
||
@media (max-width: 900px) {
|
||
.sidebar { display: none; }
|
||
.main { margin-left: 0; }
|
||
.mobile-bar { display: flex; }
|
||
.hero-section, .project-section { padding: 56px 24px; }
|
||
.content-block { grid-template-columns: 1fr; }
|
||
.content-block.flip { direction: ltr; }
|
||
.block-image { aspect-ratio: 16/9; border-right: none; border-bottom: 1px solid var(--border); }
|
||
.content-block.flip .block-image { border-left: none; border-bottom: 1px solid var(--border); }
|
||
.block-text { padding: 28px 24px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- MOBILE BAR -->
|
||
<div class="mobile-bar">
|
||
<a class="mobile-logo" href="#hero">Marvin Kraußer</a>
|
||
<button class="mobile-btn" onclick="openDrawer()">Menü ↓</button>
|
||
</div>
|
||
<div class="mobile-drawer" id="drawer">
|
||
<div class="mobile-drawer-head">
|
||
<span>Navigation</span>
|
||
<button class="mobile-btn" onclick="closeDrawer()">✕ Schließen</button>
|
||
</div>
|
||
<div id="drawer-links"></div>
|
||
</div>
|
||
|
||
<!-- SIDEBAR -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-top">
|
||
<a class="sidebar-name" href="#hero">Marvin Kraußer</a>
|
||
<span class="sidebar-sub">Student B.Sc.<br>Medieninformatik<br>Universität Ulm</span>
|
||
</div>
|
||
<nav class="sidebar-nav" id="sidebar-nav"></nav>
|
||
<div class="sidebar-bottom">
|
||
<a href="https://github.com/deinname" target="_blank">github.com/marvinkrausser ↗</a>
|
||
<a href="mailto:dein@email.de">marvin.krausser277@gmail.com</a>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- MAIN -->
|
||
<main class="main">
|
||
|
||
<!-- HERO -->
|
||
<section id="hero" data-nav="Start" class="hero-section">
|
||
<h1>Marvin Kraußer</h1>
|
||
<p>Ich studiere Medieninformatik im Bachelor und habe im Rahmen meines Studiums praktische Erfahrungen in verschiedenen Projekten gesammelt. Dabei habe ich mich unter anderem mit Backend-Entwicklung, Cybersecurity und Machine Learning beschäftigt und arbeite gerne im Team an neuen Lösungen.</p>
|
||
<div class="hero-links">
|
||
<a class="text-link" href="https://github.com/marvinkrausser" target="_blank">GitHub ↗</a>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- PROJEKTE werden per JS eingefügt -->
|
||
<div id="projects-root"></div>
|
||
|
||
</main>
|
||
|
||
|
||
<script>
|
||
/* ══════════════════════════════════════════════════════
|
||
DATEN
|
||
Jedes Projekt hat:
|
||
- id, title, description, tags, stars, forks, status,
|
||
github, demo
|
||
- blocks[]: Array von { label, heading, text, image }
|
||
image kann ein URL-String sein oder null (= Platzhalter)
|
||
══════════════════════════════════════════════════════ */
|
||
const PROJECTS = [
|
||
{
|
||
id: "pixelcnn",
|
||
title: "PixelCNN",
|
||
description: "Deeplearning Bildgenerierungsmodell auf PixelCnn Struktur mit PyTorch",
|
||
tags: ["Python", "PyTorch"],
|
||
github: "https://github.com/MarvinKrausser/pixelcnn",
|
||
blocks: [
|
||
{
|
||
label: "",
|
||
heading: "Bildgenerierung mit DeepLearning",
|
||
text: "Das Modell verwendet Convolutional Neural Networks und Deep Learning zur Generierung von Bildern. Als Trainingsdatensatz wurden die MNIST-Digits verwendet. Zusätzlich kann das Modell gezielt mit Klassen als Eingabe gesteuert werden, wie im Beispiel mit den Ziffern von 0 bis 9 gezeigt.",
|
||
image: "images/mnist_conditioned_gen_results_cropped.png",
|
||
},
|
||
{
|
||
label: "",
|
||
heading: "Aufteilung der Daten",
|
||
text: "Zur Vermeidung sogenannter Blindspots werden die Daten in einen horizontalen und einen vertikalen Stack aufgeteilt. Diese arbeiten mit unterschiedlichen Maskierungen und führen ihre Informationen am Ende wieder zusammen.",
|
||
image: "images/model_stacks.png",
|
||
},
|
||
{
|
||
label: "",
|
||
heading: "Seminararbeit",
|
||
text: "Das Projekt entstand im Rahmen einer begleitenden Seminararbeit. Sowohl die Ausarbeitung als auch die Präsentation sind im GitHub-Repository enthalten und beschreiben die Architektur und Funktionsweise des Modells im Detail.",
|
||
image: "images/seminararbeit.png",
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "boardgame",
|
||
title: "Online Multyplayer Game",
|
||
description: "Ein online multyplayer Brettspiel mit Server, Spielclient und KI-Spieler entstanden als Gruppenarbeit einer Vorlesung.",
|
||
tags: ["C#", ".net", "Unity"],
|
||
github: "https://github.com/MarvinKrausser/BoardgameServer",
|
||
blocks: [
|
||
{
|
||
label: "",
|
||
heading: "Client",
|
||
text: "Der Game Client wurde mit Unity und C# entwickelt und basiert auf einem rundenbasierten Bewegungs- und Kartensystem. Sämtliche Designs wurden eigenständig erstellt. Ein Benutzer- sowie ein Entwicklerhandbuch sind im GitHub-Repository enthalten.",
|
||
image: "images/boardgame_demo.png",
|
||
},
|
||
{
|
||
label: "",
|
||
heading: "Server",
|
||
text: "Der Server wurde in C# mit einer mehrschichtigen Architektur umgesetzt, die eine klare Trennung zwischen WebSocket-Verbindungsmanagement und spielbezogener Logik (z. B. Spielerzuweisung und Sitzungsverwaltung) ermöglicht. Die Kommunikation zwischen Server und Client wurde durch ein definiertes JSON-Schema standardisiert, um die Kompatibilität und Austauschbarkeit der von verschiedenen Entwicklerteams entwickelten Komponenten sicherzustellen.",
|
||
image: "images/boardgame_connection.png",
|
||
},
|
||
{
|
||
label: "",
|
||
heading: "KI-Spieler",
|
||
text: "Zusätzlich wurden mehrere regelbasierte KI-Spieler implementiert, die in das System integriert werden können. Die Codequalität wurde durch automatisierte Tests sichergestellt. Die Zusammenarbeit im Team erfolgte nach agilen Methoden (Scrum), und Server sowie KI wurden containerbasiert mit Docker bereitgestellt.",
|
||
image: "images/boardgame_ai.png",
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "towerdefense",
|
||
title: "Towerdefense Game",
|
||
description: "Singleplayer Towerdefense Game mit Editor entstanden als Einzelprojekt einer Vorlesung",
|
||
tags: ["C#", "Unity"],
|
||
github: "https://github.com/MarvinKrausser/TowerDefenseGame",
|
||
blocks: [
|
||
{
|
||
label: "",
|
||
heading: "Singleplayer Towerdefense Game",
|
||
text: "Einfaches Towerdefense Spiel das von Grundauf mit eigenen Designs erstellt wurde. Nutzt den A*-Algorithmus zur Berechnung des Laufweges. Ein demo build ist im Github Repository.",
|
||
image: "images/towerdefense_demo.png",
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
WEB COMPONENT: <project-section>
|
||
Rendert einen kompletten Projekt-Abschnitt mit
|
||
Header, alternierenden Content-Blocks und Link-Zeile.
|
||
══════════════════════════════════════════════════════ */
|
||
class ProjectSection extends HTMLElement {
|
||
connectedCallback() {
|
||
const p = JSON.parse(this.getAttribute("data-project") || "{}");
|
||
if (!p.id) return;
|
||
|
||
const tags = p.tags.map(t => `<span class="tag">${t}</span>`).join("");
|
||
const demo = p.demo
|
||
? `<a class="text-link ghost" href="${p.demo}" target="_blank">Live-Demo ↗</a>`
|
||
: "";
|
||
|
||
const blocks = (p.blocks || []).map((b, i) => {
|
||
const flip = i % 2 === 1 ? " flip" : "";
|
||
const imageContent = b.image
|
||
? `<img src="${b.image}" alt="${b.heading}" loading="lazy" />`
|
||
: `<div class="placeholder">
|
||
<div class="placeholder-icon">◻</div>
|
||
<div class="placeholder-label">Bild einfügen</div>
|
||
</div>`;
|
||
|
||
return `
|
||
<div class="content-block${flip}">
|
||
<div class="block-image">${imageContent}</div>
|
||
<div class="block-text">
|
||
<p class="block-label">${b.label}</p>
|
||
<h3>${b.heading}</h3>
|
||
<p>${b.text}</p>
|
||
</div>
|
||
</div>`;
|
||
}).join("");
|
||
|
||
this.innerHTML = `
|
||
<section class="project-section" id="${p.id}" data-nav="${p.title}">
|
||
<div class="section-head">
|
||
<p class="section-kicker">${p.title}</p>
|
||
<h2 class="section-title">${p.title}</h2>
|
||
<p class="section-desc">${p.description}</p>
|
||
</div>
|
||
<div class="content-blocks">${blocks}</div>
|
||
</section>`;
|
||
}
|
||
}
|
||
customElements.define("project-section", ProjectSection);
|
||
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
RENDER
|
||
══════════════════════════════════════════════════════ */
|
||
const root = document.getElementById("projects-root");
|
||
PROJECTS.forEach(p => {
|
||
const el = document.createElement("project-section");
|
||
el.setAttribute("data-project", JSON.stringify(p));
|
||
root.appendChild(el);
|
||
});
|
||
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
NAV AUFBAUEN + ACTIVE STATE
|
||
══════════════════════════════════════════════════════ */
|
||
function buildNav() {
|
||
const sidebarNav = document.getElementById("sidebar-nav");
|
||
const drawerLinks = document.getElementById("drawer-links");
|
||
sidebarNav.innerHTML = "";
|
||
drawerLinks.innerHTML = "";
|
||
|
||
// Statische Sections
|
||
const staticSections = [
|
||
{ id: "hero", label: "Start", group: null },
|
||
];
|
||
|
||
// Projekt-Sections nach dem Rendern einsammeln
|
||
const projectSections = PROJECTS.map((p, i) => ({
|
||
id: p.id,
|
||
label: p.title,
|
||
idx: String(i + 1).padStart(2, "0"),
|
||
group: "Projekte",
|
||
}));
|
||
|
||
const allSections = [...staticSections, ...projectSections];
|
||
|
||
// Gruppen
|
||
let lastGroup = "__none__";
|
||
allSections.forEach(s => {
|
||
if (s.group !== lastGroup) {
|
||
lastGroup = s.group;
|
||
if (s.group) {
|
||
const lbl = document.createElement("span");
|
||
lbl.className = "nav-group-label";
|
||
lbl.textContent = s.group;
|
||
sidebarNav.appendChild(lbl);
|
||
}
|
||
}
|
||
|
||
// Sidebar link
|
||
const a = document.createElement("a");
|
||
a.className = "nav-link";
|
||
a.href = `#${s.id}`;
|
||
a.dataset.target = s.id;
|
||
if (s.idx) {
|
||
const num = document.createElement("span");
|
||
num.className = "idx"; num.textContent = s.idx;
|
||
a.appendChild(num);
|
||
}
|
||
a.appendChild(document.createTextNode(s.label));
|
||
sidebarNav.appendChild(a);
|
||
|
||
// Mobile drawer link
|
||
const ma = document.createElement("a");
|
||
ma.href = `#${s.id}`;
|
||
ma.textContent = s.idx ? `${s.idx} – ${s.label}` : s.label;
|
||
ma.onclick = closeDrawer;
|
||
drawerLinks.appendChild(ma);
|
||
});
|
||
}
|
||
|
||
function setupObserver() {
|
||
const observer = new IntersectionObserver(entries => {
|
||
entries.forEach(e => {
|
||
if (e.isIntersecting) {
|
||
document.querySelectorAll(".nav-link").forEach(a =>
|
||
a.classList.toggle("active", a.dataset.target === e.target.id)
|
||
);
|
||
}
|
||
});
|
||
}, { rootMargin: "0px 0px -55% 0px", threshold: 0 });
|
||
|
||
document.querySelectorAll("[data-nav]").forEach(s => observer.observe(s));
|
||
}
|
||
|
||
// Custom elements render async – short timeout before building nav
|
||
setTimeout(() => { buildNav(); setupObserver(); }, 120);
|
||
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
MOBILE DRAWER
|
||
══════════════════════════════════════════════════════ */
|
||
function openDrawer() { document.getElementById("drawer").classList.add("open"); }
|
||
function closeDrawer() { document.getElementById("drawer").classList.remove("open"); }
|
||
</script>
|
||
</body>
|
||
</html> |