Files
teig-terminal/index.html
T

1113 lines
53 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>TEIG//TERMINAL — Pizzateig-Kalkulator</title>
<meta name="description" content="Pizzateig-Rechner mit Bäckerprozent, Mehl-Datenbank, Vorteig und Rückwärts-Zeitplan.">
<meta name="color-scheme" content="dark">
<style>
:root{
--bg:#07080B;
--panel:#0E1117;
--panel-2:#121722;
--line:#1B2430;
--line-hot:#243447;
--cyan:#00F0FF;
--magenta:#FF2D78;
--amber:#FFB000;
--lime:#8CFF3B;
--txt:#C9D6E2;
--dim:#6E7B8B;
--mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--r:2px;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
background:var(--bg);
color:var(--txt);
font-family:var(--mono);
font-size:15px;
line-height:1.45;
-webkit-font-smoothing:antialiased;
padding:0 0 64px;
background-image:
linear-gradient(rgba(0,240,255,.028) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,240,255,.028) 1px, transparent 1px);
background-size:32px 32px;
}
.wrap{max-width:760px;margin:0 auto;padding:0 14px}
/* ---------- Kopf ---------- */
header{padding:28px 0 18px;border-bottom:1px solid var(--line);margin-bottom:20px}
.brand{
font-size:clamp(28px,8.5vw,48px);
font-weight:700;letter-spacing:-.02em;line-height:.95;margin:0;
color:#EAF6FF;
text-shadow:0 0 18px rgba(0,240,255,.35);
}
.brand em{font-style:normal;color:var(--magenta);text-shadow:0 0 18px rgba(255,45,120,.45)}
.tag{color:var(--dim);font-size:11px;letter-spacing:.22em;text-transform:uppercase;margin:10px 0 0}
.status{display:flex;gap:8px;align-items:center;margin-top:12px;font-size:11px;letter-spacing:.14em;color:var(--dim);text-transform:uppercase}
.dot{width:7px;height:7px;background:var(--lime);border-radius:50%;box-shadow:0 0 10px var(--lime);animation:pulse 2.4s infinite}
@keyframes pulse{50%{opacity:.25}}
/* ---------- Panels ---------- */
.panel{
background:linear-gradient(180deg,var(--panel),var(--panel-2));
border:1px solid var(--line);
border-radius:var(--r);
padding:16px 14px 18px;
margin-bottom:14px;
position:relative;
}
.panel::before,.panel::after{
content:"";position:absolute;width:9px;height:9px;border:1px solid var(--cyan);opacity:.55
}
.panel::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
.panel::after{bottom:-1px;right:-1px;border-left:0;border-top:0}
.ph{
display:flex;justify-content:space-between;align-items:baseline;gap:10px;
font-size:11px;letter-spacing:.24em;text-transform:uppercase;color:var(--cyan);
margin:0 0 14px;padding-bottom:9px;border-bottom:1px dashed var(--line-hot);
}
.ph b{font-weight:400}
.ph span{color:var(--dim);letter-spacing:.1em}
/* ---------- Chips ---------- */
.chips{display:flex;flex-wrap:wrap;gap:6px}
.chip{
font:inherit;font-size:12px;
background:transparent;color:var(--dim);
border:1px solid var(--line-hot);border-radius:var(--r);
padding:7px 11px;cursor:pointer;letter-spacing:.06em;
transition:.15s;
}
.chip:hover{color:var(--txt);border-color:var(--cyan)}
.chip[aria-pressed="true"]{
color:#04121A;background:var(--cyan);border-color:var(--cyan);
box-shadow:0 0 16px rgba(0,240,255,.35);font-weight:700;
}
/* ---------- Controls ---------- */
.ctl{display:block;margin:15px 0 0}
.ctl:first-of-type{margin-top:0}
.ctl-top{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:5px}
.ctl-l{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}
.ctl-v{font-size:17px;color:var(--cyan);font-weight:700;text-shadow:0 0 12px rgba(0,240,255,.3);font-variant-numeric:tabular-nums}
.ctl-v small{color:var(--dim);font-size:11px;font-weight:400;letter-spacing:.1em;text-shadow:none}
input[type=range]{
-webkit-appearance:none;appearance:none;width:100%;height:26px;background:transparent;margin:0;cursor:pointer;display:block
}
input[type=range]::-webkit-slider-runnable-track{height:2px;background:var(--line-hot)}
input[type=range]::-moz-range-track{height:2px;background:var(--line-hot)}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;margin-top:-6px;border-radius:1px;
background:var(--cyan);box-shadow:0 0 12px rgba(0,240,255,.7);border:0
}
input[type=range]::-moz-range-thumb{
width:14px;height:14px;border-radius:1px;background:var(--cyan);
box-shadow:0 0 12px rgba(0,240,255,.7);border:0
}
input[type=range]:focus-visible::-webkit-slider-thumb{outline:2px solid var(--magenta);outline-offset:2px}
.row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:520px){.row{grid-template-columns:1fr}}
select,input[type=number],input[type=datetime-local]{
font:inherit;font-size:14px;width:100%;
background:#0A0E15;color:var(--txt);
border:1px solid var(--line-hot);border-radius:var(--r);
padding:9px 10px;
}
select:focus,input:focus{outline:2px solid var(--cyan);outline-offset:1px}
.stepper{display:flex;align-items:stretch;gap:0;border:1px solid var(--line-hot);border-radius:var(--r);overflow:hidden;width:fit-content}
.stepper button{
font:inherit;font-size:18px;line-height:1;width:44px;
background:#0A0E15;color:var(--cyan);border:0;cursor:pointer
}
.stepper button:hover{background:var(--cyan);color:#04121A}
.stepper output{
min-width:64px;text-align:center;padding:10px 6px;font-size:17px;font-weight:700;
color:var(--cyan);border-left:1px solid var(--line-hot);border-right:1px solid var(--line-hot);
font-variant-numeric:tabular-nums;
}
/* ---------- Mehl-Info ---------- */
.meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
.badge{
font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;
border:1px solid var(--line-hot);border-radius:var(--r);
padding:4px 8px;color:var(--dim);background:#0A0E15
}
.badge b{color:var(--txt);font-weight:700}
.note{font-size:12px;color:var(--dim);margin-top:10px;line-height:1.5}
/* ---------- Warnungen ---------- */
.warns{margin-top:6px}
.warn{
display:flex;gap:9px;align-items:flex-start;
border-left:2px solid var(--amber);background:rgba(255,176,0,.06);
padding:9px 11px;margin-bottom:6px;font-size:12.5px;color:#FFD98A;line-height:1.45
}
.warn.bad{border-color:var(--magenta);background:rgba(255,45,120,.07);color:#FFA8C6}
.warn i{font-style:normal;opacity:.8}
/* ---------- Rezept-Ausgabe ---------- */
.sub{font-size:10.5px;letter-spacing:.24em;text-transform:uppercase;color:var(--magenta);margin:16px 0 8px}
.sub:first-child{margin-top:0}
table{width:100%;border-collapse:collapse}
td{padding:8px 0;border-bottom:1px dotted var(--line-hot);vertical-align:baseline}
td.n{text-align:right;font-variant-numeric:tabular-nums;font-weight:700;color:#EAF6FF;white-space:nowrap}
td.p{text-align:right;width:62px;color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums}
tr.tot td{border-bottom:0;border-top:1px solid var(--line-hot);padding-top:11px;color:var(--cyan)}
tr.tot td.n{color:var(--cyan);text-shadow:0 0 12px rgba(0,240,255,.3);font-size:17px}
.hero{
display:flex;gap:18px;flex-wrap:wrap;align-items:flex-end;
border:1px solid var(--line-hot);border-left:2px solid var(--cyan);
background:#0A0E15;padding:12px 14px;margin-bottom:4px
}
.hero div{line-height:1.15}
.hero .k{font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--dim);display:block;margin-bottom:4px}
.hero .v{font-size:22px;font-weight:700;color:#EAF6FF}
.hero .v small{font-size:12px;color:var(--dim);font-weight:400}
/* ---------- Timeline ---------- */
.tl{list-style:none;margin:0;padding:0 0 0 26px;position:relative}
.tl::before{content:"";position:absolute;left:5px;top:6px;bottom:12px;width:1px;background:linear-gradient(180deg,var(--cyan),var(--magenta))}
.tl li{position:relative;padding:0 0 20px}
.tl li:last-child{padding-bottom:0}
.tl li::before{
content:"";position:absolute;left:-24px;top:5px;width:9px;height:9px;border-radius:50%;
background:var(--bg);border:2px solid var(--cyan);box-shadow:0 0 10px rgba(0,240,255,.6)
}
.tl li.next::before{background:var(--magenta);border-color:var(--magenta);box-shadow:0 0 14px var(--magenta)}
.tl li.bake::before{background:var(--amber);border-color:var(--amber);box-shadow:0 0 14px var(--amber)}
.tl .t{font-size:17px;font-weight:700;color:var(--cyan);font-variant-numeric:tabular-nums;text-shadow:0 0 12px rgba(0,240,255,.25)}
.tl .t em{font-style:normal;font-size:11px;letter-spacing:.14em;color:var(--dim);margin-left:8px;text-transform:uppercase;text-shadow:none}
.tl .a{color:#EAF6FF;font-size:14px;margin-top:1px}
.tl .d{color:var(--dim);font-size:12px;margin-top:2px;line-height:1.45}
.tl li.bake .t{color:var(--amber);text-shadow:0 0 12px rgba(255,176,0,.3)}
.countdown{
border:1px solid var(--magenta);background:rgba(255,45,120,.07);
padding:10px 12px;margin-bottom:16px;font-size:12.5px;color:#FFA8C6;letter-spacing:.04em
}
.countdown b{color:#fff;font-weight:700}
/* ---------- Aktionen ---------- */
.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
.btn{
font:inherit;font-size:12px;letter-spacing:.16em;text-transform:uppercase;
padding:11px 16px;border-radius:var(--r);cursor:pointer;
background:transparent;border:1px solid var(--line-hot);color:var(--txt);transition:.15s
}
.btn:hover{border-color:var(--cyan);color:var(--cyan)}
.btn.pri{background:var(--magenta);border-color:var(--magenta);color:#fff;font-weight:700}
.btn.pri:hover{box-shadow:0 0 20px rgba(255,45,120,.5);color:#fff}
footer{color:var(--dim);font-size:11.5px;line-height:1.6;margin-top:22px;border-top:1px solid var(--line);padding-top:16px}
footer b{color:var(--txt)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
.printonly{display:none}
@media print{
@page{size:A4;margin:15mm}
body{background:#fff;color:#000;background-image:none;font-size:11pt;padding:0}
.wrap{max-width:none;padding:0}
header,.actions,.noprint,#countdown{display:none!important}
.printonly{display:block}
#printhead{margin:0 0 8mm;border-bottom:2px solid #000;padding-bottom:4mm}
#printhead h2{font-size:20pt;margin:0 0 2mm;letter-spacing:-.01em}
#printhead p{margin:0;font-size:9.5pt;line-height:1.6;color:#333}
#printfoot{margin-top:8mm;padding-top:3mm;border-top:1px solid #999;font-size:8pt;color:#555;word-break:break-all}
.panel{border:0;background:#fff;padding:0;margin:0 0 7mm;break-inside:avoid}
.panel::before,.panel::after{display:none}
.ph{color:#000;border-bottom:1px solid #000;letter-spacing:.18em;font-size:8.5pt;margin-bottom:4mm}
.ph span{color:#555}
.hero{border:0;border-left:2.5pt solid #000;background:#f2f2f2;padding:3mm 4mm;margin-bottom:3mm}
.hero .k{color:#555}
.hero .v{font-size:15pt}
.sub{color:#000;border-bottom:0;font-weight:700}
td{border-bottom:1px dotted #bbb;padding:2mm 0}
td.n,tr.tot td.n{color:#000;text-shadow:none;font-size:11pt}
td.p{color:#666}
tr.tot td{border-top:1.5pt solid #000;color:#000}
.tl{break-inside:avoid}
.tl::before{background:#000}
.tl li::before{background:#fff;border-color:#000;box-shadow:none}
.tl li{break-inside:avoid;padding-bottom:4mm}
.tl .t{color:#000;text-shadow:none;font-size:11.5pt}
.tl .t em,.tl .d{color:#555}
.tl .a{color:#000}
.tl li.bake .t{color:#000}
.warn{background:#f2f2f2;border-left:2.5pt solid #000;color:#000}
footer{border-top:1px solid #999;color:#555;font-size:8.5pt;margin-top:6mm}
footer b{color:#000}
}
</style>
</head>
<body>
<div class="wrap">
<div id="printhead" class="printonly"></div>
<header>
<h1 class="brand">TEIG<em>//</em>TERMINAL</h1>
<p class="tag">Pizzateig-Kalkulator · Bäckerprozent · Vorteig · Rückwärts-Zeitplan</p>
<div class="status"><span class="dot"></span><span id="statusline">System bereit</span></div>
</header>
<!-- CHARGE -->
<section class="panel noprint">
<p class="ph"><b>01 // Charge</b><span id="doughsum"></span></p>
<div class="chips" id="styles" role="group" aria-label="Pizzastil"></div>
<div class="ctl">
<div class="ctl-top"><span class="ctl-l">Teiglinge</span></div>
<div class="stepper">
<button type="button" id="ballsMinus" aria-label="Weniger Teiglinge"></button>
<output id="ballsOut">4</output>
<button type="button" id="ballsPlus" aria-label="Mehr Teiglinge">+</button>
</div>
</div>
<div class="ctl">
<div class="ctl-top"><span class="ctl-l">Größe bestimmen über</span></div>
<div class="chips" id="modes" role="group" aria-label="Berechnungsart"></div>
</div>
<div id="modeW">
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Gewicht pro Teigling</span><span class="ctl-v" id="v-ball"></span></div>
<input type="range" id="ball" min="120" max="1200" step="5">
</label>
</div>
<div id="modeD" hidden>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Durchmesser</span><span class="ctl-v" id="v-dia"></span></div>
<input type="range" id="dia" min="16" max="45" step="1">
</label>
</div>
<div id="modeP" hidden>
<div class="row">
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Blech Länge</span><span class="ctl-v" id="v-panL"></span></div>
<input type="range" id="panL" min="20" max="60" step="1">
</label>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Blech Breite</span><span class="ctl-v" id="v-panW"></span></div>
<input type="range" id="panW" min="15" max="45" step="1">
</label>
</div>
</div>
</section>
<!-- MEHL -->
<section class="panel noprint">
<p class="ph"><b>02 // Mehl</b><span id="flourW"></span></p>
<select id="flour" aria-label="Mehl auswählen"></select>
<div class="meta" id="flourMeta"></div>
<p class="note" id="flourNote"></p>
<div class="actions" style="margin-top:12px">
<button type="button" class="btn" id="applyFlour">Werte vom Mehl übernehmen</button>
</div>
</section>
<!-- REZEPTUR -->
<section class="panel noprint">
<p class="ph"><b>03 // Rezeptur</b><span>Bäckerprozent</span></p>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Hydration</span><span class="ctl-v" id="v-hyd"></span></div>
<input type="range" id="hyd" min="50" max="90" step="0.5">
</label>
<div class="row">
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Salz</span><span class="ctl-v" id="v-salt"></span></div>
<input type="range" id="salt" min="0" max="4" step="0.1">
</label>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Olivenöl</span><span class="ctl-v" id="v-oil"></span></div>
<input type="range" id="oil" min="0" max="8" step="0.25">
</label>
</div>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Zucker / Malz</span><span class="ctl-v" id="v-sugar"></span></div>
<input type="range" id="sugar" min="0" max="5" step="0.25">
</label>
<div class="ctl">
<div class="ctl-top"><span class="ctl-l">Triebmittel</span></div>
<div class="chips" id="yeastTypes" role="group" aria-label="Triebmittel"></div>
</div>
</section>
<!-- GARE -->
<section class="panel noprint">
<p class="ph"><b>04 // Gare</b><span id="fermsum"></span></p>
<div class="ctl">
<div class="ctl-top"><span class="ctl-l">Führung</span></div>
<div class="chips" id="plans" role="group" aria-label="Führung"></div>
</div>
<div class="ctl">
<div class="ctl-top"><span class="ctl-l">Ablauf</span></div>
<div class="chips" id="seqs" role="group" aria-label="Ablauf"></div>
<p class="note" id="seqNote"></p>
</div>
<div class="ctl" id="staglioBox">
<div class="ctl-top"><span class="ctl-l">Ballen formen (Staglio)</span></div>
<div class="chips" id="staglios" role="group" aria-label="Staglio"></div>
</div>
<div class="row">
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Raumgare</span><span class="ctl-v" id="v-roomH"></span></div>
<input type="range" id="roomH" min="0" max="48" step="0.5">
</label>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Raumtemperatur</span><span class="ctl-v" id="v-roomT"></span></div>
<input type="range" id="roomT" min="14" max="32" step="0.5">
</label>
</div>
<div class="row">
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Kühlschrank</span><span class="ctl-v" id="v-fridgeH"></span></div>
<input type="range" id="fridgeH" min="0" max="96" step="1">
</label>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Kühlschranktemperatur</span><span class="ctl-v" id="v-fridgeT"></span></div>
<input type="range" id="fridgeT" min="2" max="10" step="0.5">
</label>
</div>
</section>
<!-- VORTEIG -->
<section class="panel noprint">
<p class="ph"><b>05 // Vorteig</b><span id="pfsum"></span></p>
<div class="chips" id="pfTypes" role="group" aria-label="Vorteig"></div>
<div id="pfBox" hidden>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Anteil am Gesamtmehl</span><span class="ctl-v" id="v-pfPct"></span></div>
<input type="range" id="pfPct" min="5" max="100" step="5">
</label>
<div class="row">
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Reifezeit Vorteig</span><span class="ctl-v" id="v-pfH"></span></div>
<input type="range" id="pfH" min="2" max="48" step="1">
</label>
<label class="ctl">
<div class="ctl-top"><span class="ctl-l">Temperatur Vorteig</span><span class="ctl-v" id="v-pfT"></span></div>
<input type="range" id="pfT" min="4" max="28" step="0.5">
</label>
</div>
</div>
</section>
<!-- WARNUNGEN -->
<div class="warns" id="warns"></div>
<!-- REZEPT -->
<section class="panel">
<p class="ph"><b>06 // Rezept</b><span id="recipeStyle"></span></p>
<div class="hero">
<div><span class="k">Teig gesamt</span><span class="v" id="h-total"></span></div>
<div><span class="k">pro Teigling</span><span class="v" id="h-ball"></span></div>
<div><span class="k">Mehl gesamt</span><span class="v" id="h-flour"></span></div>
</div>
<div id="recipeOut"></div>
</section>
<!-- ZEITPLAN -->
<section class="panel">
<p class="ph"><b>07 // Zeitplan</b><span>rückwärts ab Backzeit</span></p>
<label class="ctl noprint" style="margin-bottom:16px">
<div class="ctl-top"><span class="ctl-l">Pizza fertig am</span></div>
<input type="datetime-local" id="bakeAt">
</label>
<div id="countdown"></div>
<ol class="tl" id="timeline"></ol>
</section>
<div id="printfoot" class="printonly"></div>
<div class="actions noprint">
<button type="button" class="btn pri" id="share">Rezept-Link kopieren</button>
<button type="button" class="btn" id="pdf">Als PDF speichern</button>
<button type="button" class="btn" id="copyTxt">Als Text kopieren</button>
<button type="button" class="btn" id="reset">Zurücksetzen</button>
</div>
<footer>
<b>Hinweis:</b> Die Hefemenge wird über ein Q10-Modell aus Zeit und Temperatur geschätzt.
Teigtemperatur, Hefevitalität und Schwankungen im Raum lassen sich nicht in eine Formel pressen —
die Werte sind ein sehr guter Startpunkt, kein Gesetz. Schau auf den Teig, nicht auf die Uhr.
<br><br>
Läuft komplett im Browser, ohne Server, ohne Tracking. Rezept-Links enthalten alle Werte in der URL.
</footer>
</div>
<script>
"use strict";
/* ============================================================
MEHL-DATENBANK
w: Backstärke, hyd: sinnvolle Hydration in %, reife: Reife-
äquivalent in Stunden bei 20 °C, vt: vorteigtauglich
============================================================ */
const FLOURS = [
{g:"Italien · Typ 00", id:"cap-pizzeria", n:"Caputo Pizzeria", w:"260280", pr:12.5, hyd:[56,65], reife:[8,24], vt:false, note:"Der Klassiker für direkt geführte neapolitanische Teige. Achtung bei der Farbe: den 1-kg-Beutel gibt es rot-weiß, den 5- und 25-kg-Sack blau — identisches Mehl. „Caputo Rosso\" meint umgangssprachlich trotzdem meist das Cuoco."},
{g:"Italien · Typ 00", id:"cap-cuoco", n:"Caputo Cuoco / Saccorosso", w:"300320", pr:13.0, hyd:[60,72], reife:[16,72], vt:true, note:"Allrounder für lange Führung über 24 h und für Vorteige. Cuoco (1 kg) und Saccorosso (5/25 kg, roter Sack) sind dasselbe Mehl, auch „Chef\" genannt."},
{g:"Italien · Typ 00", id:"cap-nuvola", n:"Caputo Nuvola", w:"260270", pr:12.5, hyd:[60,70], reife:[8,24], vt:false, note:"Für sehr luftige, hohe Ränder. Direktteig bis 24 h."},
{g:"Italien · Typ 00", id:"cap-nuvolas", n:"Caputo Nuvola Super", w:"300320", pr:13.5, hyd:[65,78], reife:[24,72], vt:true, note:"Wattiger Cornicione bei hoher Hydration und langer Reife."},
{g:"Italien · Typ 00", id:"cap-classica", n:"Caputo Classica", w:"240260", pr:11.5, hyd:[55,63], reife:[4,12], vt:false, note:"Haushaltsmehl von Caputo, für schnelle Teige am selben Tag."},
{g:"Italien · Typ 00", id:"cap-manitoba", n:"Caputo Manitoba Oro", w:"370390", pr:14.5, hyd:[70,82], reife:[24,96], vt:true, note:"Sehr hohe Backstärke, verlangt mindestens 70 % Wasser. Meist als Anteil im Blend."},
{g:"Italien · Typ 00", id:"5s-napo", n:"Le 5 Stagioni Napoletana", w:"260280", pr:12.5, hyd:[58,66], reife:[8,24], vt:false, note:"Direkte Konkurrenz zum Caputo Pizzeria, sehr ähnliches Verhalten."},
{g:"Italien · Typ 00", id:"5s-sup", n:"Le 5 Stagioni Superiore", w:"300320", pr:13.0, hyd:[62,72], reife:[24,48], vt:true, note:"Für lange Gare und Vorteige ausgelegt."},
{g:"Italien · Typ 00", id:"pol-classica", n:"Polselli Classica", w:"250270", pr:12.5, hyd:[58,66], reife:[8,24], vt:false, note:"Solides Standardmehl für Direktteige."},
{g:"Italien · Typ 00", id:"pol-super", n:"Polselli Super", w:"340360", pr:14.0, hyd:[65,78], reife:[24,72], vt:true, note:"Sehr backstark, für 4872 h und hohe Hydration."},
{g:"Italien · Typ 00", id:"dg-napo", n:"Dallagiovanna La Napoletana", w:"270290", pr:12.8, hyd:[58,68], reife:[12,30], vt:false, note:"Mittlere Backstärke, gutmütig in der Verarbeitung."},
{g:"Italien · Typ 00", id:"petra3", n:"Petra 3 (Molino Quaglia)", w:"260280", pr:12.5, hyd:[60,70], reife:[8,24], vt:false, note:"Feines Weizenmehl, angenehm aromatisch."},
{g:"Italien · Typ 00", id:"petra1", n:"Petra 1 (Molino Quaglia)", w:"310330", pr:14.0, hyd:[65,78], reife:[24,72], vt:true, note:"Starkes Mehl für lange Führung und Vorteige."},
{g:"Italien · Typ 00", id:"grassi-blu", n:"Molino Grassi Uniqua Blu", w:"290310", pr:13.5, hyd:[62,74], reife:[24,48], vt:true, note:"Bio, backstark, gut für Poolish."},
{g:"Italien · Typ 0", id:"manitoba", n:"Manitoba (generisch, Typ 0)", w:"350400", pr:14.5, hyd:[68,82], reife:[24,96], vt:true, note:"Als Anteil von 1030 % zum Aufstärken schwächerer Mehle."},
{g:"Deutschland", id:"t405", n:"Weizenmehl Type 405", w:"150180", pr:10.0, hyd:[54,60], reife:[2,8], vt:false, note:"Zu schwach für lange Gare. Nur für Same-Day-Teig, sonst wird der Teig schlaff."},
{g:"Deutschland", id:"t550", n:"Weizenmehl Type 550", w:"180220", pr:11.0, hyd:[56,64], reife:[4,16], vt:false, note:"Das brauchbarste Supermarktmehl. Bis ca. 16 h Reifeäquivalent."},
{g:"Deutschland", id:"t812", n:"Weizenmehl Type 812", w:"200240", pr:12.0, hyd:[58,68], reife:[6,20], vt:false, note:"Etwas mehr Mineralstoffe, kräftigerer Geschmack."},
{g:"Deutschland", id:"t1050", n:"Weizenmehl Type 1050", w:"210250", pr:12.5, hyd:[62,72], reife:[6,24], vt:false, note:"Dunkler und durstiger. Gut als Anteil von 1030 %."},
{g:"Deutschland", id:"pizza-super", n:"Pizzamehl „Typ 00\" (Supermarkt)", w:"200240", pr:11.5, hyd:[55,64], reife:[4,14], vt:false, note:"Feine Mahlung, aber selten die Backstärke echter italienischer Pizzamehle."},
{g:"Deutschland", id:"dinkel630", n:"Dinkelmehl Type 630", w:"130170", pr:12.0, hyd:[55,63], reife:[2,8], vt:false, note:"Dinkelkleber ist dehnbar, aber wenig stabil. Kurz kneten, kurz führen."},
{g:"Deutschland", id:"vollkorn", n:"Weizenvollkornmehl", w:"—", pr:13.0, hyd:[65,78], reife:[4,16], vt:false, note:"Sehr durstig, Kleie schneidet den Kleber. Am besten 1030 % beimischen."},
{g:"Deutschland", id:"ruch", n:"Ruchmehl / Halbweiß", w:"200240", pr:12.5, hyd:[62,74], reife:[6,24], vt:false, note:"Nussig, rustikale Krume. Beliebt für Teglia-Blends."},
{g:"Spezial", id:"semola", n:"Semola rimacinata (Hartweizen)",w:"—", pr:13.0, hyd:[60,70], reife:[6,24], vt:false, note:"Als Anteil von 1030 % oder pur für sizilianische Bleche. Auch zum Ausbreiten."},
{g:"Spezial", id:"custom", n:"Eigenes Mehl / Blend", w:"—", pr:0, hyd:[50,90], reife:[0,120], vt:true, note:"Keine Prüfung — du weißt, was du tust."}
];
/* ---------- Stile (tf = Teigdichte in g/cm²) ---------- */
const STYLES = {
napo: {n:"Napoletana", ball:250, hyd:62, salt:2.8, oil:0, sugar:0, tf:0.36, mode:"w"},
contemp: {n:"Contemporanea", ball:280, hyd:70, salt:2.8, oil:0, sugar:0, tf:0.39, mode:"w"},
ny: {n:"New York", ball:300, hyd:63, salt:2.0, oil:2.5, sugar:1.5, tf:0.31, mode:"w"},
tonda: {n:"Römisch tonda", ball:200, hyd:57, salt:2.2, oil:4, sugar:0, tf:0.26, mode:"w"},
teglia: {n:"Teglia / Blech", ball:800, hyd:78, salt:2.2, oil:2, sugar:0, tf:0.55, mode:"p"},
pinsa: {n:"Pinsa", ball:250, hyd:76, salt:2.5, oil:2.5, sugar:0, tf:0.33, mode:"w"},
detroit: {n:"Detroit", ball:500, hyd:70, salt:2.0, oil:2, sugar:0.5, tf:0.62, mode:"p"},
custom: {n:"Frei", ball:250, hyd:65, salt:2.5, oil:0, sugar:0, tf:0.36, mode:"w"}
};
/* ---------- Führungen (Zeitpläne) ----------
room = alles bei Raumtemperatur, cold = kurz anspringen + Kühlschrank */
const PLANS = {
same: {n:"Same Day", room:{roomH:8, fridgeH:0}, cold:{roomH:2, fridgeH:6}},
night:{n:"Über Nacht", room:{roomH:16, fridgeH:0}, cold:{roomH:3, fridgeH:13}},
d24: {n:"24 Stunden", room:{roomH:24, fridgeH:0}, cold:{roomH:4, fridgeH:20}},
d48: {n:"48 Stunden", room:{roomH:48, fridgeH:0}, cold:{roomH:4, fridgeH:44}},
d72: {n:"72 Stunden", room:null, cold:{roomH:6, fridgeH:66}},
free: {n:"Frei", room:null, cold:null}
};
/* ---------- Ablauf: wo die warme Zeit liegt ---------- */
const SEQ = {
room: {n:"Alles warm", fridge:false,
note:"Klassisch neapolitanisch. Puntata und Appretto bei Raumtemperatur, kein Kühlschrank. Entwickelt am meisten Aroma, verlangt aber ein Zeitfenster von wenigen Stunden."},
warmcold: {n:"Warm → Kalt", fridge:true,
note:"Hybrid: erst Stockgare bei Raumtemperatur, dann in den Kühlschrank, kurz vor dem Backen aufwärmen. Sehr verbreitet, braucht wenig Hefe — aber der Teig kühlt langsam durch und gärt im Kühlschrank wärmer als gedacht."},
coldwarm: {n:"Kalt → Warm", fridge:true,
note:"Lehmann-Methode: direkt nach dem Kneten kalt stellen, die Gare passiert im Kühlschrank, die warme Zeit danach dient dem Handling. Am besten planbar, braucht deutlich mehr Hefe."}
};
const STAGLIO = {
before:"Ballen vor der Kälte",
after: "Ballen nach der Kälte"
};
const MODES = {w:"Ballgewicht", d:"Durchmesser", p:"Blech L×B"};
const YEASTS = {fresh:"Frischhefe", dry:"Trockenhefe", sour:"Sauerteig"};
const PFS = {none:"Ohne (Direktteig)", poolish:"Poolish", biga:"Biga"};
const PF_HYD = {poolish:1.0, biga:0.45};
/* ============================================================
STATE
============================================================ */
const DEFAULT = {
style:"napo", flour:"cap-pizzeria", balls:4, mode:"w",
ball:250, dia:30, panL:40, panW:30,
hyd:62, salt:2.8, oil:0, sugar:0, yeast:"fresh",
plan:"d24", seq:"warmcold", staglio:"before",
roomH:4, roomT:20, fridgeH:20, fridgeT:5,
pf:"none", pfPct:30, pfH:16, pfT:18,
bake:null
};
let S = Object.assign({}, DEFAULT);
/* ============================================================
RECHENKERN
============================================================ */
const clamp = (x,a,b)=>Math.min(b,Math.max(a,x));
const rate = T => Math.pow(2.5,(T-20)/10); // Q10 = 2.5
/* Eine Garephase mit Temperaturwechsel. tc = Stunden, bis der Teigkern
die Zieltemperatur erreicht hat. Während dieser Rampe wird mit der
mittleren Temperatur gerechnet, danach mit der Zieltemperatur. */
function phaseUnits(h, Tfrom, Tto, tc){
if (h<=0) return 0;
const ramp = Math.min(h, tc);
return ramp*rate((Tfrom+Tto)/2) + Math.max(0,h-ramp)*rate(Tto);
}
/* Abkühlzeiten: Ballen kühlen schnell durch, eine geschlossene Masse
sehr langsam — CO2 wirkt als Dämmschicht, die Hefe heizt zusätzlich. */
const tcBall = 1.5;
const tcBulkFor = total => Math.min(14, 2.5 + 0.0045*total);
/* Aufwärmphase nach dem Kühlschrank (nur bei Warm → Kalt) */
function warmupFor(s){
if (s.seq!=="warmcold" || s.fridgeH<=0) return 0;
return s.staglio==="after" ? 3 : 2;
}
/* Gäreinheiten = Stunden-Äquivalent bei 20 °C, abhängig vom Ablauf */
function fermUnits(s, total){
const tcCold = s.staglio==="before" ? tcBall : tcBulkFor(total);
if (s.seq==="room" || s.fridgeH<=0) return s.roomH*rate(s.roomT);
if (s.seq==="coldwarm"){
return phaseUnits(s.fridgeH, s.roomT, s.fridgeT, tcCold)
+ phaseUnits(s.roomH, s.fridgeT, s.roomT, tcBall);
}
return s.roomH*rate(s.roomT)
+ phaseUnits(s.fridgeH, s.roomT, s.fridgeT, tcCold)
+ phaseUnits(warmupFor(s), s.fridgeT, s.roomT, tcBall);
}
function calc(s){
const st = STYLES[s.style];
// --- Teigmenge ---
let ballW;
if (s.mode==="d") ballW = Math.PI*Math.pow(s.dia/2,2)*st.tf;
else if (s.mode==="p") ballW = s.panL*s.panW*st.tf;
else ballW = s.ball;
const total = ballW*s.balls;
// --- Gäreinheiten (Stunden-Äquivalent bei 20 °C) ---
const U = Math.max(0.5, fermUnits(s, total));
const Upf = Math.max(0.5, s.pfH*rate(s.pfT));
const hasPf = s.pf!=="none";
const pfFrac= hasPf ? s.pfPct/100 : 0;
// --- Triebmittel in % vom Mehl ---
let fresh = 6/U; // Frischhefe-Referenz
fresh *= 1 + Math.max(0, s.salt-2.5)*0.06; // Salz bremst
if (hasPf) fresh *= (1 - 0.8*pfFrac); // Vorteig bringt schon Trieb
fresh = clamp(fresh, 0.01, 6);
let yPct; // % vom Mehl, gewählte Art
if (s.yeast==="fresh") yPct = fresh;
else if (s.yeast==="dry") yPct = fresh/3;
else yPct = clamp(140/U*(hasPf?(1-0.8*pfFrac):1), 2, 45);
const pfYeastPct = hasPf ? clamp(2.1/Upf, 0.01, 3)/(s.yeast==="dry"?3:1) : 0;
// --- Bäckerprozent ---
const hyd=s.hyd/100, salt=s.salt/100, oil=s.oil/100, sugar=s.sugar/100;
const isSour = s.yeast==="sour";
const yFrac = isSour ? 0 : (yPct/100*(1-pfFrac) + pfYeastPct/100*pfFrac);
const Ssum = 1 + hyd + salt + oil + sugar + yFrac;
const flour = total/Ssum;
const water = flour*hyd;
const saltG = flour*salt;
const oilG = flour*oil;
const sugarG = flour*sugar;
// Sauerteig steckt (100 % Hydration) im Mehl/Wasser mit drin
const starter = isSour ? flour*yPct/100 : 0;
// --- Vorteig-Aufteilung ---
let pf=null, main;
if (hasPf){
const pfFlour = flour*pfFrac;
const pfWater = pfFlour*PF_HYD[s.pf];
const pfY = isSour ? pfFlour*yPct/100 : pfFlour*pfYeastPct/100;
pf = {flour:pfFlour, water:pfWater, yeast:pfY,
mass:pfFlour+pfWater+(isSour?0:pfY)};
main = {
flour: flour-pfFlour-(isSour?pfY/2:0),
water: water-pfWater-(isSour?pfY/2:0),
yeast: isSour ? 0 : (flour-pfFlour)*yPct/100
};
} else {
main = {
flour: flour-(isSour?starter/2:0),
water: water-(isSour?starter/2:0),
yeast: isSour ? starter : flour*yPct/100
};
}
return {ballW,total,flour,water,saltG,oilG,sugarG,starter,pf,main,
yPct,pfYeastPct,U,Upf,hasPf,isSour,
tcCold: s.staglio==="before" ? tcBall : tcBulkFor(total),
warmup: warmupFor(s),
hoursReal:s.roomH+(s.seq==="room"?0:s.fridgeH)+warmupFor(s)};
}
/* ============================================================
ZEITPLAN
============================================================ */
function timeline(s,r){
const steps=[];
const KNEAD=0.5, SHAPE=0.25;
const ball = `${s.balls} × ${fmtG(r.ballW)} straff rundwirken`;
const cold = h => `${h} h bei ${n1(s.fridgeT)} °C, Box geschlossen`;
if (r.hasPf) steps.push({d:s.pfH, a:(s.pf==="poolish"?"Poolish":"Biga")+" ansetzen",
t:`${fmtG(r.pf.flour)} Mehl · ${fmtG(r.pf.water)} Wasser · ${fmtG(r.pf.yeast)} ${r.isSour?"Anstellgut":YEASTS[s.yeast]} · ${s.pfH} h bei ${n1(s.pfT)} °C`});
steps.push({d:KNEAD, a:"Teig kneten",
t:r.hasPf?"Vorteig mit restlichem Mehl, Wasser und Salz verkneten, 20 min ruhen lassen"
:"Alles verkneten bis der Teig glatt ist, 20 min ruhen lassen"});
if (s.seq==="room" || s.fridgeH<=0){
const bulk=s.roomH*0.7, proof=s.roomH*0.3;
if (bulk>0) steps.push({d:bulk, a:"Puntata — Stockgare", t:`${n1(bulk)} h bei ${n1(s.roomT)} °C, abgedeckt`});
steps.push({d:SHAPE, a:"Staglio — Teiglinge formen", t:ball});
if (proof>0) steps.push({d:proof, a:"Appretto — Stückgare", t:`${n1(proof)} h bei ${n1(s.roomT)} °C bis die Ballen sichtbar aufgegangen sind`});
} else if (s.seq==="coldwarm"){
if (s.staglio==="before"){
steps.push({d:SHAPE, a:"Staglio — Teiglinge formen", t:ball+" — direkt nach dem Kneten, ohne Vorgare"});
steps.push({d:s.fridgeH, a:"Kaltgare in Ballen", t:cold(s.fridgeH)+". Behälter erst nach 12 h schließen, sonst tropft Kondenswasser auf den Teig."});
} else {
steps.push({d:s.fridgeH, a:"Kaltgare in Masse", t:cold(s.fridgeH)+`. Teig flach drücken (23 cm), sonst braucht der Kern ${Math.round(r.tcCold)} h zum Durchkühlen.`});
steps.push({d:SHAPE, a:"Staglio — Teiglinge formen", t:ball+" — kalt aus dem Kühlschrank, ggf. 20 min anwärmen lassen"});
}
steps.push({d:s.roomH, a:"Appretto — warm ausgaren", t:`${n1(s.roomH)} h bei ${n1(s.roomT)} °C. Hier passiert der sichtbare Trieb und der Teig wird dehnbar.`});
} else {
steps.push({d:s.roomH, a:"Puntata — Stockgare", t:`${n1(s.roomH)} h bei ${n1(s.roomT)} °C, abgedeckt`});
if (s.staglio==="before"){
steps.push({d:SHAPE, a:"Staglio — Teiglinge formen", t:ball});
steps.push({d:s.fridgeH, a:"Kaltgare in Ballen", t:cold(s.fridgeH)});
steps.push({d:r.warmup, a:"Akklimatisieren", t:`${r.warmup} h vorher rausnehmen. Es geht nur um Dehnbarkeit — ab etwa 1015 °C Kerntemperatur lässt sich der Teig öffnen.`});
} else {
steps.push({d:s.fridgeH, a:"Kaltgare in Masse", t:cold(s.fridgeH)+`. Kern braucht rund ${Math.round(r.tcCold)} h zum Durchkühlen — flach drücken hilft.`});
steps.push({d:SHAPE, a:"Staglio — Teiglinge formen", t:ball+" — kalt aus dem Kühlschrank"});
steps.push({d:r.warmup, a:"Aufwärmen & Stückgare", t:`${r.warmup} h bei ${n1(s.roomT)} °C, bis die Ballen entspannt und dehnbar sind`});
}
}
steps.push({d:0, a:"Backen", t:"Ofen so heiß wie möglich, Stein oder Stahl gut vorheizen", bake:true});
const totalH = steps.reduce((x,y)=>x+y.d,0);
const bake = new Date(s.bake);
let t = new Date(bake.getTime()-totalH*3600e3);
steps.forEach(st=>{ st.at=new Date(t); t=new Date(t.getTime()+st.d*3600e3); });
return {steps,totalH,start:steps[0].at};
}
/* ============================================================
FORMAT
============================================================ */
const n1 = x => (Math.round(x*10)/10).toString().replace(".",",");
function fmtG(g){
if (g>=100) return Math.round(g)+" g";
if (g>=10) return n1(g)+" g";
if (g>=1) return (Math.round(g*10)/10).toFixed(1).replace(".",",")+" g";
return (Math.round(g*100)/100).toFixed(2).replace(".",",")+" g";
}
const pct = x => n1(x)+" %";
const WD = ["So","Mo","Di","Mi","Do","Fr","Sa"];
function fmtT(d){
const hh=String(d.getHours()).padStart(2,"0"), mm=String(d.getMinutes()).padStart(2,"0");
return `${hh}:${mm}`;
}
function fmtDay(d, ref){
const a=new Date(d); a.setHours(0,0,0,0);
const b=new Date(ref||Date.now()); b.setHours(0,0,0,0);
const diff=Math.round((a-b)/864e5);
if (diff===0) return "heute";
if (diff===1) return "morgen";
if (diff===-1) return "gestern";
return `${WD[d.getDay()]} ${d.getDate()}.${d.getMonth()+1}.`;
}
function dur(h){
const m=Math.round(h*60);
if (m<60) return m+" min";
const H=Math.floor(m/60), M=m%60;
return M? `${H} h ${M} min` : `${H} h`;
}
/* ============================================================
DOM
============================================================ */
const $ = id => document.getElementById(id);
const RANGES = ["ball","dia","panL","panW","hyd","salt","oil","sugar","roomH","roomT","fridgeH","fridgeT","pfPct","pfH","pfT"];
function chips(host, obj, key, after){
host.innerHTML="";
Object.entries(obj).forEach(([k,v])=>{
const b=document.createElement("button");
b.type="button"; b.className="chip"; b.textContent=typeof v==="string"?v:v.n;
b.setAttribute("aria-pressed", S[key]===k);
b.onclick=()=>{ S[key]=k; after&&after(k); render(); };
host.appendChild(b);
});
}
function buildFlourSelect(){
const sel=$("flour"); sel.innerHTML="";
let group=null, og=null;
FLOURS.forEach(f=>{
if (f.g!==group){ group=f.g; og=document.createElement("optgroup"); og.label=group; sel.appendChild(og); }
const o=document.createElement("option"); o.value=f.id; o.textContent=f.n; og.appendChild(o);
});
sel.onchange=()=>{ S.flour=sel.value; render(); };
}
/* Führung auf die Slider anwenden. Gibt es die Variante nicht
(72 h bei Raumtemperatur), wird auf Kaltgare umgeschaltet. */
function applyPlan(){
const p=PLANS[S.plan];
if (!p || S.plan==="free") return;
let cfg = S.seq==="room" ? p.room : p.cold;
if (!cfg){ S.seq="coldwarm"; S.staglio="after"; cfg=p.cold; }
if (!cfg) return;
S.roomH=cfg.roomH; S.fridgeH=cfg.fridgeH;
// Lehmann: die warme Zeit liegt hinten und dient dem Handling
if (S.seq==="coldwarm") S.roomH=Math.min(S.roomH,3);
}
function initUI(){
buildFlourSelect();
chips($("styles"), STYLES, "style", k=>{
const st=STYLES[k];
S.ball=st.ball; S.hyd=st.hyd; S.salt=st.salt; S.oil=st.oil; S.sugar=st.sugar; S.mode=st.mode;
});
chips($("modes"), MODES, "mode");
chips($("yeastTypes"), YEASTS, "yeast");
chips($("pfTypes"), PFS, "pf");
chips($("seqs"), SEQ, "seq", k=>{
S.staglio = k==="coldwarm" ? "after" : "before"; // jeweils die übliche Praxis
applyPlan();
});
chips($("staglios"), STAGLIO, "staglio");
chips($("plans"), PLANS, "plan", ()=>applyPlan());
RANGES.forEach(id=>{
$(id).addEventListener("input", e=>{
S[id]=parseFloat(e.target.value);
if (id==="roomH"||id==="fridgeH"){ // Handbetrieb → Führung frei
S.plan="free";
if (S.fridgeH<=0) S.seq="room";
else if (S.seq==="room") S.seq="warmcold";
}
render();
});
});
$("ballsMinus").onclick=()=>{ S.balls=Math.max(1,S.balls-1); render(); };
$("ballsPlus").onclick =()=>{ S.balls=Math.min(60,S.balls+1); render(); };
$("bakeAt").addEventListener("input", e=>{
if (e.target.value){ S.bake=new Date(e.target.value).getTime(); render(); }
});
$("applyFlour").onclick=()=>{
const f=FLOURS.find(x=>x.id===S.flour);
S.hyd=Math.round((f.hyd[0]+f.hyd[1])/2*2)/2;
const target=clamp(S.roomH+S.fridgeH, f.reife[0], f.reife[1]);
if (S.fridgeH>0) S.fridgeH=Math.round(Math.max(0,target-S.roomH));
else S.roomH=Math.round(target*2)/2;
render();
};
$("share").onclick=share;
$("copyTxt").onclick=copyText;
$("pdf").onclick=savePdf;
$("reset").onclick=()=>{ S=Object.assign({},DEFAULT); S.bake=defaultBake(); location.hash=""; render(); };
}
function syncInputs(){
RANGES.forEach(id=>{ $(id).value=S[id]; });
$("ballsOut").textContent=S.balls;
$("flour").value=S.flour;
["styles","modes","yeastTypes","pfTypes","plans","seqs","staglios"].forEach(h=>{
const key={styles:"style",modes:"mode",yeastTypes:"yeast",pfTypes:"pf",plans:"plan",seqs:"seq",staglios:"staglio"}[h];
const keys=Object.keys({styles:STYLES,modes:MODES,yeastTypes:YEASTS,pfTypes:PFS,plans:PLANS,seqs:SEQ,staglios:STAGLIO}[h]);
[...$(h).children].forEach((b,i)=>b.setAttribute("aria-pressed", S[key]===keys[i]));
});
$("seqNote").textContent = SEQ[S.seq].note;
$("staglioBox").hidden = (S.seq==="room" || S.fridgeH<=0);
$("modeW").hidden = S.mode!=="w";
$("modeD").hidden = S.mode!=="d";
$("modeP").hidden = S.mode!=="p";
$("pfBox").hidden = S.pf==="none";
$("v-ball").innerHTML = S.ball+" <small>g</small>";
$("v-dia").innerHTML = S.dia+" <small>cm</small>";
$("v-panL").innerHTML = S.panL+" <small>cm</small>";
$("v-panW").innerHTML = S.panW+" <small>cm</small>";
$("v-hyd").textContent = pct(S.hyd);
$("v-salt").textContent = pct(S.salt);
$("v-oil").textContent = pct(S.oil);
$("v-sugar").textContent= pct(S.sugar);
$("v-roomH").innerHTML = n1(S.roomH)+" <small>h</small>";
$("v-roomT").innerHTML = n1(S.roomT)+" <small>°C</small>";
$("v-fridgeH").innerHTML= S.fridgeH+" <small>h</small>";
$("v-fridgeT").innerHTML= n1(S.fridgeT)+" <small>°C</small>";
$("v-pfPct").textContent= pct(S.pfPct);
$("v-pfH").innerHTML = S.pfH+" <small>h</small>";
$("v-pfT").innerHTML = n1(S.pfT)+" <small>°C</small>";
const d=new Date(S.bake);
$("bakeAt").value = new Date(d.getTime()-d.getTimezoneOffset()*60e3).toISOString().slice(0,16);
}
function renderFlour(r){
const f=FLOURS.find(x=>x.id===S.flour);
$("flourW").textContent = f.w==="—" ? "ohne W-Wert" : "W "+f.w;
$("flourMeta").innerHTML = [
f.w!=="—" ? `W-Wert <b>${f.w}</b>` : null,
f.pr ? `Protein <b>${n1(f.pr)} %</b>` : null,
`Hydration <b>${f.hyd[0]}${f.hyd[1]} %</b>`,
f.reife[1]<120 ? `Reife <b>${f.reife[0]}${f.reife[1]} h</b>` : null,
f.vt ? `<b style="color:var(--lime)">vorteigtauglich</b>` : `nur Direktteig`
].filter(Boolean).map(t=>`<span class="badge">${t}</span>`).join("");
$("flourNote").textContent = f.note;
return f;
}
function renderWarnings(f,r){
const w=[];
if (S.flour!=="custom"){
if (r.U > f.reife[1]*1.15)
w.push(["bad",`Dein Plan entspricht rund ${Math.round(r.U)} h Reife bei 20 °C. ${f.n} trägt etwa ${f.reife[1]} h — der Teig baut ab und wird schlaff. Kürzer führen oder ein backstärkeres Mehl nehmen.`]);
else if (r.U < f.reife[0]*0.6)
w.push(["",`Nur rund ${Math.round(r.U)} h Reifeäquivalent. ${f.n} kann deutlich mehr — mit längerer Gare wird der Teig aromatischer und bekömmlicher.`]);
if (S.hyd > f.hyd[1]+3)
w.push(["",`${pct(S.hyd)} Hydration liegt über dem, was ${f.n} gut trägt (${f.hyd[0]}${f.hyd[1]} %). Der Teig wird sehr klebrig.`]);
if (S.hyd < f.hyd[0]-3)
w.push(["",`${pct(S.hyd)} ist für ${f.n} recht trocken. Etwas mehr Wasser gibt eine offenere Krume.`]);
if (r.hasPf && !f.vt)
w.push(["",`${f.n} ist für Vorteige eher schwach. Poolish und Biga verlangen mehr Backstärke.`]);
}
if (S.fridgeT<4) w.push(["",`Unter 4 °C stellt die Hefe die Arbeit fast ein. 56 °C sind für die Kühlschrankgare der bessere Bereich.`]);
if (S.seq==="warmcold" && S.staglio==="after" && S.fridgeH>0 && r.tcCold > S.fridgeH*0.4)
w.push(["",`Die Masse braucht rund ${Math.round(r.tcCold)} h zum Durchkühlen — bei ${S.fridgeH} h Kaltgare gärt der Kern also die halbe Zeit deutlich wärmer als ${n1(S.fridgeT)} °C. Teig flach drücken oder auf mehrere Behälter aufteilen, sonst wird das Ergebnis schwer wiederholbar.`]);
if (S.seq==="coldwarm" && S.roomH>4)
w.push(["",`Bei der Lehmann-Methode ist die Gare im Kühlschrank abgeschlossen. ${n1(S.roomH)} h warm danach sind viel — der Teig übergärt leicht. 0,5 bis 2 h reichen, um ihn dehnbar zu bekommen.`]);
if (S.seq==="coldwarm" && S.roomH<0.5)
w.push(["",`Ohne Aufwärmzeit ist der Teig sehr elastisch und wehrt sich beim Öffnen. Ab etwa 10 °C Kerntemperatur wird er dehnbar.`]);
if (!r.isSour && r.yPct<0.03) w.push(["",`Die Hefemenge ist rechnerisch winzig. Ohne Feinwaage mit 0,01 g Auflösung wird das Raten — kürzere Gare oder Vorteig sind praktikabler.`]);
if (!r.isSour && r.yPct>3) w.push(["",`Sehr viel Hefe. Der Teig geht schnell, aber flach im Geschmack. Mehr Zeit gibt mehr Aroma.`]);
if (r.hasPf && r.main.water<0)
w.push(["bad",`Der Vorteig enthält mehr Wasser als das ganze Rezept hergibt. Vorteig-Anteil senken oder Hydration erhöhen.`]);
if (r.hasPf && r.main.flour<0)
w.push(["bad",`Vorteig-Anteil zu hoch für dieses Rezept.`]);
$("warns").innerHTML = w.map(([c,t])=>`<div class="warn ${c}"><i>▲</i><span>${t}</span></div>`).join("");
}
function renderRecipe(r){
const st=STYLES[S.style];
$("recipeStyle").textContent = st.n+" · "+FLOURS.find(x=>x.id===S.flour).n;
$("h-total").innerHTML = Math.round(r.total)+" <small>g</small>";
$("h-ball").innerHTML = Math.round(r.ballW)+" <small>g</small> <small>× "+S.balls+"</small>";
$("h-flour").innerHTML = Math.round(r.flour)+" <small>g</small>";
const row=(n,g,p)=>`<tr><td>${n}</td><td class="p">${p||""}</td><td class="n">${fmtG(g)}</td></tr>`;
const yName = r.isSour ? "Anstellgut (100 % Hydration)" : YEASTS[S.yeast];
let h="";
if (r.hasPf){
const pfName = S.pf==="poolish"?"Poolish":"Biga";
h += `<p class="sub">Vorteig · ${pfName} · ${S.pfH} h bei ${n1(S.pfT)} °C</p><table>`;
h += row("Mehl", r.pf.flour, "100 %");
h += row("Wasser", r.pf.water, pct(PF_HYD[S.pf]*100));
h += row(yName, r.pf.yeast, "");
h += `<tr class="tot"><td>Vorteig gesamt</td><td class="p"></td><td class="n">${fmtG(r.pf.mass)}</td></tr></table>`;
h += `<p class="sub">Hauptteig</p><table>`;
h += row("Vorteig", r.pf.mass, "");
h += row("Mehl", r.main.flour, "");
h += row("Wasser", r.main.water, "");
} else {
h += `<p class="sub">Zutaten</p><table>`;
h += row("Mehl", r.main.flour, "100 %");
h += row("Wasser", r.main.water, pct(S.hyd));
if (!r.isSour) h += row(yName, r.main.yeast, pct(r.yPct));
else h += row(yName, r.starter, pct(r.yPct));
}
if (r.hasPf && !r.isSour) h += row(yName, r.main.yeast, "");
if (r.hasPf && r.isSour) h += row("(Trieb komplett im Vorteig)", 0, "");
h += row("Salz", r.saltG, pct(S.salt));
if (S.oil>0) h += row("Olivenöl", r.oilG, pct(S.oil));
if (S.sugar>0) h += row("Zucker / Malz", r.sugarG, pct(S.sugar));
h += `<tr class="tot"><td>Teig gesamt</td><td class="p"></td><td class="n">${Math.round(r.total)} g</td></tr></table>`;
$("recipeOut").innerHTML=h;
}
function renderTimeline(r){
const tl=timeline(S,r);
const now=Date.now();
let nextIdx=tl.steps.findIndex(s=>s.at.getTime()>now);
const html=tl.steps.map((s,i)=>{
const cls=[s.bake?"bake":"", i===nextIdx?"next":""].filter(Boolean).join(" ");
return `<li class="${cls}">
<div class="t">${fmtT(s.at)} <em>${fmtDay(s.at)}</em></div>
<div class="a">${s.a}${s.d?` · ${dur(s.d)}`:""}</div>
<div class="d">${s.t}</div>
</li>`;
}).join("");
$("timeline").innerHTML=html;
const diff=(tl.start.getTime()-now)/3600e3;
let cd;
if (diff < -0.05){
cd=`<div class="countdown">Der Startzeitpunkt liegt <b>${dur(Math.abs(diff))}</b> in der Vergangenheit. Backzeit später legen oder Gare kürzen — insgesamt brauchst du <b>${dur(tl.totalH)}</b>.</div>`;
} else {
cd=`<div class="countdown">Gesamtdauer <b>${dur(tl.totalH)}</b> · Start in <b>${dur(diff)}</b> um <b>${fmtT(tl.start)} ${fmtDay(tl.start)}</b>.</div>`;
}
$("countdown").innerHTML=cd;
return tl;
}
/* ============================================================
RENDER
============================================================ */
let lastR=null, lastTl=null;
function render(){
if (!S.bake) S.bake=defaultBake();
syncInputs();
const r=calc(S); lastR=r;
const f=renderFlour(r);
renderWarnings(f,r);
renderRecipe(r);
lastTl=renderTimeline(r);
renderPrint(r);
$("doughsum").textContent = `${S.balls} × ${Math.round(r.ballW)} g = ${Math.round(r.total)} g`;
$("fermsum").textContent = `${PLANS[S.plan].n} · ${SEQ[S.seq].n} · ${Math.round(r.U)} h @ 20 °C`;
$("pfsum").textContent = S.pf==="none" ? "Direktteig" : `${PFS[S.pf]} · ${S.pfPct} % · ${S.pfH} h`;
$("statusline").textContent = `${STYLES[S.style].n} · ${pct(S.hyd)} Hydration · ${r.isSour?"Sauerteig":YEASTS[S.yeast]}`;
writeHash();
}
/* ============================================================
URL-SHARING
============================================================ */
const KEYS=["style","flour","balls","mode","ball","dia","panL","panW","hyd","salt","oil","sugar","yeast","plan","seq","staglio","roomH","roomT","fridgeH","fridgeT","pf","pfPct","pfH","pfT"];
let hashLock=false;
function writeHash(){
const p=new URLSearchParams();
KEYS.forEach(k=>{ if (S[k]!==DEFAULT[k]) p.set(k,S[k]); });
hashLock=true;
history.replaceState(null,"", p.toString()? "#"+p.toString() : location.pathname);
setTimeout(()=>hashLock=false,0);
}
function readHash(){
const p=new URLSearchParams(location.hash.slice(1));
if (![...p.keys()].length) return false;
KEYS.forEach(k=>{
if (!p.has(k)) return;
const v=p.get(k);
S[k] = (typeof DEFAULT[k]==="number") ? parseFloat(v) : v;
});
if (!STYLES[S.style]) S.style="napo";
if (!PLANS[S.plan]) S.plan="free";
if (p.has("method")){ // alte Links: method=room|cold
S.seq = p.get("method")==="room" ? "room" : "warmcold";
}
if (!SEQ[S.seq]) S.seq = S.fridgeH>0 ? "warmcold" : "room";
if (!STAGLIO[S.staglio]) S.staglio = S.seq==="coldwarm" ? "after" : "before";
if (!FLOURS.some(f=>f.id===S.flour)) S.flour="cap-pizzeria";
return true;
}
function defaultBake(){
const d=new Date(); d.setMinutes(0,0,0);
if (d.getHours()>=19) d.setDate(d.getDate()+1);
d.setHours(19,0,0,0);
return d.getTime();
}
async function toClipboard(text, btn, label){
const old=btn.textContent;
try{ await navigator.clipboard.writeText(text); }
catch(e){
const ta=document.createElement("textarea");
ta.value=text; ta.style.position="fixed"; ta.style.opacity="0";
document.body.appendChild(ta); ta.select();
try{ document.execCommand("copy"); }catch(_){}
ta.remove();
}
btn.textContent=label; setTimeout(()=>btn.textContent=old,1800);
}
function share(){ toClipboard(location.href, $("share"), "Link kopiert ✓"); }
/* PDF = Druckdialog mit sprechendem Dateinamen. Browser und Handys
bieten dort „Als PDF sichern" an — ganz ohne externe Bibliothek. */
const ORIG_TITLE=document.title;
function savePdf(){
const r=lastR, st=STYLES[S.style];
document.title = `Pizzateig ${st.n} ${S.balls}x${Math.round(r.ballW)}g ${PLANS[S.plan].n}`
.replace(/[\/\\:*?"<>|]/g,"-");
window.print();
}
window.addEventListener("afterprint", ()=>{ document.title=ORIG_TITLE; });
function renderPrint(r){
const st=STYLES[S.style], f=FLOURS.find(x=>x.id===S.flour);
const d=new Date();
const bits=[
`${S.balls} Teiglinge à ${Math.round(r.ballW)} g · ${Math.round(r.total)} g Teig gesamt`,
`${f.n}${f.w!=="—"?` (W ${f.w})`:""} · ${pct(S.hyd)} Hydration · ${r.isSour?"Sauerteig":YEASTS[S.yeast]}`,
`${PLANS[S.plan].n} · Ablauf: ${SEQ[S.seq].n}${S.seq!=="room"&&S.fridgeH>0?` · ${STAGLIO[S.staglio]}`:""}`,
S.seq==="coldwarm"
? `${S.fridgeH} h bei ${n1(S.fridgeT)} °C, danach ${n1(S.roomH)} h bei ${n1(S.roomT)} °C`
: `${n1(S.roomH)} h bei ${n1(S.roomT)} °C${S.fridgeH>0?` + ${S.fridgeH} h bei ${n1(S.fridgeT)} °C + ${r.warmup} h aufwärmen`:""}`,
r.hasPf ? `Vorteig: ${S.pf==="poolish"?"Poolish":"Biga"} mit ${S.pfPct} % des Mehls, ${S.pfH} h bei ${n1(S.pfT)} °C` : "Direktteig ohne Vorteig"
];
$("printhead").innerHTML =
`<h2>Pizzateig — ${st.n}</h2><p>${bits.join("<br>")}</p>`;
$("printfoot").innerHTML =
`Erstellt am ${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()} mit TEIG//TERMINAL · Rezept erneut öffnen: ${location.href}`;
}
function copyText(){
const r=lastR, tl=lastTl, st=STYLES[S.style], f=FLOURS.find(x=>x.id===S.flour);
const L=[];
L.push(`PIZZATEIG — ${st.n}`);
L.push(`${S.balls} Teiglinge à ${Math.round(r.ballW)} g · ${Math.round(r.total)} g Teig`);
L.push(`Mehl: ${f.n} · Hydration ${pct(S.hyd)}`);
L.push("");
if (r.hasPf){
L.push(`VORTEIG (${S.pf==="poolish"?"Poolish":"Biga"}, ${S.pfH} h bei ${n1(S.pfT)} °C)`);
L.push(` Mehl ${fmtG(r.pf.flour)} · Wasser ${fmtG(r.pf.water)} · ${YEASTS[S.yeast]} ${fmtG(r.pf.yeast)}`);
L.push("");
L.push("HAUPTTEIG");
L.push(` Vorteig ${fmtG(r.pf.mass)}`);
} else L.push("ZUTATEN");
L.push(` Mehl ${fmtG(r.main.flour)}`);
L.push(` Wasser ${fmtG(r.main.water)}`);
L.push(` ${r.isSour?"Anstellgut":YEASTS[S.yeast]} ${fmtG(r.isSour&&!r.hasPf?r.starter:r.main.yeast)}`);
L.push(` Salz ${fmtG(r.saltG)}`);
if (S.oil>0) L.push(` Olivenöl ${fmtG(r.oilG)}`);
if (S.sugar>0) L.push(` Zucker/Malz ${fmtG(r.sugarG)}`);
L.push("");
L.push("ZEITPLAN");
tl.steps.forEach(s=>L.push(` ${fmtT(s.at)} ${fmtDay(s.at)}${s.a}${s.d?` (${dur(s.d)})`:""}`));
L.push("");
L.push(location.href);
toClipboard(L.join("\n"), $("copyTxt"), "Text kopiert ✓");
}
window.addEventListener("hashchange",()=>{ if(!hashLock){ readHash(); render(); }});
setInterval(()=>{ if(lastR) renderTimeline(lastR); }, 30000);
/* ---------- Start ---------- */
readHash();
initUI();
S.bake=defaultBake();
render();
</script>
</body>
</html>