@@ -3,27 +3,29 @@
< head >
< head >
< meta charset = "utf-8" >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< title > TEIG//TERMINAL — Pizzateig-Kalkulator< / title >
< title > TEIG//TERMINAL v1.1 — Pizzateig-Kalkulator< / title >
< meta name = "description" content = "Pizzateig-Rechner mit Bäckerprozent, Mehl-Datenbank, Vorteig und Rückwärts-Zeitplan." >
< meta name = "description" content = "Pizzateig-Rechner mit Bäckerprozent, Mehl-Datenbank, Vorteig und Rückwärts-Zeitplan." >
< meta name = "color-scheme" content = "dark" >
< meta name = "color-scheme" content = "dark" >
< style >
< style >
: root {
: root {
--bg : #07080B ;
--bg : #07080B ;
--panel : #0E1117 ;
--panel : #10151F ;
--panel-2 : #121722 ;
--panel-2 : #151C29 ;
--line : #1B2430 ;
--line : #263241 ;
--line-hot : #24 3447 ;
--line-hot : #34475C ;
--cyan : #00F0FF ;
--cyan : #00F0FF ;
--magenta : #FF2D78 ;
--magenta : #FF2D78 ;
--amber : #FFB000 ;
--amber : #FFB000 ;
--lime : #8CFF3B ;
--lime : #8CFF3B ;
--txt : #C9D6E2 ;
--txt : #E2ECF5 ;
--dim : #6E7B8B ;
--dim : #A5B4C5 ;
--muted : #8796A8 ;
--mono : ui-monospace , "JetBrains Mono" , "SFMono-Regular" , "SF Mono" , Menlo , Consolas , "Liberation Mono" , monospace ;
--mono : ui-monospace , "JetBrains Mono" , "SFMono-Regular" , "SF Mono" , Menlo , Consolas , "Liberation Mono" , monospace ;
--r : 2 px ;
--r : 2 px ;
}
}
* { box-sizing : border-box }
* { box-sizing : border-box }
html , body { margin : 0 ; padding : 0 }
html , body { margin : 0 ; padding : 0 }
html { touch-action : manipulation }
body {
body {
background : var ( - - bg ) ;
background : var ( - - bg ) ;
color : var ( - - txt ) ;
color : var ( - - txt ) ;
@@ -37,6 +39,7 @@ body{
linear-gradient ( 90 deg , rgba ( 0 , 240 , 255 , .028 ) 1 px , transparent 1 px ) ;
linear-gradient ( 90 deg , rgba ( 0 , 240 , 255 , .028 ) 1 px , transparent 1 px ) ;
background-size : 32 px 32 px ;
background-size : 32 px 32 px ;
}
}
button , input , select , . chip , . stepper , . quick-summary { touch-action : manipulation }
. wrap { max-width : 760 px ; margin : 0 auto ; padding : 0 14 px }
. wrap { max-width : 760 px ; margin : 0 auto ; padding : 0 14 px }
/* ---------- Kopf ---------- */
/* ---------- Kopf ---------- */
@@ -48,8 +51,8 @@ header{padding:28px 0 18px;border-bottom:1px solid var(--line);margin-bottom:20p
text-shadow : 0 0 18 px rgba ( 0 , 240 , 255 , .35 ) ;
text-shadow : 0 0 18 px rgba ( 0 , 240 , 255 , .35 ) ;
}
}
. brand em { font-style : normal ; color : var ( - - magenta ) ; text-shadow : 0 0 18 px rgba ( 255 , 45 , 120 , .45 ) }
. brand em { font-style : normal ; color : var ( - - magenta ) ; text-shadow : 0 0 18 px rgba ( 255 , 45 , 120 , .45 ) }
. tag { color : var ( - - dim ) ; font-size : 11 px ; letter-spacing : .22 em ; text-transform : uppercase ; margin : 10 px 0 0 }
. tag { color : var ( - - muted ) ; font-size : 11 px ; letter-spacing : .22 em ; text-transform : uppercase ; margin : 10 px 0 0 }
. status { display : flex ; gap : 8 px ; align-items : center ; margin-top : 12 px ; font-size : 11 px ; letter-spacing : .14 em ; color : var ( - - dim ) ; text-transform : uppercase }
. status { display : flex ; gap : 8 px ; align-items : center ; margin-top : 12 px ; font-size : 11 px ; letter-spacing : .14 em ; color : var ( - - muted ) ; text-transform : uppercase }
. dot { width : 7 px ; height : 7 px ; background : var ( - - lime ) ; border-radius : 50 % ; box-shadow : 0 0 10 px var ( - - lime ) ; animation : pulse 2.4 s infinite }
. dot { width : 7 px ; height : 7 px ; background : var ( - - lime ) ; border-radius : 50 % ; box-shadow : 0 0 10 px var ( - - lime ) ; animation : pulse 2.4 s infinite }
@ keyframes pulse { 50 % { opacity : .25 } }
@ keyframes pulse { 50 % { opacity : .25 } }
@@ -79,7 +82,7 @@ header{padding:28px 0 18px;border-bottom:1px solid var(--line);margin-bottom:20p
. chips { display : flex ; flex-wrap : wrap ; gap : 6 px }
. chips { display : flex ; flex-wrap : wrap ; gap : 6 px }
. chip {
. chip {
font : inherit ; font-size : 12 px ;
font : inherit ; font-size : 12 px ;
background : transparent ; color : var ( - - dim ) ;
background : rgba ( 255 , 255 , 255 , .015 ) ; color : #B4C3D3 ;
border : 1 px solid var ( - - line - hot ) ; border-radius : var ( - - r ) ;
border : 1 px solid var ( - - line - hot ) ; border-radius : var ( - - r ) ;
padding : 7 px 11 px ; cursor : pointer ; letter-spacing : .06 em ;
padding : 7 px 11 px ; cursor : pointer ; letter-spacing : .06 em ;
transition : .15 s ;
transition : .15 s ;
@@ -90,11 +93,54 @@ header{padding:28px 0 18px;border-bottom:1px solid var(--line);margin-bottom:20p
box-shadow : 0 0 16 px rgba ( 0 , 240 , 255 , .35 ) ; font-weight : 700 ;
box-shadow : 0 0 16 px rgba ( 0 , 240 , 255 , .35 ) ; font-weight : 700 ;
}
}
/* ---------- UX-Modus & Schnellstart ---------- */
. quick-summary {
position : sticky ; top : 0 ; z-index : 20 ;
display : flex ; gap : 8 px ; overflow : auto ; align-items : center ;
margin : 0 -14 px 14 px ; padding : 8 px 14 px ;
background : rgba ( 7 , 8 , 11 , .92 ) ; backdrop-filter : blur ( 8 px ) ;
border-bottom : 1 px solid var ( - - line - hot ) ;
scrollbar-width : none ;
}
. quick-summary :: -webkit-scrollbar { display : none }
. quick-summary span {
white-space : nowrap ; border : 1 px solid var ( - - line - hot ) ; border-radius : var ( - - r ) ;
background : #0A0E15 ; color : var ( - - txt ) ; padding : 6 px 9 px ; font-size : 12 px ;
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 : 1 fr 1 fr ;
gap : 6 px ;
overflow : visible ;
padding : 8 px 14 px 10 px ;
}
. quick-summary span {
min-width : 0 ;
white-space : normal ;
line-height : 1.25 ;
font-size : 11.5 px ;
padding : 7 px 8 px ;
}
}
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 : 6 px ; width : 18 px ; height : 18 px ; border : 1 px solid var ( - - line - hot ) ; border-radius : 50 % ;
background : #0A0E15 ; color : var ( - - cyan ) ; font : inherit ; font-size : 11 px ; line-height : 16 px ;
display : inline-flex ; align-items : center ; justify-content : center ; cursor : help ; padding : 0 ;
}
. warn b { color : #fff ; font-weight : 700 }
/* ---------- Controls ---------- */
/* ---------- Controls ---------- */
. ctl { display : block ; margin : 15 px 0 0 }
. ctl { display : block ; margin : 15 px 0 0 }
. ctl : first-of-type { margin-top : 0 }
. ctl : first-of-type { margin-top : 0 }
. ctl-top { display : flex ; justify-content : space-between ; align-items : baseline ; gap : 10 px ; margin-bottom : 5 px }
. ctl-top { display : flex ; justify-content : space-between ; align-items : baseline ; gap : 10 px ; margin-bottom : 5 px }
. ctl-l { font-size : 11 px ; letter-spacing : .18 em ; text-transform : uppercase ; color : var ( - - dim ) }
. ctl-l { font-size : 11 px ; letter-spacing : .18 em ; text-transform : uppercase ; color : var ( - - muted ) }
. ctl-v { font-size : 17 px ; color : var ( - - cyan ) ; font-weight : 700 ; text-shadow : 0 0 12 px rgba ( 0 , 240 , 255 , .3 ) ; font-variant-numeric : tabular-nums }
. ctl-v { font-size : 17 px ; color : var ( - - cyan ) ; font-weight : 700 ; text-shadow : 0 0 12 px rgba ( 0 , 240 , 255 , .3 ) ; font-variant-numeric : tabular-nums }
. ctl-v small { color : var ( - - dim ) ; font-size : 11 px ; font-weight : 400 ; letter-spacing : .1 em ; text-shadow : none }
. ctl-v small { color : var ( - - dim ) ; font-size : 11 px ; font-weight : 400 ; letter-spacing : .1 em ; text-shadow : none }
input [ type = range ] {
input [ type = range ] {
@@ -112,7 +158,12 @@ input[type=range]::-moz-range-thumb{
}
}
input [ type = range ] : focus-visible :: -webkit-slider-thumb { outline : 2 px solid var ( - - magenta ) ; outline-offset : 2 px }
input [ type = range ] : focus-visible :: -webkit-slider-thumb { outline : 2 px solid var ( - - magenta ) ; outline-offset : 2 px }
. row { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 14 px }
. row { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 14 px }
@ media ( max-width : 520px ) { . row { grid-template-columns : 1 fr } }
@ media ( max-width : 520px ) { . row { grid-template-columns : 1 fr }
body { font-size : 16 px }
. ctl-v { font-size : 19 px }
. chip { font-size : 13 px ; padding : 9 px 12 px }
. panel { padding : 18 px 15 px 20 px }
}
select , input [ type = number ] , input [ type = datetime-local ] {
select , input [ type = number ] , input [ type = datetime-local ] {
font : inherit ; font-size : 14 px ; width : 100 % ;
font : inherit ; font-size : 14 px ; width : 100 % ;
@@ -138,7 +189,7 @@ select:focus,input:focus{outline:2px solid var(--cyan);outline-offset:1px}
. badge {
. badge {
font-size : 10.5 px ; letter-spacing : .12 em ; text-transform : uppercase ;
font-size : 10.5 px ; letter-spacing : .12 em ; text-transform : uppercase ;
border : 1 px solid var ( - - line - hot ) ; border-radius : var ( - - r ) ;
border : 1 px solid var ( - - line - hot ) ; border-radius : var ( - - r ) ;
padding : 4 px 8 px ; color : var ( - - dim ) ; background : #0A0E15
padding : 4 px 8 px ; color : var ( - - muted ) ; background : #0A0E15
}
}
. badge b { color : var ( - - txt ) ; font-weight : 700 }
. badge b { color : var ( - - txt ) ; font-weight : 700 }
. note { font-size : 12 px ; color : var ( - - dim ) ; margin-top : 10 px ; line-height : 1.5 }
. note { font-size : 12 px ; color : var ( - - dim ) ; margin-top : 10 px ; line-height : 1.5 }
@@ -158,8 +209,8 @@ select:focus,input:focus{outline:2px solid var(--cyan);outline-offset:1px}
. sub : first-child { margin-top : 0 }
. sub : first-child { margin-top : 0 }
table { width : 100 % ; border-collapse : collapse }
table { width : 100 % ; border-collapse : collapse }
td { padding : 8 px 0 ; border-bottom : 1 px dotted var ( - - line - hot ) ; vertical-align : baseline }
td { padding : 8 px 0 ; border-bottom : 1 px dotted var ( - - line - hot ) ; vertical-align : baseline }
td . n { text-align : right ; font-variant-numeric : tabular-nums ; font-weight : 700 ; color : #EAF6 FF ; white-space : nowrap }
td . n { text-align : right ; font-variant-numeric : tabular-nums ; font-weight : 700 ; color : #F3FA FF ; white-space : nowrap }
td . p { text-align : right ; width : 62 px ; color : var ( - - dim ) ; font-size : 12 px ; font-variant-numeric : tabular-nums }
td . p { text-align : right ; width : 62 px ; color : var ( - - muted ) ; font-size : 12 px ; font-variant-numeric : tabular-nums }
tr . tot td { border-bottom : 0 ; border-top : 1 px solid var ( - - line - hot ) ; padding-top : 11 px ; color : var ( - - cyan ) }
tr . tot td { border-bottom : 0 ; border-top : 1 px solid var ( - - line - hot ) ; padding-top : 11 px ; color : var ( - - cyan ) }
tr . tot td . n { color : var ( - - cyan ) ; text-shadow : 0 0 12 px rgba ( 0 , 240 , 255 , .3 ) ; font-size : 17 px }
tr . tot td . n { color : var ( - - cyan ) ; text-shadow : 0 0 12 px rgba ( 0 , 240 , 255 , .3 ) ; font-size : 17 px }
. hero {
. hero {
@@ -186,7 +237,7 @@ tr.tot td.n{color:var(--cyan);text-shadow:0 0 12px rgba(0,240,255,.3);font-size:
. tl . t { font-size : 17 px ; font-weight : 700 ; color : var ( - - cyan ) ; font-variant-numeric : tabular-nums ; text-shadow : 0 0 12 px rgba ( 0 , 240 , 255 , .25 ) }
. tl . t { font-size : 17 px ; font-weight : 700 ; color : var ( - - cyan ) ; font-variant-numeric : tabular-nums ; text-shadow : 0 0 12 px rgba ( 0 , 240 , 255 , .25 ) }
. tl . t em { font-style : normal ; font-size : 11 px ; letter-spacing : .14 em ; color : var ( - - dim ) ; margin-left : 8 px ; text-transform : uppercase ; text-shadow : none }
. tl . t em { font-style : normal ; font-size : 11 px ; letter-spacing : .14 em ; color : var ( - - dim ) ; margin-left : 8 px ; text-transform : uppercase ; text-shadow : none }
. tl . a { color : #EAF6FF ; font-size : 14 px ; margin-top : 1 px }
. tl . a { color : #EAF6FF ; font-size : 14 px ; margin-top : 1 px }
. tl . d { color : var ( - - dim ) ; font-size : 12 px ; margin-top : 2 px ; line-height : 1.45 }
. tl . d { color : var ( - - muted ) ; font-size : 12 px ; margin-top : 2 px ; line-height : 1.45 }
. tl li . bake . t { color : var ( - - amber ) ; text-shadow : 0 0 12 px rgba ( 255 , 176 , 0 , .3 ) }
. tl li . bake . t { color : var ( - - amber ) ; text-shadow : 0 0 12 px rgba ( 255 , 176 , 0 , .3 ) }
. countdown {
. countdown {
border : 1 px solid var ( - - magenta ) ; background : rgba ( 255 , 45 , 120 , .07 ) ;
border : 1 px solid var ( - - magenta ) ; background : rgba ( 255 , 45 , 120 , .07 ) ;
@@ -204,15 +255,53 @@ tr.tot td.n{color:var(--cyan);text-shadow:0 0 12px rgba(0,240,255,.3);font-size:
. btn : hover { border-color : var ( - - cyan ) ; color : var ( - - cyan ) }
. 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 { background : var ( - - magenta ) ; border-color : var ( - - magenta ) ; color : #fff ; font-weight : 700 }
. btn . pri : hover { box-shadow : 0 0 20 px rgba ( 255 , 45 , 120 , .5 ) ; color : #fff }
. btn . pri : hover { box-shadow : 0 0 20 px rgba ( 255 , 45 , 120 , .5 ) ; color : #fff }
footer { color : var ( - - dim ) ; font-size : 11.5 px ; line-height : 1.6 ; margin-top : 22 px ; border-top : 1 px solid var ( - - line ) ; padding-top : 16 px }
footer { color : var ( - - muted ) ; font-size : 11.5 px ; line-height : 1.6 ; margin-top : 22 px ; border-top : 1 px solid var ( - - line ) ; padding-top : 16 px }
footer b { color : var ( - - txt ) }
footer b { color : var ( - - txt ) }
/* ---------- Responsive Desktop-Arbeitsbereich ---------- */
. workspace { display : block }
. input-stack , . output-stack { min-width : 0 }
@ media ( max-width : 1049px ) {
. workspace { display : block }
}
@ media ( min-width : 1050px ) {
. wrap { max-width : 1320 px ; padding : 0 24 px }
header {
display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto ;
grid-template-areas : "brand status" "tag status" ;
column-gap : 32 px ; align-items : end ; padding : 30 px 0 20 px
}
. brand { grid-area : brand ; font-size : 48 px }
. tag { grid-area : tag }
. status { grid-area : status ; align-self : center ; justify-self : end ; margin-top : 0 }
. quick-summary {
display : grid ; grid-template-columns : repeat ( 5 , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ; overflow : visible ; margin : 0 0 18 px ; padding : 10 px ;
border : 1 px solid var ( - - line ) ; background : rgba ( 7 , 8 , 11 , .94 )
}
body [ data-ux = "simple" ] . quick-summary { grid-template-columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) }
. quick-summary span { min-width : 0 ; white-space : normal ; text-align : center ; line-height : 1.3 }
. workspace {
display : grid ;
grid-template-columns : minmax ( 0 , 1 fr ) minmax ( 430 px , .95 fr ) ;
gap : 18 px ; align-items : start
}
body [ data-ux = "simple" ] . workspace {
grid-template-columns : minmax ( 0 , .9 fr ) minmax ( 480 px , 1.1 fr )
}
. workspace . panel { padding : 19 px 18 px 21 px ; margin-bottom : 18 px }
. output-stack . hero { display : grid ; grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 12 px }
. output-stack . actions { display : grid ; grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) }
. output-stack . btn { width : 100 % }
}
@ media ( prefers-reduced-motion : reduce ) { * { animation : none !important ; transition : none !important } }
@ media ( prefers-reduced-motion : reduce ) { * { animation : none !important ; transition : none !important } }
. printonly { display : none }
. printonly { display : none }
@ media print {
@ media print {
@ page { size : A4 ; margin : 15mm }
@ page { size : A4 ; margin : 15mm }
body { background : #fff ; color : #000 ; background-image : none ; font-size : 11 pt ; padding : 0 }
body { background : #fff ; color : #000 ; background-image : none ; font-size : 11 pt ; padding : 0 }
. wrap { max-width : none ; padding : 0 }
. wrap { max-width : none ; padding : 0 }
. workspace { display : block }
header , . actions , . noprint , # countdown { display : none !important }
header , . actions , . noprint , # countdown { display : none !important }
. printonly { display : block }
. printonly { display : block }
# printhead { margin : 0 0 8 mm ; border-bottom : 2 px solid #000 ; padding-bottom : 4 mm }
# printhead { margin : 0 0 8 mm ; border-bottom : 2 px solid #000 ; padding-bottom : 4 mm }
@@ -253,9 +342,28 @@ footer b{color:var(--txt)}
< header >
< header >
< h1 class = "brand" > TEIG< em > //< / em > TERMINAL< / h1 >
< h1 class = "brand" > TEIG< em > //< / em > TERMINAL< / h1 >
< p class = "tag" > Pizzateig-Kalkulator · Bäckerprozent · Vorteig · Rückwärts-Zeitplan< / p >
< 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 >
< div class = "status" > < span class = "dot" > < / span > < span id = "statusline" > System bereit · v1.1 < / span > < / div >
< / header >
< / header >
< div class = "quick-summary noprint" id = "quickSummary" aria-label = "Kurzüberblick" > < / div >
< main class = "workspace" >
< div class = "input-stack" >
< 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 -->
<!-- CHARGE -->
< section class = "panel noprint" >
< section class = "panel noprint" >
< p class = "ph" > < b > 01 // Charge< / b > < span id = "doughsum" > —< / span > < / p >
< p class = "ph" > < b > 01 // Charge< / b > < span id = "doughsum" > —< / span > < / p >
@@ -271,7 +379,7 @@ footer b{color:var(--txt)}
< / div >
< / div >
< / div >
< / div >
< div class = "ctl" >
< div class = "ctl advanced-only " >
< div class = "ctl-top" > < span class = "ctl-l" > Größe bestimmen über< / span > < / div >
< 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 class = "chips" id = "modes" role = "group" aria-label = "Berechnungsart" > < / div >
< / div >
< / div >
@@ -279,18 +387,18 @@ footer b{color:var(--txt)}
< div id = "modeW" >
< div id = "modeW" >
< label class = "ctl" >
< label class = "ctl" >
< div class = "ctl-top" > < span class = "ctl-l" > Gewicht pro Teigling< / span > < span class = "ctl-v" id = "v-ball" > < / span > < / div >
< 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 = "12 0" max = "12 00" step = "5" >
< input type = "range" id = "ball" min = "15 0" max = "4 00" step = "5" >
< / label >
< / label >
< / div >
< / div >
< div id = "modeD" hidden >
< div id = "modeD" class = "advanced-only" hidden >
< label class = "ctl" >
< label class = "ctl" >
< div class = "ctl-top" > < span class = "ctl-l" > Durchmesser< / span > < span class = "ctl-v" id = "v-dia" > < / span > < / div >
< 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" >
< input type = "range" id = "dia" min = "16" max = "45" step = "1" >
< / label >
< / label >
< / div >
< / div >
< div id = "modeP" hidden >
< div id = "modeP" class = "advanced-only" hidden >
< div class = "row" >
< div class = "row" >
< label class = "ctl" >
< label class = "ctl" >
< div class = "ctl-top" > < span class = "ctl-l" > Blech Länge< / span > < span class = "ctl-v" id = "v-panL" > < / span > < / div >
< div class = "ctl-top" > < span class = "ctl-l" > Blech Länge< / span > < span class = "ctl-v" id = "v-panL" > < / span > < / div >
@@ -305,7 +413,7 @@ footer b{color:var(--txt)}
< / section >
< / section >
<!-- MEHL -->
<!-- MEHL -->
< section class = "panel noprint" >
< section class = "panel noprint advanced-only " >
< p class = "ph" > < b > 02 // Mehl< / b > < span id = "flourW" > —< / span > < / p >
< p class = "ph" > < b > 02 // Mehl< / b > < span id = "flourW" > —< / span > < / p >
< select id = "flour" aria-label = "Mehl auswählen" > < / select >
< select id = "flour" aria-label = "Mehl auswählen" > < / select >
< div class = "meta" id = "flourMeta" > < / div >
< div class = "meta" id = "flourMeta" > < / div >
@@ -316,7 +424,7 @@ footer b{color:var(--txt)}
< / section >
< / section >
<!-- REZEPTUR -->
<!-- REZEPTUR -->
< section class = "panel noprint" >
< section class = "panel noprint advanced-only " >
< p class = "ph" > < b > 03 // Rezeptur< / b > < span > Bäckerprozent< / span > < / p >
< p class = "ph" > < b > 03 // Rezeptur< / b > < span > Bäckerprozent< / span > < / p >
< label class = "ctl" >
< label class = "ctl" >
< div class = "ctl-top" > < span class = "ctl-l" > Hydration< / span > < span class = "ctl-v" id = "v-hyd" > < / span > < / div >
< div class = "ctl-top" > < span class = "ctl-l" > Hydration< / span > < span class = "ctl-v" id = "v-hyd" > < / span > < / div >
@@ -343,7 +451,7 @@ footer b{color:var(--txt)}
< / section >
< / section >
<!-- GARE -->
<!-- GARE -->
< section class = "panel noprint" >
< section class = "panel noprint advanced-only " >
< p class = "ph" > < b > 04 // Gare< / b > < span id = "fermsum" > —< / span > < / p >
< p class = "ph" > < b > 04 // Gare< / b > < span id = "fermsum" > —< / span > < / p >
< div class = "ctl" >
< div class = "ctl" >
@@ -385,7 +493,7 @@ footer b{color:var(--txt)}
< / section >
< / section >
<!-- VORTEIG -->
<!-- VORTEIG -->
< section class = "panel noprint" >
< section class = "panel noprint advanced-only " >
< p class = "ph" > < b > 05 // Vorteig< / b > < span id = "pfsum" > —< / span > < / p >
< p class = "ph" > < b > 05 // Vorteig< / b > < span id = "pfsum" > —< / span > < / p >
< div class = "chips" id = "pfTypes" role = "group" aria-label = "Vorteig" > < / div >
< div class = "chips" id = "pfTypes" role = "group" aria-label = "Vorteig" > < / div >
< div id = "pfBox" hidden >
< div id = "pfBox" hidden >
@@ -406,6 +514,9 @@ footer b{color:var(--txt)}
< / div >
< / div >
< / section >
< / section >
< / div >
< div class = "output-stack" >
<!-- WARNUNGEN -->
<!-- WARNUNGEN -->
< div class = "warns" id = "warns" > < / div >
< div class = "warns" id = "warns" > < / div >
@@ -423,7 +534,7 @@ footer b{color:var(--txt)}
<!-- ZEITPLAN -->
<!-- ZEITPLAN -->
< section class = "panel" >
< section class = "panel" >
< p class = "ph" > < b > 07 // Zeitplan< / b > < span > rückwärts ab Backzeit< / span > < / p >
< p class = "ph" > < b > 07 // Zeitplan< / b > < span > rückwärts ab Backzeit< / span > < / p >
< label class = "ctl noprint" style = "margin-bottom:16px" >
< label class = "ctl noprint advanced-only " style = "margin-bottom:16px" >
< div class = "ctl-top" > < span class = "ctl-l" > Pizza fertig am< / span > < / div >
< div class = "ctl-top" > < span class = "ctl-l" > Pizza fertig am< / span > < / div >
< input type = "datetime-local" id = "bakeAt" >
< input type = "datetime-local" id = "bakeAt" >
< / label >
< / label >
@@ -434,12 +545,16 @@ footer b{color:var(--txt)}
< div id = "printfoot" class = "printonly" > < / div >
< div id = "printfoot" class = "printonly" > < / div >
< div class = "actions noprint" >
< div class = "actions noprint" >
< button type = "button" class = "btn pri" id = "share" > Rezept-Link kopier en< / button >
< button type = "button" class = "btn pri" id = "share" > Rezept teil en< / button >
< button type = "button" class = "btn" id = "pdf" > Als PDF speicher n< / button >
< button type = "button" class = "btn" id = "downloadPdf" > PDF herunterlade n< / button >
< button type = "button" class = "btn" id = "pdf" > Drucken< / button >
< button type = "button" class = "btn" id = "copyTxt" > Als Text kopieren< / button >
< button type = "button" class = "btn" id = "copyTxt" > Als Text kopieren< / button >
< button type = "button" class = "btn" id = "reset" > Zurücksetzen< / button >
< button type = "button" class = "btn" id = "reset" > Zurücksetzen< / button >
< / div >
< / div >
< / div >
< / main >
< footer >
< footer >
< b > Hinweis:< / b > Die Hefemenge wird über ein Q10-Modell aus Zeit und Temperatur geschätzt.
< 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 —
Teigtemperatur, Hefevitalität und Schwankungen im Raum lassen sich nicht in eine Formel pressen —
@@ -486,10 +601,10 @@ const FLOURS = [
{ 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." }
{ 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²) ---------- */
/* ---------- Stile (tf = Teigdichte in g/cm², yeastK = Frischhefe-Referenz ) ---------- */
const STYLES = {
const STYLES = {
napo : { n : "Napoletana" , ball : 250 , hyd : 62 , salt : 2.8 , oil : 0 , sugar : 0 , tf : 0.36 , mode : "w" } ,
napo : { n : "Napoletana" , ball : 250 , hyd : 62 , salt : 2.8 , oil : 0 , sugar : 0 , tf : 0.36 , mode : "w" , yeastK : 1.5 } ,
contemp : { n : "Contemporanea" , ball : 280 , hyd : 70 , salt : 2.8 , oil : 0 , sugar : 0 , tf : 0.39 , mode : "w" } ,
contemp : { n : "Contemporanea" , ball : 280 , hyd : 70 , salt : 2.8 , oil : 0 , sugar : 0 , tf : 0.39 , mode : "w" , yeastK : 1.7 } ,
ny : { n : "New York" , ball : 300 , hyd : 63 , salt : 2.0 , oil : 2.5 , sugar : 1.5 , tf : 0.31 , 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" } ,
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" } ,
teglia : { n : "Teglia / Blech" , ball : 800 , hyd : 78 , salt : 2.2 , oil : 2 , sugar : 0 , tf : 0.55 , mode : "p" } ,
@@ -515,7 +630,7 @@ const SEQ = {
warmcold : { n : "Warm → Kalt" , fridge : true ,
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." } ,
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 ,
coldwarm : { n : "Kalt → Warm" , fridge : true ,
note : "Lehmann-Methode: d irekt 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 ." }
note : "D irekt nach dem Kneten kalt stellen, anschließend in warmer Stückgare ausreifen lassen. Gut planbar und besonders für längere Führungen geeignet; die warme Endgare macht die Ballen sichtbar aktiv und dehnbar ." }
} ;
} ;
const STAGLIO = {
const STAGLIO = {
before : "Ballen vor der Kälte" ,
before : "Ballen vor der Kälte" ,
@@ -525,13 +640,14 @@ const STAGLIO = {
const MODES = { w : "Ballgewicht" , d : "Durchmesser" , p : "Blech L× B" } ;
const MODES = { w : "Ballgewicht" , d : "Durchmesser" , p : "Blech L× B" } ;
const YEASTS = { fresh : "Frischhefe" , dry : "Trockenhefe" , sour : "Sauerteig" } ;
const YEASTS = { fresh : "Frischhefe" , dry : "Trockenhefe" , sour : "Sauerteig" } ;
const PFS = { none : "Ohne (Direktteig)" , poolish : "Poolish" , biga : "Biga" } ;
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 } ;
const PF _HYD = { poolish : 1.0 , biga : 0.45 } ;
/* ============================================================
/* ============================================================
STATE
STATE
============================================================ */
============================================================ */
const DEFAULT = {
const DEFAULT = {
style : "napo" , flour : "cap-pizzeria" , balls : 4 , mode : "w" ,
style : "napo" , flour : "cap-pizzeria" , balls : 4 , mode : "w" , uxMode : "simple" ,
ball : 250 , dia : 30 , panL : 40 , panW : 30 ,
ball : 250 , dia : 30 , panL : 40 , panW : 30 ,
hyd : 62 , salt : 2.8 , oil : 0 , sugar : 0 , yeast : "fresh" ,
hyd : 62 , salt : 2.8 , oil : 0 , sugar : 0 , yeast : "fresh" ,
plan : "d24" , seq : "warmcold" , staglio : "before" ,
plan : "d24" , seq : "warmcold" , staglio : "before" ,
@@ -540,11 +656,16 @@ const DEFAULT = {
bake : null
bake : null
} ;
} ;
let S = Object . assign ( { } , DEFAULT ) ;
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
RECHENKERN
============================================================ */
============================================================ */
const clamp = ( x , a , b ) => Math . min ( b , Math . max ( a , x ) ) ;
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
const rate = T => Math . pow ( 2.5 , ( T - 20 ) / 10 ) ; // Q10 = 2.5
/* Eine Garephase mit Temperaturwechsel. tc = Stunden, bis der Teigkern
/* Eine Garephase mit Temperaturwechsel. tc = Stunden, bis der Teigkern
@@ -597,7 +718,7 @@ function calc(s){
const pfFrac = hasPf ? s . pfPct / 100 : 0 ;
const pfFrac = hasPf ? s . pfPct / 100 : 0 ;
// --- Triebmittel in % vom Mehl ---
// --- Triebmittel in % vom Mehl ---
let fresh = 6 / U ; // Frischhefe-Referenz
let fresh = ( st . yeastK ? ? 6 ) / U ; // stilbezogene Frischhefe-Referenz
fresh *= 1 + Math . max ( 0 , s . salt - 2.5 ) * 0.06 ; // Salz bremst
fresh *= 1 + Math . max ( 0 , s . salt - 2.5 ) * 0.06 ; // Salz bremst
if ( hasPf ) fresh *= ( 1 - 0.8 * pfFrac ) ; // Vorteig bringt schon Trieb
if ( hasPf ) fresh *= ( 1 - 0.8 * pfFrac ) ; // Vorteig bringt schon Trieb
fresh = clamp ( fresh , 0.01 , 6 ) ;
fresh = clamp ( fresh , 0.01 , 6 ) ;
@@ -778,11 +899,43 @@ function applyPlan(){
if ( S . seq === "coldwarm" ) S . roomH = Math . min ( S . roomH , 3 ) ;
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 = 4 ;
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 ( ) {
function initUI ( ) {
buildFlourSelect ( ) ;
buildFlourSelect ( ) ;
chips ( $ ( "uxModes" ) , UX _MODES , "uxMode" , k => {
if ( k === "simple" ) S . mode = "w" ;
} ) ;
chips ( $ ( "styles" ) , STYLES , "style" , k => {
chips ( $ ( "styles" ) , STYLES , "style" , k => {
const st = STYLES [ 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 ;
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 ( $ ( "modes" ) , MODES , "mode" ) ;
chips ( $ ( "yeastTypes" ) , YEASTS , "yeast" ) ;
chips ( $ ( "yeastTypes" ) , YEASTS , "yeast" ) ;
@@ -807,9 +960,11 @@ function initUI(){
} ) ;
} ) ;
$ ( "ballsMinus" ) . onclick = ( ) => { S . balls = Math . max ( 1 , S . balls - 1 ) ; render ( ) ; } ;
$ ( "ballsMinus" ) . onclick = ( ) => { S . balls = Math . max ( 1 , S . balls - 1 ) ; render ( ) ; } ;
$ ( "ballsPlus" ) . onclick = ( ) => { S . balls = Math . min ( 60 , S . balls + 1 ) ; render ( ) ; } ;
$ ( "ballsPlus" ) . onclick = ( ) => { S . balls = Math . min ( 60 , S . balls + 1 ) ; render ( ) ; } ;
$ ( "bakeAt" ) . addEventListener ( "input" , e => {
const bakeInputHandler = e => {
if ( e . target . value ) { S . bake = new Date ( e . target . value ) . getTime ( ) ; render ( ) ; }
if ( e . target . value ) { S . bake = new Date ( e . target . value ) . getTime ( ) ; render ( ) ; }
} ) ;
} ;
$ ( "bakeAt" ) . addEventListener ( "input" , bakeInputHandler ) ;
$ ( "bakeAtQuick" ) . addEventListener ( "input" , bakeInputHandler ) ;
$ ( "applyFlour" ) . onclick = ( ) => {
$ ( "applyFlour" ) . onclick = ( ) => {
const f = FLOURS . find ( x => x . id === S . flour ) ;
const f = FLOURS . find ( x => x . id === S . flour ) ;
@@ -822,17 +977,22 @@ function initUI(){
$ ( "share" ) . onclick = share ;
$ ( "share" ) . onclick = share ;
$ ( "copyTxt" ) . onclick = copyText ;
$ ( "copyTxt" ) . onclick = copyText ;
$ ( "downloadPdf" ) . onclick = downloadPdf ;
$ ( "pdf" ) . onclick = savePdf ;
$ ( "pdf" ) . onclick = savePdf ;
$ ( "reset" ) . onclick = ( ) => { S = Object . assign ( { } , DEFAULT ) ; S . bake = defaultBake ( ) ; location . hash = "" ; render ( ) ; } ;
$ ( "reset" ) . onclick = ( ) => { if ( ! confirm ( "Alle Werte zurücksetzen?" ) ) return ; S = Object . assign ( { } , DEFAULT ) ; S . bake = defaultBake ( ) ; location . hash = "" ; render ( ) ; } ;
}
}
function syncInputs ( ) {
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 ] ; } ) ;
RANGES . forEach ( id => { $ ( id ) . value = S [ id ] ; } ) ;
$ ( "ballsOut" ) . textContent = S . balls ;
$ ( "ballsOut" ) . textContent = S . balls ;
$ ( "flour" ) . value = S . flour ;
$ ( "flour" ) . value = S . flour ;
[ "styles" , "modes" , "yeastTypes" , "pfTypes" , "plans" , "seqs" , "staglios" ] . forEach ( h => {
[ "uxModes" , " 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 key = { uxModes : "uxMode" , 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 ] ) ;
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 ] ) ) ;
[ ... $ ( h ) . children ] . forEach ( ( b , i ) => b . setAttribute ( "aria-pressed" , S [ key ] === keys [ i ] ) ) ;
} ) ;
} ) ;
$ ( "seqNote" ) . textContent = SEQ [ S . seq ] . note ;
$ ( "seqNote" ) . textContent = SEQ [ S . seq ] . note ;
@@ -859,7 +1019,9 @@ function syncInputs(){
$ ( "v-pfT" ) . innerHTML = n1 ( S . pfT ) + " <small>°C</small>" ;
$ ( "v-pfT" ) . innerHTML = n1 ( S . pfT ) + " <small>°C</small>" ;
const d = new Date ( S . bake ) ;
const d = new Date ( S . bake ) ;
$ ( "bakeAt" ) . value = new Date ( d . getTime ( ) - d . getTimezoneOffset ( ) * 60e3 ) . toISOString ( ) . slice ( 0 , 16 ) ;
const bakeVal = new Date ( d . getTime ( ) - d . getTimezoneOffset ( ) * 60e3 ) . toISOString ( ) . slice ( 0 , 16 ) ;
$ ( "bakeAt" ) . value = bakeVal ;
$ ( "bakeAtQuick" ) . value = bakeVal ;
}
}
function renderFlour ( r ) {
function renderFlour ( r ) {
@@ -880,7 +1042,7 @@ function renderWarnings(f,r){
const w = [ ] ;
const w = [ ] ;
if ( S . flour !== "custom" ) {
if ( S . flour !== "custom" ) {
if ( r . U > f . reife [ 1 ] * 1.15 )
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ühr en oder ein backstärkeres Mehl nehm en. ` ] ) ;
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 stell en oder ein backstärkeres Mehl wähl en. ` ] ) ;
else if ( r . U < f . reife [ 0 ] * 0.6 )
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. ` ] ) ;
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 )
if ( S . hyd > f . hyd [ 1 ] + 3 )
@@ -890,6 +1052,8 @@ function renderWarnings(f,r){
if ( r . hasPf && ! f . vt )
if ( r . hasPf && ! f . vt )
w . push ( [ "" , ` ${ f . n } ist für Vorteige eher schwach. Poolish und Biga verlangen mehr Backstärke. ` ] ) ;
w . push ( [ "" , ` ${ f . n } ist für Vorteige eher schwach. Poolish und Biga verlangen mehr Backstärke. ` ] ) ;
}
}
if ( ( S . style === "napo" || S . style === "contemp" ) && S . seq === "room" && S . roomH < 4 )
w . push ( [ "" , ` <b>Schnellteig:</b> Unter 4 h braucht neapolitanischer Teig vergleichsweise viel Hefe und entwickelt weniger Aroma. Wenn möglich, mindestens 4– 8 h einplanen. ` ] ) ;
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 . 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 )
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. ` ] ) ;
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. ` ] ) ;
@@ -897,7 +1061,8 @@ function renderWarnings(f,r){
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. ` ] ) ;
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 )
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. ` ] ) ;
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. ` ] ) ;
const yeastG = r . hasPf ? r . pf . yeast + r . main . yeast : r . main . yeast ;
if ( ! r . isSour && yeastG < 0.5 ) w . push ( [ "" , ` Nur ${ yeastG . toFixed ( 2 ) . replace ( "." , "," ) } g Hefe: Für reproduzierbare Ergebnisse eine Feinwaage mit 0,01 g Auflösung verwenden oder die Hefe in Wasser lösen und anteilig dosieren. ` ] ) ;
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 . 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 )
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. ` ] ) ;
w . push ( [ "bad" , ` Der Vorteig enthält mehr Wasser als das ganze Rezept hergibt. Vorteig-Anteil senken oder Hydration erhöhen. ` ] ) ;
@@ -970,6 +1135,20 @@ function renderTimeline(r){
return tl ;
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
RENDER
============================================================ */
============================================================ */
@@ -982,6 +1161,7 @@ function render(){
renderWarnings ( f , r ) ;
renderWarnings ( f , r ) ;
renderRecipe ( r ) ;
renderRecipe ( r ) ;
lastTl = renderTimeline ( r ) ;
lastTl = renderTimeline ( r ) ;
renderQuickSummary ( r ) ;
renderPrint ( r ) ;
renderPrint ( r ) ;
$ ( "doughsum" ) . textContent = ` ${ S . balls } × ${ Math . round ( r . ballW ) } g = ${ Math . round ( r . total ) } g ` ;
$ ( "doughsum" ) . textContent = ` ${ S . balls } × ${ Math . round ( r . ballW ) } g = ${ Math . round ( r . total ) } g ` ;
@@ -994,13 +1174,14 @@ function render(){
/* ============================================================
/* ============================================================
URL-SHARING
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" ] ;
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 ;
let hashLock = false ;
function writeHash ( ) {
function writeHash ( ) {
const p = new URLSearchParams ( ) ;
const p = new URLSearchParams ( ) ;
KEYS . forEach ( k => { if ( S [ k ] !== DEFAULT [ k ] ) p . set ( k , S [ k ] ) ; } ) ;
KEYS . forEach ( k => { if ( S [ k ] !== DEFAULT [ k ] ) p . set ( k , S [ k ] ) ; } ) ;
hashLock = true ;
hashLock = true ;
history . replaceState ( null , "" , p . toString ( ) ? "#" + p . toString ( ) : location . pathname ) ;
const baseUrl = window . location . pathname + window . location . search ;
history . replaceState ( null , "" , p . toString ( ) ? "#" + p . toString ( ) : baseUrl ) ;
setTimeout ( ( ) => hashLock = false , 0 ) ;
setTimeout ( ( ) => hashLock = false , 0 ) ;
}
}
function readHash ( ) {
function readHash ( ) {
@@ -1012,6 +1193,8 @@ function readHash(){
S [ k ] = ( typeof DEFAULT [ k ] === "number" ) ? parseFloat ( v ) : v ;
S [ k ] = ( typeof DEFAULT [ k ] === "number" ) ? parseFloat ( v ) : v ;
} ) ;
} ) ;
if ( ! STYLES [ S . style ] ) S . style = "napo" ;
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 ( ! PLANS [ S . plan ] ) S . plan = "free" ;
if ( p . has ( "method" ) ) { // alte Links: method=room|cold
if ( p . has ( "method" ) ) { // alte Links: method=room|cold
S . seq = p . get ( "method" ) === "room" ? "room" : "warmcold" ;
S . seq = p . get ( "method" ) === "room" ? "room" : "warmcold" ;
@@ -1042,6 +1225,236 @@ async function toClipboard(text, btn, label){
}
}
function share ( ) { toClipboard ( location . href , $ ( "share" ) , "Link kopiert ✓" ) ; }
function share ( ) { toClipboard ( location . href , $ ( "share" ) , "Link kopiert ✓" ) ; }
/* PDF-DOWNLOAD */
function pdfFilename ( styleName , balls , ballWeight , planName ) {
return ` Pizzateig- ${ styleName } - ${ balls } x ${ Math . round ( ballWeight ) } g- ${ planName } .pdf `
. normalize ( "NFKD" )
. replace ( /[\u0300-\u036f]/g , "" )
. replace ( /[\/\\:*?"<>|]+/g , "-" )
. replace ( /\s+/g , "-" )
. replace ( /-+/g , "-" ) ;
}
function winAnsiHex ( text ) {
const special = { "€" : 128 , "‚ " : 130 , "ƒ" : 131 , "„" : 132 , "…" : 133 , "†" : 134 , "‡" : 135 , "ˆ " : 136 , "‰" : 137 , "Š" : 138 , "‹ " : 139 , "Œ" : 140 , "Ž" : 142 , "‘ " : 145 , "’ " : 146 , "“" : 147 , "”" : 148 , "•" : 149 , "– " : 150 , "—" : 151 , "˜ " : 152 , "™" : 153 , "š" : 154 , "› " : 155 , "œ" : 156 , "ž" : 158 , "Ÿ" : 159 } ;
let out = "" ;
for ( const char of String ( text ) ) {
const point = char . codePointAt ( 0 ) ;
const byte = special [ char ] ? ? ( point <= 255 ? point : 63 ) ;
out += byte . toString ( 16 ) . padStart ( 2 , "0" ) . toUpperCase ( ) ;
}
return out ;
}
function wrapPdfLines ( lines , width = 92 ) {
const out = [ ] ;
lines . forEach ( raw => {
const line = String ( raw ) ;
if ( ! line ) { out . push ( "" ) ; return ; }
const indent = ( line . match ( /^\s*/ ) || [ "" ] ) [ 0 ] ;
let rest = line . trim ( ) ;
while ( rest . length > width ) {
let cut = rest . lastIndexOf ( " " , width ) ;
if ( cut < 20 ) cut = width ;
out . push ( indent + rest . slice ( 0 , cut ) ) ;
rest = rest . slice ( cut ) . trimStart ( ) ;
}
out . push ( indent + rest ) ;
} ) ;
return out ;
}
function parsePdfRecipe ( lines ) {
const source = lines . map ( line => String ( line ) ) ;
const title = source [ 0 ] || "PIZZATEIG" ;
const overview = [ ] ;
const sections = [ ] ;
let current = null , link = "" ;
source . slice ( 1 ) . forEach ( raw => {
const line = raw . trim ( ) ;
if ( ! line ) return ;
if ( /^https?:\/\// . test ( line ) ) { link = line ; return ; }
if ( /^(ZUTATEN|HAUPTTEIG|ZEITPLAN|VORTEIG\b)/ . test ( line ) ) {
current = { title : line , items : [ ] } ;
sections . push ( current ) ;
return ;
}
if ( current ) current . items . push ( line ) ;
else overview . push ( ... line . split ( " · " ) . map ( part => part . trim ( ) ) . filter ( Boolean ) ) ;
} ) ;
if ( overview . length > 4 ) sections . unshift ( { title : "WEITERE DATEN" , items : overview . slice ( 4 ) } ) ;
return { title , overview : overview . slice ( 0 , 4 ) , sections , link } ;
}
function buildPdfBytes ( lines ) {
const encoder = new TextEncoder ( ) ;
const data = parsePdfRecipe ( lines ) ;
const ingredients = data . sections . filter ( section => section . title !== "ZEITPLAN" ) ;
const timeline = data . sections . find ( section => section . title === "ZEITPLAN" ) || { title : "ZEITPLAN" , items : [ ] } ;
const capItems = ( items , max ) => items . length > max ? [ ... items . slice ( 0 , max - 1 ) , "... weitere Angaben gekürzt" ] : items ;
let stream = "" ;
const rect = ( x , y , w , h , fill , stroke = "" , lineWidth = . 7 ) => {
if ( fill ) stream += ` ${ fill } rg ${ x } ${ y } ${ w } ${ h } re f \n ` ;
if ( stroke ) stream += ` ${ stroke } RG ${ lineWidth } w ${ x } ${ y } ${ w } ${ h } re S \n ` ;
} ;
const line = ( x1 , y1 , x2 , y2 , color , width = . 7 ) => { stream += ` ${ color } RG ${ width } w ${ x1 } ${ y1 } m ${ x2 } ${ y2 } l S \n ` ; } ;
const text = ( value , x , y , size , font = "F1" , color = "0.886 0.925 0.961" ) => {
stream += ` BT \n / ${ font } ${ size } Tf \n ${ color } rg \n ${ x } ${ y } Td \n < ${ winAnsiHex ( value ) } > Tj \n ET \n ` ;
} ;
const wrapped = ( value , width ) => wrapPdfLines ( [ String ( value ) ] , width ) ;
// Vollflächige Cyberpunk-Bühne mit Raster, Scanlines und Neonrahmen.
rect ( 0 , 0 , 595 , 842 , "0.027 0.031 0.043" ) ;
rect ( 0 , 704 , 595 , 138 , "0.039 0.051 0.078" ) ;
stream += "0.047 0.071 0.102 RG 0.25 w\n" ;
for ( let x = 18 ; x < 595 ; x += 24 ) stream += ` ${ x } 0 m ${ x } 842 l S \n ` ;
for ( let y = 8 ; y < 842 ; y += 24 ) stream += ` 0 ${ y } m 595 ${ y } l S \n ` ;
stream += "0.075 0.094 0.133 RG 0.18 w\n" ;
for ( let y = 3 ; y < 842 ; y += 6 ) stream += ` 0 ${ y } m 595 ${ y } l S \n ` ;
rect ( 18 , 18 , 559 , 806 , "" , "0.12 0.23 0.30" , . 6 ) ;
line ( 18 , 823 , 577 , 823 , "0 0.941 1" , 2.2 ) ;
line ( 18 , 816 , 170 , 816 , "1 0.176 0.471" , 4.5 ) ;
line ( 424 , 26 , 577 , 26 , "1 0.176 0.471" , 3 ) ;
// Kopf: doppelte Neon-Typografie, Statusblock und technische Marker.
text ( "TEIG//TERMINAL" , 35 , 778 , 29 , "F2" , "0.23 0.02 0.19" ) ;
text ( "TEIG//TERMINAL" , 33 , 780 , 29 , "F2" , "0 0.941 1" ) ;
text ( "PIZZA FORMULATION SYSTEM" , 35 , 765 , 7.5 , "F1" , "1 0.176 0.471" ) ;
rect ( 414 , 770 , 145 , 28 , "0.055 0.071 0.106" , "1 0.176 0.471" , . 8 ) ;
rect ( 425 , 781 , 5 , 5 , "0.545 1 0.231" ) ;
text ( "RECIPE // READY" , 438 , 780 , 8.2 , "F2" , "0.922 0.965 1" ) ;
text ( "06" , 507 , 716 , 42 , "F2" , "0.10 0.19 0.24" ) ;
text ( data . title , 34 , 724 , 16 , "F2" , "0.922 0.965 1" ) ;
text ( "FORMULA EXPORT // DIN A4 // ONE PAGE" , 35 , 709 , 6.8 , "F1" , "0.53 0.64 0.74" ) ;
// Vier Dashboard-Kacheln nutzen die gesamte Breite für die Kerndaten.
const cards = [ 34 , 167 , 300 , 433 ] ;
cards . forEach ( ( x , index ) => {
rect ( x , 634 , 126 , 60 , index % 2 ? "0.055 0.071 0.106" : "0.063 0.082 0.122" , index === 3 ? "1 0.176 0.471" : "0 0.941 1" , . 65 ) ;
text ( ` DATA // 0 ${ index + 1 } ` , x + 9 , 678 , 6.3 , "F2" , index === 3 ? "1 0.176 0.471" : "0 0.941 1" ) ;
const value = data . overview [ index ] || "—" ;
const parts = wrapped ( value , 23 ) . slice ( 0 , 2 ) ;
parts . forEach ( ( part , row ) => text ( part , x + 9 , 659 - row * 11 , 8.2 , "F2" , "0.922 0.965 1" ) ) ;
rect ( x + 113 , 637 , 10 , 3 , index === 3 ? "1 0.176 0.471" : "0 0.941 1" ) ;
} ) ;
// Zwei große Module: Zutaten/Formel links, Prozess-Timeline rechts.
rect ( 34 , 112 , 238 , 500 , "0.043 0.059 0.086" , "0 0.941 1" , . 75 ) ;
rect ( 286 , 112 , 273 , 500 , "0.043 0.059 0.086" , "1 0.176 0.471" , . 75 ) ;
rect ( 34 , 568 , 238 , 44 , "0.00 0.22 0.26" ) ;
rect ( 286 , 568 , 273 , 44 , "0.25 0.035 0.15" ) ;
text ( "01" , 47 , 580 , 17 , "F2" , "0 0.941 1" ) ;
text ( "FORMULA // ZUTATEN" , 79 , 584 , 10.5 , "F2" , "0.922 0.965 1" ) ;
text ( "02" , 299 , 580 , 17 , "F2" , "1 0.176 0.471" ) ;
text ( "PROCESS // ZEITPLAN" , 332 , 584 , 10.5 , "F2" , "0.922 0.965 1" ) ;
let leftY = 545 , itemNo = 1 ;
const ingredientRows = [ ] ;
ingredients . forEach ( section => {
ingredientRows . push ( { heading : section . title } ) ;
capItems ( section . items , 12 ) . forEach ( item => ingredientRows . push ( { item } ) ) ;
} ) ;
if ( ! ingredientRows . length ) ingredientRows . push ( { heading : "ZUTATEN" } , { item : "Keine Angaben" } ) ;
const ingredientCount = Math . max ( 1 , ingredientRows . filter ( row => row . item ) . length ) ;
const headingCount = ingredientRows . filter ( row => row . heading ) . length ;
const leftStep = Math . max ( 28 , Math . min ( 62 , ( 390 - headingCount * 24 ) / ingredientCount ) ) ;
ingredientRows . slice ( 0 , 15 ) . forEach ( ( row , index ) => {
if ( row . heading ) {
text ( row . heading , 48 , leftY , 8.8 , "F2" , "1 0.176 0.471" ) ;
line ( 48 , leftY - 7 , 255 , leftY - 7 , "0.18 0.30 0.36" , . 45 ) ;
leftY -= 24 ;
return ;
}
const rowH = Math . max ( 25 , leftStep - 4 ) ;
rect ( 46 , leftY - rowH + 7 , 214 , rowH , index % 2 ? "0.055 0.071 0.106" : "0.063 0.082 0.122" ) ;
rect ( 46 , leftY - rowH + 7 , 4 , rowH , itemNo % 2 ? "0 0.941 1" : "1 0.176 0.471" ) ;
text ( String ( itemNo ) . padStart ( 2 , "0" ) , 57 , leftY - 8 , 8 , "F2" , "0.35 0.52 0.61" ) ;
const amount = row . item . match ( /^(.*\S)\s+([0-9][0-9.,]*\s*(?:g|%|ml))$/i ) ;
if ( amount ) {
const labelLines = wrapped ( amount [ 1 ] , 17 ) . slice ( 0 , 2 ) ;
labelLines . forEach ( ( part , i ) => text ( part , 82 , leftY - 7 - i * 12 , 10.2 , "F1" , "0.922 0.965 1" ) ) ;
const amountX = Math . max ( 184 , 248 - amount [ 2 ] . length * 7.2 ) ;
text ( amount [ 2 ] , amountX , leftY - 8 , 12 , "F2" , itemNo % 2 ? "0 0.941 1" : "1 0.176 0.471" ) ;
} else {
const itemLines = wrapped ( row . item , 28 ) . slice ( 0 , 2 ) ;
itemLines . forEach ( ( part , i ) => text ( part , 78 , leftY - 6 - i * 12 , 9.8 , "F1" , "0.922 0.965 1" ) ) ;
}
leftY -= leftStep ; itemNo ++ ;
} ) ;
text ( "BAKER'S PERCENTAGE // MASS CONTROL" , 48 , 130 , 6.5 , "F1" , "0.36 0.49 0.58" ) ;
for ( let x = 48 ; x < 255 ; x += 9 ) rect ( x , 121 , ( x % 27 === 0 ) ? 4 : 2 , 5 , "0 0.941 1" ) ;
const steps = capItems ( timeline . items , 10 ) ;
const timelineTop = 546 , timelineBottom = 150 ;
const slot = ( timelineTop - timelineBottom ) / Math . max ( 1 , steps . length ) ;
line ( 309 , timelineBottom , 309 , timelineTop , "0 0.941 1" , 1.1 ) ;
steps . forEach ( ( raw , index ) => {
const y = timelineTop - index * slot ;
const parts = raw . split ( /\s+—\s+/ ) ;
const when = parts . length > 1 ? parts . shift ( ) : String ( index + 1 ) . padStart ( 2 , "0" ) ;
const action = parts . length ? parts . join ( " — " ) : raw ;
rect ( 304 , y - 2 , 10 , 10 , "0.043 0.059 0.086" , "0 0.941 1" , 1 ) ;
rect ( 307 , y + 1 , 4 , 4 , index === steps . length - 1 ? "1 0.176 0.471" : "0 0.941 1" ) ;
text ( when , 324 , y + 1 , 10.5 , "F2" , index === steps . length - 1 ? "1 0.176 0.471" : "0 0.941 1" ) ;
const actionLines = wrapped ( action , 31 ) . slice ( 0 , 2 ) ;
actionLines . forEach ( ( part , row ) => text ( part , 324 , y - 13 - row * 12 , 9.3 , "F1" , "0.922 0.965 1" ) ) ;
if ( index < steps . length - 1 ) line ( 324 , y - slot + 9 , 545 , y - slot + 9 , "0.12 0.19 0.24" , . 35 ) ;
} ) ;
if ( ! steps . length ) text ( "Kein Zeitplan vorhanden" , 324 , 530 , 8.5 , "F1" , "0.53 0.64 0.74" ) ;
text ( "FERMENTATION SEQUENCE // REVERSE SCHEDULE" , 304 , 130 , 6.5 , "F1" , "0.45 0.39 0.57" ) ;
// Link-Zeile und Footer schließen die Seite wie ein System-Dashboard ab.
rect ( 34 , 61 , 525 , 35 , "0.039 0.051 0.078" , "0.17 0.31 0.37" , . 5 ) ;
text ( "RECIPE LINK" , 46 , 82 , 6.3 , "F2" , "1 0.176 0.471" ) ;
const linkLines = wrapped ( data . link || "Lokales Rezept ohne Link" , 102 ) . slice ( 0 , 2 ) ;
linkLines . forEach ( ( part , index ) => text ( part , 111 , 80 - index * 8 , 5.7 , "F1" , "0 0.941 1" ) ) ;
text ( "SYS.ONLINE" , 34 , 38 , 6.5 , "F2" , "0.545 1 0.231" ) ;
text ( "NO CLOUD // NO TRACKING // GENERATED LOCALLY" , 112 , 38 , 6.5 , "F1" , "0.53 0.64 0.74" ) ;
text ( "A4 // 1 OF 1" , 484 , 38 , 6.5 , "F2" , "1 0.176 0.471" ) ;
const objects = [ ] ;
objects [ 1 ] = "<< /Type /Catalog /Pages 2 0 R >>" ;
objects [ 2 ] = "<< /Type /Pages /Kids [6 0 R] /Count 1 >>" ;
objects [ 3 ] = "<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>" ;
objects [ 4 ] = "<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>" ;
objects [ 5 ] = ` << /Length ${ encoder . encode ( stream ) . length } >> \n stream \n ${ stream } endstream ` ;
objects [ 6 ] = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>" ;
const chunks = [ encoder . encode ( "%PDF-1.4\n" ) ] , offsets = [ 0 ] ;
let length = chunks [ 0 ] . length ;
for ( let id = 1 ; id < objects . length ; id ++ ) {
offsets [ id ] = length ;
const chunk = encoder . encode ( ` ${ id } 0 obj \n ${ objects [ id ] } \n endobj \n ` ) ;
chunks . push ( chunk ) ; length += chunk . length ;
}
const xrefAt = length ;
let tail = ` xref \n 0 ${ objects . length } \n 0000000000 65535 f \n ` ;
for ( let id = 1 ; id < objects . length ; id ++ ) tail += ` ${ String ( offsets [ id ] ) . padStart ( 10 , "0" ) } 00000 n \n ` ;
tail += ` trailer \n << /Size ${ objects . length } /Root 1 0 R >> \n startxref \n ${ xrefAt } \n %%EOF \n ` ;
chunks . push ( encoder . encode ( tail ) ) ; length += chunks [ chunks . length - 1 ] . length ;
const pdf = new Uint8Array ( length ) ;
let at = 0 ;
chunks . forEach ( chunk => { pdf . set ( chunk , at ) ; at += chunk . length ; } ) ;
return pdf ;
}
function downloadPdf ( ) {
const r = lastR , st = STYLES [ S . style ] , btn = $ ( "downloadPdf" ) ;
const blob = new Blob ( [ buildPdfBytes ( recipeText ( ) . split ( "\n" ) ) ] , { type : "application/pdf" } ) ;
const url = URL . createObjectURL ( blob ) , link = document . createElement ( "a" ) ;
link . href = url ;
link . download = pdfFilename ( st . n , S . balls , r . ballW , PLANS [ S . plan ] . n ) ;
document . body . appendChild ( link ) ;
link . click ( ) ;
link . remove ( ) ;
setTimeout ( ( ) => URL . revokeObjectURL ( url ) , 1000 ) ;
const old = btn . textContent ;
btn . textContent = "PDF geladen ✓" ;
setTimeout ( ( ) => btn . textContent = old , 1800 ) ;
}
/* PDF = Druckdialog mit sprechendem Dateinamen. Browser und Handys
/* PDF = Druckdialog mit sprechendem Dateinamen. Browser und Handys
bieten dort „Als PDF sichern" an — ganz ohne externe Bibliothek. */
bieten dort „Als PDF sichern" an — ganz ohne externe Bibliothek. */
const ORIG _TITLE = document . title ;
const ORIG _TITLE = document . title ;
@@ -1071,7 +1484,7 @@ function renderPrint(r){
` Erstellt am ${ d . getDate ( ) } . ${ d . getMonth ( ) + 1 } . ${ d . getFullYear ( ) } mit TEIG//TERMINAL · Rezept erneut öffnen: ${ location . href } ` ;
` Erstellt am ${ d . getDate ( ) } . ${ d . getMonth ( ) + 1 } . ${ d . getFullYear ( ) } mit TEIG//TERMINAL · Rezept erneut öffnen: ${ location . href } ` ;
}
}
function copyText ( ) {
function recipeText ( ) {
const r = lastR , tl = lastTl , st = STYLES [ S . style ] , f = FLOURS . find ( x => x . id === S . flour ) ;
const r = lastR , tl = lastTl , st = STYLES [ S . style ] , f = FLOURS . find ( x => x . id === S . flour ) ;
const L = [ ] ;
const L = [ ] ;
L . push ( ` PIZZATEIG — ${ st . n } ` ) ;
L . push ( ` PIZZATEIG — ${ st . n } ` ) ;
@@ -1096,12 +1509,24 @@ function copyText(){
tl . steps . forEach ( s => L . push ( ` ${ fmtT ( s . at ) } ${ fmtDay ( s . at ) } — ${ s . a } ${ s . d ? ` ( ${ dur ( s . d ) } ) ` : "" } ` ) ) ;
tl . steps . forEach ( s => L . push ( ` ${ fmtT ( s . at ) } ${ fmtDay ( s . at ) } — ${ s . a } ${ s . d ? ` ( ${ dur ( s . d ) } ) ` : "" } ` ) ) ;
L . push ( "" ) ;
L . push ( "" ) ;
L . push ( location . href ) ;
L . push ( location . href ) ;
toClipboard ( L . join ( "\n" ) , $ ( "copyTxt" ) , "Text kopiert ✓" ) ;
return L . join ( "\n" ) ;
}
function copyText ( ) {
toClipboard ( recipeText ( ) , $ ( "copyTxt" ) , "Text kopiert ✓" ) ;
}
}
window . addEventListener ( "hashchange" , ( ) => { if ( ! hashLock ) { readHash ( ) ; render ( ) ; } } ) ;
window . addEventListener ( "hashchange" , ( ) => { if ( ! hashLock ) { readHash ( ) ; render ( ) ; } } ) ;
setInterval ( ( ) => { if ( lastR ) renderTimeline ( lastR ) ; } , 30000 ) ;
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 ---------- */
/* ---------- Start ---------- */
readHash ( ) ;
readHash ( ) ;
initUI ( ) ;
initUI ( ) ;