Stellenliste mit Kurzbewerbung
Offene Stellen nach Bereich, Ort und Teilzeit filtern, die Trefferzahl zählt mit, jede Stelle klappt mit Gehaltsrahmen auf, beworben wird in einem Schritt mit Name, E-Mail und Datei.
Ab etwa vier Stellen oder mehreren Standorten: Kanzleien, Praxisverbünde, größere Handwerksbetriebe.
Offene Stellen als Liste mit Filtern nach Bereich, Ort und Teilzeit. Die Trefferzahl zählt live mit, aktive Filter stehen als Chips darüber. Jede Stelle klappt auf: Aufgaben, Profil, was der Betrieb bietet, Gehaltsrahmen und Ansprechperson. Beworben wird in einem Schritt: Name, E-Mail, eine Datei, Datenschutz. Danach ein klarer Erfolgszustand. Passt keine Stelle zum Filter, zeigt ein Leerzustand Auswege statt einer leeren Fläche.
Bewerbende entscheiden in Sekunden, ob eine Stelle passt. Ort, Umfang und Gehalt stehen deshalb vorne. Ein kurzes Formular ohne Anschreiben senkt die Hürde, besonders am Telefon.
Wann einsetzen
Karriereseiten von Kanzleien, Praxen, Handwerksbetrieben und Agenturen ab etwa vier offenen Stellen oder mehreren Standorten.
Wann nicht
Bei ein oder zwei Stellen: dann ohne Filter, Stellen direkt offen zeigen (Variante „Wenige Stellen“). Nicht, wenn ein Bewerbermanagement-System ohnehin eine eigene Liste mitbringt; dann nur gestalten, nicht nachbauen.
Varianten
- Liste mit Filtern (Bereich, Ort, Teilzeit), Detail aufklappbar, Kurzbewerbung im Dialog
- Wenige Stellen: ohne Filter, Stellen direkt aufgeklappt, Initiativbewerbung daneben
- Mit Messenger-Erstkontakt statt Formular (Handwerk)
Umsetzung
Stellen als Liste von <article> mit Daten zu Bereich, Ort und Teilzeit. Filter als echte Formularelemente in einem <form>; jede Änderung filtert, schreibt die Trefferzahl in eine role="status"-Zeile, zeigt an jeder Pille die Zahl bei ihrer Wahl und listet aktive Filter als entfernbare Chips. Umordnen per document.startViewTransition() mit view-transition-name je Stelle (Fallback: sofort). Detail per <button aria-expanded> und Bereich mit inert, Höhe über grid-template-rows 0fr → 1fr. Kurzbewerbung in <dialog> per showModal(): Name, E-Mail, Datei (Typ und Größe per setCustomValidity), Pflicht-Häkchen Datenschutz; Erfolg im selben Dialog mit den nächsten Schritten. Leerzustand mit „Filter lockern“ samt Trefferzahl und Initiativbewerbung. Im Einsatz die Stellen zusätzlich als schema.org JobPosting auszeichnen und die Datei serverseitig prüfen. Gehaltsangaben und Datenschutztext vom Kunden prüfen lassen.
Robust umsetzen
Telefon. Filter hinter einem Knopf, Trefferzahl und Chips kleben oben. Dialog in voller Höhe, die Dateiauswahl öffnet auch Kamera und Dateien des Telefons. Große Ziele.
Ohne Bewegung. Kein Umordnen per View Transition, kein Gleiten beim Aufklappen; Inhalte erscheinen sofort.
Barrierefrei. Filter als fieldset mit legend. Trefferzahl in role="status". Aufklapp-Knopf mit aria-expanded und aria-controls. Dialog mit aria-labelledby, Fokus auf das erste Feld, Esc schließt, Fokus zurück auf den Auslöser. Fehlertexte erst nach Verlassen des Felds, verknüpft über aria-describedby. Datei-Feld mit sichtbarem Label und Hinweis zu Format und Größe.
Code
<!DOCTYPE html>
<!--
Stellenliste mit Kurzbewerbung
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/stellen-karriere/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stellenliste mit Kurzbewerbung · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Rezept Kanzlei: ein tiefer Markenton, warme Graus, heller Akzent nur für Fokus und aktive Filter */
--grund: #f9f8f8; --flaeche: #efebeb; --linie: #d2cdc7; --marke: #1e1e50; --text: #212529; --leise: #5a5550; --meta: #75655a; --akzent: #00bee6;
--fehler: #b3261e; --gut: #1f6b4f;
--kurve: cubic-bezier(0.4, 0, 0.22, 1); /* Oppenhoff */
--kurz: 125ms; --mittel: 250ms; --lang: 330ms;
view-transition-name: none;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Schibsted Grotesk', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
button, input { font: inherit; color: inherit; }
.serif { font-family: 'Newsreader', Georgia, serif; font-optical-sizing: auto; font-weight: 400; letter-spacing: -.01em; }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }
.label { font: 500 12px/1.2 'Schibsted Grotesk', sans-serif; letter-spacing: .08em; text-transform: uppercase; color: var(--meta); }
.mono { font: 500 10px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
a { color: var(--marke); text-decoration-color: var(--akzent); text-underline-offset: 3px; }
/* Kopf */
.kopf { display: flex; align-items: center; gap: 28px; padding: 16px clamp(16px, 3.4vw, 48px); border-bottom: 1px solid var(--linie); background: var(--grund); }
.marke { display: grid; gap: 2px; margin-right: auto; color: var(--marke); text-decoration: none; }
.marke b { font: 400 22px/1 'Newsreader', serif; letter-spacing: -.01em; }
.marke span { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--meta); }
.nav { display: flex; gap: 22px; list-style: none; font-size: 15px; font-weight: 500; }
.nav a { color: var(--text); text-decoration: none; padding: 6px 0; background: linear-gradient(var(--akzent), var(--akzent)) 0 100% / 0 2px no-repeat; transition: background-size var(--kurz) var(--kurve); }
.nav a:hover, .nav a[aria-current] { background-size: 100% 2px; }
.nav a[aria-current] { color: var(--marke); }
.umschalter { display: flex; border: 1px solid var(--linie); }
.umschalter button { background: none; border: 0; padding: 8px 11px; cursor: pointer; color: var(--leise); transition: background-color var(--kurz) var(--kurve), color var(--kurz) var(--kurve); }
.umschalter button + button { border-left: 1px solid var(--linie); }
.umschalter button[aria-pressed="true"] { background: var(--marke); color: #fff; }
/* Bühne */
.buehne { display: grid; grid-template-columns: minmax(260px, .74fr) minmax(0, 1.26fr); gap: clamp(24px, 4vw, 64px); padding: clamp(24px, 3.4vw, 48px) clamp(16px, 3.4vw, 48px) 64px; max-width: 1320px; margin: 0 auto; align-items: start; }
.seite { position: sticky; top: 24px; display: grid; gap: 22px; }
.seite h1 { font-size: clamp(2rem, 1.3rem + 2.6vw, 3.25rem); line-height: 1.06; color: var(--marke); text-wrap: balance; }
.seite h1 em { font-style: italic; }
.seite > p { font-size: 17px; line-height: 1.6; color: var(--leise); max-width: 40ch; text-wrap: pretty; }
.fakten { display: grid; grid-template-columns: repeat(3, auto); justify-content: start; gap: 4px 22px; border-top: 1px solid var(--linie); padding-top: 14px; }
.fakten b { display: block; font: 400 26px/1.1 'Newsreader', serif; color: var(--marke); }
.fakten span { font-size: 13px; color: var(--leise); }
/* Filter */
.filter-knopf { display: none; }
.filter { display: grid; gap: 18px; border-top: 1px solid var(--linie); padding-top: 18px; }
.filter fieldset { border: 0; display: grid; gap: 9px; min-width: 0; }
.filter legend { margin-bottom: 8px; }
.pillen { display: flex; flex-wrap: wrap; gap: 6px; }
.pille { position: relative; display: inline-flex; align-items: baseline; gap: 6px; padding: 8px 13px; border: 1px solid var(--linie); border-radius: 999px; font-size: 14.5px; cursor: pointer; transition: background-color var(--kurz) var(--kurve), border-color var(--kurz) var(--kurve), color var(--kurz) var(--kurve); }
.pille input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.pille small { font-size: 12px; color: var(--meta); transition: color var(--kurz) var(--kurve); }
.pille:hover { border-color: var(--marke); }
.pille:has(input:checked) { background: var(--marke); border-color: var(--marke); color: #fff; box-shadow: inset 0 -2px 0 var(--akzent); }
.pille:has(input:checked) small { color: rgba(255, 255, 255, .7); }
.pille:has(input:focus-visible) { outline: 2px solid var(--akzent); outline-offset: 2px; }
.pille.leer:not(:has(input:checked)) { color: #a59c96; border-style: dashed; }
.schalter { display: flex; align-items: center; justify-content: space-between; gap: 16px; cursor: pointer; font-size: 15px; padding: 4px 0; }
.schalter span small { display: block; font-size: 13px; color: var(--leise); }
.schalter input { appearance: none; flex: none; width: 46px; height: 26px; border-radius: 999px; background: var(--linie); position: relative; cursor: pointer; transition: background-color var(--mittel) var(--kurve); }
.schalter input::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2); transition: translate var(--mittel) var(--kurve); }
.schalter input:checked { background: var(--marke); }
.schalter input:checked::after { translate: 20px 0; }
/* Ergebnis */
.ergebnis { min-width: 0; }
.leiste { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding-bottom: 14px; border-bottom: 1px solid var(--marke); min-height: 52px; }
.zahl { font: 400 22px/1 'Newsreader', serif; color: var(--marke); margin-right: 6px; outline-offset: 4px; }
.zahl b { font-weight: 400; display: inline-block; min-width: 1ch; }
.zahl b.zaehlt { animation: zaehlt var(--mittel) var(--kurve); }
@keyframes zaehlt { from { opacity: 0; translate: 0 6px; } }
.chips { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; }
.chip { display: inline-flex; align-items: center; gap: 6px; font-size: 13.5px; padding: 5px 6px 5px 11px; background: var(--flaeche); border-radius: 999px; border: 0; cursor: pointer; transition: background-color var(--kurz) var(--kurve); }
.chip:hover { background: #e3dddd; }
.chip svg { width: 16px; height: 16px; }
.zuruecksetzen { margin-left: auto; background: none; border: 0; font-size: 14px; color: var(--marke); text-decoration: underline; text-decoration-color: var(--akzent); text-underline-offset: 3px; cursor: pointer; }
.stellen { list-style: none; }
.stelle { border-bottom: 1px solid var(--linie); }
.stelle[hidden] { display: none; }
.stelle-kopf { display: grid; grid-template-columns: 1fr auto; gap: 6px 20px; align-items: center; width: 100%; padding: 20px 0 18px; background: none; border: 0; text-align: left; cursor: pointer; }
.stelle-kopf h3 { font: 400 clamp(20px, 1.4vw + 10px, 25px)/1.15 'Newsreader', serif; letter-spacing: -.01em; color: var(--marke); }
.stelle-kopf h3 span { background: linear-gradient(var(--akzent), var(--akzent)) 0 100% / 0 1.5px no-repeat; transition: background-size var(--kurz) var(--kurve); }
.stelle-kopf:hover h3 span, .stelle-kopf:focus-visible h3 span { background-size: 100% 1.5px; }
.stelle-kopf:focus-visible { outline-offset: -2px; }
.meta { grid-column: 1; display: flex; flex-wrap: wrap; gap: 4px 0; font-size: 14px; color: var(--leise); margin-left: -23px; clip-path: inset(0 0 0 23px); }
.meta span::before { content: ''; display: inline-block; width: 3px; height: 3px; border-radius: 50%; background: var(--meta); margin: 0 10px 3px; }
.meta .tz { color: var(--marke); font-weight: 500; }
.pfeil { grid-row: 1 / span 2; grid-column: 2; width: 38px; height: 38px; border: 1px solid var(--linie); border-radius: 50%; display: grid; place-items: center; transition: background-color var(--kurz) var(--kurve), border-color var(--kurz) var(--kurve), rotate var(--mittel) var(--kurve); }
.pfeil svg { width: 14px; height: 14px; stroke: var(--marke); stroke-width: 1.8; fill: none; }
.stelle-kopf:hover .pfeil { border-color: var(--marke); }
.stelle-kopf[aria-expanded="true"] .pfeil { rotate: 180deg; background: var(--marke); border-color: var(--marke); }
.stelle-kopf[aria-expanded="true"] .pfeil svg { stroke: #fff; }
.detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--mittel) var(--kurve); }
.detail > div { overflow: hidden; min-height: 0; }
.stelle.offen .detail { grid-template-rows: 1fr; }
.detail-innen { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 26px; padding: 2px 0 24px; opacity: 0; translate: 0 -8px; transition: opacity var(--mittel) var(--kurve), translate var(--mittel) var(--kurve); }
.stelle.offen .detail-innen { opacity: 1; translate: 0 0; }
.detail h4 { font: 500 12px/1.2 'Schibsted Grotesk', sans-serif; letter-spacing: .08em; text-transform: uppercase; color: var(--meta); margin-bottom: 8px; }
.detail ul { list-style: none; display: grid; gap: 6px; font-size: 15px; line-height: 1.45; }
.detail li { padding-left: 14px; position: relative; }
.detail li::before { content: ''; position: absolute; left: 0; top: .62em; width: 6px; height: 1px; background: var(--marke); }
.detail-fuss { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 14px 24px; padding: 16px 18px; background: var(--flaeche); }
.gehalt { display: grid; gap: 2px; margin-right: auto; }
.gehalt b { font: 400 21px/1.1 'Newsreader', serif; color: var(--marke); }
.gehalt span { font-size: 13px; color: var(--leise); }
.person { display: flex; align-items: center; gap: 10px; font-size: 14px; line-height: 1.35; }
.person .mono-kreis { width: 38px; height: 38px; border-radius: 50%; background: var(--marke); color: #fff; display: grid; place-items: center; font: 400 15px/1 'Newsreader', serif; flex: none; }
.person a { font-size: 13.5px; }
.knopf { display: inline-flex; align-items: center; gap: 10px; padding: 14px 20px; background: var(--marke); color: #fff; border: 0; cursor: pointer; font-weight: 500; font-size: 15px; text-decoration: none; transition: background-color var(--kurz) var(--kurve), scale var(--kurz) var(--kurve); }
.knopf:hover { background: #2c2c6e; }
.knopf:active, .knopf.gedrueckt { scale: .97; }
.knopf.hell { background: transparent; color: var(--marke); border: 1px solid var(--marke); }
.knopf.hell:hover { background: var(--flaeche); }
/* Leerzustand */
.leer-zustand { padding: 34px 0 30px; display: grid; gap: 16px; border-bottom: 1px solid var(--linie); }
.leer-zustand[hidden] { display: none; }
.leer-zustand h3 { font: 400 28px/1.15 'Newsreader', serif; color: var(--marke); }
.leer-zustand h3 em { font-style: italic; }
.leer-zustand p { font-size: 16px; line-height: 1.55; color: var(--leise); max-width: 52ch; }
.lockern { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; }
.lockern button { display: inline-flex; gap: 8px; align-items: baseline; padding: 10px 14px; border: 1px solid var(--marke); background: none; color: var(--marke); cursor: pointer; font-size: 14.5px; transition: background-color var(--kurz) var(--kurve); }
.lockern button:hover { background: var(--flaeche); }
.lockern button small { font-size: 12.5px; color: var(--leise); }
.initiativ { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; padding: 22px 0 0; font-size: 15px; color: var(--leise); }
.initiativ b { color: var(--text); font-weight: 500; }
/* Dialog */
dialog.bewerbung { width: min(560px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); margin: auto; border: 0; padding: 0; background: var(--grund); color: var(--text); box-shadow: 0 30px 80px rgba(30, 30, 80, .28); opacity: 1; translate: 0 0;
transition: opacity var(--lang) var(--kurve), translate var(--lang) var(--kurve), overlay var(--lang) allow-discrete, display var(--lang) allow-discrete; }
@starting-style { dialog.bewerbung[open] { opacity: 0; translate: 0 8px; } }
dialog.bewerbung:not([open]) { opacity: 0; translate: 0 8px; transition-duration: var(--mittel); }
dialog.bewerbung::backdrop { background: rgba(30, 30, 80, .34); transition: background-color var(--lang) var(--kurve), overlay var(--lang) allow-discrete, display var(--lang) allow-discrete; }
@starting-style { dialog.bewerbung[open]::backdrop { background: rgba(30, 30, 80, 0); } }
.dlg-kopf { display: flex; justify-content: space-between; align-items: start; gap: 16px; padding: 22px 24px 16px; border-bottom: 1px solid var(--linie); }
.dlg-kopf h2 { font: 400 26px/1.1 'Newsreader', serif; color: var(--marke); margin-top: 6px; }
.dlg-kopf p { font-size: 14px; color: var(--leise); margin-top: 4px; }
.schliessen { flex: none; width: 40px; height: 40px; border: 1px solid var(--linie); border-radius: 50%; background: none; cursor: pointer; display: grid; place-items: center; transition: border-color var(--kurz) var(--kurve); }
.schliessen:hover { border-color: var(--marke); }
.schliessen svg { width: 14px; height: 14px; stroke: var(--marke); stroke-width: 1.8; }
.dlg-form { display: grid; gap: 14px; padding: 18px 24px 24px; }
.feld { display: grid; gap: 6px; }
.feld > label, .feld > .titel { font-size: 13px; font-weight: 500; letter-spacing: .02em; }
.feld input[type="text"], .feld input[type="email"] { width: 100%; font-size: 17px; padding: 12px 14px; background: #fff; border: 1px solid #b9b3ad; border-radius: 0; outline: none; transition: border-color var(--kurz) var(--kurve), box-shadow var(--kurz) var(--kurve); }
.feld input:focus-visible, .feld.fokus input { border-color: var(--marke); box-shadow: 0 0 0 3px rgba(0, 190, 230, .28); }
.feld input:user-invalid { border-color: var(--fehler); }
.fehler { font-size: 13px; color: var(--fehler); display: none; }
.feld:has(input:user-invalid) .fehler, .feld.ungueltig .fehler { display: block; }
.datei { position: relative; display: flex; align-items: center; gap: 14px; padding: 16px; border: 1.5px dashed #b9b3ad; background: #fff; transition: border-color var(--kurz) var(--kurve), background-color var(--kurz) var(--kurve); }
.datei input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.datei:hover, .datei.ziehen, .datei:has(input:focus-visible) { border-color: var(--marke); background: var(--flaeche); }
.datei:has(input:focus-visible) { outline: 2px solid var(--akzent); outline-offset: 2px; }
.datei-icon { width: 40px; height: 48px; flex: none; }
.datei b { display: block; font-weight: 500; font-size: 15px; }
.datei span { font-size: 13px; color: var(--leise); }
.datei.hat { border-style: solid; border-color: var(--gut); }
.feld.ungueltig .datei, .feld:has(input:user-invalid) .datei { border-color: var(--fehler); }
.ds { display: grid; grid-template-columns: 22px 1fr; gap: 10px; align-items: start; font-size: 14px; line-height: 1.45; cursor: pointer; }
.ds input { appearance: none; width: 22px; height: 22px; border: 1.5px solid var(--marke); background: #fff; display: grid; place-items: center; cursor: pointer; margin-top: 1px; }
.ds input::after { content: ''; width: 11px; height: 6px; border-left: 2px solid #fff; border-bottom: 2px solid #fff; rotate: -45deg; translate: 0 -1px; opacity: 0; transition: opacity var(--kurz) var(--kurve); }
.ds input:checked { background: var(--marke); }
.ds input:checked::after { opacity: 1; }
.ds input:user-invalid { border-color: var(--fehler); }
.dlg-fuss { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; padding-top: 4px; }
.dlg-fuss small { font-size: 13px; color: var(--leise); }
.danke { display: grid; gap: 16px; padding: 26px 24px 26px; }
.danke[hidden], .dlg-form[hidden] { display: none; }
.danke .haken { width: 52px; height: 52px; border-radius: 50%; background: var(--marke); display: grid; place-items: center; }
.danke .haken svg { width: 24px; height: 24px; stroke: #fff; stroke-width: 2.2; fill: none; stroke-dasharray: 28; stroke-dashoffset: 28; animation: zeichnen 400ms var(--kurve) 150ms forwards; }
@keyframes zeichnen { to { stroke-dashoffset: 0; } }
.danke h3 { font: 400 27px/1.15 'Newsreader', serif; color: var(--marke); }
.danke p { font-size: 16px; line-height: 1.55; color: var(--leise); }
.schritte { list-style: none; display: grid; gap: 0; counter-reset: s; border-top: 1px solid var(--linie); }
.schritte li { counter-increment: s; display: grid; grid-template-columns: 30px 1fr auto; gap: 10px; padding: 11px 0; border-bottom: 1px solid var(--linie); font-size: 15px; }
.schritte li::before { content: counter(s); font: 400 18px/1.1 'Newsreader', serif; color: var(--marke); }
.schritte li small { color: var(--meta); font-size: 13px; }
/* Werte */
.werte { color: var(--meta); display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 18px; }
.werte b { font-weight: 500; color: var(--text); }
/* Variante „wenige“: ohne Filter, Stellen offen */
[data-variante="wenige"] .filter, [data-variante="wenige"] .filter-knopf, [data-variante="wenige"] .leiste .chips, [data-variante="wenige"] .zuruecksetzen { display: none; }
/* View Transitions für das Umordnen */
::view-transition-group(*) { animation-duration: var(--mittel); animation-timing-function: var(--kurve); }
::view-transition-old(*), ::view-transition-new(*) { animation-duration: var(--mittel); animation-timing-function: var(--kurve); }
/* Stellen, die wegfallen, gehen schneller, damit nachrückender Inhalt nicht durch sie hindurch zu lesen ist */
::view-transition-old(*):only-child { animation-duration: var(--kurz); }
/* Zeiger der Vorführung */
.zeiger { position: fixed; inset: auto; left: 0; top: 0; width: 24px; height: 24px; margin: -12px 0 0 -12px; padding: 0; overflow: visible; border-radius: 50%; border: 2px solid var(--marke); background: rgba(0, 190, 230, .3); pointer-events: none; z-index: 2147483001; opacity: 0; transition: translate 600ms var(--kurve), opacity 200ms var(--kurve), scale var(--kurz) var(--kurve); }
.vorfuehrung .zeiger.an { opacity: 1; }
.zeiger.klick { scale: .7; }
/* Reduziert */
.reduziert *, .reduziert *::before, .reduziert *::after, .reduziert dialog::backdrop { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-delay: 0s !important; }
.reduziert ::view-transition-group(*), .reduziert ::view-transition-old(*), .reduziert ::view-transition-new(*) { animation: none !important; }
@media (hover: none) {
.pille:hover, .stelle-kopf:hover .pfeil { border-color: var(--linie); }
.knopf:hover { background: var(--marke); }
.pille { padding: 11px 15px; }
.chip { padding: 8px 8px 8px 13px; }
}
@media (max-width: 980px) {
.nav { display: none; }
}
@media (max-width: 860px) {
.buehne { grid-template-columns: 1fr; gap: 20px; }
.seite { position: static; gap: 16px; }
.fakten { display: none; }
.filter-knopf { display: inline-flex; justify-content: space-between; align-items: center; gap: 10px; width: 100%; padding: 13px 16px; border: 1px solid var(--marke); background: none; color: var(--marke); font-size: 15px; font-weight: 500; cursor: pointer; }
.filter-knopf svg { width: 14px; height: 14px; stroke: currentColor; stroke-width: 1.8; fill: none; transition: rotate var(--mittel) var(--kurve); }
.filter-knopf[aria-expanded="true"] svg { rotate: 180deg; }
.filter { border-top: 0; padding-top: 4px; }
.filter[data-zu] { display: none; }
.detail-innen { grid-template-columns: 1fr; }
.leiste { position: sticky; top: 0; z-index: 2; background: var(--grund); padding-top: 12px; }
}
@media (max-width: 640px) {
.kopf { flex-wrap: wrap; gap: 12px; }
.umschalter { width: 100%; }
.umschalter button { flex: 1; }
.detail-fuss { padding: 14px; }
.detail-fuss .knopf { width: 100%; justify-content: center; }
dialog.bewerbung { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; }
.dlg-kopf, .dlg-form, .danke { padding-inline: 18px; }
.dlg-fuss .knopf { width: 100%; justify-content: center; }
}
/* Vorschau 960 × 720 */
.vorschau body { overflow: hidden; }
.vorschau .werte, .vorschau .seite > p, .vorschau .fakten, .vorschau .nav { display: none; }
.vorschau .buehne { padding-top: 22px; gap: 34px; grid-template-columns: minmax(260px, .7fr) minmax(0, 1.3fr); }
.vorschau .seite h1 { font-size: 34px; }
.vorschau .stelle-kopf { padding: 14px 0 12px; }
.vorschau .detail-innen { padding-bottom: 16px; gap: 12px 20px; }
.vorschau .detail ul { font-size: 13.5px; gap: 4px; }
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#start"><b>Eberhardt Oettle Partner</b><span>Steuerberatung · Recht · Heilbronn</span></a>
<ul class="nav"><li><a href="#leistungen">Leistungen</a></li><li><a href="#koepfe">Köpfe</a></li><li><a href="#insights">Insights</a></li><li><a href="#karriere" aria-current="page">Karriere</a></li><li><a href="#kontakt">Kontakt</a></li></ul>
<div class="umschalter mono" role="group" aria-label="Variante der Demo" data-umschalter>
<button type="button" data-v="liste" aria-pressed="true">Viele Stellen</button>
<button type="button" data-v="wenige" aria-pressed="false">Wenige Stellen</button>
</div>
</header>
<main class="buehne" id="karriere" data-variante="liste">
<div class="seite">
<p class="label">Karriere</p>
<h1 class="serif" data-h1>Neun offene Stellen. <em>Eine Bewerbung in zwei Minuten.</em></h1>
<p data-vorspann>Ohne Anschreiben: Name, E-Mail und Lebenslauf genügen. Carolin Sautter meldet sich innerhalb von fünf Werktagen.</p>
<button type="button" class="filter-knopf" aria-expanded="false" aria-controls="filter" data-filter-knopf><span>Filter <span data-filter-anzahl></span></span><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 5l5 5 5-5"/></svg></button>
<form class="filter" id="filter" data-filter aria-label="Stellen filtern">
<fieldset>
<legend class="label">Bereich</legend>
<div class="pillen" data-bereiche></div>
</fieldset>
<fieldset>
<legend class="label">Ort</legend>
<div class="pillen" data-orte></div>
</fieldset>
<label class="schalter"><span>Teilzeit möglich<small>ab 20 Stunden, Zeiten nach Absprache</small></span><input type="checkbox" role="switch" name="teilzeit" data-teilzeit></label>
</form>
<div class="fakten ziffern"><div><b>41</b><span>Kolleginnen und Kollegen</span></div><div><b>3</b><span>Standorte</span></div><div><b>34 %</b><span>in Teilzeit</span></div></div>
</div>
<section class="ergebnis" aria-labelledby="ergebnis-titel">
<h2 class="vh" id="ergebnis-titel">Offene Stellen</h2>
<div class="leiste">
<p class="zahl ziffern" role="status" tabindex="-1" data-status><b data-zahl>9</b> <span data-zahl-text>offene Stellen</span></p>
<ul class="chips" data-chips aria-label="Aktive Filter"></ul>
<button type="button" class="zuruecksetzen" data-zuruecksetzen hidden>Alle Filter zurücksetzen</button>
</div>
<ol class="stellen" data-stellen></ol>
<div class="leer-zustand" data-leer hidden style="view-transition-name: leer">
<h3>Für diese Auswahl ist gerade <em>nichts frei.</em></h3>
<p>Das ändert sich oft schneller, als man denkt. Lockern Sie einen Filter oder schicken Sie uns eine Initiativbewerbung.</p>
<ul class="lockern" data-lockern></ul>
</div>
<div class="initiativ" style="view-transition-name: initiativ"><span><b>Nichts Passendes dabei?</b> Wir lesen jede Initiativbewerbung.</span><button type="button" class="knopf hell" data-bewerben="initiativ">Initiativ bewerben</button></div>
<p class="werte mono" data-werte><span>Kurve <b>cubic-bezier(0.4, 0, 0.22, 1)</b></span><span>Farbe <b>125 ms</b></span><span>Aufklappen, Umordnen <b>250 ms</b></span><span>Dialog <b>330 ms</b></span><span>Einblendung <b>8 px</b></span></p>
</section>
</main>
<dialog class="bewerbung" aria-labelledby="dlg-titel" data-dialog>
<div class="dlg-kopf">
<div><p class="label">Kurzbewerbung</p><h2 id="dlg-titel" data-dlg-titel>Steuerfachangestellte:r</h2><p data-dlg-meta>Heilbronn · Vollzeit oder Teilzeit</p></div>
<button type="button" class="schliessen" data-schliessen aria-label="Schließen"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 2l10 10M12 2 2 12"/></svg></button>
</div>
<form class="dlg-form" data-form novalidate>
<input type="hidden" name="stelle" data-stelle-feld>
<div class="feld"><label for="b-name">Vor- und Nachname</label><input type="text" id="b-name" name="name" required autocomplete="name" aria-describedby="e-name"><p class="fehler" id="e-name">Bitte Ihren Namen eintragen.</p></div>
<div class="feld"><label for="b-mail">E-Mail</label><input type="email" id="b-mail" name="email" required autocomplete="email" aria-describedby="e-mail"><p class="fehler" id="e-mail">Bitte eine E-Mail-Adresse wie name@beispiel.de.</p></div>
<div class="feld" data-feld-datei>
<label for="b-datei">Lebenslauf oder Zeugnis</label>
<div class="datei" data-datei>
<svg class="datei-icon" viewBox="0 0 40 48" aria-hidden="true"><path d="M4 2h22l10 10v34H4z" fill="#fff" stroke="#1e1e50" stroke-width="1.5"/><path d="M26 2v10h10" fill="none" stroke="#1e1e50" stroke-width="1.5"/><path d="M11 24h18M11 30h18M11 36h11" stroke="#d2cdc7" stroke-width="2"/></svg>
<div><b data-datei-name>Datei wählen oder hierher ziehen</b><span data-datei-info>PDF, Word oder Foto, bis 10 MB</span></div>
<input type="file" id="b-datei" name="datei" required accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,application/pdf,image/*" aria-describedby="e-datei">
</div>
<p class="fehler" id="e-datei" data-datei-fehler>Bitte eine Datei anhängen: PDF, Word oder Foto, bis 10 MB.</p>
</div>
<div class="feld"><label class="ds"><input type="checkbox" name="datenschutz" required aria-describedby="e-ds"><span>Ich habe die <a href="#datenschutz">Datenschutzhinweise für Bewerbende</a> gelesen. Unterlagen löschen wir sechs Monate nach Abschluss des Verfahrens.</span></label><p class="fehler" id="e-ds">Bitte bestätigen Sie die Datenschutzhinweise.</p></div>
<div class="dlg-fuss"><button type="submit" class="knopf" data-senden>Bewerbung senden <span aria-hidden="true">→</span></button><small>Kein Anschreiben nötig.</small></div>
</form>
<div class="danke" data-danke hidden>
<div class="haken"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5 10 17l9-10"/></svg></div>
<h3 tabindex="-1" data-danke-titel>Danke, Jana.</h3>
<p data-danke-text></p>
<ol class="schritte"><li>Eingangsbestätigung per E-Mail <small>gleich</small></li><li>Rückmeldung von Carolin Sautter <small>bis 5 Werktage</small></li><li>Kennenlernen, vor Ort oder per Video <small>45 Minuten</small></li></ol>
<div><button type="button" class="knopf hell" data-schliessen>Schließen</button></div>
</div>
</dialog>
<div class="zeiger" data-zeiger aria-hidden="true" popover="manual"></div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const BIETEN = ['Zwei feste Homeoffice-Tage pro Woche', '30 Tage Urlaub, 24. und 31.12. frei', 'Fortbildung bis zum Examen, wir zahlen den Kurs', 'Dienstrad-Leasing und Zuschuss zum Deutschlandticket'];
const STELLEN = [
{ id: 'stfa', titel: 'Steuerfachangestellte:r', bereich: 'steuer', orte: ['Heilbronn'], tz: true, umfang: 'Vollzeit oder ab 25 Std.', start: 'ab sofort', gehalt: '3.300–3.900 €', gehaltZu: 'brutto im Monat bei Vollzeit',
aufgaben: ['Finanz- und Lohnbuchhaltung für Handwerk und Handel', 'Einkommen- und Umsatzsteuererklärungen', 'Erste Ansprechperson für 40 bis 60 Mandate'], profil: ['Ausbildung als Steuerfachangestellte:r', 'Sicher in DATEV oder bereit, es zu lernen', 'Freude an klaren Erklärungen'] },
{ id: 'stb', titel: 'Steuerberater:in', bereich: 'steuer', orte: ['Heilbronn'], tz: true, umfang: 'Vollzeit oder ab 30 Std.', start: 'ab 01.02.2027', gehalt: '5.800–7.200 €', gehaltZu: 'brutto im Monat bei Vollzeit',
aufgaben: ['Jahresabschlüsse und Gestaltungsberatung für Familienunternehmen', 'Betreuung von Betriebsprüfungen', 'Ein Team von vier Kolleg:innen fachlich führen'], profil: ['Bestandenes Steuerberaterexamen', 'Erfahrung mit Personengesellschaften', 'Lust, Wissen weiterzugeben'] },
{ id: 'bilanz', titel: 'Bilanzbuchhalter:in', bereich: 'steuer', orte: ['Öhringen'], tz: true, umfang: 'Vollzeit oder ab 25 Std.', start: 'ab sofort', gehalt: '4.200–5.000 €', gehaltZu: 'brutto im Monat bei Vollzeit',
aufgaben: ['Monats- und Jahresabschlüsse nach HGB', 'Auswertungen für Mandanten aufbereiten', 'Kontakt zu Banken und Prüfern'], profil: ['Fortbildung Bilanzbuchhalter:in (IHK)', 'Drei Jahre Erfahrung im Abschluss', 'Sorgfalt ohne Pedanterie'] },
{ id: 'lohn', titel: 'Lohn- und Gehaltsbuchhalter:in', bereich: 'lohn', orte: ['Neckarsulm'], tz: true, umfang: 'Teilzeit 20–30 Std.', start: 'ab 01.01.2027', gehalt: '3.400–4.000 €', gehaltZu: 'brutto im Monat bei Vollzeit, anteilig',
aufgaben: ['Abrechnungen für 25 Mandate mit bis zu 120 Beschäftigten', 'Meldungen an Krankenkassen und Finanzamt', 'Fragen von Mandanten zu Lohnthemen'], profil: ['Erfahrung in der Lohnabrechnung', 'Kenntnisse in Sozialversicherungsrecht', 'Verlässlich auch zum Monatsende'] },
{ id: 'lohnhn', titel: 'Fachkraft Lohn mit Schwerpunkt Bau', bereich: 'lohn', orte: ['Heilbronn'], tz: false, umfang: 'Vollzeit', start: 'ab sofort', gehalt: '3.500–4.100 €', gehaltZu: 'brutto im Monat',
aufgaben: ['Baulohn mit Saison-Kurzarbeitergeld und SOKA-Meldungen', 'Prüfungen der Sozialversicherung begleiten', 'Neue Mandate einrichten'], profil: ['Erfahrung im Baulohn', 'Genaues Arbeiten mit Fristen', 'Teamgeist'] },
{ id: 'ra', titel: 'Rechtsanwält:in Arbeitsrecht', bereich: 'recht', orte: ['Heilbronn'], tz: false, umfang: 'Vollzeit', start: 'nach Absprache', gehalt: '6.000–7.500 €', gehaltZu: 'brutto im Monat',
aufgaben: ['Beratung von Unternehmen und Führungskräften', 'Verhandlungen vor dem Arbeitsgericht Heilbronn', 'Workshops für Mandanten'], profil: ['Zwei gute Staatsexamen', 'Gern Fachanwaltslehrgang Arbeitsrecht', 'Klare Sprache vor Gericht und am Telefon'] },
{ id: 'rafa', titel: 'Rechtsanwaltsfachangestellte:r', bereich: 'recht', orte: ['Neckarsulm'], tz: true, umfang: 'Teilzeit ab 20 Std.', start: 'ab sofort', gehalt: '3.100–3.600 €', gehaltZu: 'brutto im Monat bei Vollzeit, anteilig',
aufgaben: ['Fristen und Termine im Blick behalten', 'Schriftsätze vorbereiten und versenden', 'Mandanten am Telefon empfangen'], profil: ['Ausbildung als ReFa oder vergleichbar', 'Sicher mit beA und Fristenkalender', 'Ruhe auch an vollen Tagen'] },
{ id: 'empfang', titel: 'Empfang & Kanzleiorganisation', bereich: 'kanzlei', orte: ['Öhringen'], tz: true, umfang: 'Teilzeit, vormittags', start: 'ab 15.01.2027', gehalt: '2.700–3.100 €', gehaltZu: 'brutto im Monat bei Vollzeit, anteilig',
aufgaben: ['Erster Eindruck am Empfang und am Telefon', 'Post, Termine und Besprechungsräume', 'Bestellungen und kleine Projekte'], profil: ['Kaufmännische Ausbildung', 'Freundlich und strukturiert', 'Gute Deutschkenntnisse in Wort und Schrift'] },
{ id: 'azubi', titel: 'Ausbildung Steuerfachangestellte:r 2027', bereich: 'kanzlei', orte: ['Heilbronn', 'Öhringen'], tz: false, umfang: 'Ausbildung, 3 Jahre', start: '01.09.2027', gehalt: '1.150 / 1.250 / 1.350 €', gehaltZu: 'Vergütung im 1. / 2. / 3. Jahr',
aufgaben: ['Buchhaltung und Steuererklärungen von Anfang an', 'Berufsschule in Heilbronn', 'Eigene Mandate ab dem zweiten Jahr'], profil: ['Mittlere Reife oder Abitur', 'Interesse an Zahlen und Menschen', 'Schnuppertag vorher möglich'] }
];
const BEREICHE = [{ id: 'alle', name: 'Alle' }, { id: 'steuer', name: 'Steuerberatung' }, { id: 'lohn', name: 'Lohn' }, { id: 'recht', name: 'Recht' }, { id: 'kanzlei', name: 'Kanzlei & Ausbildung' }];
const ORTE = ['Heilbronn', 'Neckarsulm', 'Öhringen'];
const WENIGE = ['stfa', 'azubi'];
const ZAHLWORT = ['Keine', 'Eine', 'Zwei', 'Drei', 'Vier', 'Fünf', 'Sechs', 'Sieben', 'Acht', 'Neun'];
const PFEIL = '<svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 5l5 5 5-5"/></svg>';
const X = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 5l6 6M11 5l-6 6" stroke="currentColor" stroke-width="1.5"/></svg>';
const filter = $('[data-filter]'), liste = $('[data-stellen]'), leer = $('[data-leer]');
const main = $('main');
let v = 'liste';
// Liste aufbauen
const bereichName = id => BEREICHE.find(b => b.id === id).name;
liste.innerHTML = STELLEN.map(s => `
<li class="stelle" data-id="${s.id}" style="view-transition-name: stelle-${s.id}">
<article aria-labelledby="t-${s.id}">
<button type="button" class="stelle-kopf" aria-expanded="false" aria-controls="d-${s.id}">
<h3 id="t-${s.id}"><span>${s.titel}</span> <span class="vh">(m/w/d)</span></h3>
<p class="meta"><span>${bereichName(s.bereich)}</span><span>${s.orte.join(' oder ')}</span><span class="${s.tz ? 'tz' : ''}">${s.umfang}</span><span class="ziffern">${s.start}</span></p>
<span class="pfeil">${PFEIL}</span>
</button>
<div class="detail" id="d-${s.id}" inert><div><div class="detail-innen">
<div><h4>Ihre Aufgaben</h4><ul>${s.aufgaben.map(x => `<li>${x}</li>`).join('')}</ul></div>
<div><h4>Was Sie mitbringen</h4><ul>${s.profil.map(x => `<li>${x}</li>`).join('')}</ul></div>
<div><h4>Was wir bieten</h4><ul>${BIETEN.slice(0, 3).map(x => `<li>${x}</li>`).join('')}</ul></div>
<div class="detail-fuss">
<p class="gehalt"><b class="ziffern">${s.gehalt}</b><span>${s.gehaltZu}</span></p>
<p class="person"><span class="mono-kreis" aria-hidden="true">CS</span><span>Carolin Sautter, Personal<br><a href="tel:+49713164298012" class="ziffern">07131 64 29 80-12</a></span></p>
<button type="button" class="knopf" data-bewerben="${s.id}">Kurzbewerbung <span aria-hidden="true">→</span></button>
</div>
</div></div></div>
</article>
</li>`).join('');
const zeilen = Object.fromEntries($$('.stelle').map(li => [li.dataset.id, li]));
// Filter aufbauen
$('[data-bereiche]').innerHTML = BEREICHE.map((b, i) => `<label class="pille"><input type="radio" name="bereich" value="${b.id}"${i ? '' : ' checked'}>${b.name} <small class="ziffern" data-n-bereich="${b.id}"></small></label>`).join('');
$('[data-orte]').innerHTML = ORTE.map(o => `<label class="pille"><input type="checkbox" name="ort" value="${o}">${o} <small class="ziffern" data-n-ort="${o}"></small></label>`).join('');
function zustand() {
const fd = new FormData(filter);
return { bereich: fd.get('bereich') || 'alle', orte: fd.getAll('ort'), tz: !!fd.get('teilzeit') };
}
const passt = (s, z) => (z.bereich === 'alle' || s.bereich === z.bereich) && (!z.orte.length || s.orte.some(o => z.orte.includes(o))) && (!z.tz || s.tz);
const treffer = z => STELLEN.filter(s => (v === 'wenige' ? WENIGE.includes(s.id) : true) && passt(s, z));
function stellenText(n) { return n === 1 ? 'offene Stelle' : 'offene Stellen'; }
let altZahl = null;
function anwenden(z) {
const t = treffer(z), ids = new Set(t.map(s => s.id));
STELLEN.forEach(s => { zeilen[s.id].hidden = !ids.has(s.id); });
// Trefferzahl live
const zahl = $('[data-zahl]');
zahl.textContent = t.length;
if (altZahl !== null && altZahl !== t.length && !reduziert()) { zahl.classList.remove('zaehlt'); void zahl.offsetWidth; zahl.classList.add('zaehlt'); }
altZahl = t.length;
$('[data-zahl-text]').textContent = stellenText(t.length) + (z.bereich !== 'alle' || z.orte.length || z.tz ? ` von ${v === 'wenige' ? WENIGE.length : STELLEN.length}` : '');
// Zahlen an den Filtern: was käme heraus, wenn man diesen Wert wählt?
BEREICHE.forEach(b => { const n = treffer({ ...z, bereich: b.id }).length; const el = $(`[data-n-bereich="${b.id}"]`); el.textContent = n; el.closest('.pille').classList.toggle('leer', !n); });
ORTE.forEach(o => { const n = treffer({ ...z, orte: [o] }).length; const el = $(`[data-n-ort="${o}"]`); el.textContent = n; el.closest('.pille').classList.toggle('leer', !n); });
// Aktive Filter als Chips
const chips = [];
if (z.bereich !== 'alle') chips.push({ typ: 'bereich', text: bereichName(z.bereich) });
z.orte.forEach(o => chips.push({ typ: 'ort', wert: o, text: o }));
if (z.tz) chips.push({ typ: 'tz', text: 'Teilzeit möglich' });
$('[data-chips]').innerHTML = chips.map(c => `<li><button type="button" class="chip" data-chip="${c.typ}" data-wert="${c.wert || ''}" aria-label="Filter ${c.text} entfernen">${c.text}${X}</button></li>`).join('');
$('[data-zuruecksetzen]').hidden = !chips.length;
$('[data-filter-anzahl]').textContent = chips.length ? `(${chips.length})` : '';
// Leerzustand mit Auswegen samt Trefferzahl
leer.hidden = t.length > 0;
if (!t.length) {
const wege = [];
if (z.tz) wege.push({ text: 'Ohne Teilzeit-Filter', z: { ...z, tz: false } });
if (z.orte.length) wege.push({ text: 'Alle Orte', z: { ...z, orte: [] } });
if (z.bereich !== 'alle') wege.push({ text: 'Alle Bereiche', z: { ...z, bereich: 'alle' } });
$('[data-lockern]').innerHTML = wege.map((w, i) => { const n = treffer(w.z).length; return n ? `<li><button type="button" data-lockern-i="${i}">${w.text} <small class="ziffern">${n} ${n === 1 ? 'Stelle' : 'Stellen'}</small></button></li>` : ''; }).join('');
$('[data-lockern]').wege = wege;
}
}
function setzen(z) {
filter.querySelector(`input[name="bereich"][value="${z.bereich}"]`).checked = true;
filter.querySelectorAll('input[name="ort"]').forEach(i => { i.checked = z.orte.includes(i.value); });
$('[data-teilzeit]').checked = z.tz;
}
// Umordnen mit View Transition (Fallback: sofort)
function mitUebergang(f) {
if (document.startViewTransition && !reduziert() && !document.hidden) document.startViewTransition(f);
else f();
}
filter.addEventListener('change', () => { const z = zustand(); mitUebergang(() => anwenden(z)); });
$('[data-chips]').addEventListener('click', e => {
const c = e.target.closest('[data-chip]'); if (!c) return;
const z = zustand();
if (c.dataset.chip === 'bereich') z.bereich = 'alle';
if (c.dataset.chip === 'ort') z.orte = z.orte.filter(o => o !== c.dataset.wert);
if (c.dataset.chip === 'tz') z.tz = false;
setzen(z); mitUebergang(() => anwenden(z));
$('[data-status]').focus();
});
$('[data-zuruecksetzen]').addEventListener('click', () => { const z = { bereich: 'alle', orte: [], tz: false }; setzen(z); mitUebergang(() => anwenden(z)); });
$('[data-lockern]').addEventListener('click', e => {
const b = e.target.closest('[data-lockern-i]'); if (!b) return;
const z = $('[data-lockern]').wege[+b.dataset.lockernI].z; setzen(z); mitUebergang(() => anwenden(z));
});
// Aufklappen
function klappe(li, auf) {
const k = li.querySelector('.stelle-kopf'), d = li.querySelector('.detail');
auf = auf ?? k.getAttribute('aria-expanded') !== 'true';
k.setAttribute('aria-expanded', String(auf)); li.classList.toggle('offen', auf); d.inert = !auf;
}
liste.addEventListener('click', e => { const k = e.target.closest('.stelle-kopf'); if (k) klappe(k.closest('.stelle')); });
// Mobil: Filter hinter einem Knopf
const fk = $('[data-filter-knopf]');
const schmal = matchMedia('(max-width: 860px)');
function filterZu(zu) { fk.setAttribute('aria-expanded', String(!zu)); filter.toggleAttribute('data-zu', zu); }
fk.addEventListener('click', () => filterZu(fk.getAttribute('aria-expanded') === 'true'));
filterZu(schmal.matches);
schmal.addEventListener?.('change', () => filterZu(schmal.matches));
// ------------------------------------------------------------------
// Kurzbewerbung im Dialog
// ------------------------------------------------------------------
const dlg = $('[data-dialog]'), form = $('[data-form]'), danke = $('[data-danke]');
const datei = form.elements.datei, dateiBox = $('[data-datei]'), dateiFeld = $('[data-feld-datei]');
const MAX = 10 * 1048576, ENDUNG = /\.(pdf|docx?|jpe?g|png)$/i;
let ausloeser = null;
function oeffnen(id, von) {
const s = STELLEN.find(x => x.id === id);
$('[data-dlg-titel]').textContent = s ? s.titel : 'Initiativbewerbung';
$('[data-dlg-meta]').textContent = s ? `${s.orte.join(' oder ')} · ${s.umfang}` : 'Für alle Bereiche und Standorte';
$('[data-stelle-feld]').value = s ? s.id : 'initiativ';
form.reset(); dateiAnzeigen(); dateiFeld.classList.remove('ungueltig');
form.hidden = false; danke.hidden = true;
ausloeser = von || null;
dlg.showModal();
if (html.classList.contains('vorfuehrung')) { zeigerOben(); document.activeElement?.blur(); }
if (!html.classList.contains('vorfuehrung')) form.elements.name.focus();
}
function schliessen() { dlg.close(); }
dlg.addEventListener('close', () => { if (ausloeser && !html.classList.contains('vorfuehrung')) ausloeser.focus(); });
dlg.addEventListener('click', e => { if (e.target.closest('[data-schliessen]') || e.target === dlg) schliessen(); });
document.addEventListener('click', e => { const b = e.target.closest('[data-bewerben]'); if (b) oeffnen(b.dataset.bewerben, b); });
function dateiPruefen() {
const f = datei.files[0];
let fehler = '';
if (f && !ENDUNG.test(f.name)) fehler = `„${f.name}“ können wir nicht öffnen. Bitte PDF, Word oder ein Foto.`;
else if (f && f.size > MAX) fehler = `Die Datei ist ${(f.size / 1048576).toLocaleString('de-DE', { maximumFractionDigits: 1 })} MB groß. Bitte höchstens 10 MB.`;
datei.setCustomValidity(fehler);
if (fehler) $('[data-datei-fehler]').textContent = fehler;
else $('[data-datei-fehler]').textContent = 'Bitte eine Datei anhängen: PDF, Word oder Foto, bis 10 MB.';
return !fehler && !!f;
}
function dateiAnzeigen() {
const f = datei.files[0];
$('[data-datei-name]').textContent = f ? f.name : 'Datei wählen oder hierher ziehen';
$('[data-datei-info]').textContent = f ? `${(f.size / 1024 > 1024 ? (f.size / 1048576).toLocaleString('de-DE', { maximumFractionDigits: 1 }) + ' MB' : Math.max(1, Math.round(f.size / 1024)) + ' KB')} · ersetzen per Klick` : 'PDF, Word oder Foto, bis 10 MB';
const ok = dateiPruefen();
dateiBox.classList.toggle('hat', ok);
if (f) dateiFeld.classList.toggle('ungueltig', !ok);
}
datei.addEventListener('change', dateiAnzeigen);
['dragenter', 'dragover'].forEach(t => dateiBox.addEventListener(t, e => { e.preventDefault(); dateiBox.classList.add('ziehen'); }));
['dragleave', 'drop'].forEach(t => dateiBox.addEventListener(t, () => dateiBox.classList.remove('ziehen')));
dateiBox.addEventListener('drop', e => { e.preventDefault(); if (e.dataTransfer.files.length) { datei.files = e.dataTransfer.files; dateiAnzeigen(); } });
form.addEventListener('submit', e => {
e.preventDefault();
dateiPruefen();
const falsch = [...form.elements].filter(x => x.willValidate && !x.validity.valid);
dateiFeld.classList.toggle('ungueltig', !datei.validity.valid);
if (falsch.length) { if (!html.classList.contains('vorfuehrung')) falsch[0].focus(); return; }
const vorname = form.elements.name.value.trim().split(/\s+/)[0];
$('[data-danke-titel]').textContent = `Danke, ${vorname}.`;
$('[data-danke-text]').textContent = `Ihre Bewerbung als ${$('[data-dlg-titel]').textContent} ist bei Carolin Sautter angekommen. Eine Bestätigung geht an ${form.elements.email.value.trim()}.`;
form.hidden = true; danke.hidden = false;
if (!html.classList.contains('vorfuehrung')) $('[data-danke-titel]').focus();
});
// ------------------------------------------------------------------
// Varianten (?v=liste|wenige)
// ------------------------------------------------------------------
const schalter = $$('[data-umschalter] [data-v]');
function variante(neu, url = true) {
v = neu === 'wenige' ? 'wenige' : 'liste';
main.dataset.variante = v;
schalter.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
setzen({ bereich: 'alle', orte: [], tz: false });
$$('.stelle').forEach(li => klappe(li, v === 'wenige'));
const n = v === 'wenige' ? WENIGE.length : STELLEN.length;
$('[data-h1]').innerHTML = `${ZAHLWORT[n]} offene Stellen. <em>Eine Bewerbung in zwei Minuten.</em>`;
altZahl = null; anwenden(zustand());
if (url) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', location.pathname + '?' + q.toString() + location.hash); }
}
schalter.forEach(b => b.addEventListener('click', () => { const lief = laeuft; neustart(); variante(b.dataset.v); if (lief) vorfuehren(); }));
variante(new URLSearchParams(location.search).get('v') || 'liste', false);
// ------------------------------------------------------------------
// Vorführung
// ------------------------------------------------------------------
const zeiger = $('[data-zeiger]');
function zeigerOben() { try { if (zeiger.matches(':popover-open')) zeiger.hidePopover(); zeiger.showPopover(); } catch (e) { /* ohne Popover-API: Zeiger bleibt unter dem Dialog */ } }
let uhr = [], laeuft = false;
const nach = (ms, f) => uhr.push(setTimeout(f, ms));
function zeigeAuf(el, dx = .5, dy = .5) { if (!el) return; const r = el.getBoundingClientRect(); zeiger.style.translate = `${r.left + r.width * dx}px ${r.top + r.height * dy}px`; zeiger.classList.add('an'); }
function klick(sel, dx, dy, t) {
nach(t, () => zeigeAuf(typeof sel === 'function' ? sel() : $(sel), dx, dy));
nach(t + 560, () => { const el = typeof sel === 'function' ? sel() : $(sel); if (!el) return; zeiger.classList.add('klick'); el.click(); setTimeout(() => zeiger.classList.remove('klick'), 150); });
}
function tippen(t, el, text, ms = 55) { [...text].forEach((z, k) => nach(t + k * ms, () => { el.value += z; el.dispatchEvent(new Event('input', { bubbles: true })); })); return t + text.length * ms; }
function anhaengen() {
try { const dt = new DataTransfer(); dt.items.add(new File([new Uint8Array(184320)], 'Lebenslauf_Jana_Wild.pdf', { type: 'application/pdf' })); datei.files = dt.files; dateiAnzeigen(); } catch (e) { /* ohne DataTransfer: Feld bleibt leer */ }
}
function runde() {
uhr.forEach(clearTimeout); uhr = [];
if (dlg.open) dlg.close();
variante(v, false);
let t = 500;
if (v === 'liste') {
klick('.pille input[value="recht"]', .5, .5, t); t += 1300;
klick('.pille input[value="Heilbronn"]', .5, .5, t); t += 1300;
klick('[data-teilzeit]', .5, .5, t); t += 1700; // → Leerzustand
klick(() => $('[data-lockern-i="1"]') || $('[data-lockern] button'), .4, .5, t); t += 1500;
klick(() => $$('.stelle:not([hidden]) .stelle-kopf')[0], .9, .5, t); t += 1500;
} else {
nach(t, () => zeigeAuf($('.stelle .detail-fuss .knopf'), .5, .5)); t += 900;
}
klick(() => $$('.stelle:not([hidden]) [data-bewerben]')[0], .4, .5, t); t += 1100;
nach(t, () => zeigeAuf(form.elements.name, .3)); t = tippen(t + 250, form.elements.name, 'Jana Wild', 60);
nach(t + 100, () => zeigeAuf(form.elements.email, .3)); t = tippen(t + 350, form.elements.email, 'jana.wild@posteo.de', 45);
klick(() => dateiBox, .5, .5, t + 150); nach(t + 600, anhaengen); t += 1300;
nach(t, () => zeigeAuf(form.elements.datenschutz, .5, .5)); nach(t + 500, () => { form.elements.datenschutz.checked = true; }); t += 900;
klick('[data-senden]', .4, .5, t); t += 1000;
nach(t + 2600, () => zeiger.classList.remove('an'));
nach(t + 3000, () => { if (dlg.open) dlg.close(); });
nach(t + 3600, runde);
}
function vorfuehren() { pause(); html.classList.add('vorfuehrung'); zeigerOben(); laeuft = true; runde(); }
function pause() { uhr.forEach(clearTimeout); uhr = []; laeuft = false; }
function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { laeuft = true; runde(); } }
function neustart() { pause(); html.classList.remove('vorfuehrung'); zeiger.classList.remove('an'); if (dlg.open) dlg.close(); variante(v, false); }
})();
</script>
</body>
</html>