Landingpage/index.html
JaysonCleve 980baafcc7
All checks were successful
Deploy landingpage / deploy (push) Successful in 1s
add datenschutzerklaerung
2026-11-05 20:25:09 +01:00

694 lines
27 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>noctura.dev: Webseiten und Tiny Factory</title>
<meta name="description" content="Ich arbeite als Fullstackentwickler für Webseiten und baue an Tiny Factory, meinem eigenen Puzzlespiel fürs Handy. Aktuell nehme ich keine Bewerbungen an."/>
<meta name="theme-color" content="#2b2a80"/>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%232b2a80'/%3E%3Cpath d='M2 14V7l4 3V7l4 3V3h4v11z' fill='%23ffc93c'/%3E%3C/svg%3E"/>
<link rel="preload" href="fonts/lilita-one.woff2" as="font" type="font/woff2" crossorigin/>
<style>
@font-face{
font-family:'Lilita One';
src:url('fonts/lilita-one.woff2') format('woff2');
font-display:swap;
}
@font-face{
font-family:'Nunito';
src:url('fonts/nunito.woff2') format('woff2');
font-weight:200 1000;
font-display:swap;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--ink:#1a1648;
--night:#2b2a80;
--night-soft:#3d3ca6;
--lamp:#ffc93c;
--mint:#7ce8c8;
--coral:#ff8f7a;
--sky:#94d2ff;
--paper:#fffaf0;
--crate:#d9975a;
--ff-display:'Lilita One',system-ui,sans-serif;
--ff-body:'Nunito',system-ui,sans-serif;
--gutter:clamp(1rem,4vw,3rem);
--edge:3px solid var(--ink);
--pop:5px 5px 0 var(--ink);
}
html{font-size:100%;-webkit-text-size-adjust:100%}
@media(prefers-reduced-motion:no-preference){
html{scroll-behavior:smooth}
}
body{
background:var(--night);
color:var(--paper);
font-family:var(--ff-body);
font-size:1.125rem;
font-weight:600;
line-height:1.55;
overflow-x:clip;
}
::selection{background:var(--coral);color:var(--ink)}
a{color:inherit}
:focus-visible{outline:3px solid var(--lamp);outline-offset:3px}
.band :focus-visible,.game :focus-visible{outline-color:var(--ink);outline-offset:2px}
h1,h2,h3{font-family:var(--ff-display);font-weight:400;line-height:1.05;text-wrap:balance}
p{max-width:60ch}
svg{display:block}
.wrap{max-width:76rem;margin-inline:auto;padding-inline:var(--gutter)}
.skip{
position:absolute;left:var(--gutter);top:-5rem;z-index:10;
padding:.6rem 1rem;background:var(--lamp);color:var(--ink);font-weight:800;
}
.skip:focus{top:.75rem}
/* Kopfzeile */
.top{
display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
gap:.5rem 2rem;padding-block:1.25rem;
}
.logo{font-family:var(--ff-display);font-size:1.6rem;text-decoration:none;color:var(--lamp)}
.top ul{display:flex;flex-wrap:wrap;gap:.25rem 1.25rem;list-style:none}
.top li a{display:inline-block;padding-block:.3rem;font-weight:800;text-decoration:none}
.top li a:hover{color:var(--lamp);text-decoration:underline;text-decoration-thickness:3px;text-underline-offset:.25em}
/* Hero */
.hero{position:relative;padding-bottom:clamp(4rem,8vw,6.5rem);overflow:clip}
.stars{
position:absolute;left:0;top:0;width:4px;height:4px;
box-shadow:31vw 9vh 0 var(--paper),47vw 14vh 0 var(--lamp),54vw 6vh 0 var(--mint),
76vw 8vh 0 var(--paper),96vw 34vh 0 var(--lamp),97vw 62vh 0 var(--sky),50vw 84vh 0 var(--coral);
}
.hero-grid{
position:relative;
display:grid;grid-template-columns:minmax(0,1fr) minmax(0,35rem);
gap:clamp(2rem,4vw,3.5rem);align-items:center;
padding-top:clamp(1.5rem,4vw,3rem);
}
h1{font-size:clamp(3rem,6.4vw,5.5rem);line-height:1}
h1 span{display:block;color:var(--lamp)}
.lead{margin-top:1.25rem;font-size:clamp(1.2rem,1.9vw,1.4rem);line-height:1.45;max-width:30em}
.lead a{color:var(--mint);text-decoration-thickness:3px;text-underline-offset:.18em}
.sign{
display:inline-block;margin-top:1.5rem;padding:.65rem 1rem .65rem 2.1rem;
background:
repeating-linear-gradient(-45deg,var(--ink) 0 7px,var(--lamp) 7px 14px) left/1.25rem 100% no-repeat,
var(--lamp);
color:var(--ink);
border:var(--edge);border-radius:6px;box-shadow:var(--pop);
font-family:var(--ff-display);font-size:clamp(1.1rem,1.9vw,1.3rem);line-height:1.25;
transform:rotate(-2deg);
}
/* Mini-Puzzle */
.game{
--cell:14px;
background:var(--paper);color:var(--ink);
border:var(--edge);border-radius:18px;
box-shadow:8px 8px 0 var(--ink);
padding:clamp(.75rem,2vw,1.15rem);
}
.game[hidden]{display:none}
.game-head{display:flex;align-items:center;justify-content:space-between;gap:1rem}
.order{display:flex;align-items:center;gap:.8rem;min-width:0}
.order-card{
flex:none;padding:.4rem;background:var(--sky);
border:var(--edge);border-radius:8px;transform:rotate(-3deg);
}
.order small{display:block;font-size:.95rem;font-weight:800;line-height:1.2}
.order strong{display:block;font-family:var(--ff-display);font-weight:400;font-size:1.4rem;line-height:1.15}
.stamps{display:flex;gap:.3rem;flex:none}
.stamps i{
width:1.6rem;height:1.6rem;border:var(--edge);border-radius:50%;background:#fff;
display:grid;place-items:center;font-style:normal;font-weight:900;font-size:.9rem;line-height:1;
}
.stamps i.done{background:var(--mint)}
.stamps i.done::before{content:'✓'}
.piece{display:grid;grid-template-columns:repeat(3,var(--cell));gap:2px;width:max-content}
.piece span{width:var(--cell);height:var(--cell);border-radius:3px}
.piece .c1{background:var(--mint);box-shadow:inset 0 0 0 2px var(--ink)}
.piece .c2{background:var(--coral);box-shadow:inset 0 0 0 2px var(--ink)}
.floor{position:relative;margin-top:.85rem;border-radius:12px;background:var(--night);overflow:hidden;--belt:68px}
.spots{display:grid;grid-template-columns:repeat(5,1fr);gap:clamp(.15rem,1vw,.4rem);padding:.5rem .35rem 0;align-items:end}
.end{color:var(--paper);font-weight:800;font-size:.9rem;text-align:center;align-self:center}
.spot{
aspect-ratio:1;min-width:0;width:100%;padding:0;
background:transparent;color:var(--paper);
border:3px dashed var(--night-soft);border-radius:10px;
font:inherit;font-family:var(--ff-display);font-size:1.6rem;
cursor:pointer;touch-action:manipulation;
}
.spot:hover:not(:disabled){border-color:var(--lamp)}
.spot.full{border:0}
.spot svg{width:100%;height:100%}
.spot.press svg{animation:press .26s ease-in-out}
@keyframes press{50%{transform:translateY(8%) scale(1.06,.9)}}
.belt{
position:relative;height:var(--belt);
background:
repeating-linear-gradient(90deg,var(--night-soft) 0 12px,transparent 12px 28px) 0 calc(100% - 7px)/100% 5px no-repeat,
linear-gradient(var(--ink),var(--ink)) 0 100%/100% 19px no-repeat;
}
.floor.run .belt{animation:roll .5s linear infinite}
@keyframes roll{to{background-position:28px calc(100% - 7px),0 100%}}
.mover{
position:absolute;left:10%;bottom:19px;
transform:translateX(-50%);
transition:left .5s ease-in-out;
}
.mover .piece{--cell:13px}
.mover.nope{animation:nope .4s}
@keyframes nope{25%{transform:translateX(-58%)}75%{transform:translateX(-42%)}}
.tray{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.5rem;margin-top:.75rem}
.tool{
display:flex;align-items:center;gap:.4rem;
min-height:48px;padding:.3rem .45rem;
background:#fff;color:var(--ink);
border:var(--edge);border-radius:10px;box-shadow:0 4px 0 var(--ink);
font:inherit;font-weight:800;font-size:1rem;line-height:1.1;text-align:left;
cursor:pointer;touch-action:manipulation;
}
.tool svg{width:2.1rem;height:2.1rem;flex:none}
.tool:hover:not(:disabled){background:var(--lamp)}
.tool:active:not(:disabled){transform:translateY(3px);box-shadow:0 1px 0 var(--ink)}
.go{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .9rem;margin-top:.85rem}
.play{
min-height:48px;padding:.45rem 1.4rem;
background:var(--coral);color:var(--ink);
border:var(--edge);border-radius:10px;box-shadow:0 4px 0 var(--ink);
font:inherit;font-family:var(--ff-display);font-size:1.3rem;
cursor:pointer;touch-action:manipulation;
}
.play:hover:not(:disabled){background:var(--lamp)}
.play:active:not(:disabled){transform:translateY(3px);box-shadow:0 1px 0 var(--ink)}
button:disabled{opacity:.6;cursor:default}
.spot:disabled{opacity:1}
.msg{flex:1 1 12rem;font-size:1rem;font-weight:700;line-height:1.3}
.bit{position:fixed;left:0;top:0;width:10px;height:10px;z-index:20;pointer-events:none}
/* Bänder mit Sheddach als Kante */
.band{position:relative;background:var(--bg);color:var(--ink);padding-block:clamp(3.5rem,8vw,6rem)}
.band::before{
content:'';position:absolute;left:0;right:0;bottom:100%;height:24px;
background:linear-gradient(to top right,var(--bg) 50%,transparent 51%) 0 100%/48px 24px repeat-x;
}
.band h2{font-size:clamp(2.4rem,5.6vw,4.25rem);max-width:17ch}
.intro{margin-top:.9rem;font-size:1.2rem;line-height:1.5}
/* Gebäudekarten */
.work{--bg:var(--mint)}
.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem;margin-top:2.5rem;list-style:none}
.card{
display:flex;flex-direction:column;padding:1.1rem 1.25rem 1.25rem;
background:var(--paper);border:var(--edge);border-radius:16px;box-shadow:var(--pop);
transition:transform .15s;
}
.card:nth-child(1){transform:rotate(-1.2deg)}
.card:nth-child(3){transform:rotate(1.2deg)}
.card:hover{transform:translateY(-6px) rotate(0)}
.card svg{width:6.5rem;height:6.5rem}
.card h3{margin-top:.5rem;font-size:1.9rem}
.card p{margin-top:.35rem}
.chips{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:auto;padding-top:.9rem;list-style:none}
.chips li{
padding:.05rem .55rem;font-size:.95rem;font-weight:800;
background:var(--lamp);border:2px solid var(--ink);border-radius:99px;
}
/* Tiny Factory */
.tiny{--bg:var(--lamp)}
.tiny h2{font-size:clamp(3.4rem,12.5vw,10rem);line-height:.95;max-width:none}
.tiny-grid{
display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2rem 3rem;align-items:center;
margin-top:1.5rem;
}
.tiny-text{font-size:clamp(1.25rem,2vw,1.5rem);line-height:1.4;max-width:32em}
.tiny-note{margin-top:.8rem;font-size:1.05rem}
.tiny-note a{text-decoration-thickness:3px;text-underline-offset:.18em}
.shelf{display:flex;gap:1rem;align-items:flex-end}
.shelf .piece{--cell:20px}
.shelf>div{padding:.5rem;background:var(--paper);border:var(--edge);border-radius:10px;box-shadow:4px 4px 0 var(--ink)}
.shelf>div:nth-child(1){transform:rotate(-5deg)}
.shelf>div:nth-child(2){transform:translateY(-.9rem) rotate(3deg)}
.shelf>div:nth-child(3){transform:rotate(-2deg)}
.state{
display:inline-block;margin-top:1.1rem;padding:.2rem .8rem;
background:var(--ink);color:var(--lamp);border-radius:8px;
font-family:var(--ff-display);font-size:1.2rem;transform:rotate(-1.5deg);
}
/* Auftragsbuch */
.jobs{--bg:var(--sky)}
.tickets{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1.4rem;margin-top:2.5rem;list-style:none}
.ticket{
position:relative;padding:1.1rem 1.25rem 1.2rem;
background:var(--paper);border:var(--edge);border-radius:14px;box-shadow:var(--pop);
}
.ticket h3{font-size:1.6rem;padding-right:0}
.ticket p{margin-top:.35rem}
.ticket .tag{
display:inline-block;margin-top:.7rem;padding:0 .55rem;
font-family:var(--ff-display);font-size:1.1rem;
border:3px solid currentColor;border-radius:8px;transform:rotate(-3deg);
}
.tag.live{color:#07664c}
.tag.wip{color:#8a4b00}
.tag.plan{color:#55507f}
/* Hausregeln */
.rules{--bg:var(--coral)}
.rules dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem clamp(1.5rem,4vw,3rem);margin-top:2rem}
.rules dt{font-family:var(--ff-display);font-size:1.5rem;line-height:1.15}
.rules dd{margin-top:.4rem}
/* Kontakt */
.mailroom{--bg:var(--night);color:var(--paper);text-align:center}
.mailroom h2{margin-inline:auto}
.mailroom .intro{margin-inline:auto}
.mail{
display:inline-block;margin-top:1.75rem;padding:.6rem 1.1rem;
background:var(--lamp);color:var(--ink);
border:var(--edge);border-radius:12px;box-shadow:0 6px 0 var(--ink);
font-family:var(--ff-display);font-size:clamp(1.35rem,4.8vw,2.5rem);line-height:1.15;
text-decoration:none;overflow-wrap:anywhere;
}
.mail:hover{background:var(--mint)}
.mail:active{transform:translateY(4px);box-shadow:0 2px 0 var(--ink)}
footer{padding-block:0 2rem;text-align:center}
footer p{margin-inline:auto}
.legal a{font-weight:800;margin-inline:.6rem}
@media(max-width:1000px){
.hero-grid{grid-template-columns:minmax(0,1fr)}
.game{max-width:35rem}
.tiny-grid{grid-template-columns:minmax(0,1fr)}
.stars{display:none}
}
@media(max-width:820px){
.cards,.tickets,.rules dl{grid-template-columns:minmax(0,1fr)}
.card:nth-child(n){transform:none}
}
@media(max-width:420px){
.tool{flex-direction:column;align-items:center;text-align:center;gap:.1rem;font-size:.95rem}
.shelf .piece{--cell:16px}
}
@media(prefers-reduced-motion:reduce){
*,*::before,*::after{transition:none!important;animation:none!important}
}
</style>
</head>
<body>
<a class="skip" href="#inhalt">Zum Inhalt springen</a>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="b-stanze" viewBox="0 0 64 64">
<path d="M6 58V14h52v44H46V34H18v24z" fill="#ff8f7a" stroke="#1a1648" stroke-width="3" stroke-linejoin="round"/>
<rect x="27" y="4" width="10" height="12" fill="#fffaf0" stroke="#1a1648" stroke-width="3"/>
<rect x="28" y="34" width="8" height="10" fill="#fffaf0" stroke="#1a1648" stroke-width="3"/>
<rect x="21" y="44" width="22" height="6" rx="1" fill="#ffc93c" stroke="#1a1648" stroke-width="3"/>
<rect x="13" y="21" width="8" height="7" fill="#1a1648"/><rect x="43" y="21" width="8" height="7" fill="#1a1648"/>
</symbol>
<symbol id="b-dreher" viewBox="0 0 64 64">
<path d="M6 58V22l13-11v11l13-11v11l13-11v11h13v36H46V46H18v12z" fill="#94d2ff" stroke="#1a1648" stroke-width="3" stroke-linejoin="round"/>
<circle cx="32" cy="34" r="9" fill="#fffaf0" stroke="#1a1648" stroke-width="3"/>
<path d="M32 29v5l4 3" fill="none" stroke="#1a1648" stroke-width="3" stroke-linecap="round"/>
</symbol>
<symbol id="b-farbe" viewBox="0 0 64 64">
<rect x="42" y="6" width="10" height="16" fill="#fffaf0" stroke="#1a1648" stroke-width="3"/>
<path d="M6 58V20h52v38H46V44H18v14z" fill="#ffc93c" stroke="#1a1648" stroke-width="3" stroke-linejoin="round"/>
<path d="M32 24c5 6 7 8 7 11a7 7 0 0 1-14 0c0-3 2-5 7-11z" fill="#ff8f7a" stroke="#1a1648" stroke-width="3" stroke-linejoin="round"/>
</symbol>
<symbol id="b-front" viewBox="0 0 64 64">
<path d="M6 58V24l14-12v12l14-12v12l14-12v12h10v34z" fill="#94d2ff" stroke="#1a1648" stroke-width="3" stroke-linejoin="round"/>
<rect x="14" y="33" width="36" height="17" rx="2" fill="#fffaf0" stroke="#1a1648" stroke-width="3"/>
<path d="M19 39h14M19 44h22" stroke="#ff8f7a" stroke-width="3"/>
</symbol>
<symbol id="b-logik" viewBox="0 0 64 64">
<rect x="41" y="5" width="11" height="20" fill="#fffaf0" stroke="#1a1648" stroke-width="3"/>
<rect x="41" y="5" width="11" height="6" fill="#ff8f7a" stroke="#1a1648" stroke-width="3"/>
<path d="M6 58V24h52v34z" fill="#ff8f7a" stroke="#1a1648" stroke-width="3" stroke-linejoin="round"/>
<circle cx="19" cy="41" r="6" fill="#ffc93c" stroke="#1a1648" stroke-width="3"/>
<circle cx="45" cy="41" r="6" fill="#7ce8c8" stroke="#1a1648" stroke-width="3"/>
<path d="M25 41h14" stroke="#1a1648" stroke-width="3"/>
</symbol>
<symbol id="b-betrieb" viewBox="0 0 64 64">
<path d="M32 5v19M22 12h20" stroke="#1a1648" stroke-width="3" stroke-linecap="round"/>
<circle cx="32" cy="6" r="4" fill="#7ce8c8" stroke="#1a1648" stroke-width="3"/>
<rect x="10" y="24" width="44" height="34" rx="2" fill="#ffc93c" stroke="#1a1648" stroke-width="3"/>
<path d="M10 41h44" stroke="#1a1648" stroke-width="3"/>
<circle cx="19" cy="32.5" r="2.5" fill="#1a1648"/><circle cx="19" cy="49.5" r="2.5" fill="#1a1648"/>
<path d="M28 32h18M28 49h18" stroke="#1a1648" stroke-width="3"/>
</symbol>
</svg>
<div class="hero">
<div class="stars" aria-hidden="true"></div>
<header class="wrap top">
<a class="logo" href="#inhalt">noctura.dev</a>
<nav aria-label="Hauptnavigation">
<ul>
<li><a href="#arbeit">Arbeit</a></li>
<li><a href="#tiny-factory">Tiny Factory</a></li>
<li><a href="#projekte">Projekte</a></li>
<li><a href="#kontakt">Kontakt</a></li>
</ul>
</nav>
</header>
<div class="wrap hero-grid" id="inhalt">
<div>
<h1>Ich baue Webseiten. <span>Und eine kleine Fabrik.</span></h1>
<p class="lead">Aktuell arbeite ich als Fullstackentwickler für Webseiten. Außerdem entsteht gerade mein eigenes Mobilegame: <a href="#tiny-factory">Tiny Factory</a>.</p>
<p class="sign">Aktuell nehme ich keine Bewerbungen an.</p>
</div>
<section class="game" id="game" aria-label="Mini-Puzzle: Stelle die Gebäude so auf, dass die bestellte Form entsteht" hidden>
<div class="game-head">
<div class="order">
<div class="order-card"><div class="piece" id="target" aria-hidden="true"></div></div>
<div>
<small id="order-no"></small>
<strong id="order-name"></strong>
</div>
</div>
<div class="stamps" id="stamps" aria-hidden="true"><i></i><i></i><i></i></div>
</div>
<div class="floor" id="floor">
<div class="spots">
<span class="end">Start</span>
<button class="spot" type="button"></button>
<button class="spot" type="button"></button>
<button class="spot" type="button"></button>
<span class="end">Versand</span>
</div>
<div class="belt">
<div class="mover" id="mover"><div class="piece" id="piece" aria-hidden="true"></div></div>
</div>
</div>
<div class="tray" id="tray"></div>
<div class="go">
<button class="play" id="play" type="button">Los!</button>
<p class="msg" id="msg" role="status"></p>
</div>
</section>
</div>
</div>
<main>
<section class="band work" id="arbeit">
<div class="wrap">
<h2>Drei Gebäude, eine Linie.</h2>
<p class="intro">Für mich hört Webentwicklung nicht im Browser auf. Ich baue Webseiten vom ersten Layout bis zur laufenden Anwendung.</p>
<ul class="cards">
<li class="card">
<svg aria-hidden="true"><use href="#b-front"/></svg>
<h3>Oberfläche</h3>
<p>Responsive Seiten mit klarer Struktur und guter Typografie, die sich bewusst gestaltet anfühlen.</p>
<ul class="chips"><li>HTML</li><li>CSS</li><li>JavaScript</li></ul>
</li>
<li class="card">
<svg aria-hidden="true"><use href="#b-logik"/></svg>
<h3>Logik</h3>
<p>Services verbinden, Datenflüsse ordnen und APIs so aufbauen, dass man eine Anwendung auch versteht.</p>
<ul class="chips"><li>Node.js</li><li>REST APIs</li></ul>
</li>
<li class="card">
<svg aria-hidden="true"><use href="#b-betrieb"/></svg>
<h3>Betrieb</h3>
<p>Server einrichten, absichern und selbst betreiben, vom Deployment bis zum Alltag.</p>
<ul class="chips"><li>Debian Linux</li><li>Docker</li><li>Caddy</li><li>systemd</li><li>Forgejo</li><li>ufw</li><li>fail2ban</li><li>SSH-Härtung</li></ul>
</li>
</ul>
</div>
</section>
<section class="band tiny" id="tiny-factory">
<div class="wrap">
<h2>Tiny Factory</h2>
<div class="tiny-grid">
<div>
<p class="tiny-text">Mein eigenes Puzzlespiel fürs Handy. Du setzt kleine Fabrikgebäude zusammen, um eine bestimmte Form oder zusammengesetzte Formen zu bauen, und lieferst sie ab.</p>
<p class="tiny-note">Das Mini-Puzzle <a href="#game">oben auf dieser Seite</a> habe ich nur für hier gebaut, es ist nicht das Spiel selbst.</p>
<span class="state">In Entwicklung</span>
</div>
<div class="shelf" aria-hidden="true">
<div><div class="piece" data-cells="110111011"></div></div>
<div><div class="piece" data-cells="212020212"></div></div>
<div><div class="piece" data-cells="011211211"></div></div>
</div>
</div>
</div>
</section>
<section class="band jobs" id="projekte">
<div class="wrap">
<h2>Was sonst noch auf dem Band liegt.</h2>
<ul class="tickets">
<li class="ticket">
<h3>noctura.dev Infrastruktur</h3>
<p>Ein selbst verwalteter VPS mit echten Services und Sicherheitsmaßnahmen.</p>
<span class="tag live">Live</span>
</li>
<li class="ticket">
<h3>Developer Utilities</h3>
<p>Kleine Web-Tools, bei denen schnelle Nutzung und klare Interaktion wichtiger sind als große Verpackung.</p>
<span class="tag wip">In Entwicklung</span>
</li>
<li class="ticket">
<h3>Custom Backend API</h3>
<p>Eine eigene API-Struktur für persönliche Anwendungen. Routing, Datenmodelle und Hosting bleiben in meiner Hand.</p>
<span class="tag wip">In Entwicklung</span>
</li>
<li class="ticket">
<h3>Lab</h3>
<p>Platz für Prototypen, visuelle Versuche und technische Experimente, bevor daraus etwas Größeres wird.</p>
<span class="tag plan">Geplant</span>
</li>
</ul>
</div>
</section>
<section class="band rules">
<div class="wrap">
<h2>Hausregeln.</h2>
<dl>
<div>
<dt>Klarheit vor Effekten</dt>
<dd>Gestaltung darf auffallen, aber nicht auf Kosten der Lesbarkeit.</dd>
</div>
<div>
<dt>Technik mitdenken</dt>
<dd>Nicht nur das Ergebnis muss stimmen, auch der Aufbau dahinter soll nachvollziehbar sein.</dd>
</div>
<div>
<dt>Wartbarkeit gehört dazu</dt>
<dd>Sauberer Code und vernünftige Strukturen sind kein Extra, sondern Teil der Lösung.</dd>
</div>
</dl>
</div>
</section>
<section class="band mailroom" id="kontakt">
<div class="wrap">
<h2>Lust auf ein technisches Gespräch?</h2>
<p class="intro">Schreib mir gern zu meinen Projekten, zu Tiny Factory oder zu Technik im Allgemeinen. Bewerbungen nehme ich aktuell nicht an.</p>
<a class="mail" href="mailto:Jaysonmatthew2005@gmail.com">Jaysonmatthew2005@gmail.com</a>
</div>
</section>
</main>
<footer class="wrap">
<p>noctura.dev läuft auf eigener Infrastruktur mit Debian.</p>
<p class="legal"><a href="impressum.html">Impressum</a><a href="datenschutz.html">Datenschutz</a></p>
</footer>
<script>
// Formen zeichnen: 0 leer, 1 Rohform, 2 lackiert
function drawPiece(el,cells){
el.replaceChildren(...cells.map(c=>{
const s=document.createElement('span');
s.className='c'+c;
return s;
}));
}
document.querySelectorAll('[data-cells]').forEach(el=>drawPiece(el,[...el.dataset.cells].map(Number)));
// Mini-Puzzle
(function(){
const START=[1,1,1,1,1,1,1,1,1];
const BUILDINGS={
stanze:{name:'Stanze',icon:'b-stanze',run:c=>c.map((v,i)=>i===2?0:v)},
dreher:{name:'Dreher',icon:'b-dreher',run:c=>c.map((_,i)=>{
const r=Math.floor(i/3),q=i%3;
return c[(2-q)*3+r];
})},
farbe:{name:'Lackierer',icon:'b-farbe',run:c=>c.map((v,i)=>i<3&&v?2:v)}
};
const ORDERS=[
{name:'Eckstück',target:[1,1,1,1,1,1,1,1,0]},
{name:'Doppelkerbe',target:[1,1,0,1,1,1,1,1,0]},
{name:'Rote Kante',target:[1,1,0,1,1,2,1,1,2]}
];
const STEP=window.matchMedia('(prefers-reduced-motion: reduce)').matches?0:1;
const game=document.getElementById('game');
const floor=document.getElementById('floor');
const mover=document.getElementById('mover');
const piece=document.getElementById('piece');
const target=document.getElementById('target');
const msg=document.getElementById('msg');
const play=document.getElementById('play');
const tray=document.getElementById('tray');
const spots=[...document.querySelectorAll('.spot')];
const stamps=[...document.querySelectorAll('#stamps i')];
const tools=[];
let level=0,slots=[null,null,null],busy=false,finished=false;
const wait=ms=>new Promise(r=>setTimeout(r,ms*STEP));
const moveTo=i=>{mover.style.left=(10+i*20)+'%'};
const same=(a,b)=>a.every((v,i)=>v===b[i]);
function buildingSvg(id){
return '<svg aria-hidden="true"><use href="#'+id+'"/></svg>';
}
function drawSlots(){
spots.forEach((spot,i)=>{
const b=BUILDINGS[slots[i]];
spot.classList.toggle('full',!!b);
spot.innerHTML=b?buildingSvg(b.icon):'';
spot.setAttribute('aria-label',b?'Platz '+(i+1)+': '+b.name+', entfernen':'Platz '+(i+1)+': leer');
});
}
function lock(on){
busy=on;
spots.concat(tools).forEach(b=>{b.disabled=on});
play.disabled=on;
}
function loadOrder(){
const o=ORDERS[level];
document.getElementById('order-no').textContent='Auftrag '+(level+1)+' von '+ORDERS.length;
document.getElementById('order-name').textContent=o.name;
drawPiece(target,o.target);
drawPiece(piece,START);
slots=[null,null,null];
drawSlots();
moveTo(0);
play.textContent='Los!';
msg.textContent=level?'Nächster Auftrag: '+o.name+'.':'Tippe Gebäude an, um sie auf die Plätze zu stellen. Die Reihenfolge zählt.';
}
function confetti(){
if(!STEP) return;
const box=floor.getBoundingClientRect();
const colors=['#ffc93c','#7ce8c8','#ff8f7a','#94d2ff'];
for(let i=0;i<28;i++){
const bit=document.createElement('i');
bit.className='bit';
bit.style.background=colors[i%4];
document.body.appendChild(bit);
const x=box.right-box.width*.1,y=box.top+box.height*.6;
bit.animate([
{transform:`translate(${x}px,${y}px)`},
{transform:`translate(${x-40-Math.random()*200}px,${y-60-Math.random()*160}px) rotate(${Math.random()*720}deg)`,offset:.55},
{transform:`translate(${x-60-Math.random()*220}px,${y+90}px) rotate(900deg)`,opacity:0}
],{duration:900+Math.random()*400,easing:'ease-out'}).onfinish=()=>bit.remove();
}
}
async function run(){
if(finished){
finished=false;
level=0;
stamps.forEach(s=>s.classList.remove('done'));
loadOrder();
return;
}
lock(true);
msg.textContent='Die Linie läuft.';
floor.classList.add('run');
let cells=START;
drawPiece(piece,cells);
for(let i=0;i<3;i++){
moveTo(i+1);
await wait(520);
const b=BUILDINGS[slots[i]];
if(b){
spots[i].classList.add('press');
await wait(130);
cells=b.run(cells);
drawPiece(piece,cells);
await wait(170);
spots[i].classList.remove('press');
}
}
moveTo(4);
await wait(520);
floor.classList.remove('run');
if(same(cells,ORDERS[level].target)){
stamps[level].classList.add('done');
confetti();
await wait(700);
level++;
if(level<ORDERS.length){
loadOrder();
}else{
finished=true;
level=ORDERS.length-1;
msg.textContent='Alle Aufträge geliefert. Schichtende!';
play.textContent='Nochmal';
}
}else{
mover.classList.add('nope');
msg.textContent='Das ist nicht die bestellte Form. Tausch die Gebäude und versuch es nochmal.';
await wait(420);
mover.classList.remove('nope');
moveTo(0);
drawPiece(piece,START);
}
lock(false);
}
Object.entries(BUILDINGS).forEach(([key,b])=>{
const tool=document.createElement('button');
tool.type='button';
tool.className='tool';
tool.innerHTML=buildingSvg(b.icon)+b.name;
tool.addEventListener('click',()=>{
const free=slots.indexOf(null);
if(busy||finished) return;
if(free<0){
msg.textContent='Alle Plätze sind belegt. Tippe ein Gebäude auf der Linie an, um es zu entfernen.';
return;
}
slots[free]=key;
drawSlots();
});
tools.push(tool);
tray.appendChild(tool);
});
spots.forEach((spot,i)=>spot.addEventListener('click',()=>{
if(busy||finished) return;
slots[i]=null;
drawSlots();
}));
play.addEventListener('click',run);
loadOrder();
game.hidden=false;
})();
</script>
</body>
</html>