Stellenseite mit Gehalt, Ablauf und JobPosting-Daten
Eine Stelle als eigene Seite: Gehaltsspanne auf dem Zettel, drei Blöcke mit Zahlen, Ablauf mit Fristen, Ansprechperson und daneben die Kurzbewerbung; JobPosting-Daten aus demselben Datensatz, beim Vergeben entfernt.
Handwerksbetrieb oder Haustechnik: Stundenlohn als Spanne, Einsatzorte und Baustelle, Führerschein, bezahlter Probetag, Rufbereitschaft mit Pauschale.
Eine einzelne Stelle als eigene Seite. Oben stehen Titel, Ort, Stunden, Gehaltsspanne und Start, darunter drei Blöcke mit konkreten Zahlen: Das machst du, das bringst du mit, das bekommst du. Es folgt der Ablauf der Bewerbung mit Fristen und die Ansprechperson mit Telefon und WhatsApp. Daneben steht die Kurzbewerbung für genau diese Stelle, auf dem Desktop mitlaufend. Die JobPosting-Daten für Google werden aus demselben Datensatz erzeugt wie der sichtbare Text. Ist die Stelle vergeben, verschwinden Formular und Daten, und es erscheinen ähnliche Stellen und ein Job-Alert.
Wer einen Job sucht, will in einer Minute wissen: Was tue ich, was verdiene ich, wann, wo und wer meldet sich. Eine Gehaltsspanne in Tabellenziffern und ein Ablauf mit echten Fristen beantworten das und wirken ehrlich. Die Kurzbewerbung daneben senkt die Hürde, weil niemand erst ein Anschreiben braucht. Saubere JobPosting-Daten bringen die Stelle in Google for Jobs, und weil sie zum sichtbaren Text passen, gibt es keine abgelaufenen oder falschen Einträge.
Wann einsetzen
Jede offene Stelle eines kleinen oder mittleren Betriebs: Handwerk mit Stundenlohn und Probetag, Pflege mit Tarif, Schichten und Touren, Ausbildung mit Vergütung je Ausbildungsjahr. Als Ziel jeder Zeile in der Stellenübersicht (stellen-karriere).
Wann nicht
Für die Übersicht mehrerer Stellen: JobPosting gehört laut Google nur auf die Seite einer einzelnen Stelle, die Übersicht ist stellen-karriere. Wenn es keine echte Stelle gibt (Initiativbewerbung, Talentpool): dann ohne JobPosting-Daten. Wenn Gehalt oder Ablauf nicht genannt werden dürfen oder nicht feststehen: erst klären, sonst verliert die Seite ihren Kern.
Varianten
- Handwerk: Stundenlohn als Spanne, Einsatzorte und Baustellen, Führerschein, bezahlter Probetag
- Pflege: Tarif mit Entgeltgruppe statt freier Spanne, Vollzeit und Teilzeit mit zwei Gehaltsangaben, Schichten, Touren und Dienstwagen
- Ausbildung: Vergütung je Ausbildungsjahr, Ausbildungsstart, Berufsschule, Praktikum zum Reinschnuppern, Info für Eltern
Umsetzung
Ein Datensatz je Stelle (Titel, Ort, Stunden, Gehalt mit Bezug, Start, Aufgaben, Anforderungen, Leistungen, Ablauf mit Fristen, Ansprechperson, datePosted, validThrough, Status). Daraus entstehen die sichtbare Seite und das JSON-LD JobPosting im <head>: description ist das HTML derselben Blöcke, baseSalary ein MonetaryAmount mit minValue, maxValue und unitText (HOUR oder MONTH), jobLocation ein Place mit PostalAddress und addressCountry DE, identifier die Stellennummer, directApply true, weil die Kurzbewerbung auf der Seite steht. Gehalt in Tabellenziffern mit Bezug und „brutto“; bei Tarif die Entgeltgruppe, bei Teilzeit eine zweite Zeile, bei Ausbildung je Jahr. Kurzbewerbung mit höchstens fünf Pflichtangaben, Lebenslauf freiwillig, keine Frage nach dem bisherigen Gehalt; Fehler erst nach der Interaktion, beim Absenden eine Fehlerliste mit Sprunglinks. Die Bestätigung nennt Name der Ansprechperson und das Datum, bis wann sie sich meldet. Auf dem Desktop läuft die Bewerbung mit position: sticky mit (höchstens so hoch wie der Bildschirm), auf dem Telefon steht sie unten und eine feste Leiste führt hin. Vergeben oder abgelaufen: JSON-LD entfernen (oder validThrough in der Vergangenheit, oder 404/410), Hinweis oben, ähnliche Stellen und Job-Alert mit Double-Opt-in statt Formular. „Stelle teilen“ kopiert den Link mit sichtbarer Rückmeldung. In der Demo sind Übersicht, ähnliche Stellen und WhatsApp simuliert. Texte AGG-neutral; Datenschutz nach Art. 13 DSGVO, Speicherdauer und die deutsche Umsetzung der Entgelttransparenzrichtlinie vom Kunden prüfen lassen.
Robust umsetzen
Telefon. Einspaltig: Kopf mit Gehalt, dann die drei Blöcke, Ablauf und Ansprechperson, die Kurzbewerbung am Ende. Eine feste Leiste unten führt mit „In 2 Minuten bewerben“ zum Formular und hat einen Anruf-Knopf. Alle Felder und Knöpfe mindestens 44 px hoch, Eingaben mit passenden autocomplete- und inputmode-Werten.
Ohne Bewegung. Zettel und Haken stehen sofort, Füllung des Knopfs als kurzes Überblenden, Zustandswechsel ohne Übergang.
Barrierefrei. Eckdaten als <dl>, Ablauf als <ol>. Formular mit sichtbaren Labels, Pflichtfelder als „Pflicht“ im Label und mit required, Fehlermeldungen per aria-describedby und aria-invalid, Fehlerliste beim Absenden mit Fokus und Links zu den Feldern (WCAG 3.3.1, 3.3.3). Erfahrungsauswahl als echte Radio-Gruppe in einem fieldset. Bestätigung und Kopier-Rückmeldung in einer Live-Region. Gehalt nicht nur als Zahl, sondern mit Bezug und „brutto“ ausgeschrieben. Kontrast mindestens 4,5:1; Klatschmohn als Fläche mit weißer Schrift.
Code
<!DOCTYPE html>
<!--
Stellenseite mit Gehalt, Ablauf und JobPosting-Daten
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/stellenseite-mit-jobposting/
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>Stellenseite mit Gehalt, Ablauf und JobPosting-Daten · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<!-- JobPosting nur auf der Seite einer einzelnen Stelle, erzeugt aus demselben Datensatz wie der sichtbare Text (siehe STELLENDATEN unten). -->
<script type="application/ld+json" id="jobposting">{"@context":"https://schema.org","@type":"JobPosting","title":"Elektroniker für Energie- und Gebäudetechnik (m/w/d)","description":"<p>Drei Häuser, 140 Bewohnerinnen und Bewohner und eine Baustelle: Du kümmerst dich um alles, was Strom hat, und planst deine Woche selbst.</p><p>Stundenlohn: 19,80 € bis 24,50 € pro Stunde, brutto.</p><h3>Das machst du</h3><ul><li>Du hältst Elektrik, Notstrom, Lichtruf und Brandmeldeanlage in <b>drei Häusern</b> in Schuss.</li><li>Die Prüfung ortsveränderlicher Geräte planst und machst du selbst: rund <b>1.200 Geräte</b> im Jahr.</li><li>Auf der Baustelle der neuen Tagespflege (fertig im <b>Sommer 2027</b>) stimmst du dich mit zwei Fachfirmen ab.</li><li>Störungen kommen per Tablet. Was dringend ist, entscheidest du mit der Pflege vor Ort.</li></ul><h3>Das bringst du mit</h3><ul><li>Gesellenbrief als Elektroniker:in oder eine gleichwertige Ausbildung.</li><li>Führerschein Klasse B: Zwischen den Häusern fährst du unseren Dienstwagen (höchstens <b>6 km</b>).</li><li>Deutsch gut genug für Absprachen mit Pflege, Bewohnerinnen, Bewohnern und Firmen.</li></ul><h3>Das bekommst du</h3><ul><li><b>19,80 bis 24,50 €</b> pro Stunde brutto, je nach Erfahrung. Rufbereitschaft eine Woche von sechs: <b>180 €</b> Pauschale, Einsätze zählen als Arbeitszeit.</li><li><b>30 Tage</b> Urlaub, freitags um 14:30 Uhr Feierabend.</li><li>Jobrad: Wir zahlen <b>40 €</b> im Monat dazu.</li><li>Weiterbildung: <b>800 €</b> im Jahr und <b>3</b> Bildungstage.</li><li>Betriebliche Altersvorsorge: Wir zahlen <b>4 %</b> vom Bruttolohn ein.</li></ul><p><b>Was es nicht gibt:</b> Montage außerhalb, Übernachtungen, Wochenendarbeit außerhalb der Rufbereitschaft.</p>","identifier":{"@type":"PropertyValue","name":"Haus Brückenfeld","value":"HT-2611"},"datePosted":"2026-10-26","validThrough":"2026-12-31T23:59:59+01:00","employmentType":"FULL_TIME","hiringOrganization":{"@type":"Organization","name":"Haus Brückenfeld Pflege und Wohnen gGmbH","sameAs":"https://haus-brueckenfeld.example/"},"jobLocation":{"@type":"Place","address":{"@type":"PostalAddress","streetAddress":"Am Brückenfeld 12","addressLocality":"Bad Hersfeld","postalCode":"36251","addressRegion":"Hessen","addressCountry":"DE"}},"baseSalary":{"@type":"MonetaryAmount","currency":"EUR","value":{"@type":"QuantitativeValue","minValue":19.8,"maxValue":24.5,"unitText":"HOUR"}},"directApply":true}</script>
<style>
@font-face { font-family: 'Funnel Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-display:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Funnel Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-sans:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; 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 {
--kreide: #f6f3ec; --tinte: #161616; --leise: #5a5650; --zettel: #efe6d2; --tanne: #1d4a3a; --mohn: #c8382a; --mohn-druck: #a92e22;
--karte: #fffdf8; --linie: rgba(22, 22, 22, .16);
--auf: cubic-bezier(0.16, 1, 0.3, 1); /* UI öffnen */
--zu: cubic-bezier(0.7, 0, 0.84, 0); /* UI schließen */
--feder: cubic-bezier(0.34, 1.56, 0.64, 1);/* kleine ankommende Teile */
--t-auf: 280ms; --t-zu: 190ms;
--rand: clamp(16px, 4vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
[hidden] { display: none !important; }
html { background: var(--kreide); color: var(--tinte); font-family: 'Funnel Sans', system-ui, sans-serif; -webkit-text-size-adjust: 100%; }
body { min-height: 100svh; font-size: 1.125rem; line-height: 1.6; }
:root:has(:focus-visible) { scroll-behavior: auto; }
:focus-visible { outline: 3px solid var(--tinte); outline-offset: 3px; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
a { color: inherit; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.zahl, .tnum { font-variant-numeric: tabular-nums; }
.platzhalter { background: #fff1c2; box-shadow: 0 0 0 1px #b58f12 inset; border-radius: 3px; padding: 0 4px; color: var(--tinte); font-style: normal; }
/* Kopf */
.kopf { border-bottom: 1px solid var(--linie); }
.kopf-innen { max-width: 1280px; margin: 0 auto; padding: 14px var(--rand); display: flex; flex-wrap: wrap; align-items: center; gap: 10px 24px; }
.logo { text-decoration: none; display: flex; align-items: baseline; gap: 10px; margin-right: auto; }
.logo b { font: 700 1.375rem/1 'Funnel Display', sans-serif; letter-spacing: -.02em; }
.logo span { font-size: .9375rem; color: var(--leise); }
.kopf .tel { font-weight: 700; font-variant-numeric: tabular-nums; text-decoration: none; min-height: 44px; display: inline-flex; align-items: center; gap: 8px; }
.ico { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
/* Pfeil-Link (pfeil-icon-button, Variante linie) */
.pfeil-link { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; font-weight: 600; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .35ex; }
.pfeil-link svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; overflow: visible; }
.pfeil-link .schaft { opacity: 0; transition: opacity 175ms cubic-bezier(0.5, 0, 0.75, 0); }
.pfeil-link .spitze { transition: transform 175ms cubic-bezier(0.5, 0, 0.75, 0); }
.pfeil-link:hover .schaft, .pfeil-link:focus-visible .schaft { opacity: 1; transition: opacity 250ms cubic-bezier(0.25, 1, 0.5, 1); }
.pfeil-link:hover .spitze, .pfeil-link:focus-visible .spitze { transform: translateX(3px); transition: transform 250ms cubic-bezier(0.25, 1, 0.5, 1); }
.pfeil-link.zurueck svg { transform: scaleX(-1); }
.krumen { max-width: 1280px; margin: 0 auto; padding: 14px var(--rand) 0; font-size: .9375rem; color: var(--leise); }
.krumen ol { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 8px; }
.krumen li + li::before { content: '/'; margin-right: 8px; }
.krumen a { color: var(--leise); }
/* Seite: Inhalt links, Kurzbewerbung rechts */
.seite { max-width: 1280px; margin: 0 auto; padding: clamp(20px, 3vw, 36px) var(--rand) clamp(48px, 7vw, 96px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 400px); gap: clamp(28px, 4.5vw, 72px); align-items: start; }
.inhalt { display: grid; gap: clamp(36px, 5vw, 64px); min-width: 0; }
/* Stellenkopf */
.status { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; font-size: .9375rem; color: var(--leise); }
.status b { color: var(--tanne); display: inline-flex; align-items: center; gap: 7px; }
.status b::before { content: ''; width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
.vergeben .status b { color: var(--tinte); }
.vergeben .status b::before { background: transparent; border: 2px solid currentColor; }
.stellenkopf h1 { font: 700 clamp(2.25rem, 1.35rem + 2.7vw, 3.875rem)/.98 'Funnel Display', sans-serif; letter-spacing: -.03em; margin-top: 14px; text-wrap: balance; hyphens: manual; }
.intro { margin-top: 18px; max-width: 58ch; font-size: 1.1875rem; text-wrap: pretty; }
.zettel { position: relative; margin-top: clamp(22px, 3vw, 32px); background: var(--zettel); border-radius: 4px; padding: clamp(18px, 2.4vw, 28px); transform: rotate(-.6deg); box-shadow: 0 1px 0 rgba(22, 22, 22, .08), 0 18px 32px -24px rgba(22, 22, 22, .45); }
.zettel::before { content: ''; position: absolute; top: -10px; left: 50%; width: 92px; height: 22px; translate: -50% 0; rotate: 2deg; background: rgba(255, 253, 248, .72); box-shadow: 0 1px 2px rgba(22, 22, 22, .12); }
.zettel.landet { animation: landen 500ms var(--feder) both, einblenden 200ms linear both; }
.pflegeton .zettel.landet { animation-timing-function: var(--auf), linear; }
@keyframes landen { from { transform: translateY(24px) rotate(-3deg); } to { transform: rotate(-.6deg); } }
@keyframes einblenden { from { opacity: 0; } }
.gehalt { display: grid; gap: 4px; padding-bottom: 16px; border-bottom: 1px dashed rgba(22, 22, 22, .35); }
.gehalt .label { font-size: .875rem; font-weight: 600; color: var(--leise); }
.gehalt-zahl { font: 650 clamp(1.875rem, 1.3rem + 1.8vw, 2.75rem)/1.05 'Funnel Display', sans-serif; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.gehalt-bezug { font-size: 1rem; font-weight: 600; }
.gehalt-extra { list-style: none; padding: 0; margin-top: 8px; display: grid; gap: 4px; font-size: 1rem; }
.gehalt-extra li { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 16px; font-variant-numeric: tabular-nums; }
.gehalt-extra li b { font-weight: 700; }
.gehalt-tarif { font-size: .9375rem; color: var(--leise); margin-top: 4px; }
.fakten { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 24px; padding-top: 16px; }
.fakten dt { font-size: .875rem; font-weight: 600; color: var(--leise); }
.fakten dd { font-size: 1rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.teilen-zeile { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin-top: 22px; font-size: .9375rem; color: var(--leise); }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 16px; border: 2px solid var(--tinte); border-radius: 12px; background: transparent; font-weight: 700; font-size: 1rem; text-decoration: none; transition: background-color 150ms linear, color 150ms linear, transform 150ms var(--feder); }
.knopf:hover { background: var(--tinte); color: var(--kreide); }
.knopf:active { transform: scale(.95); }
.teilen-rueck { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--tanne); }
.teilen-rueck svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
/* Vergeben-Hinweis */
.vergeben-hinweis { margin-top: 22px; background: var(--karte); border: 2px solid var(--tinte); border-radius: 12px; padding: clamp(16px, 2.2vw, 24px); display: grid; gap: 10px; }
.vergeben-hinweis h2 { font: 650 1.625rem/1.1 'Funnel Display', sans-serif; letter-spacing: -.02em; }
.vergeben-hinweis ul { list-style: none; padding: 0; display: grid; }
.vergeben .zettel { opacity: .9; }
/* Blöcke */
.block h2, .ablauf h2, .person h2, .google h2 { font: 600 clamp(1.75rem, 1.3rem + 1.6vw, 2.5rem)/1.05 'Funnel Display', sans-serif; letter-spacing: -.02em; }
.block ul { margin-top: 14px; padding: 0; list-style: none; display: grid; gap: 10px; max-width: 62ch; }
.block li { position: relative; padding-left: 26px; }
.block li::before { content: ''; position: absolute; left: 2px; top: .62em; width: 12px; height: 2px; background: var(--tanne); }
.block b { font-weight: 700; font-variant-numeric: tabular-nums; }
.nicht { margin-top: 16px; padding: 12px 16px; background: var(--zettel); border-radius: 4px; font-style: italic; max-width: 62ch; }
.nicht b { font-style: normal; }
/* Ablauf auf Tannengrün */
.ablauf { background: var(--tanne); color: var(--kreide); border-radius: 16px; padding: clamp(22px, 3.2vw, 40px); }
.ablauf h2 { color: var(--kreide); }
.ablauf ol { list-style: none; padding: 0; margin-top: 22px; display: grid; gap: 0; counter-reset: s; }
.ablauf li { position: relative; display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 4px 16px; padding-bottom: 20px; counter-increment: s; }
.ablauf li::before { content: counter(s); grid-row: span 2; width: 40px; height: 40px; border-radius: 50%; background: var(--kreide); color: var(--tanne); display: grid; place-items: center; font: 700 1.125rem/1 'Funnel Display', sans-serif; }
.ablauf li:not(:last-child)::after { content: ''; position: absolute; left: 19px; top: 44px; bottom: 4px; width: 2px; background: rgba(246, 243, 236, .35); }
.ablauf li b { font-size: 1.125rem; }
.ablauf li span { color: #e3ebe6; font-size: 1rem; }
.ablauf .frist { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--kreide); }
/* Ansprechperson */
.person { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 18px 24px; align-items: start; }
.person h2 { grid-column: 1 / -1; }
.portraet { width: clamp(96px, 12vw, 132px); aspect-ratio: 1; border-radius: 50%; background: var(--zettel); display: grid; place-items: center; position: relative; overflow: hidden; border: 2px solid var(--tinte); }
.portraet b { font: 700 2.5rem/1 'Funnel Display', sans-serif; letter-spacing: -.02em; }
.person-text { display: grid; gap: 8px; justify-items: start; }
.person-text .name { font: 650 1.5rem/1.1 'Funnel Display', sans-serif; }
.person-text .rolle { color: var(--leise); font-size: 1rem; }
.person-text .knoepfe { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
.person-text small { font-size: .875rem; color: var(--leise); max-width: 52ch; }
/* JobPosting-Daten sichtbar machen */
.google { border-top: 2px solid var(--tinte); padding-top: 18px; }
.google summary { cursor: pointer; font-weight: 700; min-height: 44px; display: flex; align-items: center; gap: 10px; }
.google summary::marker { content: ''; }
.google summary::before { content: '+'; display: grid; place-items: center; width: 28px; height: 28px; border: 2px solid var(--tinte); border-radius: 50%; font: 700 1rem/1 'Funnel Sans', sans-serif; transition: rotate 280ms var(--auf); }
.google details[open] summary::before { rotate: 45deg; }
.google .pruefliste { list-style: none; padding: 0; margin: 12px 0; display: flex; flex-wrap: wrap; gap: 6px; font-size: .875rem; }
.google .pruefliste li { border: 1.5px solid var(--tanne); color: var(--tanne); border-radius: 999px; padding: 2px 10px; font-weight: 600; }
.google .pruefliste li.fehlt { border-color: var(--leise); color: var(--leise); text-decoration: line-through; }
.google pre { background: var(--tinte); color: #ecebe6; border-radius: 10px; padding: 16px; overflow: auto; max-height: 360px; font: 400 .8125rem/1.55 'Geist Mono', ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.google p { font-size: .9375rem; color: var(--leise); }
/* Kurzbewerbung */
.seitenleiste { position: sticky; top: 20px; max-height: calc(100vh - 40px); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; padding: 4px; margin: -4px; display: grid; gap: 14px; min-width: 0; }
.bewerbung { background: var(--karte); border: 2px solid var(--tinte); border-radius: 16px; padding: clamp(18px, 2.2vw, 26px); display: grid; gap: 16px; scroll-margin-top: 20px; }
.bewerbung > header .klein { font-size: .875rem; font-weight: 600; color: var(--mohn-druck); letter-spacing: .02em; }
.bewerbung h2 { font: 650 1.75rem/1.05 'Funnel Display', sans-serif; letter-spacing: -.02em; margin-top: 4px; }
.bewerbung > header p { font-size: 1rem; color: var(--leise); margin-top: 6px; }
.feld { display: grid; gap: 6px; }
.feld > label, .feld legend { font-weight: 700; font-size: 1rem; }
.feld .pflicht { font-weight: 500; color: var(--leise); font-size: .875rem; }
.feld input[type=text], .feld input[type=email], .feld input[type=tel], .feld select { min-height: 48px; padding: 0 14px; border: 2px solid #8b867e; border-radius: 10px; background: #fff; font-size: 1rem; width: 100%; transition: border-color 150ms linear; }
.feld select { appearance: none; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23161616' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 14px center; padding-right: 40px; }
.feld input:hover, .feld select:hover { border-color: var(--tinte); }
.feld input:focus-visible, .feld select:focus-visible { outline: 3px solid var(--tinte); outline-offset: 1px; border-color: var(--tinte); }
.feld [aria-invalid="true"] { border-color: var(--mohn-druck) !important; }
.feld fieldset { border: 0; padding: 0; min-width: 0; display: grid; gap: 8px; }
.feld legend { margin-bottom: 8px; padding: 0; }
.wahl { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 8px 12px; border: 2px solid #8b867e; border-radius: 10px; background: #fff; font-size: 1rem; line-height: 1.3; cursor: pointer; transition: border-color 150ms linear, background-color 150ms linear; }
.wahl:hover { border-color: var(--tinte); }
.wahl input { width: 20px; height: 20px; accent-color: var(--tanne); flex: none; margin: 0; }
.wahl:has(input:checked) { border-color: var(--tanne); background: #eef3ef; }
.wahl:has(input:focus-visible) { outline: 3px solid var(--tinte); outline-offset: 2px; }
.wahl input:focus-visible { outline: none; }
.reihe { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.datei { font-size: .9375rem; }
.datei input { width: 100%; font-size: .9375rem; }
.datei input::file-selector-button { min-height: 44px; margin-right: 10px; padding: 0 14px; border: 2px solid var(--tinte); border-radius: 10px; background: transparent; font-weight: 700; cursor: pointer; }
.fehler { display: flex; gap: 6px; align-items: flex-start; color: var(--mohn-druck); font-size: .9375rem; font-weight: 600; line-height: 1.4; }
.fehler::before { content: '!'; flex: none; width: 18px; height: 18px; margin-top: 1px; border-radius: 50%; background: var(--mohn-druck); color: #fff; display: grid; place-items: center; font-size: .75rem; font-weight: 800; }
.hinweis { font-size: .875rem; color: var(--leise); line-height: 1.45; }
.fehlerliste { border: 2px solid var(--mohn-druck); border-left-width: 6px; border-radius: 10px; padding: 12px 14px; background: #fdf1ef; display: grid; gap: 6px; animation: oeffnen var(--t-auf) var(--auf); }
.fehlerliste h3 { font-size: 1.0625rem; }
.fehlerliste ul { padding-left: 1.1em; font-size: .9375rem; }
.fehlerliste a { color: var(--mohn-druck); font-weight: 600; }
@keyframes oeffnen { from { opacity: 0; transform: translateY(-4px); } }
/* Bewerben-Knopf: Füllung vom Eintrittspunkt (fuellung-vom-eintrittspunkt, basis) */
.bewerben { position: relative; overflow: hidden; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; width: 100%; min-height: 54px; padding: 0 20px; border: 0; border-radius: 12px; background: var(--mohn); color: #fff; font: 700 1.0625rem/1.2 'Funnel Sans', sans-serif; transition: transform 150ms var(--feder), background-color 150ms linear; }
.bewerben .flaeche { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center; padding: 0 20px; background: var(--tinte); color: #fff; clip-path: circle(0px at 50% 50%); transition: clip-path 600ms var(--auf); pointer-events: none; }
.bewerben:active { transform: scale(.95); }
.bewerben:focus-visible { outline: 3px solid var(--tinte); outline-offset: 3px; }
.bewerben:focus-visible .flaeche { clip-path: circle(150% at 50% 50%); }
a.bewerben { text-decoration: none; }
.anders { display: grid; gap: 8px; padding-top: 14px; border-top: 1px solid var(--linie); font-size: .9375rem; }
.anders .knoepfe { display: flex; flex-wrap: wrap; gap: 8px; }
/* Bestätigung */
.danke { display: grid; gap: 12px; justify-items: start; animation: oeffnen var(--t-auf) var(--auf); }
.danke h2 { font: 650 1.75rem/1.05 'Funnel Display', sans-serif; letter-spacing: -.02em; }
.haken { width: 64px; height: 64px; }
.haken circle { fill: var(--tanne); }
.haken path { fill: none; stroke: #fff; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 40; stroke-dashoffset: 0; }
.danke.neu .haken { animation: haken-rein 400ms var(--feder) both; }
.danke.neu .haken path { animation: haken-strich 400ms var(--auf) 120ms both; }
@keyframes haken-rein { from { transform: scale(.6); } }
@keyframes haken-strich { from { stroke-dashoffset: 40; } }
.danke .wer { display: flex; align-items: center; gap: 12px; }
.danke .wer .portraet { width: 56px; border-width: 1.5px; }
.danke .wer .portraet b { font-size: 1.25rem; }
.danke dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 14px; font-size: .9375rem; }
.danke dt { color: var(--leise); }
/* Job-Alert (Stelle vergeben) */
.alert-form { display: grid; gap: 14px; }
/* Mobile Leiste */
.mobilleiste { display: none; }
/* Toast */
.toast { position: fixed; left: 50%; bottom: 20px; z-index: 30; translate: -50% 0; max-width: min(520px, calc(100vw - 32px)); background: var(--tinte); color: var(--kreide); border-radius: 10px; padding: 12px 16px; font-size: .9375rem; opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity var(--t-zu) linear, transform var(--t-zu) var(--zu); }
.toast.an { opacity: 1; transform: none; transition: opacity 200ms linear, transform var(--t-auf) var(--auf); }
/* Gedachter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%; border: 2px solid var(--tinte); background: rgba(200, 56, 42, .35); pointer-events: none; opacity: 0; transition: transform 700ms var(--auf), opacity 200ms linear; }
.vorfuehrung .zeiger { opacity: 1; }
.zeiger.drueckt { scale: .78; }
/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 8px 10px 8px 14px; max-width: min(560px, calc(100vw - 32px)); background: rgba(22, 22, 22, .94); color: #efece5; border-radius: 6px; box-shadow: 0 10px 30px -12px rgba(0, 0, 0, .5); }
.pult, .pult button { font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.pult .gruppe { display: flex; flex-wrap: wrap; gap: 2px; }
.pult button { color: rgba(239, 236, 229, .78); background: transparent; border: 0; padding: 8px 9px; border-radius: 3px; transition: background-color 150ms linear, color 150ms linear; }
.pult button[aria-pressed="true"] { background: #efece5; color: var(--tinte); }
.pult button:focus-visible { outline: 2px solid #efece5; outline-offset: 2px; }
.pult .werte { text-transform: none; letter-spacing: .02em; color: rgba(239, 236, 229, .72); }
@media (max-width: 960px) {
.seite { grid-template-columns: minmax(0, 1fr) minmax(0, 340px); }
}
@media (max-width: 860px) {
.seite { grid-template-columns: minmax(0, 1fr); }
.seitenleiste { position: static; max-height: none; overflow: visible; }
.mobilleiste { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 15; gap: 10px; padding: 10px var(--rand) calc(10px + env(safe-area-inset-bottom)); background: var(--kreide); border-top: 1px solid var(--linie); box-shadow: 0 -10px 24px -18px rgba(22, 22, 22, .5); }
.mobilleiste .bewerben { flex: 1; min-height: 50px; }
.mobilleiste .knopf { width: 50px; min-height: 50px; padding: 0; }
body { padding-bottom: 76px; }
.vergeben .mobilleiste, .gesendet .mobilleiste { display: none; }
.pult { bottom: 84px; }
}
@media (max-width: 560px) {
.logo span { display: none; }
.kopf .tel { display: none; }
.fakten { grid-template-columns: minmax(0, 1fr); }
.person { grid-template-columns: minmax(0, 1fr); }
.reihe { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 360px) {
.bewerbung { padding: 16px 14px; }
.zettel { padding: 16px 14px; }
}
/* Vorschau 960 × 720 */
.vorschau body { overflow: hidden; padding-bottom: 0; }
.vorschau .kopf, .vorschau .krumen, .vorschau .pult, .vorschau .mobilleiste, .vorschau fundus-pult-knopf, .vorschau .intro { display: none !important; }
.vorschau .seite { padding-top: 22px; grid-template-columns: minmax(0, 1fr) minmax(0, 360px); gap: 32px; }
.vorschau .stellenkopf h1 { font-size: 2.5rem; }
.vorschau .seitenleiste { top: 16px; }
.vorschau .bewerbung { gap: 12px; padding: 18px 20px; }
.vorschau .bewerbung > header p, .vorschau .bewerbung .hinweis, .vorschau .bewerbung .datei, .vorschau .bewerbung .anders { display: none; }
.vorschau .bewerbung h2 { font-size: 1.5rem; }
.vorschau .wahl { min-height: 40px; padding: 6px 12px; }
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-delay: 0s !important; }
:root.reduziert .bewerben .flaeche { clip-path: none !important; opacity: 0; transition: opacity 150ms linear !important; }
:root.reduziert .bewerben.gefuellt .flaeche, :root.reduziert .bewerben:focus-visible .flaeche { opacity: 1; }
:root.reduziert .zeiger { transition: opacity 200ms linear !important; }
@media (hover: none) {
.knopf:hover { background: transparent; color: var(--tinte); }
.knopf:active { background: var(--tinte); color: var(--kreide); }
.bewerben:active { background: var(--mohn-druck); }
.pfeil-link .schaft { opacity: 1; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.bewerben .flaeche { display: none; }
.wahl { border: 1px solid ButtonText; }
}
</style>
</head>
<body>
<header class="kopf">
<div class="kopf-innen">
<a class="logo" href="#stelle" aria-label="Haus Brückenfeld, Karriere"><b>Haus Brückenfeld</b><span>Karriere · Pflege und Wohnen in Bad Hersfeld</span></a>
<a class="pfeil-link zurueck" href="#stelle" data-demo-link="Alle offenen Stellen"><svg viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h13"/><path class="spitze" d="M13 6l6 6-6 6"/></svg>Alle Stellen</a>
<a class="tel" href="tel:+4966210503980"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg><span class="nummer">06621 050398-0</span><span class="nur-sr">Anrufen</span></a>
</div>
</header>
<nav class="krumen" aria-label="Brotkrümel">
<ol><li><a href="#stelle" data-demo-link="Karriere">Karriere</a></li><li><a href="#stelle" data-demo-link="Offene Stellen">Offene Stellen</a></li><li aria-current="page" id="krumen-bereich">Haustechnik</li></ol>
</nav>
<main class="seite" id="stelle">
<article class="inhalt" aria-labelledby="titel">
<header class="stellenkopf">
<p class="status" id="status"></p>
<h1 id="titel"></h1>
<p class="intro" id="intro"></p>
<div class="vergeben-hinweis" id="vergeben-hinweis" hidden>
<h2>Diese Stelle ist vergeben.</h2>
<p>Danke für dein Interesse! Diese Stellen sind gerade offen:</p>
<ul id="aehnlich"></ul>
</div>
<div class="zettel" id="zettel">
<div class="gehalt">
<span class="label" id="gehalt-label">Gehalt</span>
<span class="gehalt-zahl" id="gehalt-zahl"></span>
<span class="gehalt-bezug" id="gehalt-bezug"></span>
<ul class="gehalt-extra" id="gehalt-extra"></ul>
<p class="gehalt-tarif" id="gehalt-tarif"></p>
</div>
<dl class="fakten" id="fakten"></dl>
</div>
<div class="teilen-zeile">
<button class="knopf" type="button" id="teilen"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>Stelle teilen</button>
<span class="teilen-rueck" id="teilen-rueck" role="status" aria-live="polite"></span>
<span id="stellennr"></span>
</div>
</header>
<section class="block" aria-labelledby="h-machen"><h2 id="h-machen">Das machst du</h2><ul id="machen"></ul></section>
<section class="block" aria-labelledby="h-mitbringen"><h2 id="h-mitbringen">Das bringst du mit</h2><ul id="mitbringen"></ul></section>
<section class="block" aria-labelledby="h-bekommen"><h2 id="h-bekommen">Das bekommst du</h2><ul id="bekommen"></ul><p class="nicht" id="nicht"></p></section>
<section class="ablauf" aria-labelledby="h-ablauf"><h2 id="h-ablauf">So läuft es ab</h2><ol id="ablauf"></ol></section>
<section class="person" aria-labelledby="h-person">
<h2 id="h-person">Deine Ansprechperson</h2>
<div class="portraet" role="img" id="portraet" aria-label="Platzhalter für das Foto der Ansprechperson"><b id="initialen"></b></div>
<div class="person-text">
<span class="name" id="p-name"></span>
<span class="rolle" id="p-rolle"></span>
<div class="knoepfe">
<a class="knopf" id="p-tel" href="tel:+4966210503980"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg><span class="tnum" id="p-telnr"></span></a>
<a class="knopf" href="#stelle" data-demo-link="WhatsApp-Chat mit der Ansprechperson"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l1.4-4A8 8 0 1 1 8 18.6z"/></svg>WhatsApp</a>
</div>
<small id="p-zeiten"></small>
<small>Foto: <span class="platzhalter">echtes Foto mit schriftlicher Einwilligung</span>. WhatsApp: Daten gehen an den Anbieter des Dienstes; <span class="platzhalter">Hinweis im Datenschutz, vom Kunden prüfen lassen</span>.</small>
</div>
</section>
<section class="google" aria-labelledby="h-google">
<details>
<summary id="h-google">JobPosting-Daten dieser Seite (für Google)</summary>
<ul class="pruefliste" id="pruefliste" aria-label="Felder im JobPosting"></ul>
<p id="google-text"></p>
<pre tabindex="0" role="region" aria-label="JSON-LD dieser Seite"><code id="ld-anzeige"></code></pre>
</details>
</section>
</article>
<aside class="seitenleiste" aria-label="Bewerbung">
<section class="bewerbung" id="bewerbung" aria-labelledby="h-bewerbung" tabindex="-1">
<!-- Formular, Bestätigung oder Job-Alert: per Skript -->
</section>
</aside>
</main>
<div class="mobilleiste" id="mobilleiste">
<a class="bewerben" href="#bewerbung" id="zur-bewerbung"><span>In 2 Minuten bewerben</span><span class="flaeche" aria-hidden="true">In 2 Minuten bewerben</span></a>
<a class="knopf" href="tel:+4966210503980" id="leiste-tel" aria-label="Anrufen"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg></a>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="zeiger" id="zeiger" aria-hidden="true"></div>
<div class="pult">
<span>Variante</span>
<div class="gruppe" role="group" aria-label="Variante der Demo">
<button type="button" data-v="handwerk" aria-pressed="true">Handwerk</button>
<button type="button" data-v="pflege" aria-pressed="false">Pflege</button>
<button type="button" data-v="ausbildung" aria-pressed="false">Ausbildung</button>
</div>
<span>Stelle</span>
<div class="gruppe" role="group" aria-label="Zustand der Stelle">
<button type="button" data-zustand="offen" aria-pressed="true">Offen</button>
<button type="button" data-zustand="vergeben" aria-pressed="false">Vergeben</button>
</div>
<span class="werte" id="werte"></span>
</div>
<script id="stellendaten">
/* STELLENDATEN: ein Datensatz je Stelle. Daraus entstehen Seite und JSON-LD (deckungsgleich). Alles erfunden. */
const BETRIEB = {
name: 'Haus Brückenfeld Pflege und Wohnen gGmbH', kurz: 'Haus Brückenfeld', url: 'https://haus-brueckenfeld.example/',
strasse: 'Am Brückenfeld 12', plz: '36251', ort: 'Bad Hersfeld', region: 'Hessen', land: 'DE',
};
const STELLEN = {
handwerk: {
nr: 'HT-2611', bereich: 'Haustechnik',
titel: 'Elektroniker für Energie- und Gebäudetechnik (m/w/d)',
intro: 'Drei Häuser, 140 Bewohnerinnen und Bewohner und eine Baustelle: Du kümmerst dich um alles, was Strom hat, und planst deine Woche selbst.',
datePosted: '2026-10-26', validThrough: '2026-12-31T23:59:59+01:00', employmentType: ['FULL_TIME'],
lohn: { min: 19.8, max: 24.5, einheit: 'HOUR', nachkomma: 2, bezug: 'pro Stunde, brutto', label: 'Stundenlohn', zusatz: [['je nach Erfahrung und Abschluss', ''], ['Rufbereitschaft, eine Woche von sechs', '180 € Pauschale']], tarif: '' },
fakten: [['Arbeitszeit', '39 Std./Woche: Mo bis Do 7:00 bis 15:30, Fr bis 14:30'], ['Ort', 'Bad Hersfeld: drei Häuser und die Baustelle Tagespflege'], ['Start', 'ab 01.01.2027 oder früher'], ['Vertrag', 'unbefristet, Vollzeit']],
machen: [
'Du hältst Elektrik, Notstrom, Lichtruf und Brandmeldeanlage in <b>drei Häusern</b> in Schuss.',
'Die Prüfung ortsveränderlicher Geräte planst und machst du selbst: rund <b>1.200 Geräte</b> im Jahr.',
'Auf der Baustelle der neuen Tagespflege (fertig im <b>Sommer 2027</b>) stimmst du dich mit zwei Fachfirmen ab.',
'Störungen kommen per Tablet. Was dringend ist, entscheidest du mit der Pflege vor Ort.',
],
mitbringen: [
'Gesellenbrief als Elektroniker:in oder eine gleichwertige Ausbildung.',
'Führerschein Klasse B: Zwischen den Häusern fährst du unseren Dienstwagen (höchstens <b>6 km</b>).',
'Deutsch gut genug für Absprachen mit Pflege, Bewohnerinnen, Bewohnern und Firmen.',
],
bekommen: [
'<b>19,80 bis 24,50 €</b> pro Stunde brutto, je nach Erfahrung. Rufbereitschaft eine Woche von sechs: <b>180 €</b> Pauschale, Einsätze zählen als Arbeitszeit.',
'<b>30 Tage</b> Urlaub, freitags um 14:30 Uhr Feierabend.',
'Jobrad: Wir zahlen <b>40 €</b> im Monat dazu.',
'Weiterbildung: <b>800 €</b> im Jahr und <b>3</b> Bildungstage.',
'Betriebliche Altersvorsorge: Wir zahlen <b>4 %</b> vom Bruttolohn ein.',
],
nicht: '<b>Was es nicht gibt:</b> Montage außerhalb, Übernachtungen, Wochenendarbeit außerhalb der Rufbereitschaft.',
ablauf: [['Kurzbewerbung', 'zwei Minuten, ohne Anschreiben', 'Eingang sofort bestätigt'], ['Anruf von Jörg', 'kurzes Gespräch, Fragen in beide Richtungen', 'innerhalb von 3 Werktagen'], ['Kennenlernen vor Ort', '45 Minuten mit Rundgang durch Technikraum und Baustelle', 'Termin nach deiner Wahl'], ['Probetag', 'ein Tag mit dem Team, bezahlt: 8 Stunden zum Stundenlohn', 'innerhalb von 2 Wochen'], ['Zusage', 'schriftlich mit Vertrag zum Durchlesen', 'spätestens 1 Woche nach dem Probetag']],
antwortTage: 3,
person: { name: 'Jörg Ruppel', vorname: 'Jörg', rolle: 'Leitung Haustechnik', tel: '06621 050398-40', telHref: '+49662105039840', zeiten: 'Erreichbar Mo bis Fr, 7 bis 15:30 Uhr.', ini: 'JR' },
frage: { name: 'erfahrung', legend: 'Deine Ausbildung', optionen: ['Gesellenbrief Elektro', 'Andere Ausbildung mit Elektro-Erfahrung', 'Ausbildung endet bis Sommer 2027'] },
extra: null,
aehnlich: [['Haustechniker:in in Teilzeit (20 Std.)', 'HT-2614'], ['Initiativbewerbung Haustechnik', 'HT-0000']],
alertBereich: 'Haustechnik',
},
pflege: {
nr: 'AP-2604', bereich: 'Ambulante Pflege',
titel: 'Pflegefachkraft in der ambulanten Pflege (m/w/d)',
intro: 'Feste Tour, feste Kundinnen und Kunden, ein Dienstplan, der sechs Wochen vorher steht. In Vollzeit oder Teilzeit.',
datePosted: '2026-10-19', validThrough: '2027-01-31T23:59:59+01:00', employmentType: ['FULL_TIME', 'PART_TIME'],
lohn: { min: 3720, max: 4310, einheit: 'MONTH', nachkomma: 0, bezug: 'brutto im Monat in Vollzeit (38,5 Std.)', label: 'Gehalt nach Haustarif', zusatz: [['Teilzeit 80 % (30,8 Std.)', '2.976 bis 3.448 €'], ['Zuschläge: Nacht 20 %, Sonntag 25 %, Feiertag 35 %', '']], tarif: 'Haustarifvertrag Brückenfeld, Entgeltgruppe 7, Stufe 1 bis 5 nach Berufsjahren.' },
fakten: [['Dienste', 'Früh 6:30 bis 13:00, Spät 16:00 bis 21:30; jedes zweite Wochenende frei'], ['Ort', 'Bad Hersfeld und sechs Orte im Umkreis von 15 km'], ['Start', 'ab sofort oder nach Absprache'], ['Vertrag', 'unbefristet, Vollzeit oder Teilzeit ab 20 Std.']],
machen: [
'Du fährst eine feste Tour mit <b>9 bis 12</b> Besuchen, mit Zeit für ein Gespräch.',
'Behandlungspflege nach ärztlicher Verordnung: Wunden, Injektionen, Kompression.',
'Du planst die Pflege mit Angehörigen und dokumentierst auf dem Tablet, nicht abends am Küchentisch.',
'Neue Kundinnen und Kunden besuchst du zum Erstgespräch gemeinsam mit der Pflegedienstleitung.',
],
mitbringen: [
'Abschluss als Pflegefachfrau oder Pflegefachmann, in der Gesundheits- und Krankenpflege oder Altenpflege.',
'Abschluss aus dem Ausland? Wir begleiten die Anerkennung und zahlen die Gebühren.',
'Führerschein Klasse B; unser Dienstwagen ist ein Kleinwagen mit Automatik.',
'Deutsch gut genug für Gespräche mit Kundinnen, Kunden und Arztpraxen.',
],
bekommen: [
'Haustarif Entgeltgruppe 7: <b>3.720 bis 4.310 €</b> brutto im Monat in Vollzeit, in Teilzeit anteilig, dazu Zuschläge.',
'Dienstwagen, auch für den Weg zur Arbeit.',
'Dienstplan <b>6 Wochen</b> im Voraus. Einspringen nur freiwillig, mit <b>30 €</b> Bonus je Dienst.',
'<b>30 Tage</b> Urlaub und <b>2</b> Tage Zusatzurlaub für Schichtarbeit.',
'Betriebliche Altersvorsorge: <b>4 %</b> vom Brutto.',
],
nicht: '<b>Was es nicht gibt:</b> Nachtdienste in der ambulanten Pflege, geteilte Dienste am selben Tag.',
ablauf: [['Kurzbewerbung', 'zwei Minuten, ohne Anschreiben', 'Eingang sofort bestätigt'], ['Anruf von Hedda', 'kurzes Gespräch, gern nach deinem Dienst', 'innerhalb von 2 Werktagen'], ['Kennenlernen', '30 Minuten vor Ort oder per Video', 'Termin nach deiner Wahl'], ['Hospitation', 'eine halbe Tour mitfahren, bezahlt', 'innerhalb von 10 Tagen'], ['Zusage', 'schriftlich mit Vertrag zum Durchlesen', 'innerhalb von 5 Werktagen']],
antwortTage: 2,
person: { name: 'Hedda Kaltwasser', vorname: 'Hedda', rolle: 'Pflegedienstleitung ambulant', tel: '06621 050398-20', telHref: '+49662105039820', zeiten: 'Erreichbar Mo bis Fr, 8 bis 14 Uhr; WhatsApp auch am Abend.', ini: 'HK' },
frage: { name: 'abschluss', legend: 'Dein Abschluss', optionen: ['Pflegefachkraft (3 Jahre)', 'Abschluss aus dem Ausland', 'Pflegehilfe (1 Jahr)'] },
extra: { name: 'umfang', legend: 'Umfang', optionen: ['Vollzeit', 'Teilzeit'] },
aehnlich: [['Pflegefachkraft stationär, Nachtdienst (m/w/d)', 'SP-2609'], ['Pflegehilfskraft ambulant (m/w/d)', 'AP-2607']],
alertBereich: 'Pflege',
},
ausbildung: {
nr: 'AZ-2704', bereich: 'Ausbildung',
titel: 'Ausbildung Pflegefachfrau oder Pflegefachmann (m/w/d)',
intro: 'Drei Jahre, drei Lernorte, eine Praxisanleiterin, die Zeit für dich hat. Start im April oder Oktober 2027.',
datePosted: '2026-10-12', validThrough: '2027-08-31T23:59:59+02:00', employmentType: ['FULL_TIME'],
lohn: { min: 1415, max: 1578, einheit: 'MONTH', nachkomma: 0, bezug: 'Ausbildungsvergütung brutto im Monat', label: 'Vergütung je Ausbildungsjahr', zusatz: [['1. Ausbildungsjahr', '1.415 €'], ['2. Ausbildungsjahr', '1.477 €'], ['3. Ausbildungsjahr', '1.578 €']], tarif: 'Nach Haustarif Brückenfeld; Dauer 3 Jahre, also 3 Vergütungsstufen.' },
fakten: [['Start', '01.04.2027 oder 01.10.2027'], ['Dauer', '3 Jahre in Vollzeit (38,5 Std.), Teilzeit möglich'], ['Schule', 'Pflegeschule Fuldatal in Bad Hersfeld, Unterricht in Blöcken'], ['Abschluss', 'Mittlerer Schulabschluss; andere Wege besprechen wir mit dir']],
machen: [
'Praxis bei uns in der stationären und ambulanten Pflege, dazu Einsätze im Krankenhaus, in der Kinderklinik und in der Psychiatrie.',
'Dunja leitet dich an, mindestens <b>10 %</b> deiner Praxiszeit ganz für dich.',
'Am Ende steht das Examen als Pflegefachfrau oder Pflegefachmann, anerkannt in der ganzen EU.',
],
mitbringen: [
'Mittlerer Schulabschluss oder ein anderer Weg, den das Pflegeberufegesetz zulässt. Wir prüfen das gern mit dir.',
'Interesse an Menschen und an Medizin.',
'Kein Führerschein nötig: Die Fahrkarte für Bus und Bahn zahlen wir.',
],
bekommen: [
'Vergütung: <b>1.415 €</b> im 1., <b>1.477 €</b> im 2. und <b>1.578 €</b> im 3. Jahr, brutto im Monat.',
'Übernahme nach bestandenem Examen in Vollzeit oder Teilzeit, wenn du willst.',
'<b>2 Stunden</b> Lernzeit pro Woche im Dienst und einen Laptop für die Schule.',
'<b>5</b> freie Tage zur Prüfungsvorbereitung.',
],
nicht: '<b>Was es nicht gibt:</b> Nachtdienste im ersten Ausbildungsjahr.',
ablauf: [['Kurzbewerbung', 'zwei Minuten, ohne Anschreiben und ohne Zeugnis', 'Eingang sofort bestätigt'], ['Anruf von Dunja', 'kurzes Gespräch, gern auch mit deinen Eltern', 'innerhalb von 3 Werktagen'], ['Praktikum zum Reinschnuppern', '1 bis 5 Tage, wann es dir passt', 'auch in den Ferien'], ['Kennenlernen', 'mit dir und, wenn du willst, deinen Eltern', 'Termin nach deiner Wahl'], ['Ausbildungsvertrag', 'zum Durchlesen und Unterschreiben', 'spätestens 6 Wochen vor Start']],
antwortTage: 3,
person: { name: 'Dunja Lüdeke', vorname: 'Dunja', rolle: 'Praxisanleiterin, Ansprechperson für Ausbildung', tel: '06621 050398-31', telHref: '+49662105039831', zeiten: 'Für Eltern: Fragen zu Vertrag, Schule und Dienstzeiten beantwortet Dunja auch euch.', ini: 'DL' },
frage: { name: 'schule', legend: 'Dein Schulabschluss', optionen: ['Mittlerer Schulabschluss', 'Abitur oder Fachabitur', 'Hauptschulabschluss', 'Noch in der Schule'] },
extra: null, praktikum: true,
aehnlich: [['Ausbildung Pflegefachassistenz (1 Jahr)', 'AZ-2706'], ['Praktikum zum Reinschnuppern', 'PR-0000']],
alertBereich: 'Ausbildung',
},
};
const euroText = (n, d = 0) => new Intl.NumberFormat('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d }).format(n) + ' €';
function stellenHtml(s) {
const liste = a => '<ul>' + a.map(x => '<li>' + x + '</li>').join('') + '</ul>';
return '<p>' + s.intro + '</p>'
+ '<p>' + s.lohn.label + ': ' + euroText(s.lohn.min, s.lohn.nachkomma) + ' bis ' + euroText(s.lohn.max, s.lohn.nachkomma) + ' ' + s.lohn.bezug + '.</p>'
+ '<h3>Das machst du</h3>' + liste(s.machen) + '<h3>Das bringst du mit</h3>' + liste(s.mitbringen) + '<h3>Das bekommst du</h3>' + liste(s.bekommen)
+ '<p>' + s.nicht + '</p>';
}
function jobPosting(s) {
return {
'@context': 'https://schema.org', '@type': 'JobPosting',
title: s.titel,
description: stellenHtml(s),
identifier: { '@type': 'PropertyValue', name: BETRIEB.kurz, value: s.nr },
datePosted: s.datePosted, validThrough: s.validThrough,
employmentType: s.employmentType.length === 1 ? s.employmentType[0] : s.employmentType,
hiringOrganization: { '@type': 'Organization', name: BETRIEB.name, sameAs: BETRIEB.url },
jobLocation: { '@type': 'Place', address: { '@type': 'PostalAddress', streetAddress: BETRIEB.strasse, addressLocality: BETRIEB.ort, postalCode: BETRIEB.plz, addressRegion: BETRIEB.region, addressCountry: BETRIEB.land } },
baseSalary: { '@type': 'MonetaryAmount', currency: 'EUR', value: { '@type': 'QuantitativeValue', minValue: s.lohn.min, maxValue: s.lohn.max, unitText: s.lohn.einheit } },
directApply: true,
};
}
</script>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const HEUTE = new Date(2026, 10, 2); // Demo-Datum 02.11.2026 (Montag)
const datum = iso => new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso.slice(0, 10) + 'T12:00'));
const werktag = (ab, n) => { const d = new Date(ab); while (n > 0) { d.setDate(d.getDate() + 1); if (d.getDay() % 6) n--; } return d; };
const langDatum = d => new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: '2-digit', month: '2-digit', year: 'numeric' }).format(d);
const esc = t => String(t).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const VARIANTEN = ['handwerk', 'pflege', 'ausbildung'];
const S = { v: 'handwerk', zustand: 'offen', gesendet: null, versucht: false, beruehrt: new Set() };
const WERTE = {
handwerk: 'Zettel 500 ms cubic-bezier(0.34, 1.56, 0.64, 1) · Haken 400 ms · UI 280 / 190 ms · Füllung 600 ms expo.out',
pflege: 'Pflege gedämpft: Zettel 500 ms expo.out ohne Überschwinger · Haken 400 ms federnd · UI 280 / 190 ms',
ausbildung: 'Zettel 500 ms cubic-bezier(0.34, 1.56, 0.64, 1) · Haken 400 ms · UI 280 / 190 ms · Füllung 600 ms expo.out',
};
const stelle = () => STELLEN[S.v];
/* ── Seite aus dem Datensatz ── */
function zeichneSeite() {
const s = stelle(), p = s.person, vergeben = S.zustand === 'vergeben';
document.body.classList.toggle('vergeben', vergeben);
document.body.classList.toggle('pflegeton', S.v === 'pflege');
$('#krumen-bereich').textContent = s.bereich;
$('#status').innerHTML = vergeben
? `<b>Vergeben</b><span>seit ${datum('2026-11-02')}</span><span>Stelle ${s.nr}</span>`
: `<b>Offen</b><span>veröffentlicht am <span class="tnum">${datum(s.datePosted)}</span></span><span>Bewerbung bis <span class="tnum">${datum(s.validThrough)}</span></span>`;
$('#titel').textContent = s.titel;
$('#intro').textContent = s.intro;
$('#gehalt-label').textContent = s.lohn.label;
$('#gehalt-zahl').textContent = `${euroText(s.lohn.min, s.lohn.nachkomma)} bis ${euroText(s.lohn.max, s.lohn.nachkomma)}`.replace(' € bis', ' bis');
$('#gehalt-bezug').textContent = s.lohn.bezug;
$('#gehalt-extra').innerHTML = s.lohn.zusatz.map(([a, b]) => `<li><span>${a}</span>${b ? `<b>${b}</b>` : ''}</li>`).join('');
$('#gehalt-tarif').textContent = s.lohn.tarif; $('#gehalt-tarif').hidden = !s.lohn.tarif;
$('#fakten').innerHTML = s.fakten.map(([t, d]) => `<div><dt>${t}</dt><dd>${d}</dd></div>`).join('');
$('#stellennr').textContent = `Stellen-Nr. ${s.nr}`;
$('#machen').innerHTML = s.machen.map(x => `<li>${x}</li>`).join('');
$('#mitbringen').innerHTML = s.mitbringen.map(x => `<li>${x}</li>`).join('');
$('#bekommen').innerHTML = s.bekommen.map(x => `<li>${x}</li>`).join('');
$('#nicht').innerHTML = s.nicht;
$('#ablauf').innerHTML = s.ablauf.map(([t, d, f]) => `<li><b>${t}</b><span>${d}. <span class="frist">${f}</span></span></li>`).join('');
$('#initialen').textContent = p.ini;
$('#portraet').setAttribute('aria-label', `Platzhalter für das Foto von ${p.name}`);
$('#p-name').textContent = p.name; $('#p-rolle').textContent = p.rolle;
$('#p-telnr').textContent = p.tel; $('#p-tel').href = 'tel:' + p.telHref; $('#p-tel').setAttribute('aria-label', `${p.vorname} anrufen: ${p.tel}`);
$('#p-zeiten').textContent = p.zeiten;
$('#leiste-tel').href = 'tel:' + p.telHref; $('#leiste-tel').setAttribute('aria-label', `${p.vorname} anrufen`);
$('#vergeben-hinweis').hidden = !vergeben;
$('#aehnlich').innerHTML = s.aehnlich.map(([t, nr]) => `<li><a class="pfeil-link" href="#stelle" data-demo-link="Stelle ${nr}">${t}<svg viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h13"/><path class="spitze" d="M13 6l6 6-6 6"/></svg></a></li>`).join('');
$('#zur-bewerbung').hidden = vergeben;
zeichneDaten();
zeichneLeiste();
}
/* JSON-LD: nur solange die Stelle offen ist; vergeben → Markup entfernen */
function zeichneDaten() {
const s = stelle(), alt = $('#jobposting');
const ld = jobPosting(s);
if (S.zustand === 'offen') {
const el = alt || Object.assign(document.createElement('script'), { type: 'application/ld+json', id: 'jobposting' });
el.textContent = JSON.stringify(ld);
if (!alt) document.head.append(el);
$('#ld-anzeige').textContent = JSON.stringify(ld, null, 2);
$('#google-text').textContent = 'Erzeugt aus demselben Datensatz wie diese Seite. Alles, was hier steht, ist auch oben sichtbar.';
const felder = [['title', 1], ['description', 1], ['datePosted', 1], ['hiringOrganization', 1], ['jobLocation', 1], ['baseSalary', 0], ['validThrough', 0], ['employmentType', 0], ['directApply', 0], ['identifier', 0]];
$('#pruefliste').innerHTML = felder.map(([f, p]) => `<li>${f}${p ? ' (Pflicht)' : ''} ✓</li>`).join('');
} else {
alt && alt.remove();
$('#ld-anzeige').textContent = '// Kein JobPosting mehr auf dieser Seite.\n// Google erlaubt keine abgelaufenen Stellen: Markup entfernen,\n// validThrough in die Vergangenheit setzen oder die Seite mit 404/410 beenden.';
$('#google-text').textContent = 'Die Stelle ist vergeben, deshalb wurde das Markup entfernt. Die Seite bleibt für Menschen erreichbar und zeigt ähnliche Stellen.';
$('#pruefliste').innerHTML = '<li class="fehlt">JobPosting entfernt</li>';
}
}
/* ── Kurzbewerbung ── */
const box = $('#bewerbung');
function feldRadio(f, pflicht = true) {
return `<div class="feld" data-feld="${f.name}"><fieldset aria-describedby="${f.name}-fehler"><legend>${f.legend}${pflicht ? ' <span class="pflicht">(Pflicht)</span>' : ''}</legend>`
+ f.optionen.map((o, i) => `<label class="wahl"><input type="radio" name="${f.name}" value="${esc(o)}" id="${f.name}-${i}"${pflicht && i === 0 ? ' required' : ''}>${o}</label>`).join('')
+ `</fieldset><p class="fehler" id="${f.name}-fehler" hidden></p></div>`;
}
function zeichneLeiste() {
const s = stelle(), p = s.person;
if (S.zustand === 'vergeben') {
box.innerHTML = `<header><p class="klein">Job-Alert</p><h2 id="h-bewerbung">Neue Stellen per E-Mail</h2><p>Wir schreiben dir, sobald im Bereich ${s.alertBereich} etwas frei wird. Höchstens einmal pro Woche.</p></header>
<form class="alert-form" id="alert-form" novalidate>
<div class="feld"><label for="alert-mail">E-Mail <span class="pflicht">(Pflicht)</span></label><input type="email" id="alert-mail" autocomplete="email" required aria-describedby="alert-mail-fehler"><p class="fehler" id="alert-mail-fehler" hidden></p></div>
<div class="feld"><label for="alert-bereich">Bereich</label><select id="alert-bereich"><option>${s.alertBereich}</option><option>Alle Bereiche</option></select></div>
<button class="bewerben" type="submit"><span>Job-Alert anlegen</span><span class="flaeche" aria-hidden="true">Job-Alert anlegen</span></button>
<p class="hinweis">Du bekommst zuerst eine E-Mail zur Bestätigung (Double-Opt-in). Abmelden mit einem Klick in jeder E-Mail. <span class="platzhalter">Datenschutzhinweis vom Kunden</span></p>
</form>`;
$('#alert-form').addEventListener('submit', e => {
e.preventDefault();
const m = $('#alert-mail'), ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(m.value.trim()), f = $('#alert-mail-fehler');
m.setAttribute('aria-invalid', !ok); f.hidden = ok; f.textContent = ok ? '' : 'Bitte gib eine E-Mail-Adresse ein, zum Beispiel name@beispiel.de.';
if (!ok) { m.focus(); return; }
box.innerHTML = `<div class="danke neu" tabindex="-1" id="alert-danke"><svg class="haken" viewBox="0 0 64 64" aria-hidden="true"><circle cx="32" cy="32" r="32"/><path d="M19 33l9 9 17-19"/></svg><h2 id="h-bewerbung">Fast geschafft.</h2><p>Bitte bestätige den Link in der E-Mail an <b>${esc(m.value.trim())}</b>. Erst dann schicken wir dir neue Stellen.</p></div>`;
$('#alert-danke').focus();
});
bindeFuellung(box);
return;
}
if (S.gesendet) { zeichneDanke(); return; }
box.innerHTML = `<header><p class="klein">Kurzbewerbung · 2 Minuten</p><h2 id="h-bewerbung">Bewirb dich für diese Stelle</h2><p>Ohne Anschreiben. Lebenslauf freiwillig. ${p.vorname} meldet sich innerhalb von ${s.antwortTage} Werktagen.</p></header>
<div class="fehlerliste" id="fehlerliste" tabindex="-1" hidden></div>
<form id="form" novalidate>
<div style="display:grid;gap:16px">
<div class="feld" data-feld="name"><label for="f-name">Vor- und Nachname <span class="pflicht">(Pflicht)</span></label><input type="text" id="f-name" name="name" autocomplete="name" required aria-describedby="name-fehler"><p class="fehler" id="name-fehler" hidden></p></div>
<div class="feld" data-feld="kontakt"><label for="f-kontakt">Telefon oder E-Mail <span class="pflicht">(Pflicht)</span></label><input type="text" id="f-kontakt" name="kontakt" autocomplete="tel" inputmode="email" required aria-describedby="kontakt-hinweis kontakt-fehler"><p class="hinweis" id="kontakt-hinweis">Wie du lieber erreicht werden willst.</p><p class="fehler" id="kontakt-fehler" hidden></p></div>
${feldRadio(s.frage)}
${s.extra ? `<div class="feld" data-feld="${s.extra.name}"><fieldset aria-describedby="${s.extra.name}-fehler"><legend>${s.extra.legend} <span class="pflicht">(Pflicht)</span></legend><div class="reihe">${s.extra.optionen.map((o, i) => `<label class="wahl"><input type="radio" name="${s.extra.name}" value="${o}" id="${s.extra.name}-${i}"${i === 0 ? ' required' : ''}>${o}</label>`).join('')}</div></fieldset><p class="fehler" id="${s.extra.name}-fehler" hidden></p></div>` : ''}
<div class="feld" data-feld="start"><label for="f-start">Frühester Start <span class="pflicht">(Pflicht)</span></label><select id="f-start" name="start" required aria-describedby="start-fehler"><option value="">Bitte wählen</option><option>So bald wie möglich</option><option>In 1 Monat</option><option>In 2 bis 3 Monaten</option><option>Später</option></select><p class="fehler" id="start-fehler" hidden></p></div>
${s.praktikum ? `<label class="wahl"><input type="checkbox" name="praktikum" id="f-praktikum">Ich möchte zuerst ein Praktikum machen (freiwillig)</label>` : ''}
<div class="feld datei"><label for="f-cv">Lebenslauf <span class="pflicht">(freiwillig)</span></label><input type="file" id="f-cv" name="cv" accept=".pdf,.jpg,.jpeg,.png" aria-describedby="cv-hinweis"><p class="hinweis" id="cv-hinweis">PDF oder Foto, bis 10 MB. Geht auch ohne.</p></div>
<button class="bewerben" type="submit" id="absenden"><span>Bewerbung abschicken</span><span class="flaeche" aria-hidden="true">Bewerbung abschicken</span></button>
<p class="hinweis">Wir fragen nicht nach deinem bisherigen Gehalt. Deine Angaben nutzen wir nur für diese Bewerbung und löschen sie <span class="platzhalter">Frist vom Kunden</span> nach Abschluss. <span class="platzhalter">Datenschutzhinweis nach Art. 13 DSGVO, vom Kunden prüfen lassen</span></p>
</div>
</form>
<div class="anders"><span>Lieber direkt sprechen?</span><div class="knoepfe"><a class="knopf" href="tel:${p.telHref}"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg><span class="tnum">${p.tel}</span></a><a class="knopf" href="#stelle" data-demo-link="WhatsApp-Chat mit ${p.vorname}"><svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l1.4-4A8 8 0 1 1 8 18.6z"/></svg>WhatsApp</a></div></div>`;
S.beruehrt = new Set(); S.versucht = false;
const form = $('#form');
form.addEventListener('input', e => { const f = e.target.closest('[data-feld]'); if (f && (S.beruehrt.has(f.dataset.feld) || S.versucht)) pruefeFeld(f.dataset.feld); });
form.addEventListener('change', e => { const f = e.target.closest('[data-feld]'); if (f && e.target.type !== 'text') { S.beruehrt.add(f.dataset.feld); pruefeFeld(f.dataset.feld); } });
form.addEventListener('focusout', e => { const f = e.target.closest('[data-feld]'); if (f && e.target.value && e.target.type === 'text') { S.beruehrt.add(f.dataset.feld); pruefeFeld(f.dataset.feld); } });
form.addEventListener('submit', e => { e.preventDefault(); absenden(); });
bindeFuellung(box);
}
function regeln() {
const s = stelle();
const r = {
name: () => $('#f-name').value.trim().length >= 2 ? '' : 'Bitte gib deinen Namen ein.',
kontakt: () => {
const v = $('#f-kontakt').value.trim();
if (!v) return 'Bitte gib eine Telefonnummer oder E-Mail-Adresse ein.';
if (v.includes('@')) return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? '' : 'Die E-Mail-Adresse ist unvollständig, zum Beispiel name@beispiel.de.';
return (v.match(/\d/g) || []).length >= 6 ? '' : 'Die Telefonnummer ist zu kurz. Bitte mit Vorwahl.';
},
};
r[s.frage.name] = () => $(`input[name="${s.frage.name}"]:checked`) ? '' : 'Bitte wähle aus, was auf dich zutrifft.';
if (s.extra) r[s.extra.name] = () => $(`input[name="${s.extra.name}"]:checked`) ? '' : 'Bitte wähle Vollzeit oder Teilzeit.';
r.start = () => $('#f-start').value ? '' : 'Bitte wähle, ab wann du anfangen kannst.'; // Reihenfolge wie im Formular
return r;
}
const LABEL = { name: 'Vor- und Nachname', kontakt: 'Telefon oder E-Mail', start: 'Frühester Start' };
function pruefeFeld(name) {
const fehler = regeln()[name](), feld = $(`[data-feld="${name}"]`), p = $(`#${name}-fehler`);
p.hidden = !fehler; p.textContent = fehler;
feld.querySelectorAll('input:not([type=file]), select').forEach(i => i.setAttribute('aria-invalid', !!fehler));
if (S.versucht) zeichneFehlerliste();
return fehler;
}
function zeichneFehlerliste() {
const s = stelle(), r = regeln();
const liste = Object.keys(r).map(k => [k, r[k]()]).filter(([, f]) => f);
const box = $('#fehlerliste');
if (!liste.length) { box.hidden = true; return liste; }
const name = k => LABEL[k] || (k === s.frage.name ? s.frage.legend : s.extra && s.extra.legend);
const ziel = k => k === s.frage.name || (s.extra && k === s.extra.name) ? `${k}-0` : `f-${k}`;
box.innerHTML = `<h3>Bitte prüfe ${liste.length === 1 ? 'eine Angabe' : liste.length + ' Angaben'}:</h3><ul>${liste.map(([k, f]) => `<li><a href="#${ziel(k)}" data-ziel="${ziel(k)}">${name(k)}</a>: ${f}</li>`).join('')}</ul>`;
box.hidden = false;
box.querySelectorAll('a').forEach(a => a.addEventListener('click', e => { e.preventDefault(); const el = document.getElementById(a.dataset.ziel); el.focus(); el.scrollIntoView({ block: 'center' }); }));
return liste;
}
function absenden(stumm) {
S.versucht = true;
const r = regeln();
Object.keys(r).forEach(k => pruefeFeld(k));
const liste = zeichneFehlerliste();
if (liste.length) { if (!stumm) $('#fehlerliste').focus(); return false; }
const s = stelle();
S.gesendet = { name: $('#f-name').value.trim(), kontakt: $('#f-kontakt').value.trim(), start: $('#f-start').value, wahl: $(`input[name="${s.frage.name}"]:checked`).value, cv: $('#f-cv').files.length > 0 };
zeichneDanke(true, stumm);
return true;
}
function zeichneDanke(neu, stumm) {
const s = stelle(), p = s.person, g = S.gesendet, bis = langDatum(werktag(HEUTE, s.antwortTage));
const vorname = g.name.split(/\s+/)[0];
document.body.classList.add('gesendet');
box.innerHTML = `<div class="danke${neu && !reduziert() ? ' neu' : ''}" tabindex="-1" id="danke" role="status">
<svg class="haken" viewBox="0 0 64 64" aria-hidden="true"><circle cx="32" cy="32" r="32"/><path d="M19 33l9 9 17-19"/></svg>
<h2 id="h-bewerbung">Danke, ${esc(vorname)}! Deine Bewerbung ist da.</h2>
<div class="wer"><span class="portraet" aria-hidden="true"><b>${p.ini}</b></span><p><b>${p.name}</b> ruft dich bis <b>${bis}</b> an.</p></div>
<dl><dt>Stelle</dt><dd>${s.titel}</dd><dt>Kontakt</dt><dd>${esc(g.kontakt)}</dd><dt>${s.frage.legend}</dt><dd>${esc(g.wahl)}</dd><dt>Start</dt><dd>${esc(g.start)}</dd><dt>Lebenslauf</dt><dd>${g.cv ? 'angehängt' : 'nicht angehängt (kein Problem)'}</dd></dl>
<p class="hinweis">Eine Bestätigung geht ${g.kontakt.includes('@') ? 'per E-Mail' : 'per SMS'} an dich. Eilt es? <a href="tel:${p.telHref}" class="tnum">${p.tel}</a></p>
</div>`;
if (neu && !stumm) $('#danke').focus();
}
/* Füllung vom Eintrittspunkt (nur Maus); Touch: nur Druck und Farbe */
function bindeFuellung(wurzel) {
$$('.bewerben', wurzel).forEach(k => {
if (k.dataset.gebunden) return; k.dataset.gebunden = '1';
const fl = k.querySelector('.flaeche');
const punkt = e => { const r = k.getBoundingClientRect(); return [e.clientX - r.left, e.clientY - r.top, Math.hypot(Math.max(e.clientX - r.left, r.right - e.clientX), Math.max(e.clientY - r.top, r.bottom - e.clientY)) + 2]; };
k.addEventListener('pointerenter', e => {
if (e.pointerType !== 'mouse') return;
if (reduziert()) { k.classList.add('gefuellt'); return; }
const [x, y, rad] = punkt(e);
fl.style.transition = 'none'; fl.style.clipPath = `circle(0px at ${x}px ${y}px)`; fl.getBoundingClientRect();
fl.style.transition = ''; fl.style.clipPath = `circle(${rad}px at ${x}px ${y}px)`;
});
k.addEventListener('pointerleave', e => {
if (e.pointerType !== 'mouse') return;
k.classList.remove('gefuellt');
const [x, y] = punkt(e); fl.style.clipPath = `circle(0px at ${x}px ${y}px)`;
});
});
}
bindeFuellung(document);
/* Stelle teilen (kopieren-feedback) */
let teilenUhr;
function teilenRueck(text) {
const r = $('#teilen-rueck');
r.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>${text}`;
clearTimeout(teilenUhr); teilenUhr = setTimeout(() => { r.textContent = ''; }, 2600);
}
async function teilen(nurZeigen) {
const url = location.origin + location.pathname + '?v=' + S.v;
if (nurZeigen) { teilenRueck('Link kopiert'); return; }
try { await navigator.clipboard.writeText(url); teilenRueck('Link kopiert'); }
catch (e) {
const t = Object.assign(document.createElement('textarea'), { value: url }); t.style.position = 'fixed'; t.style.opacity = '0';
document.body.append(t); t.select(); const ok = document.execCommand && document.execCommand('copy'); t.remove();
teilenRueck(ok ? 'Link kopiert' : 'Kopieren ging nicht: ' + url);
}
}
$('#teilen').addEventListener('click', () => teilen());
/* Zettel landet einmal */
function landen() { const z = $('#zettel'); z.classList.remove('landet'); if (reduziert()) return; void z.offsetWidth; z.classList.add('landet'); }
/* Varianten und Zustände */
function setzeVariante(v, url = true) {
S.v = VARIANTEN.includes(v) ? v : 'handwerk';
S.gesendet = null; document.body.classList.remove('gesendet');
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === S.v));
$('#werte').textContent = WERTE[S.v];
if (url) { const q = new URLSearchParams(location.search); q.set('v', S.v); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); }
zeichneSeite(); landen();
}
function setzeZustand(z) {
S.zustand = z; S.gesendet = null; document.body.classList.remove('gesendet');
$$('[data-zustand]').forEach(b => b.setAttribute('aria-pressed', b.dataset.zustand === z));
zeichneSeite();
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.v); }));
$$('[data-zustand]').forEach(b => b.addEventListener('click', () => { stopp(); setzeZustand(b.dataset.zustand); }));
let toastUhr;
function toast(t) { const el = $('#toast'); el.textContent = t; el.classList.add('an'); clearTimeout(toastUhr); toastUhr = setTimeout(() => el.classList.remove('an'), 3000); }
document.addEventListener('click', e => { const a = e.target.closest('[data-demo-link]'); if (!a) return; e.preventDefault(); toast(`Demo: Hier öffnet „${a.dataset.demoLink}“.`); });
$('#zur-bewerbung').addEventListener('click', e => { e.preventDefault(); box.scrollIntoView({ block: 'start', behavior: reduziert() ? 'auto' : 'smooth' }); const f = $('#f-name'); f ? f.focus({ preventScroll: true }) : box.focus({ preventScroll: true }); });
/* ── Vorführung ── */
const zeiger = $('#zeiger');
let uhren = [], schleife = null;
const nach = (ms, fn) => uhren.push(setTimeout(fn, ms));
function stopp() { uhren.forEach(clearTimeout); uhren = []; clearTimeout(schleife); html.classList.remove('vorfuehrung'); }
function zeigeAuf(el, klick) {
if (!el) return;
const r = el.getBoundingClientRect();
zeiger.style.transform = `translate(${Math.round(r.left + Math.min(r.width * .5, 160))}px, ${Math.round(r.top + r.height * .55)}px)`;
if (klick) setTimeout(() => { zeiger.classList.add('drueckt'); setTimeout(() => zeiger.classList.remove('drueckt'), 140); }, reduziert() ? 0 : 650);
}
function tippe(el, text, ab) {
[...text].forEach((c, i) => nach(ab + i * 55, () => { el = document.getElementById(el.id) || el; el.value = text.slice(0, i + 1); el.dispatchEvent(new Event('input', { bubbles: true })); }));
return ab + text.length * 55;
}
function grundzustand() { S.gesendet = null; S.zustand = 'offen'; $$('[data-zustand]').forEach(b => b.setAttribute('aria-pressed', b.dataset.zustand === 'offen')); document.body.classList.remove('gesendet'); zeichneSeite(); }
function neustart() { stopp(); grundzustand(); landen(); window.scrollTo(0, 0); }
function vorfuehren() {
stopp(); grundzustand(); landen(); window.scrollTo(0, 0);
html.classList.add('vorfuehrung');
const s = stelle();
zeiger.style.transform = `translate(${innerWidth * .45}px, ${innerHeight * .8}px)`;
let t = 700;
nach(t, () => zeigeAuf($('#f-name'), true));
t = tippe($('#f-name'), 'Pauline Kuhlmann', t + 700) + 400;
nach(t, () => zeigeAuf($('#absenden'), true));
nach(t + 750, () => absenden(true));
t += 2600;
nach(t, () => zeigeAuf($('#f-kontakt'), true));
t = tippe($('#f-kontakt'), '0176 5550 1234', t + 700) + 300;
nach(t, () => { const r = $(`#${s.frage.name}-0`); zeigeAuf(r.closest('label'), true); });
nach(t + 750, () => { const r = $(`#${s.frage.name}-0`); r.checked = true; r.dispatchEvent(new Event('change', { bubbles: true })); });
t += 1300;
if (s.extra) {
nach(t, () => zeigeAuf($(`#${s.extra.name}-1`).closest('label'), true));
nach(t + 750, () => { const r = $(`#${s.extra.name}-1`); r.checked = true; r.dispatchEvent(new Event('change', { bubbles: true })); });
t += 1300;
}
nach(t, () => zeigeAuf($('#f-start'), true));
nach(t + 750, () => { const sel = $('#f-start'); sel.value = 'In 1 Monat'; sel.dispatchEvent(new Event('change', { bubbles: true })); });
t += 1400;
nach(t, () => zeigeAuf($('#absenden'), true));
nach(t + 750, () => absenden(true));
t += 3600;
nach(t, () => zeigeAuf($('#teilen'), true));
nach(t + 750, () => teilen(true));
t += 2200;
nach(t, () => setzeZustand('vergeben'));
t += 3200;
schleife = setTimeout(vorfuehren, t * (reduziert() ? 1.2 : 1));
}
function pause() { stopp(); }
function weiter() { vorfuehren(); }
setzeVariante(new URLSearchParams(location.search).get('v'), false);
})();
</script>
</body>
</html>