{"id":3046,"date":"2024-10-14T16:11:59","date_gmt":"2024-10-14T14:11:59","guid":{"rendered":"https:\/\/fahrtauglich.ch\/?page_id=3046"},"modified":"2026-10-12T13:03:51","modified_gmt":"2026-10-12T11:03:51","slug":"sudoku","status":"publish","type":"page","link":"https:\/\/fahrtauglich.ch\/fr\/praxisportal\/sudoku\/","title":{"rendered":"Sudoku"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"3046\" class=\"elementor elementor-3046\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-279daf0 e-flex e-con-boxed e-con e-parent\" data-id=\"279daf0\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1f2f64f elementor-widget elementor-widget-html\" data-id=\"1f2f64f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!--\r\n    Sudoku \u2014 fahrtauglich.ch\/praxisportal\/sudoku\/\r\n    Einbau: Inhalt dieser Datei vollstaendig in EIN Elementor-HTML-Widget einfuegen.\r\n    Das Snippet deckt die ganze Seite zwischen Header und Footer ab (Titel,\r\n    Einleitung mit Link auf trainmybrain.ch, Spiel, Spielanleitung, Texte, Quelle).\r\n    Alle uebrigen Elementor-Widgets der Seite werden geloescht. Kein\r\n    <html>\/<head>\/<body>: Header und Footer liefert das Theme.\r\n\r\n    Spiellogik aus trainmybrain-sudoku.html (Generator mit Eindeutigkeits- und\r\n    Technikpruefung, Notizen, Fehlergrenze, Spielstand). Unabhaengig geprueft\r\n    2026-10-12: je 300 Raetsel pro Stufe eindeutig und ohne Raten loesbar,\r\n    \u00abLeicht\u00bb allein mit Einzel-Schritten. Ergaenzt (Wunsch: \u00abim Zweifel eine Zahl\r\n    mehr\u00bb): Bei \u00abLeicht\u00bb gibt es bei 10+ offenen Feldern immer mindestens drei\r\n    moegliche Zuege \u2014 sonst wird eine Zahl mehr vorgegeben.\r\n\r\n    Optik: fahrtauglich.ch \u2014 Poppins, Gelb #FFBF00, Flaechen #F3F5F8, Pill-Buttons.\r\n    Alles CSS haengt an #fts, damit nichts in die Elementor-Seite durchschlaegt.\r\n-->\r\n<style>\r\n    #fts {\r\n        --fts-gelb: #FFBF00;\r\n        --fts-gelb-dunkel: #E6AB00;\r\n        --fts-gelb-hell: #FFF4CC;\r\n        --fts-schwarz: #000000;\r\n        --fts-text: #1F2937;\r\n        --fts-text-2: #4B5563;\r\n        --fts-flaeche: #F3F5F8;\r\n        --fts-linie: #C8D5DC;\r\n        --fts-gruen: #047857;\r\n        --fts-blau: #1D4ED8;\r\n        --fts-fehler: #B91C1C;\r\n\r\n        font-family: 'Poppins', sans-serif;\r\n        color: var(--fts-text);\r\n        line-height: 1.5;\r\n        width: 100%;\r\n        max-width: 850px;\r\n        margin: 20px auto 60px;\r\n        box-sizing: border-box;\r\n    }\r\n    #fts *, #fts *::before, #fts *::after { box-sizing: border-box; }\r\n    #fts [hidden] { display: none !important; }\r\n\r\n    \/* ===== Seitenkopf \u2014 Masse wie die bisherigen Elementor-Widgets ===== *\/\r\n    #fts .fts-titel { margin: 0; font-family: inherit; font-size: 48px; font-weight: 600; line-height: 1.2; letter-spacing: 0; text-align: center; color: var(--fts-schwarz); }\r\n    #fts .fts-intro { max-width: 760px; margin: 50px auto 0; font-size: 20px; font-weight: 300; line-height: 1.5; text-align: center; color: var(--fts-schwarz); }\r\n    #fts .fts-intro a, #fts .fts-box a {\r\n        color: var(--fts-schwarz); font-weight: 400;\r\n        text-decoration: underline; text-decoration-color: var(--fts-gelb); text-decoration-thickness: 3px; text-underline-offset: 3px;\r\n        overflow-wrap: anywhere;\r\n    }\r\n    #fts .fts-intro a:hover, #fts .fts-box a:hover, #fts .fts-intro a:focus, #fts .fts-box a:focus { color: var(--fts-schwarz); background: rgba(255, 191, 0, 0.18); }\r\n    #fts a:focus-visible { outline: 3px solid var(--fts-schwarz); outline-offset: 2px; }\r\n\r\n    \/* ===== Spielbereich ===== *\/\r\n    #fts .fts-spiel { max-width: 680px; margin: 34px auto 0; text-align: center; }\r\n    #fts .fts-karte {\r\n        position: relative;\r\n        overflow: hidden;\r\n        overflow: clip;   \/* clip: Fokus auf ein Feld darf den Karteninhalt nicht verschieben *\/\r\n        background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);\r\n        border: 2px solid #dee2e6;\r\n        border-radius: 20px;\r\n        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\r\n        padding: 30px;\r\n    }\r\n    #fts .fts-karte::before {\r\n        content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%;\r\n        background: radial-gradient(circle, rgba(52, 58, 64, 0.05) 1px, transparent 1px);\r\n        background-size: 30px 30px; animation: fts-hintergrund 25s linear infinite; z-index: 0; pointer-events: none;\r\n    }\r\n    @keyframes fts-hintergrund { 0% { transform: translate(0, 0) rotate(0deg); } 100% { transform: translate(-30px, -30px) rotate(360deg); } }\r\n    #fts .fts-inhalt { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 16px; }\r\n\r\n    \/* Schwierigkeit *\/\r\n    #fts .fts-stufen { display: inline-flex; padding: 3px; background: #ffffff; border: 2px solid var(--fts-schwarz); border-radius: 100px; }\r\n    #fts .schwierigkeit-knopf {\r\n        -webkit-appearance: none; appearance: none; margin: 0;\r\n        min-height: 40px; padding: 6px 18px; background: transparent; border: none; border-radius: 100px;\r\n        font-family: inherit; font-size: 0.95rem; font-weight: 500; color: var(--fts-schwarz); cursor: pointer;\r\n    }\r\n    #fts .schwierigkeit-knopf:hover, #fts .schwierigkeit-knopf:focus { background: var(--fts-flaeche); color: var(--fts-schwarz); }\r\n    #fts .schwierigkeit-knopf.aktiv { background: var(--fts-gelb); color: var(--fts-schwarz); }\r\n\r\n    \/* Fehlergrenze + Zeit *\/\r\n    #fts .fts-kontrollen { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 12px 24px; width: 100%; }\r\n    #fts .fts-fehler { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; color: var(--fts-text-2); }\r\n    #fts .fts-fehler input[type=\"range\"] { accent-color: var(--fts-gelb-dunkel); cursor: pointer; width: 110px; margin: 0; }\r\n    #fts .fts-fehler-anzeige { min-width: 3.4rem; font-weight: 600; color: var(--fts-text); font-variant-numeric: tabular-nums; }\r\n    #fts .fts-fehler-anzeige.warnung { color: var(--fts-fehler); }\r\n    #fts .fts-zeit { display: flex; align-items: center; gap: 10px; }\r\n    #fts .fts-zeit-anzeige { min-width: 3.6rem; font-size: 1.15rem; font-weight: 600; color: var(--fts-text); font-variant-numeric: tabular-nums; }\r\n    #fts .fts-zeit-knopf {\r\n        -webkit-appearance: none; appearance: none; margin: 0;\r\n        min-height: 38px; padding: 4px 16px; background: #ffffff; border: 2px solid var(--fts-schwarz); border-radius: 100px;\r\n        font-family: inherit; font-size: 0.9rem; font-weight: 500; color: var(--fts-schwarz); cursor: pointer;\r\n    }\r\n    #fts .fts-zeit-knopf:hover, #fts .fts-zeit-knopf:focus { background: var(--fts-flaeche); color: var(--fts-schwarz); }\r\n\r\n    \/* Spielfeld \u2014 quadratisch, beide Achsen explizit (aus der TMB-Version) *\/\r\n    #fts .fts-brett { width: 100%; max-width: 504px; container-type: inline-size; }\r\n    #fts .sudoku-grid {\r\n        display: grid;\r\n        grid-template-columns: repeat(9, 1fr);\r\n        grid-template-rows: repeat(9, 1fr);\r\n        width: 100%;\r\n        aspect-ratio: 1;\r\n        background: #ffffff;\r\n        border: 3px solid var(--fts-schwarz);\r\n        border-radius: 10px;\r\n        overflow: hidden;\r\n        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);\r\n    }\r\n    #fts .sudoku-zelle {\r\n        position: relative; min-width: 0; min-height: 0;\r\n        display: flex; align-items: center; justify-content: center;\r\n        background: #ffffff;\r\n        border-right: 1px solid var(--fts-linie);\r\n        border-bottom: 1px solid var(--fts-linie);\r\n        cursor: pointer; touch-action: manipulation;\r\n        transition: background 0.12s ease;\r\n    }\r\n    \/* 3x3-Blockgrenzen *\/\r\n    #fts .sudoku-zelle:nth-child(3n)  { border-right: 2.5px solid var(--fts-schwarz); }\r\n    #fts .sudoku-zelle:nth-child(9n)  { border-right: none; }\r\n    #fts .sudoku-zelle:nth-child(n+19):nth-child(-n+27),\r\n    #fts .sudoku-zelle:nth-child(n+46):nth-child(-n+54) { border-bottom: 2.5px solid var(--fts-schwarz); }\r\n    #fts .sudoku-zelle:nth-child(n+73) { border-bottom: none; }\r\n\r\n    #fts .notiz-gitter { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); pointer-events: none; padding: 1px; }\r\n    #fts .notiz-gitter span {\r\n        display: flex; align-items: center; justify-content: center;\r\n        font-weight: 500; line-height: 1; color: var(--fts-text-2);\r\n        font-size: clamp(0.42rem, 1.25vw, 0.6rem);\r\n        font-size: max(0.5rem, 2.9cqw);\r\n        visibility: hidden;\r\n    }\r\n    #fts .notiz-gitter span.gesetzt { visibility: visible; }\r\n    #fts .sudoku-zelle.hat-wert .notiz-gitter { display: none; }\r\n\r\n    #fts .sudoku-zelle input {\r\n        -webkit-appearance: none; appearance: none;\r\n        width: 100%; height: 100%; margin: 0; padding: 0;\r\n        text-align: center; border: none; border-radius: 0; background: transparent; box-shadow: none;\r\n        font-family: inherit; font-weight: 500;\r\n        \/* mind. 16px, sonst zoomt iOS beim Fokus hinein *\/\r\n        font-size: max(1rem, clamp(0.7rem, 3.5vw, 1.2rem));\r\n        font-size: max(1rem, 6.2cqw);\r\n        color: var(--fts-blau);\r\n        cursor: pointer; outline: none; caret-color: transparent;\r\n    }\r\n    #fts .sudoku-zelle input.vorgegeben    { color: var(--fts-schwarz); font-weight: 700; cursor: default; }\r\n    #fts .sudoku-zelle input.loesung-zelle { color: #6B7280; }\r\n    #fts .sudoku-zelle.gleiche-zahl        { background: #EEF2FF; }\r\n    #fts .sudoku-zelle.ausgewaehlt         { background: var(--fts-gelb-hell); box-shadow: inset 0 0 0 2px var(--fts-schwarz); }\r\n    #fts .sudoku-zelle.fehler              { background: #FEE2E2; box-shadow: inset 0 0 0 2px #EF4444; }\r\n    #fts .sudoku-zelle.fehler input        { color: var(--fts-fehler); }\r\n\r\n    \/* Ziffernblock \u2014 auf allen Geraeten sichtbar *\/\r\n    #fts .ziffern-pad { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; width: 100%; max-width: 504px; }\r\n    #fts .ziffer-knopf {\r\n        -webkit-appearance: none; appearance: none; margin: 0; padding: 0;\r\n        min-height: 52px; touch-action: manipulation;\r\n        background: #ffffff; border: 1px solid var(--fts-linie); border-radius: 10px;\r\n        font-family: inherit; font-size: 1.35rem; font-weight: 600; color: var(--fts-text);\r\n        cursor: pointer; transition: background 0.12s ease;\r\n    }\r\n    #fts .ziffer-knopf:hover, #fts .ziffer-knopf:focus { background: var(--fts-gelb-hell); color: var(--fts-text); }\r\n    #fts .ziffer-knopf:active { background: var(--fts-gelb); }\r\n    #fts .ziffer-knopf.loeschen { font-size: 1rem; color: var(--fts-fehler); }\r\n    \/* Vollstaendig gesetzte Ziffer: unsichtbar, Platz bleibt (Fingerposition) *\/\r\n    #fts .ziffer-knopf.fertig { visibility: hidden; pointer-events: none; }\r\n\r\n    \/* ===== Buttons (Pill-Stil der fahrtauglich.ch-Buttons) ===== *\/\r\n    #fts .fts-aktionen { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }\r\n    #fts .fts-knopf {\r\n        -webkit-appearance: none; appearance: none; margin: 0;\r\n        display: inline-flex; align-items: center; justify-content: center; gap: 6px;\r\n        min-height: 48px; padding: 10px 22px;\r\n        background: #ffffff; border: 2px solid var(--fts-schwarz); border-radius: 100px; box-shadow: none;\r\n        font-family: inherit; font-size: 0.98rem; font-weight: 500; line-height: 1.2; text-transform: none; text-decoration: none;\r\n        color: var(--fts-schwarz); cursor: pointer;\r\n        transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;\r\n    }\r\n    #fts .fts-knopf:hover, #fts .fts-knopf:focus { background: var(--fts-flaeche); color: var(--fts-schwarz); }\r\n    #fts .fts-knopf:hover { transform: translateY(-1px); }\r\n    #fts .fts-knopf-gelb { background: var(--fts-gelb); border-color: var(--fts-gelb); }\r\n    #fts .fts-knopf-gelb:hover, #fts .fts-knopf-gelb:focus { background: var(--fts-gelb-dunkel); border-color: var(--fts-gelb-dunkel); }\r\n    #fts .fts-knopf[aria-pressed=\"true\"] { background: var(--fts-schwarz); color: #ffffff; }\r\n    #fts .fts-knopf svg { width: 17px; height: 17px; flex-shrink: 0; }\r\n    #fts .fts-knopf:focus-visible, #fts .schwierigkeit-knopf:focus-visible, #fts .ziffer-knopf:focus-visible,\r\n    #fts .fts-zeit-knopf:focus-visible, #fts .fts-modal-schliessen:focus-visible, #fts summary:focus-visible { outline: 3px solid var(--fts-schwarz); outline-offset: 2px; }\r\n\r\n    \/* ===== Spielanleitung ===== *\/\r\n    #fts .fts-anleitung { margin-top: 24px; text-align: left; background: #ffffff; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); padding: 15px 18px; }\r\n    #fts .fts-anleitung summary { cursor: pointer; list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 4px 0; font-size: 1.15rem; font-weight: 600; color: var(--fts-schwarz); user-select: none; }\r\n    #fts .fts-anleitung summary::-webkit-details-marker { display: none; }\r\n    #fts .fts-chev { width: 20px; height: 20px; flex: 0 0 auto; transition: transform 0.2s ease; }\r\n    #fts .fts-anleitung[open] .fts-chev { transform: rotate(180deg); }\r\n    #fts .fts-anleitung-liste { list-style: none; margin: 12px 0 4px; padding: 0; display: grid; gap: 8px; }\r\n    #fts .fts-anleitung-liste li { margin: 0; padding: 10px 14px; background: var(--fts-flaeche); border-left: 3px solid var(--fts-gelb); border-radius: 6px; font-size: 0.95rem; font-weight: 300; line-height: 1.55; color: var(--fts-schwarz); }\r\n    #fts .fts-anleitung-liste li strong { font-weight: 600; }\r\n\r\n    \/* ===== Textabschnitte (Kartenstil der bisherigen Textboxen) ===== *\/\r\n    #fts .fts-abschnitt { margin-top: 70px; }\r\n    #fts .fts-abschnitt h2 { margin: 0 0 20px; font-family: inherit; font-size: 36px; font-weight: 600; line-height: 1.2; letter-spacing: 0; text-transform: none; text-align: center; color: var(--fts-schwarz); }\r\n    #fts .fts-box { background: #ffffff; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); padding: 15px 20px; font-size: 22px; font-weight: 300; line-height: 1.5; color: var(--fts-schwarz); text-align: left; }\r\n    #fts .fts-box p { margin: 0 0 14px; }\r\n    #fts .fts-box p:last-child { margin-bottom: 0; }\r\n    #fts .fts-box strong { font-weight: 500; }\r\n    #fts .fts-tipps { margin: 0; padding-left: 1.2em; }\r\n    #fts .fts-tipps li { margin: 0 0 14px; padding-left: 0.2em; }\r\n    #fts .fts-tipps li:last-child { margin-bottom: 0; }\r\n    #fts .fts-tipps li::marker { color: var(--fts-gelb-dunkel); }\r\n    #fts .fts-tipps strong { display: block; }\r\n    #fts .fts-nutzen { list-style: none; margin: 6px 0 0; padding: 0; }\r\n    #fts .fts-nutzen li { display: grid; grid-template-columns: 2.4em 1fr; gap: 0 0.6em; padding: 14px 0; border-top: 1px solid var(--fts-linie); }\r\n    #fts .fts-nutzen li:last-child { border-bottom: 1px solid var(--fts-linie); }\r\n    #fts .fts-nutzen-nr { font-weight: 600; line-height: 1.5; color: var(--fts-gelb-dunkel); }\r\n    #fts .fts-nutzen strong { display: block; }\r\n    #fts .fts-quellen-titel { margin-top: 20px !important; font-weight: 500; }\r\n    #fts .fts-quellen { list-style: none; margin: 0; padding: 0; font-size: 0.8em; }\r\n\r\n    \/* ===== Dialoge ===== *\/\r\n    #fts .fts-modal { position: fixed; inset: 0; z-index: 100000; display: none; align-items: center; justify-content: center; padding: 20px; }\r\n    #fts .fts-modal.active { display: flex; }\r\n    #fts .fts-modal-hintergrund { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px); }\r\n    #fts .fts-modal-inhalt { position: relative; width: 100%; max-width: 440px; max-height: 85vh; overflow-y: auto; background: #ffffff; border-radius: 15px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); padding: 32px 28px 28px; text-align: center; animation: fts-modal-ein 0.25s ease; }\r\n    @keyframes fts-modal-ein { from { opacity: 0; transform: scale(0.96) translateY(12px); } to { opacity: 1; transform: none; } }\r\n    #fts .fts-modal-schliessen { -webkit-appearance: none; appearance: none; position: absolute; top: 8px; right: 8px; width: 44px; height: 44px; margin: 0; padding: 0; display: flex; align-items: center; justify-content: center; background: none; border: none; border-radius: 50%; font-family: inherit; font-size: 28px; line-height: 1; color: #6B7280; cursor: pointer; }\r\n    #fts .fts-modal-schliessen:hover, #fts .fts-modal-schliessen:focus { background: var(--fts-flaeche); color: #374151; }\r\n    #fts .fts-modal h2 { margin: 0 0 8px; font-family: inherit; font-size: 1.75rem; font-weight: 600; line-height: 1.2; color: var(--fts-text); }\r\n    #fts .fts-modal p { margin: 0 0 6px; color: var(--fts-text-2); }\r\n    #fts .fts-ergebnis { display: flex; justify-content: center; gap: 24px; margin: 16px 0 14px; flex-wrap: wrap; }\r\n    #fts .fts-ergebnis strong { display: block; font-size: 1.8rem; font-weight: 700; line-height: 1.1; color: var(--fts-schwarz); font-variant-numeric: tabular-nums; }\r\n    #fts .fts-ergebnis span { font-size: 0.85rem; color: var(--fts-text-2); }\r\n    #fts .fts-modal-knoepfe { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }\r\n    #fts .fts-modal-hinweis { color: var(--fts-gruen) !important; font-size: 0.9rem; }\r\n\r\n    #fts .fts-sr { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }\r\n\r\n    \/* ===== Responsive (Elementor: Tablet <= 1024px, Handy <= 767px) ===== *\/\r\n    @media (max-width: 1024px) {\r\n        #fts .fts-titel { font-size: 45px; }\r\n        #fts .fts-intro { font-size: 16px; margin-top: 30px; }\r\n        #fts .fts-box   { font-size: 16px; }\r\n    }\r\n    @media (max-width: 768px) {\r\n        #fts { max-width: 95%; }\r\n        #fts .fts-karte { padding: 20px; }\r\n    }\r\n    @media (max-width: 767px) {\r\n        #fts .fts-titel { font-size: 28px; }\r\n        #fts .fts-intro { margin-top: 20px; }\r\n        #fts .fts-spiel { margin-top: 24px; }\r\n        #fts .fts-abschnitt { margin-top: 50px; }\r\n        #fts .fts-abschnitt h2 { font-size: 28px; }\r\n        #fts .fts-box { padding: 15px; }\r\n    }\r\n    @media (max-width: 520px) {\r\n        #fts .fts-karte  { padding: 14px 6px; border-radius: 16px; }\r\n        #fts .fts-inhalt { gap: 12px; }\r\n        #fts .fts-stufen { width: 100%; }\r\n        #fts .schwierigkeit-knopf { flex: 1; padding: 6px 8px; }\r\n        #fts .fts-kontrollen { gap: 10px 16px; }\r\n        #fts .fts-fehler input[type=\"range\"] { width: 80px; }\r\n        #fts .ziffer-knopf { min-height: 48px; font-size: 1.2rem; }\r\n        #fts .fts-aktionen { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; width: 100%; }\r\n        #fts .fts-knopf { padding: 10px 8px; font-size: 0.92rem; }\r\n    }\r\n    @media (prefers-reduced-motion: reduce) {\r\n        #fts *, #fts *::before, #fts *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }\r\n    }\r\n<\/style>\r\n\r\n<div id=\"fts\">\r\n\r\n    <div class=\"fts-sr\" aria-live=\"polite\" aria-atomic=\"true\" id=\"fts-spielmeldung\"><\/div>\r\n\r\n    <h1 class=\"fts-titel\">Sudoku<\/h1>\r\n\r\n    <p class=\"fts-intro\">F\u00fcllen Sie das Raster so, dass jede Ziffer von 1 bis 9 in jeder Zeile, Spalte und jedem 3 \u00d7 3-Block genau einmal vorkommt. Jedes R\u00e4tsel hat genau eine L\u00f6sung. Weitere Spiele f\u00fcr Ged\u00e4chtnis, Konzentration und Logik finden Sie auf <a href=\"https:\/\/trainmybrain.ch\/\">trainmybrain.ch<\/a>, unserer Plattform f\u00fcr kognitive Spiele.<\/p>\r\n\r\n    <div class=\"fts-spiel\">\r\n\r\n        <section class=\"fts-karte\" aria-label=\"Sudoku Spielbereich\">\r\n            <div class=\"fts-inhalt\">\r\n\r\n                <div class=\"fts-stufen\" role=\"group\" aria-label=\"Schwierigkeitsgrad\">\r\n                    <button type=\"button\" class=\"schwierigkeit-knopf\" data-level=\"leicht\" aria-pressed=\"false\">Leicht<\/button>\r\n                    <button type=\"button\" class=\"schwierigkeit-knopf aktiv\" data-level=\"mittel\" aria-pressed=\"true\">Mittel<\/button>\r\n                    <button type=\"button\" class=\"schwierigkeit-knopf\" data-level=\"schwer\" aria-pressed=\"false\">Schwer<\/button>\r\n                <\/div>\r\n\r\n                <div class=\"fts-kontrollen\">\r\n                    <div class=\"fts-fehler\">\r\n                        <label for=\"fts-fehler-slider\">Fehlergrenze:<\/label>\r\n                        <input type=\"range\" id=\"fts-fehler-slider\" min=\"0\" max=\"10\" value=\"5\" aria-valuenow=\"5\" aria-valuemin=\"0\" aria-valuemax=\"10\" aria-label=\"Fehlergrenze einstellen\">\r\n                        <span class=\"fts-fehler-anzeige\" id=\"fts-fehler-anzeige\" aria-live=\"polite\">0 \/ 5<\/span>\r\n                    <\/div>\r\n                    <div class=\"fts-zeit\">\r\n                        <time class=\"fts-zeit-anzeige\" id=\"fts-timer-anzeige\" aria-label=\"Spielzeit\">00:00<\/time>\r\n                        <button type=\"button\" class=\"fts-zeit-knopf\" id=\"fts-timer-knopf\" aria-label=\"Spiel starten\">Start<\/button>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <div class=\"fts-brett\">\r\n                    <div class=\"sudoku-grid\" id=\"fts-sudoku-grid\" role=\"grid\" aria-label=\"Sudoku Spielfeld 9 mal 9\"><\/div>\r\n                <\/div>\r\n\r\n                <div class=\"ziffern-pad\" id=\"fts-ziffern-pad\" aria-label=\"Zifferneingabe\">\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"1\" aria-label=\"1 eingeben\">1<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"2\" aria-label=\"2 eingeben\">2<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"3\" aria-label=\"3 eingeben\">3<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"4\" aria-label=\"4 eingeben\">4<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"5\" aria-label=\"5 eingeben\">5<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"6\" aria-label=\"6 eingeben\">6<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"7\" aria-label=\"7 eingeben\">7<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"8\" aria-label=\"8 eingeben\">8<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf\" data-zahl=\"9\" aria-label=\"9 eingeben\">9<\/button>\r\n                    <button type=\"button\" class=\"ziffer-knopf loeschen\" data-zahl=\"0\" aria-label=\"Eingabe l\u00f6schen\">\u2715<\/button>\r\n                <\/div>\r\n\r\n                <div class=\"fts-aktionen\" role=\"toolbar\" aria-label=\"Spielaktionen\">\r\n                    <button type=\"button\" class=\"fts-knopf fts-knopf-gelb\" id=\"fts-btn-neu\">\r\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"\/><path d=\"M3 3v5h5\"\/><\/svg>\r\n                        Neues Spiel\r\n                    <\/button>\r\n                    <button type=\"button\" class=\"fts-knopf\" id=\"fts-btn-hinweis\" aria-label=\"Hinweis: eine richtige Zahl einsetzen\">\r\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.26C17.81 13.47 19 11.38 19 9c0-3.87-3.13-7-7-7z\"\/><line x1=\"9\" y1=\"21\" x2=\"15\" y2=\"21\"\/><\/svg>\r\n                        Hinweis\r\n                    <\/button>\r\n                    <button type=\"button\" class=\"fts-knopf\" id=\"fts-btn-notizen\" aria-pressed=\"false\" aria-label=\"Notizmodus umschalten\">\r\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 20h9\"\/><path d=\"M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z\"\/><\/svg>\r\n                        Notizen\r\n                    <\/button>\r\n                    <button type=\"button\" class=\"fts-knopf\" id=\"fts-btn-loesung\" aria-label=\"L\u00f6sung anzeigen\">\r\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"\/><circle cx=\"12\" cy=\"12\" r=\"3\"\/><\/svg>\r\n                        L\u00f6sung\r\n                    <\/button>\r\n                <\/div>\r\n\r\n            <\/div>\r\n        <\/section>\r\n\r\n        <details class=\"fts-anleitung\" id=\"fts-anleitung-details\">\r\n            <summary>\r\n                Spielanleitung\r\n                <svg class=\"fts-chev\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M6 9l6 6 6-6\"\/><\/svg>\r\n            <\/summary>\r\n            <ul class=\"fts-anleitung-liste\">\r\n                <li><strong>Ziffer setzen:<\/strong> Feld antippen und Ziffer im Ziffernblock w\u00e4hlen, oder mit der Tastatur: Pfeiltasten zum Bewegen, Ziffern 1 bis 9 zum Eintragen, Entf zum L\u00f6schen.<\/li>\r\n                <li><strong>Leicht \/ Mittel \/ Schwer:<\/strong> \u00abLeicht\u00bb l\u00f6sen Sie allein durch direktes Ausschliessen, und Sie haben dabei immer mehrere Felder zur Auswahl. \u00abMittel\u00bb und \u00abSchwer\u00bb geben weniger Zahlen vor und verlangen teils fortgeschrittene Techniken. Jedes R\u00e4tsel hat genau eine L\u00f6sung und ist ohne Raten l\u00f6sbar.<\/li>\r\n                <li><strong>Fehlergrenze:<\/strong> Legt fest, wie viele falsche Eintr\u00e4ge erlaubt sind. Falsche Ziffern werden rot markiert. Bei 0 werden Fehler nicht gez\u00e4hlt.<\/li>\r\n                <li><strong>Start \/ Pause:<\/strong> Startet die Uhr. Ein zweiter Klick pausiert. Wechseln Sie den Tab, pausiert die Uhr automatisch.<\/li>\r\n                <li><strong>Notizen:<\/strong> Im Notizmodus landen Ziffern als kleine Merkzahlen im Feld. Setzen Sie sp\u00e4ter eine Zahl, verschwindet sie automatisch aus den Notizen in Zeile, Spalte und Block.<\/li>\r\n                <li><strong>Hinweis:<\/strong> Setzt eine richtige Zahl in ein leeres Feld.<\/li>\r\n                <li><strong>L\u00f6sung:<\/strong> Zeigt die ganze L\u00f6sung und beendet das Spiel.<\/li>\r\n                <li><strong>Spielstand:<\/strong> Ein angefangenes R\u00e4tsel bleibt in Ihrem Browser gespeichert, samt Notizen, Fehlern und Zeit. Beim n\u00e4chsten Besuch k\u00f6nnen Sie weiterspielen. Der Spielstand verl\u00e4sst Ihr Ger\u00e4t nie und verf\u00e4llt nach 14 Tagen.<\/li>\r\n                <li><strong>Adaptiv:<\/strong> Nach jedem gel\u00f6sten R\u00e4tsel gibt das n\u00e4chste eine Zahl weniger vor, h\u00f6chstens vier weniger innerhalb der Stufe. \u00abLeicht\u00bb bleibt dabei mit einfachen Schritten l\u00f6sbar.<\/li>\r\n            <\/ul>\r\n        <\/details>\r\n\r\n    <\/div><!-- \/.fts-spiel -->\r\n\r\n    <!-- ===== Wie funktioniert Sudoku? ===== -->\r\n    <section class=\"fts-abschnitt\" aria-labelledby=\"fts-h-regeln\">\r\n        <h2 id=\"fts-h-regeln\">Wie funktioniert Sudoku?<\/h2>\r\n        <div class=\"fts-box\">\r\n            <p>Sudoku wird auf einem Raster von 9 \u00d7 9 Feldern gespielt, das in neun Bl\u00f6cke zu 3 \u00d7 3 Feldern unterteilt ist. Einige Felder sind vorgegeben. Ihre Aufgabe: die leeren Felder so f\u00fcllen, dass jede Ziffer von 1 bis 9 in jeder <strong>Zeile<\/strong>, jeder <strong>Spalte<\/strong> und jedem <strong>Block<\/strong> genau einmal vorkommt.<\/p>\r\n            <p>Mathematik braucht es daf\u00fcr nicht, die Ziffern k\u00f6nnten genauso gut Symbole sein. Gefragt ist logisches Ausschliessen: Steht eine Ziffer schon in der Zeile, kann sie nirgends sonst in dieser Zeile stehen.<\/p>\r\n            <p>Jedes R\u00e4tsel auf dieser Seite hat genau eine L\u00f6sung und l\u00e4sst sich ohne Raten l\u00f6sen. Das wird beim Erstellen jedes R\u00e4tsels gepr\u00fcft. Sudoku l\u00e4uft im Browser, kostenlos und ohne Anmeldung.<\/p>\r\n        <\/div>\r\n    <\/section>\r\n\r\n    <!-- ===== Nutzen ===== -->\r\n    <section class=\"fts-abschnitt\" aria-labelledby=\"fts-h-nutzen\">\r\n        <h2 id=\"fts-h-nutzen\">Was ist der Nutzen von Sudoku?<\/h2>\r\n        <div class=\"fts-box\">\r\n            <p>Sudoku ist ein beliebtes Denkspiel und fordert dabei mehrere F\u00e4higkeiten:<\/p>\r\n            <ol class=\"fts-nutzen\">\r\n                <li><span class=\"fts-nutzen-nr\" aria-hidden=\"true\">01<\/span><div><strong>Logisches Denken<\/strong>Jede Ziffer folgt aus den bereits gesetzten. Wer Schritt f\u00fcr Schritt ausschliesst, kommt sicher zum Ziel.<\/div><\/li>\r\n                <li><span class=\"fts-nutzen-nr\" aria-hidden=\"true\">02<\/span><div><strong>Konzentration<\/strong>Zeilen, Spalten und Bl\u00f6cke gleichzeitig im Blick zu behalten, verlangt anhaltende Aufmerksamkeit.<\/div><\/li>\r\n                <li><span class=\"fts-nutzen-nr\" aria-hidden=\"true\">03<\/span><div><strong>Arbeitsged\u00e4chtnis<\/strong>M\u00f6gliche Ziffern f\u00fcr ein Feld im Kopf zu behalten und abzugleichen, fordert das kurzfristige Merken.<\/div><\/li>\r\n                <li><span class=\"fts-nutzen-nr\" aria-hidden=\"true\">04<\/span><div><strong>Geduld und Sorgfalt<\/strong>Ein voreilig gesetztes Feld f\u00fchrt zu Folgefehlern. Ruhiges, genaues Arbeiten zahlt sich aus.<\/div><\/li>\r\n            <\/ol>\r\n        <\/div>\r\n    <\/section>\r\n\r\n    <!-- ===== Tipps ===== -->\r\n    <section class=\"fts-abschnitt\" aria-labelledby=\"fts-h-tipps\">\r\n        <h2 id=\"fts-h-tipps\">Sudoku-Tipps: Strategien zum L\u00f6sen<\/h2>\r\n        <div class=\"fts-box\">\r\n            <ul class=\"fts-tipps\">\r\n                <li><strong>Mit h\u00e4ufigen Ziffern beginnen<\/strong>Kommt eine Ziffer schon oft vor, bleiben f\u00fcr sie nur wenige Felder \u00fcbrig. Dort finden Sie die ersten sicheren Eintr\u00e4ge.<\/li>\r\n                <li><strong>Block f\u00fcr Block pr\u00fcfen<\/strong>Fragen Sie f\u00fcr einen Block: Wo kann die 1 noch hin? Die 2? Passt eine Ziffer nur in ein Feld, geh\u00f6rt sie dorthin.<\/li>\r\n                <li><strong>Fast volle Zeilen nutzen<\/strong>Fehlen in einer Zeile, Spalte oder einem Block nur noch ein oder zwei Ziffern, ist der Rest oft schnell bestimmt.<\/li>\r\n                <li><strong>Notizen einsetzen<\/strong>Ab \u00abMittel\u00bb lohnt es sich, m\u00f6gliche Ziffern als Notiz einzutragen. Sie werden beim Setzen einer Zahl automatisch bereinigt.<\/li>\r\n                <li><strong>Nicht raten<\/strong>Jedes R\u00e4tsel ist ohne Raten l\u00f6sbar. Finden Sie keinen sicheren Schritt, schauen Sie eine andere Ziffer oder einen anderen Block an.<\/li>\r\n            <\/ul>\r\n        <\/div>\r\n    <\/section>\r\n\r\n    <!-- ===== Hintergrund ===== -->\r\n    <section class=\"fts-abschnitt\" aria-labelledby=\"fts-h-hintergrund\">\r\n        <h2 id=\"fts-h-hintergrund\">Hintergrund<\/h2>\r\n        <div class=\"fts-box\">\r\n            <p>Das heutige Sudoku geht auf den Amerikaner Howard Garns zur\u00fcck: Es erschien 1979 unter dem Namen \u00abNumber Place\u00bb in einem R\u00e4tselheft von Dell. In Japan wurde es ab 1984 vom Verlag Nikoli als \u00abSudoku\u00bb bekannt, kurz f\u00fcr \u00abSuji wa dokushin ni kagiru\u00bb, etwa \u00abdie Ziffern m\u00fcssen einzeln bleiben\u00bb.<\/p>\r\n            <p>Weltweit verbreitet hat sich Sudoku ab 2004, als britische Zeitungen begannen, es t\u00e4glich abzudrucken.<\/p>\r\n            <p class=\"fts-quellen-titel\">Quelle<\/p>\r\n            <ul class=\"fts-quellen\">\r\n                <li>Sudoku. In: Wikipedia, englischsprachige Ausgabe. <a href=\"https:\/\/en.wikipedia.org\/wiki\/Sudoku\" target=\"_blank\" rel=\"noopener\">https:\/\/en.wikipedia.org\/wiki\/Sudoku<\/a><\/li>\r\n            <\/ul>\r\n        <\/div>\r\n    <\/section>\r\n\r\n    <!-- ===== Dialoge ===== -->\r\n    <div class=\"fts-modal\" id=\"fts-modal-erfolg\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"fts-erfolg-titel\">\r\n        <div class=\"fts-modal-hintergrund\"><\/div>\r\n        <div class=\"fts-modal-inhalt\">\r\n            <button type=\"button\" class=\"fts-modal-schliessen\" aria-label=\"Schliessen\">\u00d7<\/button>\r\n            <h2 id=\"fts-erfolg-titel\">Geschafft!<\/h2>\r\n            <p>Sie haben das Sudoku gel\u00f6st.<\/p>\r\n            <div class=\"fts-ergebnis\">\r\n                <div><strong id=\"fts-erfolg-zeit\">00:00<\/strong><span>Zeit<\/span><\/div>\r\n                <div><strong id=\"fts-erfolg-fehler\">0<\/strong><span>Fehler<\/span><\/div>\r\n            <\/div>\r\n            <p class=\"fts-modal-hinweis\" id=\"fts-erfolg-naechstes\">Das n\u00e4chste Spiel wird etwas schwieriger!<\/p>\r\n            <div class=\"fts-modal-knoepfe\"><button type=\"button\" class=\"fts-knopf fts-knopf-gelb\" id=\"fts-btn-neues-spiel\">Neues Spiel<\/button><\/div>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"fts-modal\" id=\"fts-modal-fehlergrenze\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"fts-fehlergrenze-titel\">\r\n        <div class=\"fts-modal-hintergrund\"><\/div>\r\n        <div class=\"fts-modal-inhalt\">\r\n            <button type=\"button\" class=\"fts-modal-schliessen\" aria-label=\"Schliessen\">\u00d7<\/button>\r\n            <h2 id=\"fts-fehlergrenze-titel\">Fehlergrenze erreicht<\/h2>\r\n            <p>Sie haben die eingestellte Anzahl Fehler erreicht. Versuchen Sie es mit einem neuen R\u00e4tsel.<\/p>\r\n            <div class=\"fts-modal-knoepfe\"><button type=\"button\" class=\"fts-knopf fts-knopf-gelb\" id=\"fts-btn-nochmals\">Neues R\u00e4tsel<\/button><\/div>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"fts-modal\" id=\"fts-modal-fortsetzen\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"fts-fortsetzen-titel\">\r\n        <div class=\"fts-modal-hintergrund\"><\/div>\r\n        <div class=\"fts-modal-inhalt\">\r\n            <button type=\"button\" class=\"fts-modal-schliessen\" aria-label=\"Schliessen\">\u00d7<\/button>\r\n            <h2 id=\"fts-fortsetzen-titel\">Weiterspielen?<\/h2>\r\n            <p>Sie haben ein angefangenes Sudoku. M\u00f6chten Sie dort weitermachen?<\/p>\r\n            <div class=\"fts-ergebnis\">\r\n                <div><strong id=\"fts-fortsetzen-stufe\">Mittel<\/strong><span>Stufe<\/span><\/div>\r\n                <div><strong id=\"fts-fortsetzen-zeit\">00:00<\/strong><span>Zeit<\/span><\/div>\r\n                <div><strong id=\"fts-fortsetzen-fehler\">0<\/strong><span>Fehler<\/span><\/div>\r\n            <\/div>\r\n            <div class=\"fts-modal-knoepfe\">\r\n                <button type=\"button\" class=\"fts-knopf fts-knopf-gelb\" id=\"fts-btn-fortsetzen\">Weiterspielen<\/button>\r\n                <button type=\"button\" class=\"fts-knopf\" id=\"fts-btn-verwerfen\">Neues Spiel<\/button>\r\n            <\/div>\r\n        <\/div>\r\n    <\/div>\r\n\r\n<\/div>\r\n\r\n<script>\r\n(function () {\r\n    'use strict';\r\n\r\n    if (document.readyState === 'loading') {\r\n        document.addEventListener('DOMContentLoaded', ftsStart);\r\n    } else {\r\n        ftsStart();\r\n    }\r\n\r\n    function ftsStart() {\r\n        const root = document.getElementById('fts');\r\n        if (!root || root.dataset.ftsBereit) return;\r\n        root.dataset.ftsBereit = '1';\r\n\r\n        \/\/ \u2550\u2550\u2550 Spiellogik aus trainmybrain-sudoku.html (per Skript uebernommen) \u2550\u2550\u2550\r\n        \/\/ ================================================================\r\n        \/\/  STATE\r\n        \/\/ ================================================================\r\n        let timerInterval  = null;\r\n        let timerLaeuft    = false;\r\n        let gesamtSekunden = 0;\r\n        let fehlerGrenze   = 5;\r\n        let gesamtFehler   = 0;\r\n        let aktuellesSchwierigkeitsgrad = 'mittel';\r\n        let ausgewaehlteIndex = null;\r\n        let notizModus  = false;\r\n        let notizen     = [];      \/\/ 81 Bitmasken: Bit z gesetzt = Kandidat z notiert\r\n        let vollstaendigeLoesung = [];\r\n        let raetselBoard = [];\r\n        let zellElemente = [];\r\n        let zifferFertig = new Array(10).fill(false);  \/\/ Ziffer z ist neunmal RICHTIG gesetzt\r\n\r\n        \/\/ Zwei Riegel fuer die Speicherung:\r\n        \/\/   spielVorbei  \u2014 Sieg, Fehlergrenze oder \"Loesung\": ab hier gibt es nichts\r\n        \/\/                  mehr fortzusetzen, jeder Schreibversuch waere falsch.\r\n        \/\/   speichernAus \u2014 wirft der Browser einmal beim Schreiben (privater Modus,\r\n        \/\/                  volle Quote), hoeren wir auf, es alle 15s erneut zu probieren.\r\n        let spielVorbei   = false;\r\n        let speichernAus  = false;\r\n        let speicherTimer = null;\r\n\r\n        \/\/ Technikstufen, aufsteigend. Muss vor PROFIL stehen, das darauf zugreift.\r\n        const TECHNIK = { nacktesEinzel: 1, verstecktesEinzel: 2, blockAbgleich: 3, paar: 4 };\r\n\r\n        \/\/ Schwierigkeitsprofile. Der Deckel ist die schwierigste L\u00f6setechnik, die ein\r\n        \/\/ R\u00e4tsel dieser Stufe verlangen darf \u2014 DAS bestimmt die Schwierigkeit. Die\r\n        \/\/ Vorgabenzahl steuert nur, wie viel Sucharbeit dazukommt.\r\n        \/\/ Kalibriert an den etablierten Anbietern: Leicht wie Sudoku.com Easy,\r\n        \/\/ Mittel wie NYT Medium, Schwer wie NYT Hard.\r\n        const PROFIL = {\r\n            leicht: { vorgaben: 38, deckel: TECHNIK.verstecktesEinzel },\r\n            mittel: { vorgaben: 32, deckel: TECHNIK.blockAbgleich },\r\n            schwer: { vorgaben: 28, deckel: TECHNIK.paar },\r\n        };\r\n\r\n        \/\/ Adaptiv angepasste Vorgabenzahl je Stufe. Wandert bei Siegen nach unten,\r\n        \/\/ aber nur bis PROFIL[stufe].vorgaben - 4, und ein Stufenwechsel setzt zur\u00fcck.\r\n        \/\/ Der Technikdeckel wandert NICHT mit: auch eine adaptierte \"Leicht\" bleibt\r\n        \/\/ mit einfachen Techniken l\u00f6sbar. Fr\u00fcher lief das bis 17 Vorgaben ohne R\u00fcckweg,\r\n        \/\/ wodurch \"Leicht\" mit der Zeit zu \"Schwer\" wurde.\r\n        const ADAPTIV_MIN_ABSTAND = 4;\r\n        let adaptiveVorgaben = { leicht: null, mittel: null, schwer: null };\r\n\r\n        function zielVorgaben(stufe) {\r\n            return adaptiveVorgaben[stufe] === null ? PROFIL[stufe].vorgaben : adaptiveVorgaben[stufe];\r\n        }\r\n\r\n        function setzeAdaptivZurueck(stufe) { adaptiveVorgaben[stufe] = null; }\r\n\r\n        \/\/ DOM refs\r\n        const grid          = document.getElementById('fts-sudoku-grid');\r\n        const timerAnzeige  = document.getElementById('fts-timer-anzeige');\r\n        const timerKnopf    = document.getElementById('fts-timer-knopf');\r\n        const fehlerAnzeige = document.getElementById('fts-fehler-anzeige');\r\n        const fehlerSlider  = document.getElementById('fts-fehler-slider');\r\n        const spielmeldung  = document.getElementById('fts-spielmeldung');\r\n\r\n        \/\/ ================================================================\r\n        \/\/  SPEICHER \u2014 angefangenes Spiel ueberlebt das Schliessen des Tabs\r\n        \/\/ ================================================================\r\n        \/\/ Jeder Zugriff ist abgesichert: schon das Lesen der localStorage-Eigenschaft\r\n        \/\/ wirft, wenn der Speicher gesperrt ist (Cookies blockiert, einzelne\r\n        \/\/ Privacy-Erweiterungen). Form und Benennung folgen den Schwesterspielen:\r\n        \/\/ tmb_<spiel>_<zweck>_v<N>, Version im Schluesselnamen statt im Objekt.\r\n        const STAND_KEY = 'ft_sudoku_stand_v1';   \/\/ eigener Schluesselraum ft_*\r\n        const MAX_ALTER = 14 * 24 * 60 * 60 * 1000;   \/\/ 14 Tage\r\n\r\n        function zeitText(sek) {\r\n            const m = String(Math.floor(sek \/ 60)).padStart(2, '0');\r\n            const s = String(sek % 60).padStart(2, '0');\r\n            return `${m}:${s}`;\r\n        }\r\n\r\n        \/** Prueft eines der vier 81er-Felder. Liefert eine saubere Kopie oder null. *\/\r\n        function pruefeFeld(x, min, max) {\r\n            if (!Array.isArray(x) || x.length !== 81) return null;\r\n            const out = new Array(81);\r\n            for (let i = 0; i < 81; i++) {\r\n                const v = Math.floor(Number(x[i]));\r\n                if (!Number.isFinite(v) || v < min || v > max) return null;\r\n                out[i] = v;\r\n            }\r\n            return out;\r\n        }\r\n\r\n        \/**\r\n         * null heisst \"nicht angepasst\" und MUSS null bleiben. Ein `Number(null) || 0`\r\n         * ergaebe 0, zielVorgaben reichte das durch und stanzeAus versuchte ein Brett\r\n         * mit null Vorgaben zu erzeugen.\r\n         *\/\r\n        function pruefeAdaptiv(v, stufe) {\r\n            const n = Math.floor(Number(v));\r\n            const max = PROFIL[stufe].vorgaben;\r\n            return (Number.isFinite(n) && n >= max - ADAPTIV_MIN_ABSTAND && n <= max) ? n : null;\r\n        }\r\n\r\n        function loescheStand() {\r\n            clearTimeout(speicherTimer);   \/\/ sonst schreibt ein wartender Timer alles zurueck\r\n            speicherTimer = null;\r\n            try { localStorage.removeItem(STAND_KEY); } catch (e) { \/* egal *\/ }\r\n        }\r\n\r\n        \/**\r\n         * Der einzige Ort, an dem validiert wird. Zwei Ausgaenge: ein vollstaendig\r\n         * geprueftes Objekt, oder null \u2014 dann ist der Schluessel auch gleich weg,\r\n         * sonst wird bei jedem Laden dieselbe Ruine geparst.\r\n         *\/\r\n        function ladeStand() {\r\n            let roh;\r\n            try { roh = localStorage.getItem(STAND_KEY); } catch (e) { return null; }\r\n            if (!roh) return null;\r\n            try {\r\n                const s = JSON.parse(roh);\r\n                \/\/ Gegen beschaedigte oder fremde Werte absichern\r\n                if (!s || typeof s !== 'object') throw 0;\r\n                if (!PROFIL[s.stufe]) throw 0;\r\n\r\n                const raetsel  = pruefeFeld(s.raetsel,  0, 9);\r\n                const loesung  = pruefeFeld(s.loesung,  1, 9);\r\n                const eingaben = pruefeFeld(s.eingaben, 0, 9);\r\n                const notizenR = pruefeFeld(s.notizen,  0, 1023);\r\n                if (!raetsel || !loesung || !eingaben || !notizenR) throw 0;\r\n\r\n                const grenze   = Math.floor(Number(s.grenze));\r\n                const fehler   = Math.floor(Number(s.fehler));\r\n                const sekunden = Math.floor(Number(s.sekunden));\r\n                const zeit     = Number(s.zeit);\r\n                \/\/ Bereichspruefung statt `|| 5`: die Fehlergrenze 0 ist gueltig und\r\n                \/\/ bedeutet \"Fehler werden nicht gezaehlt\".\r\n                if (!(grenze >= 0 && grenze <= 10)) throw 0;\r\n                if (!(fehler >= 0 && fehler <= 999)) throw 0;\r\n                if (!(sekunden >= 0 && sekunden <= 86400)) throw 0;\r\n                if (!Number.isFinite(zeit) || Date.now() - zeit > MAX_ALTER) throw 0;\r\n\r\n                let offen = 0;\r\n                for (let i = 0; i < 81; i++) {\r\n                    \/\/ Eine Vorgabe, die der gespeicherten Loesung widerspricht, ergaebe\r\n                    \/\/ ein Brett, das man gar nicht gewinnen KANN.\r\n                    if (raetsel[i] !== 0 && raetsel[i] !== loesung[i]) throw 0;\r\n                    if (eingaben[i] !== loesung[i]) offen++;\r\n                }\r\n                if (offen === 0) throw 0;   \/\/ laengst geloest, nichts fortzusetzen\r\n\r\n                return {\r\n                    raetsel, loesung, eingaben,\r\n                    notizen: notizenR.map(n => n & 0x3FE),   \/\/ nur die Bits 1..9\r\n                    hinweise: Array.isArray(s.hinweise)\r\n                        ? s.hinweise.map(Number).filter(i => Number.isInteger(i) && i >= 0 && i < 81)\r\n                        : [],\r\n                    fehler, grenze, sekunden, stufe: s.stufe,\r\n                    adaptiv: {\r\n                        leicht: pruefeAdaptiv(s.adaptiv && s.adaptiv.leicht, 'leicht'),\r\n                        mittel: pruefeAdaptiv(s.adaptiv && s.adaptiv.mittel, 'mittel'),\r\n                        schwer: pruefeAdaptiv(s.adaptiv && s.adaptiv.schwer, 'schwer')\r\n                    },\r\n                    zeit\r\n                };\r\n            } catch (e) { loescheStand(); return null; }\r\n        }\r\n\r\n        \/** Die Wahrheit ueber die Eingaben steht im DOM, nicht in einem Array. *\/\r\n        function sammleStand() {\r\n            const eingaben = new Array(81).fill(0);\r\n            const hinweise = [];\r\n            for (let i = 0; i < 81; i++) {\r\n                const input = zellElemente[i].querySelector('input');\r\n                eingaben[i] = parseInt(input.value, 10) || 0;\r\n                \/\/ Ein Hinweisfeld traegt die richtige Ziffer und ist vom Wert her\r\n                \/\/ nicht von einer richtigen Spielereingabe zu unterscheiden \u2014 die\r\n                \/\/ Klasse ist der einzige Beleg.\r\n                if (input.classList.contains('loesung-zelle')) hinweise.push(i);\r\n            }\r\n            return {\r\n                raetsel:  raetselBoard.map(v => (v === null ? 0 : v)),\r\n                loesung:  vollstaendigeLoesung.slice(),\r\n                eingaben, hinweise,\r\n                notizen:  notizen.slice(),\r\n                fehler:   gesamtFehler,\r\n                grenze:   fehlerGrenze,\r\n                sekunden: gesamtSekunden,\r\n                stufe:    aktuellesSchwierigkeitsgrad,\r\n                adaptiv:  Object.assign({}, adaptiveVorgaben),\r\n                zeit:     Date.now()\r\n            };\r\n        }\r\n\r\n        function speichereStandSofort() {\r\n            clearTimeout(speicherTimer);\r\n            speicherTimer = null;\r\n            if (spielVorbei || speichernAus || zellElemente.length !== 81) return;\r\n            try { localStorage.setItem(STAND_KEY, JSON.stringify(sammleStand())); }\r\n            catch (e) { speichernAus = true; }\r\n        }\r\n\r\n        \/**\r\n         * Ein frisch erzeugtes, nie angeruehrtes Brett wird zwar gespeichert \u2014 damit\r\n         * ein Reload dasselbe Raetsel zeigt statt ein neues zu wuerfeln \u2014, ist aber\r\n         * nichts zum \"Fortsetzen\". Der Dialog erscheint deshalb nur bei echtem\r\n         * Fortschritt, sonst faende ihn jeder vor, der die Seite nur kurz oeffnet.\r\n         *\/\r\n        function hatFortschritt(s) {\r\n            if (s.sekunden > 0 || s.fehler > 0 || s.hinweise.length) return true;\r\n            for (let i = 0; i < 81; i++) {\r\n                if (s.notizen[i]) return true;\r\n                if (s.raetsel[i] === 0 && s.eingaben[i] !== 0) return true;\r\n            }\r\n            return false;\r\n        }\r\n\r\n        \/** Gebuendelt, damit schnelle Eingabefolgen nicht jedes Mal serialisieren. *\/\r\n        function planeSpeichern() {\r\n            if (spielVorbei || speichernAus) return;\r\n            clearTimeout(speicherTimer);\r\n            speicherTimer = setTimeout(speichereStandSofort, 400);\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  GAME \u2014 NEW GAME\r\n        \/\/ ================================================================\r\n        function neuesSpiel(schwierigkeit) {\r\n            aktuellesSchwierigkeitsgrad = schwierigkeit;\r\n            \/\/ Frueh loeschen ist die Versicherung fuer den Fall, dass der Generator\r\n            \/\/ wirft \u2014 dann steht kein Stand mehr da, der zum alten Brett gehoert.\r\n            spielVorbei = false;\r\n            loescheStand();\r\n            stoppTimer();\r\n            gesamtSekunden = 0;\r\n            gesamtFehler   = 0;\r\n            ausgewaehlteIndex = null;\r\n            timerAnzeige.textContent = '00:00';\r\n            timerKnopf.textContent = 'Start';\r\n            timerKnopf.setAttribute('aria-label', 'Spiel starten');\r\n            timerLaeuft = false;\r\n            aktualisiereFehlerAnzeige();\r\n\r\n            notizen = new Array(81).fill(0);\r\n            const profil = { deckel: PROFIL[schwierigkeit].deckel, vorgaben: zielVorgaben(schwierigkeit) };\r\n            const { raetsel, loesung } = generiereGueltigenSudoku(profil);\r\n            raetselBoard = raetsel;\r\n            vollstaendigeLoesung = loesung;\r\n            rendereGrid();\r\n            \/\/ Ein Reload direkt nach \"Neu\" soll dasselbe frische Raetsel zeigen,\r\n            \/\/ nicht schon wieder ein neues erzeugen.\r\n            speichereStandSofort();\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  GAME \u2014 STAND WIEDERHERSTELLEN\r\n        \/\/ ================================================================\r\n        function stelleStandHer(s) {\r\n            aktuellesSchwierigkeitsgrad = s.stufe;\r\n            fehlerGrenze         = s.grenze;\r\n            gesamtFehler         = s.fehler;\r\n            gesamtSekunden       = s.sekunden;\r\n            notizen              = s.notizen;\r\n            adaptiveVorgaben     = s.adaptiv;\r\n            vollstaendigeLoesung = s.loesung;\r\n            \/\/ 0 zurueck auf null: rendereGrid prueft `!== null`. Ohne diese\r\n            \/\/ Rueckabbildung wuerde JEDE leere Zelle zur Vorgabe \"0\", readOnly und\r\n            \/\/ aus der Tab-Reihe genommen \u2014 das Spiel waere unbedienbar.\r\n            raetselBoard         = s.raetsel.map(v => (v === 0 ? null : v));\r\n            spielVorbei          = false;\r\n            ausgewaehlteIndex    = null;\r\n            setzeNotizModus(false);\r\n\r\n            rendereGrid();\r\n            wendeStandAn(s);\r\n\r\n            \/\/ Bedienelemente nachziehen \u2014 ihre Startwerte stehen fest im HTML\r\n            fehlerSlider.value = String(fehlerGrenze);\r\n            fehlerSlider.setAttribute('aria-valuenow', fehlerGrenze);\r\n            aktualisiereFehlerAnzeige();\r\n            document.querySelectorAll('.schwierigkeit-knopf').forEach(b => {\r\n                const an = b.dataset.level === s.stufe;\r\n                b.classList.toggle('aktiv', an);\r\n                b.setAttribute('aria-pressed', String(an));\r\n            });\r\n\r\n            \/\/ \"Weiter\" statt \"Start\": die Uhr steht nicht auf 00:00. Automatisch\r\n            \/\/ starten waere falsch \u2014 der Timer ist per Vertrag manuell.\r\n            timerLaeuft = false;\r\n            timerAnzeige.textContent = zeitText(gesamtSekunden);\r\n            timerKnopf.textContent = 'Weiter';\r\n            timerKnopf.setAttribute('aria-label', 'Spiel fortsetzen');\r\n\r\n            aktualisiereZiffernPad();\r\n            meldung('Spielstand wiederhergestellt. Feld antippen, dann Ziffer waehlen.');\r\n        }\r\n\r\n        function wendeStandAn(s) {\r\n            const hinweisSet = new Set(s.hinweise);\r\n            for (let i = 0; i < 81; i++) {\r\n                const zelle = zellElemente[i];\r\n                const input = zelle.querySelector('input');\r\n                const reihe = Math.floor(i \/ 9) + 1, spalte = (i % 9) + 1;\r\n\r\n                \/\/ Muss fuer ALLE Zellen laufen: die Notiz-Spans stehen auf\r\n                \/\/ visibility:hidden, erst .gesetzt macht sie sichtbar.\r\n                zeichneNotizen(i);\r\n                if (raetselBoard[i] !== null) continue;   \/\/ Vorgaben hat rendereGrid erledigt\r\n\r\n                const wert = s.eingaben[i];\r\n                if (!wert) {\r\n                    input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, leer${notizenText(i)}`);\r\n                    continue;\r\n                }\r\n                input.value = wert;\r\n                zelle.classList.add('hat-wert');   \/\/ blendet das Notizraster aus (CSS)\r\n\r\n                if (hinweisSet.has(i)) {\r\n                    input.readOnly = true;\r\n                    input.classList.add('loesung-zelle');\r\n                    input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, Hinweis: ${wert}`);\r\n                } else if (wert !== vollstaendigeLoesung[i]) {\r\n                    zelle.classList.add('fehler');\r\n                    input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, falsch: ${wert}`);\r\n                } else {\r\n                    input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}: ${wert}`);\r\n                }\r\n            }\r\n        }\r\n\r\n        function zeigeFortsetzenDialog(s) {\r\n            const namen = { leicht: 'Leicht', mittel: 'Mittel', schwer: 'Schwer' };\r\n            document.getElementById('fts-fortsetzen-stufe').textContent  = namen[s.stufe] || s.stufe;\r\n            document.getElementById('fts-fortsetzen-zeit').textContent   = zeitText(s.sekunden);\r\n            document.getElementById('fts-fortsetzen-fehler').textContent = s.fehler;\r\n            zeigeModal('fts-modal-fortsetzen');\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  GRID RENDERING\r\n        \/\/ ================================================================\r\n        function rendereGrid() {\r\n            grid.innerHTML = '';\r\n            zellElemente   = [];\r\n\r\n            for (let i = 0; i < 81; i++) {\r\n                const reihe  = Math.floor(i \/ 9) + 1;\r\n                const spalte = (i % 9) + 1;\r\n\r\n                const zelle = document.createElement('div');\r\n                zelle.classList.add('sudoku-zelle');\r\n                zelle.setAttribute('role', 'gridcell');\r\n                zelle.dataset.index = i;\r\n\r\n                const input = document.createElement('input');\r\n                input.type       = 'text';\r\n                input.inputMode  = 'none';\r\n                input.maxLength  = 1;\r\n                input.setAttribute('autocomplete', 'off');\r\n                input.setAttribute('autocorrect',  'off');\r\n                input.setAttribute('spellcheck',   'false');\r\n\r\n                if (raetselBoard[i] !== null) {\r\n                    input.value    = raetselBoard[i];\r\n                    input.readOnly = true;\r\n                    input.classList.add('vorgegeben');\r\n                    input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, vorgegeben: ${raetselBoard[i]}`);\r\n                    input.tabIndex = -1;\r\n                } else {\r\n                    input.value = '';\r\n                    input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, leer`);\r\n                }\r\n\r\n                const notizGitter = document.createElement('div');\r\n                notizGitter.className = 'notiz-gitter';\r\n                notizGitter.setAttribute('aria-hidden', 'true');   \/\/ Inhalt steckt im aria-label der Zelle\r\n                for (let z = 1; z <= 9; z++) {\r\n                    const sp = document.createElement('span');\r\n                    sp.textContent = z;\r\n                    notizGitter.appendChild(sp);\r\n                }\r\n                zelle.appendChild(notizGitter);\r\n                if (raetselBoard[i] !== null) zelle.classList.add('hat-wert');\r\n\r\n                zelle.appendChild(input);\r\n                grid.appendChild(zelle);\r\n                zellElemente.push(zelle);\r\n\r\n                zelle.addEventListener('click', () => zelleAuswaehlen(i));\r\n                input.addEventListener('focus', () => zelleAuswaehlen(i));\r\n                input.addEventListener('keydown', (e) => handleKeydown(e, i));\r\n            }\r\n\r\n            aktualisiereZiffernPad();\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  CELL SELECTION & HIGHLIGHT\r\n        \/\/ ================================================================\r\n        \/**\r\n         * @param {number}  index\r\n         * @param {boolean} inSichtScrollen  Nur bei Pfeiltasten-Navigation sinnvoll:\r\n         *        dort soll die Zielzelle in den Blick r\u00fccken. Beim Antippen dagegen\r\n         *        NICHT \u2014 sonst scrollt der Browser das Spielfeld unter dem Finger weg.\r\n         *\/\r\n        function zelleAuswaehlen(index, inSichtScrollen) {\r\n            zellElemente.forEach(z => z.classList.remove('ausgewaehlt', 'gleiche-zahl'));\r\n            ausgewaehlteIndex = index;\r\n            zellElemente[index].classList.add('ausgewaehlt');\r\n            highlightGleicheZahlen(index);\r\n            zellElemente[index].querySelector('input').focus({ preventScroll: !inSichtScrollen });\r\n        }\r\n\r\n        function highlightGleicheZahlen(index) {\r\n            const val = zellElemente[index].querySelector('input').value;\r\n            if (!val || val === '0') return;\r\n            zellElemente.forEach((z, i) => {\r\n                if (i !== index && z.querySelector('input').value === val) {\r\n                    z.classList.add('gleiche-zahl');\r\n                }\r\n            });\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  CELL INPUT\r\n        \/\/ ================================================================\r\n        \/\/ ================================================================\r\n        \/\/  NOTIZEN (Kandidatenziffern)\r\n        \/\/ ================================================================\r\n        function zeichneNotizen(i) {\r\n            const gitter = zellElemente[i].querySelector('.notiz-gitter');\r\n            if (!gitter) return;\r\n            const felder = gitter.children;\r\n            for (let z = 1; z <= 9; z++) {\r\n                felder[z - 1].classList.toggle('gesetzt', !!(notizen[i] & (1 << z)));\r\n            }\r\n        }\r\n\r\n        function notizenText(i) {\r\n            const zs = [];\r\n            for (let z = 1; z <= 9; z++) if (notizen[i] & (1 << z)) zs.push(z);\r\n            return zs.length ? `, Notizen: ${zs.join(' ')}` : '';\r\n        }\r\n\r\n        \/**\r\n         * Setzt der Spieler einen Wert, verschwindet dieser Kandidat automatisch aus\r\n         * Reihe, Spalte und Block. Das ist der eigentliche Nutzen von Notizen \u2014 ohne\r\n         * diese Bereinigung m\u00fcsste man die Buchhaltung von Hand f\u00fchren.\r\n         *\/\r\n        function bereinigeNotizen(i, zahl) {\r\n            notizen[i] = 0;\r\n            zeichneNotizen(i);\r\n            for (const p of NACHBARN[i]) {\r\n                if (notizen[p] & (1 << zahl)) {\r\n                    notizen[p] &= ~(1 << zahl);\r\n                    zeichneNotizen(p);\r\n                    const inp = zellElemente[p].querySelector('input');\r\n                    if (!inp.value) {\r\n                        const r = Math.floor(p \/ 9) + 1, c = (p % 9) + 1;\r\n                        inp.setAttribute('aria-label', `Reihe ${r}, Spalte ${c}, leer${notizenText(p)}`);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        function setzeNotizModus(an) {\r\n            notizModus = an;\r\n            const btn = document.getElementById('fts-btn-notizen');\r\n            if (btn) btn.setAttribute('aria-pressed', String(an));\r\n            meldung(an ? 'Notizmodus ein: Ziffern werden als Kandidaten eingetragen.'\r\n                       : 'Notizmodus aus: Ziffern werden als Antwort eingetragen.');\r\n        }\r\n\r\n        \/**\r\n         * Einziger Eingabepunkt fuer Ziffernpad UND Tastatur. Der Wrapper haengt das\r\n         * Nachziehen von Ziffernpad und Speicherung an ALLE Rueckgabepfade der\r\n         * inneren Funktion \u2014 sonst wird bei jedem neuen `return` einer vergessen.\r\n         *\/\r\n        function zelleEingabe(zahl) {\r\n            zelleEingabeIntern(zahl);\r\n            aktualisiereZiffernPad();\r\n            planeSpeichern();\r\n        }\r\n\r\n        function zelleEingabeIntern(zahl) {\r\n            if (ausgewaehlteIndex === null) return;\r\n            const i     = ausgewaehlteIndex;\r\n            const zelle = zellElemente[i];\r\n            const input = zelle.querySelector('input');\r\n            if (input.readOnly) return;\r\n\r\n            \/\/ Vollstaendig gesetzte Ziffer: im Pad ist sie ausgeblendet, also muss\r\n            \/\/ die Tastatur genauso reagieren. Beide Wege laufen hier durch.\r\n            if (zahl !== 0 && zifferFertig[zahl]) {\r\n                meldung(`Die Ziffer ${zahl} ist bereits vollst\u00e4ndig gesetzt.`);\r\n                return;\r\n            }\r\n\r\n            const reihe  = Math.floor(i \/ 9) + 1;\r\n            const spalte = (i % 9) + 1;\r\n\r\n            \/\/ Notizmodus: Ziffer schaltet einen Kandidaten um, statt zu antworten.\r\n            \/\/ Beide Eingabewege (Ziffernpad und Tastatur) laufen hier durch \u2014 es\r\n            \/\/ braucht deshalb keinen zweiten Eingabepfad.\r\n            if (notizModus && zahl !== 0 && !input.value) {\r\n                notizen[i] ^= (1 << zahl);\r\n                zeichneNotizen(i);\r\n                input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, leer${notizenText(i)}`);\r\n                meldung((notizen[i] & (1 << zahl)) ? `Notiz ${zahl} gesetzt.` : `Notiz ${zahl} entfernt.`);\r\n                return;\r\n            }\r\n\r\n            if (zahl === 0) {\r\n                input.value = '';\r\n                zelle.classList.remove('hat-wert');\r\n                zelle.classList.remove('fehler');\r\n                zeichneNotizen(i);\r\n                input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, leer${notizenText(i)}`);\r\n                zellElemente.forEach(z => z.classList.remove('gleiche-zahl'));\r\n                zellElemente[i].classList.add('ausgewaehlt');\r\n                meldung('');\r\n                return;\r\n            }\r\n\r\n            input.value = zahl;\r\n            zelle.classList.remove('fehler');\r\n            zelle.classList.add('hat-wert');\r\n            bereinigeNotizen(i, zahl);\r\n\r\n            if (parseInt(zahl, 10) !== vollstaendigeLoesung[i]) {\r\n                zelle.classList.add('fehler');\r\n                gesamtFehler++;\r\n                aktualisiereFehlerAnzeige();\r\n                input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}, falsch: ${zahl}`);\r\n                meldung(`Falsch! Fehler ${gesamtFehler} von ${fehlerGrenze}.`);\r\n\r\n                if (fehlerGrenze > 0 && gesamtFehler >= fehlerGrenze) {\r\n                    stoppTimer();\r\n                    \/\/ Ohne Loeschen landete man nach einem Reload in einem toten\r\n                    \/\/ Spiel \u2014 Fehlergrenze ueberschritten, aber ohne Modal.\r\n                    spielVorbei = true;\r\n                    loescheStand();\r\n                    setTimeout(() => zeigeModal('fts-modal-fehlergrenze'), 300);\r\n                }\r\n            } else {\r\n                input.setAttribute('aria-label', `Reihe ${reihe}, Spalte ${spalte}: ${zahl}`);\r\n                meldung('Korrekt!');\r\n                zellElemente.forEach(z => z.classList.remove('gleiche-zahl'));\r\n                highlightGleicheZahlen(i);\r\n                pruefeAbschluss();\r\n            }\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  KEYBOARD NAVIGATION\r\n        \/\/ ================================================================\r\n        function handleKeydown(e, index) {\r\n            const key = e.key;\r\n\r\n            if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) {\r\n                e.preventDefault();\r\n                let next = index;\r\n                if (key === 'ArrowUp'    && index >= 9)    next = index - 9;\r\n                if (key === 'ArrowDown'  && index <  72)   next = index + 9;\r\n                if (key === 'ArrowLeft'  && index % 9 > 0) next = index - 1;\r\n                if (key === 'ArrowRight' && index % 9 < 8) next = index + 1;\r\n                if (next !== index) zelleAuswaehlen(next, true);\r\n                return;\r\n            }\r\n\r\n            if (\/^[1-9]$\/.test(key)) { e.preventDefault(); zelleEingabe(parseInt(key, 10)); return; }\r\n            if (key === 'Delete' || key === 'Backspace') { e.preventDefault(); zelleEingabe(0); return; }\r\n            e.preventDefault();\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  COMPLETION CHECK\r\n        \/\/ ================================================================\r\n        function pruefeAbschluss() {\r\n            for (let i = 0; i < 81; i++) {\r\n                if (parseInt(zellElemente[i].querySelector('input').value, 10) !== vollstaendigeLoesung[i]) return;\r\n            }\r\n            stoppTimer();\r\n            spielVorbei = true;\r\n            loescheStand();          \/\/ geloest ist nicht fortsetzbar\r\n\r\n            document.getElementById('fts-erfolg-zeit').textContent   = zeitText(gesamtSekunden);\r\n            document.getElementById('fts-erfolg-fehler').textContent = gesamtFehler;\r\n\r\n            \/\/ Adaptive: n\u00e4chstes Spiel etwas fordernder \u2014 aber nur innerhalb der Stufe\r\n            const s = aktuellesSchwierigkeitsgrad;\r\n            const untergrenze = PROFIL[s].vorgaben - ADAPTIV_MIN_ABSTAND;\r\n            if (zielVorgaben(s) > untergrenze) {\r\n                adaptiveVorgaben[s] = zielVorgaben(s) - 1;\r\n                document.getElementById('fts-erfolg-naechstes').textContent = 'Das n\u00e4chste Spiel wird etwas schwieriger!';\r\n            } else {\r\n                document.getElementById('fts-erfolg-naechstes').textContent = 'Sie beherrschen diese Stufe \u2014 probieren Sie die n\u00e4chste!';\r\n            }\r\n\r\n            meldung('Herzlichen Gl\u00fcckwunsch! Sudoku gel\u00f6st!');\r\n            ftEvent({ event: 'sudoku_geloest', sudoku_stufe: s, sudoku_sekunden: gesamtSekunden, sudoku_fehler: gesamtFehler });\r\n            setTimeout(() => zeigeModal('fts-modal-erfolg'), 400);\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  HELPERS\r\n        \/\/ ================================================================\r\n        function aktualisiereFehlerAnzeige() {\r\n            fehlerAnzeige.textContent = `${gesamtFehler} \/ ${fehlerGrenze}`;\r\n            fehlerAnzeige.classList.toggle('warnung', fehlerGrenze > 0 && gesamtFehler >= Math.ceil(fehlerGrenze * 0.7));\r\n        }\r\n\r\n        function meldung(text) { spielmeldung.textContent = text; }\r\n\r\n        \/**\r\n         * Blendet jede Ziffer aus, von der alle neun Felder RICHTIG gesetzt sind.\r\n         * Nur richtige Eintraege zaehlen: ein falsch gesetzter Wert darf die Ziffer\r\n         * nicht abschliessen, sonst fehlt sie genau dann, wenn man sie zum\r\n         * Korrigieren braucht.\r\n         *\/\r\n        function aktualisiereZiffernPad() {\r\n            const gesetzt = new Array(10).fill(0);\r\n            for (let i = 0; i < 81; i++) {\r\n                const v = parseInt(zellElemente[i].querySelector('input').value, 10);\r\n                if (v && v === vollstaendigeLoesung[i]) gesetzt[v]++;\r\n            }\r\n            for (let z = 1; z <= 9; z++) zifferFertig[z] = (gesetzt[z] === 9);\r\n\r\n            document.querySelectorAll('.ziffer-knopf').forEach(btn => {\r\n                const z = parseInt(btn.dataset.zahl, 10);\r\n                if (!z) return;                       \/\/ Loeschtaste bleibt immer da\r\n                btn.classList.toggle('fertig', zifferFertig[z]);\r\n                btn.disabled = zifferFertig[z];\r\n            });\r\n        }\r\n\r\n        function hinweisGeben() {\r\n            const kandidaten = [];\r\n            for (let i = 0; i < 81; i++) {\r\n                const inp = zellElemente[i].querySelector('input');\r\n                if (!inp.readOnly && (inp.value === '' || zellElemente[i].classList.contains('fehler'))) {\r\n                    kandidaten.push(i);\r\n                }\r\n            }\r\n            if (!kandidaten.length) return;\r\n            const idx = kandidaten[Math.floor(Math.random() * kandidaten.length)];\r\n            const inp = zellElemente[idx].querySelector('input');\r\n            const richtig = vollstaendigeLoesung[idx];\r\n            inp.value    = richtig;\r\n            inp.readOnly = true;\r\n            inp.classList.add('loesung-zelle');\r\n            zellElemente[idx].classList.remove('fehler');\r\n            zellElemente[idx].classList.add('hat-wert');\r\n            bereinigeNotizen(idx, richtig);\r\n            inp.setAttribute('aria-label', `Reihe ${Math.floor(idx\/9)+1}, Spalte ${idx%9+1}, Hinweis: ${richtig}`);\r\n            pruefeAbschluss();\r\n            aktualisiereZiffernPad();\r\n            planeSpeichern();\r\n        }\r\n\r\n        function loesungZeigen() {\r\n            stoppTimer();\r\n            \/\/ VOR der Schleife: sie vergibt .loesung-zelle an alle 81 Felder, ein\r\n            \/\/ Schreiben danach wuerde jedes Feld als \"Hinweis\" einfrieren.\r\n            spielVorbei = true;\r\n            loescheStand();\r\n            for (let i = 0; i < 81; i++) {\r\n                const inp = zellElemente[i].querySelector('input');\r\n                inp.value    = vollstaendigeLoesung[i];\r\n                inp.readOnly = true;\r\n                inp.classList.add('loesung-zelle');\r\n                zellElemente[i].classList.remove('fehler');\r\n                zellElemente[i].classList.add('hat-wert');\r\n                notizen[i] = 0;\r\n            }\r\n            aktualisiereZiffernPad();\r\n            meldung('L\u00f6sung wird angezeigt.');\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  TIMER\r\n        \/\/ ================================================================\r\n        function starteTimer() {\r\n            timerInterval = setInterval(() => {\r\n                gesamtSekunden++;\r\n                timerAnzeige.textContent = zeitText(gesamtSekunden);\r\n                \/\/ Nicht im Sekundentakt schreiben. Schlimmstenfalls gehen 15\r\n                \/\/ Sekunden Spielzeit verloren, dafuer bleibt der Speicher ruhig.\r\n                if (gesamtSekunden % 15 === 0) planeSpeichern();\r\n            }, 1000);\r\n            timerLaeuft = true;\r\n        }\r\n\r\n        function stoppTimer() {\r\n            clearInterval(timerInterval);\r\n            timerInterval = null;\r\n            timerLaeuft   = false;\r\n        }\r\n\r\n        \/\/ Auto-pause when user switches tabs. Gesichert wird in JEDEM Fall, nicht\r\n        \/\/ nur bei laufendem Timer \u2014 der Wechsel ist oft der letzte Moment vor dem\r\n        \/\/ Schliessen des Tabs.\r\n        document.addEventListener('visibilitychange', () => {\r\n            if (!document.hidden) return;\r\n            if (timerLaeuft) {\r\n                stoppTimer();\r\n                timerKnopf.textContent = 'Weiter';\r\n                timerKnopf.setAttribute('aria-label', 'Spiel fortsetzen');\r\n            }\r\n            speichereStandSofort();\r\n        });\r\n\r\n        \/\/ pagehide statt beforeunload: auf iOS Safari ist beforeunload unzuverlaessig.\r\n        window.addEventListener('pagehide', speichereStandSofort);\r\n\r\n        \/\/ ================================================================\r\n        \/\/  EVENT BINDINGS\r\n        \/\/ ================================================================\r\n        function bindEvents() {\r\n            \/\/ Difficulty buttons\r\n            document.querySelectorAll('.schwierigkeit-knopf').forEach(btn => {\r\n                btn.addEventListener('click', () => {\r\n                    document.querySelectorAll('.schwierigkeit-knopf').forEach(b => {\r\n                        b.classList.remove('aktiv');\r\n                        b.setAttribute('aria-pressed', 'false');\r\n                    });\r\n                    btn.classList.add('aktiv');\r\n                    btn.setAttribute('aria-pressed', 'true');\r\n                    setzeAdaptivZurueck(btn.dataset.level);\r\n                    neuesSpiel(btn.dataset.level);\r\n                });\r\n            });\r\n\r\n            \/\/ Timer toggle\r\n            timerKnopf.addEventListener('click', () => {\r\n                if (!timerLaeuft) {\r\n                    starteTimer();\r\n                    timerKnopf.textContent = 'Pause';\r\n                    timerKnopf.setAttribute('aria-label', 'Spiel pausieren');\r\n                } else {\r\n                    stoppTimer();\r\n                    timerKnopf.textContent = 'Weiter';\r\n                    timerKnopf.setAttribute('aria-label', 'Spiel fortsetzen');\r\n                }\r\n                planeSpeichern();\r\n            });\r\n\r\n            \/\/ Error slider\r\n            fehlerSlider.addEventListener('input', (e) => {\r\n                fehlerGrenze = parseInt(e.target.value, 10);\r\n                fehlerSlider.setAttribute('aria-valuenow', fehlerGrenze);\r\n                aktualisiereFehlerAnzeige();\r\n                planeSpeichern();   \/\/ feuert pro Rasterschritt \u2014 deshalb gebuendelt\r\n            });\r\n\r\n            \/\/ Action buttons\r\n            document.getElementById('fts-btn-neu').addEventListener('click', () => neuesSpiel(aktuellesSchwierigkeitsgrad));\r\n            document.getElementById('fts-btn-hinweis').addEventListener('click', hinweisGeben);\r\n            document.getElementById('fts-btn-loesung').addEventListener('click', loesungZeigen);\r\n            document.getElementById('fts-btn-notizen').addEventListener('click', () => setzeNotizModus(!notizModus));\r\n\r\n            \/\/ Number pad\r\n            document.querySelectorAll('.ziffer-knopf').forEach(btn => {\r\n                btn.addEventListener('click', () => zelleEingabe(parseInt(btn.dataset.zahl, 10)));\r\n            });\r\n\r\n            \/\/ Game modal actions\r\n            document.getElementById('fts-btn-neues-spiel').addEventListener('click', () => {\r\n                schliesseModal('fts-modal-erfolg');\r\n                neuesSpiel(aktuellesSchwierigkeitsgrad);\r\n            });\r\n            document.getElementById('fts-btn-nochmals').addEventListener('click', () => {\r\n                schliesseModal('fts-modal-fehlergrenze');\r\n                neuesSpiel(aktuellesSchwierigkeitsgrad);\r\n            });\r\n\r\n            \/\/ Fortsetzen-Dialog. Das Brett steht bereits wiederhergestellt dahinter,\r\n            \/\/ \"Fortsetzen\" muss den Dialog also nur schliessen \u2014 auch das Wegklicken\r\n            \/\/ ueber Overlay, Kreuz oder Escape fuehrt damit zum richtigen Ergebnis.\r\n            document.getElementById('fts-btn-fortsetzen').addEventListener('click', () => {\r\n                schliesseModal('fts-modal-fortsetzen');\r\n            });\r\n            document.getElementById('fts-btn-verwerfen').addEventListener('click', () => {\r\n                schliesseModal('fts-modal-fortsetzen');\r\n                neuesSpiel(aktuellesSchwierigkeitsgrad);\r\n            });\r\n        }\r\n\r\n        \/\/ ================================================================\r\n        \/\/  SUDOKU GENERATOR\r\n        \/\/ ================================================================\r\n        \/\/ Ein Sudoku ist per Definition ein R\u00e4tsel mit GENAU EINER L\u00f6sung.\r\n        \/\/ Der fr\u00fchere Generator hat Felder rein zuf\u00e4llig entfernt und diese\r\n        \/\/ Eigenschaft nie gepr\u00fcft \u2014 je nach Stufe hatten dadurch 76\u2013100 % der\r\n        \/\/ R\u00e4tsel mehrere L\u00f6sungen. Da jede Eingabe gegen eine bestimmte davon\r\n        \/\/ gepr\u00fcft wird, wurde korrektes Spiel als Fehler gewertet.\r\n        \/\/\r\n        \/\/ Jetzt wird ein Feld nur entfernt, wenn BEIDES gilt:\r\n        \/\/   1. das R\u00e4tsel bleibt eindeutig l\u00f6sbar\r\n        \/\/   2. es bleibt mit den f\u00fcr die Stufe erlaubten Techniken l\u00f6sbar\r\n        \/\/\r\n        \/\/ Damit ergibt sich die Schwierigkeit aus der n\u00f6tigen L\u00f6setechnik statt\r\n        \/\/ aus der blossen Vorgabenzahl \u2014 so arbeiten auch Sudoku.com und die NYT.\r\n        \/\/ Intern rechnen die Generatorfunktionen mit 0 f\u00fcr \"leer\"; nach aussen\r\n        \/\/ bleibt null, damit die Darstellung unver\u00e4ndert weiterl\u00e4uft.\r\n\r\n        \/\/ Nachbarfelder und Einheiten einmalig vorberechnen\r\n        const NACHBARN = (() => {\r\n            const n = [];\r\n            for (let i = 0; i < 81; i++) {\r\n                const r = Math.floor(i \/ 9), c = i % 9;\r\n                const br = Math.floor(r \/ 3) * 3, bc = Math.floor(c \/ 3) * 3;\r\n                const s = new Set();\r\n                for (let k = 0; k < 9; k++) { s.add(r * 9 + k); s.add(k * 9 + c); }\r\n                for (let dr = 0; dr < 3; dr++) for (let dc = 0; dc < 3; dc++) s.add((br + dr) * 9 + bc + dc);\r\n                s.delete(i);\r\n                n.push([...s]);\r\n            }\r\n            return n;\r\n        })();\r\n\r\n        const EINHEITEN = (() => {\r\n            const u = [];\r\n            for (let r = 0; r < 9; r++) u.push(Array.from({ length: 9 }, (_, c) => r * 9 + c));\r\n            for (let c = 0; c < 9; c++) u.push(Array.from({ length: 9 }, (_, r) => r * 9 + c));\r\n            for (let b = 0; b < 9; b++) {\r\n                const br = Math.floor(b \/ 3) * 3, bc = (b % 3) * 3, feld = [];\r\n                for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) feld.push((br + r) * 9 + bc + c);\r\n                u.push(feld);\r\n            }\r\n            return u;\r\n        })();\r\n\r\n        function generiereGueltigenSudoku(profil) {\r\n            const board = new Array(81).fill(null);\r\n            fuelleBoard(board);\r\n            const loesung = [...board];\r\n            const raetsel = stanzeAus(loesung, profil.deckel, profil.vorgaben);\r\n            \/\/ fahrtauglich: \u00abLeicht\u00bb ohne Engstellen (mind. 3 moegliche Zuege bei 10+ offenen Feldern)\r\n            if (profil.deckel === TECHNIK.verstecktesEinzel) entschaerfeEngstellen(raetsel, loesung, 3);\r\n            return { raetsel, loesung };\r\n        }\r\n\r\n        \/**\r\n         * Z\u00e4hlt L\u00f6sungen bis h\u00f6chstens `limit` (2 gen\u00fcgt f\u00fcr die Eindeutigkeitsfrage).\r\n         * W\u00e4hlt immer das Feld mit den wenigsten Kandidaten zuerst \u2014 das beschneidet\r\n         * den Suchbaum drastisch.\r\n         *\/\r\n        function zaehleLoesungen(board, limit) {\r\n            const b = board.map(v => (v === null ? 0 : v));\r\n            let gefunden = 0;\r\n\r\n            (function rekursion() {\r\n                if (gefunden >= limit) return;\r\n                let bestes = -1, wenigste = 10, bestMaske = 0;\r\n                for (let i = 0; i < 81; i++) {\r\n                    if (b[i] !== 0) continue;\r\n                    let maske = 0, anzahl = 0;\r\n                    for (let z = 1; z <= 9; z++) {\r\n                        if (!NACHBARN[i].some(p => b[p] === z)) { maske |= 1 << z; anzahl++; }\r\n                    }\r\n                    if (anzahl < wenigste) { wenigste = anzahl; bestes = i; bestMaske = maske; if (anzahl <= 1) break; }\r\n                }\r\n                if (bestes === -1) { gefunden++; return; }\r\n                for (let z = 1; z <= 9; z++) {\r\n                    if (bestMaske & (1 << z)) {\r\n                        b[bestes] = z;\r\n                        rekursion();\r\n                        b[bestes] = 0;\r\n                        if (gefunden >= limit) return;\r\n                    }\r\n                }\r\n            })();\r\n\r\n            return gefunden;\r\n        }\r\n\r\n        \/**\r\n         * L\u00f6st das R\u00e4tsel rein logisch und liefert die schwierigste daf\u00fcr n\u00f6tige\r\n         * Technikstufe zur\u00fcck \u2014 oder null, wenn die implementierten Techniken nicht\r\n         * ausreichen. Der L\u00f6ser ist bewusst schw\u00e4cher als ein ge\u00fcbter Spieler:\r\n         * Was er nicht schafft, kommt gar nicht erst zum Nutzer.\r\n         *\/\r\n        function bewerteRaetsel(board) {\r\n            const b = board.map(v => (v === null ? 0 : v));\r\n            const kand = new Array(81).fill(0);\r\n            let hoechste = 0, schutz = 0;\r\n\r\n            const anzahlBits = m => { let n = 0; while (m) { n += m & 1; m >>= 1; } return n; };\r\n            const ersteZahl  = m => { for (let z = 1; z <= 9; z++) if (m & (1 << z)) return z; return 0; };\r\n            const kandidatenNeu = () => {\r\n                for (let i = 0; i < 81; i++) {\r\n                    if (b[i]) { kand[i] = 0; continue; }\r\n                    let m = 0;\r\n                    for (let z = 1; z <= 9; z++) if (!NACHBARN[i].some(p => b[p] === z)) m |= 1 << z;\r\n                    kand[i] = m;\r\n                }\r\n            };\r\n\r\n            while (b.includes(0)) {\r\n                if (++schutz > 400) return null;\r\n                kandidatenNeu();\r\n                for (let i = 0; i < 81; i++) if (!b[i] && kand[i] === 0) return null;  \/\/ Sackgasse\r\n\r\n                let fortschritt = false;\r\n\r\n                \/\/ \u2500\u2500 1 \u00b7 Nacktes Einzel: nur ein Kandidat im Feld \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n                for (let i = 0; i < 81 && !fortschritt; i++) {\r\n                    if (!b[i] && anzahlBits(kand[i]) === 1) {\r\n                        b[i] = ersteZahl(kand[i]);\r\n                        hoechste = Math.max(hoechste, TECHNIK.nacktesEinzel);\r\n                        fortschritt = true;\r\n                    }\r\n                }\r\n                if (fortschritt) continue;\r\n\r\n                \/\/ \u2500\u2500 2 \u00b7 Verstecktes Einzel: Ziffer passt in der Einheit nur hierhin \u2500\u2500\r\n                for (const u of EINHEITEN) {\r\n                    for (let z = 1; z <= 9 && !fortschritt; z++) {\r\n                        if (u.some(i => b[i] === z)) continue;\r\n                        const pos = u.filter(i => !b[i] && (kand[i] & (1 << z)));\r\n                        if (pos.length === 1) {\r\n                            b[pos[0]] = z;\r\n                            hoechste = Math.max(hoechste, TECHNIK.verstecktesEinzel);\r\n                            fortschritt = true;\r\n                        }\r\n                    }\r\n                    if (fortschritt) break;\r\n                }\r\n                if (fortschritt) continue;\r\n\r\n                \/\/ \u2500\u2500 3 \u00b7 Block-Abgleich (locked candidates) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n                \/\/ Liegen alle Pl\u00e4tze einer Ziffer innerhalb einer Einheit auch in\r\n                \/\/ einer zweiten, kann sie aus dem Rest der zweiten gestrichen werden.\r\n                for (let z = 1; z <= 9 && !fortschritt; z++) {\r\n                    for (const u of EINHEITEN) {\r\n                        const pos = u.filter(i => !b[i] && (kand[i] & (1 << z)));\r\n                        if (pos.length < 2 || pos.length > 3) continue;\r\n                        for (const andere of EINHEITEN) {\r\n                            if (andere === u) continue;\r\n                            if (!pos.every(i => andere.includes(i))) continue;\r\n                            let geaendert = false;\r\n                            for (const i of andere) {\r\n                                if (!pos.includes(i) && !b[i] && (kand[i] & (1 << z))) {\r\n                                    kand[i] &= ~(1 << z);\r\n                                    geaendert = true;\r\n                                }\r\n                            }\r\n                            if (geaendert) {\r\n                                hoechste = Math.max(hoechste, TECHNIK.blockAbgleich);\r\n                                fortschritt = true;\r\n                                break;\r\n                            }\r\n                        }\r\n                        if (fortschritt) break;\r\n                    }\r\n                }\r\n                if (fortschritt) { setzeAbgeleitete(b, kand, anzahlBits, ersteZahl); continue; }\r\n\r\n                \/\/ \u2500\u2500 4a \u00b7 Nacktes Paar: zwei Felder teilen dieselben zwei Kandidaten \u2500\u2500\r\n                for (const u of EINHEITEN) {\r\n                    const zweier = u.filter(i => !b[i] && anzahlBits(kand[i]) === 2);\r\n                    for (let x = 0; x < zweier.length && !fortschritt; x++) {\r\n                        for (let y = x + 1; y < zweier.length && !fortschritt; y++) {\r\n                            if (kand[zweier[x]] !== kand[zweier[y]]) continue;\r\n                            let geaendert = false;\r\n                            for (const i of u) {\r\n                                if (i !== zweier[x] && i !== zweier[y] && !b[i] && (kand[i] & kand[zweier[x]])) {\r\n                                    kand[i] &= ~kand[zweier[x]];\r\n                                    geaendert = true;\r\n                                }\r\n                            }\r\n                            if (geaendert) { hoechste = Math.max(hoechste, TECHNIK.paar); fortschritt = true; }\r\n                        }\r\n                    }\r\n                    if (fortschritt) break;\r\n                }\r\n                if (fortschritt) { setzeAbgeleitete(b, kand, anzahlBits, ersteZahl); continue; }\r\n\r\n                \/\/ \u2500\u2500 4b \u00b7 Verstecktes Paar: zwei Ziffern nur in denselben zwei Feldern \u2500\u2500\r\n                for (const u of EINHEITEN) {\r\n                    for (let z1 = 1; z1 <= 9 && !fortschritt; z1++) {\r\n                        const p1 = u.filter(i => !b[i] && (kand[i] & (1 << z1)));\r\n                        if (p1.length !== 2) continue;\r\n                        for (let z2 = z1 + 1; z2 <= 9 && !fortschritt; z2++) {\r\n                            const p2 = u.filter(i => !b[i] && (kand[i] & (1 << z2)));\r\n                            if (p2.length !== 2 || p1[0] !== p2[0] || p1[1] !== p2[1]) continue;\r\n                            const behalten = (1 << z1) | (1 << z2);\r\n                            let geaendert = false;\r\n                            for (const i of p1) {\r\n                                if (kand[i] & ~behalten) { kand[i] &= behalten; geaendert = true; }\r\n                            }\r\n                            if (geaendert) { hoechste = Math.max(hoechste, TECHNIK.paar); fortschritt = true; }\r\n                        }\r\n                    }\r\n                    if (fortschritt) break;\r\n                }\r\n                if (fortschritt) { setzeAbgeleitete(b, kand, anzahlBits, ersteZahl); continue; }\r\n\r\n                return null;   \/\/ braucht mehr als die zugelassenen Techniken\r\n            }\r\n\r\n            return hoechste;\r\n        }\r\n\r\n        \/\/ Nach einer Kandidaten-Streichung die unmittelbar ableitbaren Felder setzen\r\n        function setzeAbgeleitete(b, kand, anzahlBits, ersteZahl) {\r\n            for (let i = 0; i < 81; i++) {\r\n                if (!b[i] && anzahlBits(kand[i]) === 1) b[i] = ersteZahl(kand[i]);\r\n            }\r\n        }\r\n\r\n        \/**\r\n         * Stanzt Felder aus der L\u00f6sung, solange Eindeutigkeit UND Technikdeckel\r\n         * halten. Wird das Vorgabenziel nicht erreicht, bleiben mehr Felder stehen \u2014\r\n         * das R\u00e4tsel wird dann leichter als geplant, nie schwerer.\r\n         *\/\r\n        function stanzeAus(loesung, deckel, zielVorgaben) {\r\n            const b = [...loesung];\r\n            const reihenfolge = mischen([...Array(81).keys()]);\r\n            const maxEntfernen = 81 - zielVorgaben;\r\n            let entfernt = 0;\r\n\r\n            for (const i of reihenfolge) {\r\n                if (entfernt >= maxEntfernen) break;\r\n                const alt = b[i];\r\n                b[i] = null;\r\n                if (zaehleLoesungen(b, 2) !== 1) { b[i] = alt; continue; }\r\n                const stufe = bewerteRaetsel(b);\r\n                if (stufe === null || stufe > deckel) { b[i] = alt; continue; }\r\n                entfernt++;\r\n            }\r\n            return b;\r\n        }\r\n\r\n        function fuelleBoard(board) {\r\n            function kannPlatzieren(b, reihe, spalte, zahl) {\r\n                for (let i = 0; i < 9; i++) {\r\n                    if (b[reihe * 9 + i] === zahl) return false;\r\n                    if (b[i * 9 + spalte] === zahl) return false;\r\n                }\r\n                const sr = Math.floor(reihe \/ 3) * 3;\r\n                const sc = Math.floor(spalte \/ 3) * 3;\r\n                for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) {\r\n                    if (b[(sr + r) * 9 + (sc + c)] === zahl) return false;\r\n                }\r\n                return true;\r\n            }\r\n\r\n            function solve(b) {\r\n                for (let i = 0; i < 81; i++) {\r\n                    if (b[i] === null) {\r\n                        const zahlen = mischen([1,2,3,4,5,6,7,8,9]);\r\n                        for (const z of zahlen) {\r\n                            if (kannPlatzieren(b, Math.floor(i\/9), i%9, z)) {\r\n                                b[i] = z;\r\n                                if (solve(b)) return true;\r\n                                b[i] = null;\r\n                            }\r\n                        }\r\n                        return false;\r\n                    }\r\n                }\r\n                return true;\r\n            }\r\n\r\n            solve(board);\r\n        }\r\n\r\n        function mischen(arr) {\r\n            for (let i = arr.length - 1; i > 0; i--) {\r\n                const j = Math.floor(Math.random() * (i + 1));\r\n                [arr[i], arr[j]] = [arr[j], arr[i]];\r\n            }\r\n            return arr;\r\n        }\r\n        \/\/ \u2550\u2550\u2550 Ende der uebernommenen Spiellogik \u2550\u2550\u2550\r\n\r\n        \/\/ \u2500\u2500 \u00abLeicht\u00bb: im Zweifel eine Zahl mehr \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n        \/\/ Loest das Raetsel mit Einzel-Schritten nach. Gibt es bei 10 oder mehr\r\n        \/\/ offenen Feldern weniger als MIN_ZUEGE moegliche Zuege, wird ein noch\r\n        \/\/ offenes Feld aus der Loesung vorgegeben und neu geprueft. Zusaetzliche\r\n        \/\/ Vorgaben koennen Eindeutigkeit und Loesbarkeit nie verschlechtern.\r\n        function entschaerfeEngstellen(raetsel, loesung, MIN_ZUEGE) {\r\n            for (let runde = 0; runde < 30; runde++) {\r\n                const b = raetsel.map(v => (v === null ? 0 : v));\r\n                let eng = -1;\r\n                while (b.includes(0)) {\r\n                    const zuege = moeglicheEinzel(b);\r\n                    if (!zuege.size) break;   \/\/ sollte bei \u00abLeicht\u00bb nie eintreten (geprueft)\r\n                    const offen = b.filter(v => !v).length;\r\n                    if (offen >= 10 && zuege.size < MIN_ZUEGE) {\r\n                        \/\/ ein offenes Feld waehlen, das gerade KEIN Zug ist\r\n                        const kandidaten = [];\r\n                        for (let i = 0; i < 81; i++) if (!b[i] && raetsel[i] === null && !zuege.has(i)) kandidaten.push(i);\r\n                        if (kandidaten.length) eng = kandidaten[Math.floor(Math.random() * kandidaten.length)];\r\n                        break;\r\n                    }\r\n                    const [i, z] = zuege.entries().next().value;\r\n                    b[i] = z;\r\n                }\r\n                if (eng < 0) return raetsel;\r\n                raetsel[eng] = loesung[eng];\r\n            }\r\n            return raetsel;\r\n        }\r\n\r\n        \/\/ Alle Felder, die sich gerade mit einem nackten oder versteckten Einzel\r\n        \/\/ setzen lassen: Map Feld -> Ziffer\r\n        function moeglicheEinzel(b) {\r\n            const kand = new Array(81).fill(0);\r\n            for (let i = 0; i < 81; i++) {\r\n                if (b[i]) continue;\r\n                let m = 0;\r\n                for (let z = 1; z <= 9; z++) if (!NACHBARN[i].some(p => b[p] === z)) m |= 1 << z;\r\n                kand[i] = m;\r\n            }\r\n            const zuege = new Map();\r\n            for (let i = 0; i < 81; i++) {\r\n                if (!b[i] && kand[i] && (kand[i] & (kand[i] - 1)) === 0) zuege.set(i, Math.log2(kand[i]));\r\n            }\r\n            for (const u of EINHEITEN) {\r\n                for (let z = 1; z <= 9; z++) {\r\n                    if (u.some(i => b[i] === z)) continue;\r\n                    const pos = u.filter(i => !b[i] && (kand[i] & (1 << z)));\r\n                    if (pos.length === 1) zuege.set(pos[0], z);\r\n                }\r\n            }\r\n            return zuege;\r\n        }\r\n\r\n        \/\/ \u2500\u2500 Dialoge (fahrtauglich) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n        function zeigeModal(id) {\r\n            const m = document.getElementById(id);\r\n            if (!m) return;\r\n            m.classList.add('active');\r\n            document.body.style.overflow = 'hidden';\r\n            const ziel = m.querySelector('.fts-knopf');\r\n            if (ziel) ziel.focus();\r\n        }\r\n        function schliesseModal(id) {\r\n            const m = document.getElementById(id);\r\n            if (m) m.classList.remove('active');\r\n            if (!root.querySelector('.fts-modal.active')) document.body.style.overflow = '';\r\n        }\r\n        root.querySelectorAll('.fts-modal-schliessen, .fts-modal-hintergrund').forEach(el => {\r\n            el.addEventListener('click', () => schliesseModal(el.closest('.fts-modal').id));\r\n        });\r\n        document.addEventListener('keydown', e => {\r\n            if (e.key !== 'Escape') return;\r\n            const offen = root.querySelector('.fts-modal.active');\r\n            if (offen) { e.preventDefault(); schliesseModal(offen.id); }\r\n        });\r\n\r\n        \/** Spielereignis an GTM (dataLayer). Nur Spieldaten, keine Personendaten. *\/\r\n        function ftEvent(daten) {\r\n            try {\r\n                window.dataLayer = window.dataLayer || [];\r\n                window.dataLayer.push(Object.assign({ spiel_seite: 'fahrtauglich' }, daten));\r\n            } catch (e) {}\r\n        }\r\n\r\n        \/\/ \u2500\u2500 Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n        try {\r\n            if (!localStorage.getItem('ft_sudoku_seen')) {\r\n                document.getElementById('fts-anleitung-details').open = true;\r\n                localStorage.setItem('ft_sudoku_seen', '1');\r\n            }\r\n        } catch (e) {}\r\n        const stand = ladeStand();\r\n        if (stand) stelleStandHer(stand); else neuesSpiel('mittel');\r\n        bindEvents();\r\n        if (stand && hatFortschritt(stand)) zeigeFortsetzenDialog(stand);\r\n    }\r\n})();\r\n<\/script>\r\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.<\/p>","protected":false},"author":1,"featured_media":0,"parent":1467,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"content-type":"","iawp_total_views":8702,"footnotes":""},"class_list":["post-3046","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Sudoku: Gratis online spielen - leicht bis schwer<\/title>\n<meta name=\"description\" content=\"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fahrtauglich.ch\/fr\/praxisportal\/sudoku\/\" \/>\n<meta property=\"og:locale\" content=\"fr_FR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Sudoku: Gratis online spielen - leicht bis schwer\" \/>\n<meta property=\"og:description\" content=\"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fahrtauglich.ch\/fr\/praxisportal\/sudoku\/\" \/>\n<meta property=\"og:site_name\" content=\"Fahrtauglich\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-12T11:03:51+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Dur\u00e9e de lecture estim\u00e9e\" \/>\n\t<meta name=\"twitter:data1\" content=\"9 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/praxisportal\\\/sudoku\\\/\",\"url\":\"https:\\\/\\\/fahrtauglich.ch\\\/praxisportal\\\/sudoku\\\/\",\"name\":\"Sudoku: Gratis online spielen - leicht bis schwer\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/#website\"},\"datePublished\":\"2024-10-14T14:11:59+00:00\",\"dateModified\":\"2026-10-12T11:03:51+00:00\",\"description\":\"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/praxisportal\\\/sudoku\\\/#breadcrumb\"},\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/fahrtauglich.ch\\\/praxisportal\\\/sudoku\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/praxisportal\\\/sudoku\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Startseite\",\"item\":\"https:\\\/\\\/fahrtauglich.ch\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Praxisportal\",\"item\":\"https:\\\/\\\/fahrtauglich.ch\\\/praxisportal\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Sudoku\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/#website\",\"url\":\"https:\\\/\\\/fahrtauglich.ch\\\/\",\"name\":\"Fahrtauglich\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/fahrtauglich.ch\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fr-FR\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/#organization\",\"name\":\"Fahrtauglich\",\"url\":\"https:\\\/\\\/fahrtauglich.ch\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/fahrtauglich.ch\\\/wp-content\\\/uploads\\\/2024\\\/08\\\/cropped-Logo-2.jpg\",\"contentUrl\":\"https:\\\/\\\/fahrtauglich.ch\\\/wp-content\\\/uploads\\\/2024\\\/08\\\/cropped-Logo-2.jpg\",\"width\":307,\"height\":188,\"caption\":\"Fahrtauglich\"},\"image\":{\"@id\":\"https:\\\/\\\/fahrtauglich.ch\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Sudoku: Gratis online spielen - leicht bis schwer","description":"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fahrtauglich.ch\/fr\/praxisportal\/sudoku\/","og_locale":"fr_FR","og_type":"article","og_title":"Sudoku: Gratis online spielen - leicht bis schwer","og_description":"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.","og_url":"https:\/\/fahrtauglich.ch\/fr\/praxisportal\/sudoku\/","og_site_name":"Fahrtauglich","article_modified_time":"2026-10-12T11:03:51+00:00","twitter_card":"summary_large_image","twitter_misc":{"Dur\u00e9e de lecture estim\u00e9e":"9 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/fahrtauglich.ch\/praxisportal\/sudoku\/","url":"https:\/\/fahrtauglich.ch\/praxisportal\/sudoku\/","name":"Sudoku: Gratis online spielen - leicht bis schwer","isPartOf":{"@id":"https:\/\/fahrtauglich.ch\/#website"},"datePublished":"2024-10-14T14:11:59+00:00","dateModified":"2026-10-12T11:03:51+00:00","description":"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.","breadcrumb":{"@id":"https:\/\/fahrtauglich.ch\/praxisportal\/sudoku\/#breadcrumb"},"inLanguage":"fr-FR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fahrtauglich.ch\/praxisportal\/sudoku\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/fahrtauglich.ch\/praxisportal\/sudoku\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Startseite","item":"https:\/\/fahrtauglich.ch\/"},{"@type":"ListItem","position":2,"name":"Praxisportal","item":"https:\/\/fahrtauglich.ch\/praxisportal\/"},{"@type":"ListItem","position":3,"name":"Sudoku"}]},{"@type":"WebSite","@id":"https:\/\/fahrtauglich.ch\/#website","url":"https:\/\/fahrtauglich.ch\/","name":"Fahrtauglich","description":"","publisher":{"@id":"https:\/\/fahrtauglich.ch\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fahrtauglich.ch\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fr-FR"},{"@type":"Organization","@id":"https:\/\/fahrtauglich.ch\/#organization","name":"Fahrtauglich","url":"https:\/\/fahrtauglich.ch\/","logo":{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/fahrtauglich.ch\/#\/schema\/logo\/image\/","url":"https:\/\/fahrtauglich.ch\/wp-content\/uploads\/2024\/08\/cropped-Logo-2.jpg","contentUrl":"https:\/\/fahrtauglich.ch\/wp-content\/uploads\/2024\/08\/cropped-Logo-2.jpg","width":307,"height":188,"caption":"Fahrtauglich"},"image":{"@id":"https:\/\/fahrtauglich.ch\/#\/schema\/logo\/image\/"}}]}},"rttpg_featured_image_url":null,"rttpg_author":{"display_name":"fahrtauglich.ch","author_link":"https:\/\/fahrtauglich.ch\/fr\/blog\/author\/mintcream-quetzal-259960-hostingersite-com\/"},"rttpg_comment":0,"rttpg_category":null,"rttpg_excerpt":"Spielen Sie Sudoku online mit individuell einstellbarem Fehlerlimit. Unser adaptives Sudoku wird nach jedem Erfolg schwieriger.","_links":{"self":[{"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/pages\/3046","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/comments?post=3046"}],"version-history":[{"count":223,"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/pages\/3046\/revisions"}],"predecessor-version":[{"id":8581,"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/pages\/3046\/revisions\/8581"}],"up":[{"embeddable":true,"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/pages\/1467"}],"wp:attachment":[{"href":"https:\/\/fahrtauglich.ch\/fr\/wp-json\/wp\/v2\/media?parent=3046"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}