1253 lines
58 KiB
HTML
1253 lines
58 KiB
HTML
<!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:#10151F;
|
||
--panel-2:#151C29;
|
||
--line:#263241;
|
||
--line-hot:#34475C;
|
||
--cyan:#00F0FF;
|
||
--magenta:#FF2D78;
|
||
--amber:#FFB000;
|
||
--lime:#8CFF3B;
|
||
--txt:#E2ECF5;
|
||
--dim:#A5B4C5;
|
||
--muted:#8796A8;
|
||
--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}
|
||
html{touch-action:manipulation}
|
||
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;
|
||
}
|
||
button,input,select,.chip,.stepper,.quick-summary{touch-action:manipulation}
|
||
.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(--muted);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(--muted);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:rgba(255,255,255,.015);color:#B4C3D3;
|
||
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;
|
||
}
|
||
|
||
|
||
/* ---------- UX-Modus & Schnellstart ---------- */
|
||
.quick-summary{
|
||
position:sticky;top:0;z-index:20;
|
||
display:flex;gap:8px;overflow:auto;align-items:center;
|
||
margin:0 -14px 14px;padding:8px 14px;
|
||
background:rgba(7,8,11,.92);backdrop-filter:blur(8px);
|
||
border-bottom:1px solid var(--line-hot);
|
||
scrollbar-width:none;
|
||
}
|
||
.quick-summary::-webkit-scrollbar{display:none}
|
||
.quick-summary span{
|
||
white-space:nowrap;border:1px solid var(--line-hot);border-radius:var(--r);
|
||
background:#0A0E15;color:var(--txt);padding:6px 9px;font-size:12px;
|
||
font-variant-numeric:tabular-nums;
|
||
}
|
||
.quick-summary b{color:var(--cyan);font-weight:700}
|
||
@media(max-width:520px){
|
||
.quick-summary{
|
||
display:grid;
|
||
grid-template-columns:1fr 1fr;
|
||
gap:6px;
|
||
overflow:visible;
|
||
padding:8px 14px 10px;
|
||
}
|
||
.quick-summary span{
|
||
min-width:0;
|
||
white-space:normal;
|
||
line-height:1.25;
|
||
font-size:11.5px;
|
||
padding:7px 8px;
|
||
}
|
||
}
|
||
body[data-ux="simple"] .advanced-only{display:none!important}
|
||
body[data-ux="simple"] .pro-only-note{display:block}
|
||
.pro-only-note{display:none}
|
||
.help{
|
||
margin-left:6px;width:18px;height:18px;border:1px solid var(--line-hot);border-radius:50%;
|
||
background:#0A0E15;color:var(--cyan);font:inherit;font-size:11px;line-height:16px;
|
||
display:inline-flex;align-items:center;justify-content:center;cursor:help;padding:0;
|
||
}
|
||
.warn b{color:#fff;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(--muted)}
|
||
.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}
|
||
body{font-size:16px}
|
||
.ctl-v{font-size:19px}
|
||
.chip{font-size:13px;padding:9px 12px}
|
||
.panel{padding:18px 15px 20px}
|
||
}
|
||
|
||
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(--muted);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:#F3FAFF;white-space:nowrap}
|
||
td.p{text-align:right;width:62px;color:var(--muted);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(--muted);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(--muted);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>
|
||
|
||
<div class="quick-summary noprint" id="quickSummary" aria-label="Kurzüberblick"></div>
|
||
|
||
<section class="panel noprint">
|
||
<p class="ph"><b>00 // Schnellstart</b><span>Einfach oder Profi</span></p>
|
||
<div class="ctl">
|
||
<div class="ctl-top"><span class="ctl-l">Ansicht</span></div>
|
||
<div class="chips" id="uxModes" role="group" aria-label="Ansicht"></div>
|
||
<p class="note">Einfach fragt nur: Wann soll die Pizza fertig sein, welcher Stil, wie viele Ballen und wie schwer pro Ballen. Profi blendet alle Stellschrauben ein.</p>
|
||
</div>
|
||
<label class="ctl">
|
||
<div class="ctl-top"><span class="ctl-l">Pizza fertig am</span></div>
|
||
<input type="datetime-local" id="bakeAtQuick">
|
||
</label>
|
||
<p class="note pro-only-note">Profi-Werte wie Hydration, Gare und Vorteig laufen mit sinnvollen Defaults im Hintergrund. Wechsel auf „Profi“, wenn du sie anpassen möchtest.</p>
|
||
</section>
|
||
|
||
<!-- 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 advanced-only">
|
||
<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="150" max="400" step="5">
|
||
</label>
|
||
</div>
|
||
|
||
<div id="modeD" class="advanced-only" 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" class="advanced-only" 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 advanced-only">
|
||
<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 advanced-only">
|
||
<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 advanced-only">
|
||
<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 advanced-only">
|
||
<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 advanced-only" 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 teilen</button>
|
||
<button type="button" class="btn" id="pdf">Druck/PDF</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:"260–280", 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:"300–320", 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:"260–270", 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:"300–320", 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:"240–260", 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:"370–390", 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:"260–280", 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:"300–320", 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:"250–270", 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:"340–360", pr:14.0, hyd:[65,78], reife:[24,72], vt:true, note:"Sehr backstark, für 48–72 h und hohe Hydration."},
|
||
{g:"Italien · Typ 00", id:"dg-napo", n:"Dallagiovanna La Napoletana", w:"270–290", 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:"260–280", 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:"310–330", 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:"290–310", 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:"350–400", pr:14.5, hyd:[68,82], reife:[24,96], vt:true, note:"Als Anteil von 10–30 % zum Aufstärken schwächerer Mehle."},
|
||
{g:"Deutschland", id:"t405", n:"Weizenmehl Type 405", w:"150–180", 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:"180–220", 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:"200–240", 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:"210–250", pr:12.5, hyd:[62,72], reife:[6,24], vt:false, note:"Dunkler und durstiger. Gut als Anteil von 10–30 %."},
|
||
{g:"Deutschland", id:"pizza-super", n:"Pizzamehl „Typ 00\" (Supermarkt)", w:"200–240", 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:"130–170", 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 10–30 % beimischen."},
|
||
{g:"Deutschland", id:"ruch", n:"Ruchmehl / Halbweiß", w:"200–240", 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 10–30 % 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 UX_MODES = {simple:"Einfach", pro:"Profi"};
|
||
const PF_HYD = {poolish:1.0, biga:0.45};
|
||
|
||
/* ============================================================
|
||
STATE
|
||
============================================================ */
|
||
const DEFAULT = {
|
||
style:"napo", flour:"cap-pizzeria", balls:4, mode:"w", uxMode:"simple",
|
||
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);
|
||
const BALL_MIN = 150;
|
||
const BALL_MAX = 400;
|
||
const SIMPLE_PREP_H = 0.75; // Kneten + Formen im Zeitplan
|
||
const SIMPLE_SAME_DAY_MAX_H = 12; // Same-Day im Einfach-Modus immer warm führen
|
||
|
||
/* ============================================================
|
||
RECHENKERN
|
||
============================================================ */
|
||
const clamp = (x,a,b)=>Math.min(b,Math.max(a,x));
|
||
const clampBall = x => clamp(Number.isFinite(+x) ? +x : DEFAULT.ball, BALL_MIN, BALL_MAX);
|
||
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 1–2 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 (2–3 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 10–15 °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);
|
||
}
|
||
|
||
/* Einfach-Modus: Der Nutzer gibt nur Zielzeit, Stil und Teigmenge vor.
|
||
Die Führung wird daraus automatisch gewählt:
|
||
- Same-Day: immer warm, ohne Kühlschrank.
|
||
- Länger als Same-Day: Lehmann-Methode (Kalt → Warm) als Standard. */
|
||
function applySimpleFermentation(){
|
||
if (S.uxMode!=="simple" || !S.bake) return;
|
||
const available = (S.bake - Date.now())/3600e3;
|
||
const doughH = clamp(Math.round((available - SIMPLE_PREP_H)*2)/2, 2, 72);
|
||
S.yeast = "fresh";
|
||
S.pf = "none";
|
||
|
||
if (doughH <= SIMPLE_SAME_DAY_MAX_H){
|
||
S.plan = "same";
|
||
S.seq = "room";
|
||
S.staglio = "before";
|
||
S.fridgeH = 0;
|
||
S.roomH = doughH;
|
||
return;
|
||
}
|
||
|
||
S.seq = "coldwarm";
|
||
S.staglio = "after";
|
||
S.roomH = doughH <= 18 ? 2 : 3;
|
||
S.fridgeH = Math.max(4, Math.round(doughH - S.roomH));
|
||
const total = S.roomH + S.fridgeH;
|
||
S.plan = total <= 18 ? "night" : total <= 30 ? "d24" : total <= 54 ? "d48" : "d72";
|
||
}
|
||
|
||
function initUI(){
|
||
buildFlourSelect();
|
||
chips($("uxModes"), UX_MODES, "uxMode", k=>{
|
||
if (k==="simple") S.mode="w";
|
||
});
|
||
chips($("styles"), STYLES, "style", k=>{
|
||
const st=STYLES[k];
|
||
S.ball=clampBall(st.ball); S.hyd=st.hyd; S.salt=st.salt; S.oil=st.oil; S.sugar=st.sugar; S.mode=st.mode;
|
||
if (S.uxMode==="simple") S.mode="w";
|
||
});
|
||
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(); };
|
||
const bakeInputHandler = e=>{
|
||
if (e.target.value){ S.bake=new Date(e.target.value).getTime(); render(); }
|
||
};
|
||
$("bakeAt").addEventListener("input", bakeInputHandler);
|
||
$("bakeAtQuick").addEventListener("input", bakeInputHandler);
|
||
|
||
$("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=()=>{ if(!confirm("Alle Werte zurücksetzen?")) return; S=Object.assign({},DEFAULT); S.bake=defaultBake(); location.hash=""; render(); };
|
||
}
|
||
|
||
function syncInputs(){
|
||
document.body.dataset.ux = S.uxMode;
|
||
S.ball = clampBall(S.ball);
|
||
if (S.uxMode==="simple" && S.mode!=="w") S.mode="w";
|
||
applySimpleFermentation();
|
||
RANGES.forEach(id=>{ $(id).value=S[id]; });
|
||
$("ballsOut").textContent=S.balls;
|
||
$("flour").value=S.flour;
|
||
["uxModes","styles","modes","yeastTypes","pfTypes","plans","seqs","staglios"].forEach(h=>{
|
||
const key={uxModes:"uxMode",styles:"style",modes:"mode",yeastTypes:"yeast",pfTypes:"pf",plans:"plan",seqs:"seq",staglios:"staglio"}[h];
|
||
const keys=Object.keys({uxModes:UX_MODES,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);
|
||
const bakeVal = new Date(d.getTime()-d.getTimezoneOffset()*60e3).toISOString().slice(0,16);
|
||
$("bakeAt").value = bakeVal;
|
||
$("bakeAtQuick").value = bakeVal;
|
||
}
|
||
|
||
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",`<b>Zu lange Gare für dieses Mehl.</b><br>Dein Plan entspricht rund ${Math.round(r.U)} h Reife bei 20 °C. ${f.n} trägt etwa ${f.reife[1]} h. Empfehlung: kürzer führen, Kühlschrank kälter stellen oder ein backstärkeres Mehl wählen.`]);
|
||
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. 5–6 °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;
|
||
}
|
||
|
||
|
||
function renderQuickSummary(r){
|
||
const tl = lastTl || timeline(S,r);
|
||
const start = tl.start;
|
||
const items = [
|
||
`<span><b>${STYLES[S.style].n}</b></span>`,
|
||
`<span><b>${S.balls} × ${Math.round(r.ballW)} g</b> Teiglinge</span>`,
|
||
`<span>Backen <b>${fmtT(new Date(S.bake))} ${fmtDay(new Date(S.bake))}</b></span>`,
|
||
`<span>Start <b>${fmtT(start)} ${fmtDay(start)}</b></span>`
|
||
];
|
||
if (S.uxMode!=="simple") items.splice(2,0,`<span><b>${Math.round(r.flour)} g</b> Mehl</span>`);
|
||
$("quickSummary").innerHTML = items.join("");
|
||
}
|
||
|
||
/* ============================================================
|
||
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);
|
||
renderQuickSummary(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","uxMode","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;
|
||
const baseUrl = window.location.pathname + window.location.search;
|
||
history.replaceState(null,"", p.toString()? "#"+p.toString() : baseUrl);
|
||
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 (!UX_MODES[S.uxMode]) S.uxMode="simple";
|
||
S.ball = clampBall(S.ball);
|
||
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);
|
||
|
||
/* iOS Safari zoomt sonst bei schnellem Doppeltippen auf Buttons/Slider.
|
||
Pinch-Zoom bleibt unberührt; verhindert wird nur der Double-Tap-Default. */
|
||
let lastTouchEnd = 0;
|
||
document.addEventListener("touchend", e=>{
|
||
const now = Date.now();
|
||
if (now - lastTouchEnd <= 320) e.preventDefault();
|
||
lastTouchEnd = now;
|
||
}, {passive:false});
|
||
|
||
/* ---------- Start ---------- */
|
||
readHash();
|
||
initUI();
|
||
S.bake=defaultBake();
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|