{"id":70,"date":"2026-07-18T04:30:22","date_gmt":"2026-07-18T03:30:22","guid":{"rendered":"https:\/\/chrisbrans.com\/?page_id=70"},"modified":"2026-07-18T04:30:57","modified_gmt":"2026-07-18T03:30:57","slug":"bread-timer","status":"publish","type":"page","link":"https:\/\/chrisbrans.com\/index.php\/bread-timer\/","title":{"rendered":"Bread Timer"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html>\n&lt;html lang=\"en\">\n&lt;head>\n    &lt;meta charset=\"UTF-8\">\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    &lt;title>The Perfect Loaf Timeline Calculator&lt;\/title>\n    &lt;style>\n        body { \n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; \n            padding: 20px; \n            max-width: 650px; \n            margin: 0 auto; \n            background: #fdfbf7; \n            color: #333; \n        }\n        h1, h2 { color: #8b5a2b; text-align: center; }\n        .card { \n            background: white; \n            padding: 20px; \n            border-radius: 12px; \n            box-shadow: 0 4px 6px rgba(0,0,0,0.05); \n            margin-bottom: 20px; \n            border: 1px solid #eee;\n        }\n        label { display: block; margin-top: 15px; font-weight: bold; color: #555; }\n        select, input { \n            width: 100%; \n            padding: 12px; \n            margin-top: 5px; \n            border: 1px solid #ccc; \n            border-radius: 6px; \n            box-sizing: border-box; \n            font-size: 16px; \n        }\n        .step-row { \n            display: flex; \n            justify-content: space-between; \n            align-items: center; \n            padding: 14px 0; \n            border-bottom: 1px solid #f0f0f0; \n            flex-wrap: wrap;\n            gap: 10px;\n        }\n        .step-row.overlap {\n            background-color: #f7f4eb;\n            margin: 4px 0;\n            padding: 14px 8px;\n            border-radius: 6px;\n            border-left: 4px solid #d4b28c;\n        }\n        .step-info { \n            flex: 1; \n            min-width: 180px; \n        }\n        .step-time { \n            flex: 1; \n            min-width: 250px; \n            text-align: right; \n        }\n        .step-time input { \n            width: 100%; \n            max-width: 260px; \n            font-size: 15px; \n            padding: 8px;\n            text-align: center; \n            background-color: #fff;\n        }\n        button { \n            background: #8b5a2b; \n            color: white; \n            border: none; \n            padding: 14px; \n            width: 100%; \n            border-radius: 6px; \n            font-size: 16px; \n            font-weight: bold; \n            cursor: pointer; \n            margin-top: 20px; \n        }\n        button:hover { background: #6f441f; }\n        \n        @media (max-width: 480px) {\n            .step-time { text-align: left; }\n            .step-time input { max-width: 100%; }\n        }\n    &lt;\/style>\n&lt;\/head>\n&lt;body>\n\n    &lt;h1>Baking Timeline Calculator&lt;\/h1>\n    &lt;h3 style=\"text-align:center; color:#666; font-weight:normal; margin-top:-10px;\">Inspired by &lt;em>The Perfect Loaf&lt;\/em>&lt;\/h3>\n\n    &lt;div class=\"card\">\n        &lt;label for=\"recipeSelect\">Select Recipe:&lt;\/label>\n        &lt;select id=\"recipeSelect\">\n            &lt;option value=\"beginners\">The Beginner's Sourdough&lt;\/option>\n            &lt;option value=\"country\">Pain de Campagne&lt;\/option>\n        &lt;\/select>\n\n        &lt;label for=\"calcMode\">Scheduling Mode:&lt;\/label>\n        &lt;select id=\"calcMode\" onchange=\"toggleModeInputs()\">\n            &lt;option value=\"forward\">Forward (Plan from Levain Start)&lt;\/option>\n            &lt;option value=\"backward\">Backward (Plan from Target End Time)&lt;\/option>\n        &lt;\/select>\n\n        &lt;div id=\"forwardInputGroup\">\n            &lt;label for=\"startTime\">Start Date &amp; Time (Mix Levain):&lt;\/label>\n            &lt;input type=\"datetime-local\" id=\"startTime\">\n        &lt;\/div>\n\n        &lt;div id=\"backwardInputGroup\" style=\"display:none;\">\n            &lt;label for=\"endTime\">Target End Date &amp; Time (Out of Oven):&lt;\/label>\n            &lt;input type=\"datetime-local\" id=\"endTime\">\n        &lt;\/div>\n\n        &lt;button onclick=\"calculateInitialTimeline()\">Generate Timeline&lt;\/button>\n    &lt;\/div>\n\n    &lt;div class=\"card\" id=\"timelineCard\" style=\"display:none;\">\n        &lt;h2>Your Custom Schedule&lt;\/h2>\n        &lt;p style=\"font-size: 0.85em; color: #666; text-align: center; margin-bottom: 15px;\">\n            \ud83d\udca1 &lt;strong>Parallel Steps:&lt;\/strong> Levain and Autolyse run together. Adjusting &lt;em>Begin Mix &amp; Knead&lt;\/em> shifts all future steps if your levain is slow. Adjusting &lt;em>Divide &amp; Pre-shape&lt;\/em> updates your schedule if bulk fermentation speeds up or slows down.\n        &lt;\/p>\n        &lt;div id=\"timelineSteps\">&lt;\/div>\n    &lt;\/div>\n\n    &lt;script>\n        \/\/ Durations define how long THAT specific step lasts before the next action occurs.\n        const recipes = {\n            beginners: &#91;\n                { name: \"Start Levain Build\", duration: 300, desc: \"Mix starter, flour, and water\", isOverlap: true }, \/\/ 5h\n                { name: \"Start Autolyse\", duration: 60, desc: \"Mix flour and water; rest until levain ready\", isOverlap: true },    \/\/ 1h\n                { name: \"Begin Mix &amp; Knead\", duration: 30, desc: \"Combine levain, autolyse, salt, and water\" },\n                { name: \"Begin Bulk Fermentation\", duration: 240, desc: \"Perform stretch &amp; folds throughout\" },\n                { name: \"Divide, Pre-shape &amp; Rest\", duration: 50, desc: \"Divide dough and let rest on bench\" },\n                { name: \"Final Shape &amp; Cold Proof\", duration: 960, desc: \"Shape into bannetons and retard in fridge\" }, \n                { name: \"Bake\", duration: 50, desc: \"Bake in preheated combo cooker \/ Dutch oven\" },\n                { name: \"Out of Oven \ud83c\udf89\", duration: 0, desc: \"Cool on a wire rack before slicing!\" }\n            ],\n            country: &#91;\n                { name: \"Start Levain Build\", duration: 300, desc: \"Mix build parameters\", isOverlap: true },\n                { name: \"Start Autolyse\", duration: 45, desc: \"Mix flour and water ahead of mix\", isOverlap: true },\n                { name: \"Begin Mix &amp; Knead\", duration: 20, desc: \"Incorporate levain and salt\" },\n                { name: \"Begin Bulk Fermentation\", duration: 270, desc: \"Ferment with ambient structural folds\" },\n                { name: \"Divide &amp; Shape\", duration: 30, desc: \"Divide and shape cleanly\" },\n                { name: \"Cold Proof\", duration: 840, desc: \"Place in cold storage overnight\" },\n                { name: \"Bake\", duration: 50, desc: \"Bake until deep mahogany crust forms\" },\n                { name: \"Out of Oven \ud83c\udf89\", duration: 0, desc: \"Loaf complete!\" }\n            ]\n        };\n\n        window.onload = function() {\n            const now = new Date();\n            now.setMinutes(now.getMinutes() - now.getTimezoneOffset());\n            document.getElementById('startTime').value = now.toISOString().slice(0, 16);\n            \n            const tomorrow = new Date();\n            tomorrow.setDate(tomorrow.getDate() + 1);\n            tomorrow.setMinutes(tomorrow.getMinutes() - tomorrow.getTimezoneOffset());\n            document.getElementById('endTime').value = tomorrow.toISOString().slice(0, 16);\n        };\n\n        function toggleModeInputs() {\n            const mode = document.getElementById('calcMode').value;\n            document.getElementById('forwardInputGroup').style.display = mode === 'forward' ? 'block' : 'none';\n            document.getElementById('backwardInputGroup').style.display = mode === 'backward' ? 'block' : 'none';\n        }\n\n        let currentTimelineData = &#91;];\n\n        function calculateInitialTimeline() {\n            const recipeKey = document.getElementById('recipeSelect').value;\n            const steps = recipes&#91;recipeKey];\n            const mode = document.getElementById('calcMode').value;\n            \n            currentTimelineData = &#91;];\n            steps.forEach(s => currentTimelineData.push({ name: s.name, desc: s.desc, duration: s.duration, isOverlap: s.isOverlap, time: null }));\n\n            if (mode === 'forward') {\n                const startVal = document.getElementById('startTime').value;\n                if (!startVal) return alert(\"Please pick a start time.\");\n                \n                let levainStart = new Date(startVal);\n                let levainDuration = steps&#91;0].duration;\n                let autolyseDuration = steps&#91;1].duration;\n                \n                \/\/ The magic meeting point where both parallel tracks finish\n                let mixStart = new Date(levainStart.getTime() + levainDuration * 60000);\n                let autolyseStart = new Date(mixStart.getTime() - autolyseDuration * 60000);\n                \n                currentTimelineData&#91;0].time = levainStart;\n                currentTimelineData&#91;1].time = autolyseStart;\n                currentTimelineData&#91;2].time = mixStart;\n                \n                let runningTime = new Date(mixStart);\n                for (let i = 3; i &lt; steps.length; i++) {\n                    runningTime = new Date(runningTime.getTime() + steps&#91;i-1].duration * 60000);\n                    currentTimelineData&#91;i].time = new Date(runningTime);\n                }\n            } else {\n                const endVal = document.getElementById('endTime').value;\n                if (!endVal) return alert(\"Please pick a target end time.\");\n                \n                let runningTime = new Date(endVal);\n                currentTimelineData&#91;steps.length - 1].time = new Date(runningTime);\n                \n                \/\/ Work backward from out-of-oven to mix step\n                for (let i = steps.length - 2; i >= 2; i--) {\n                    runningTime = new Date(runningTime.getTime() - steps&#91;i].duration * 60000);\n                    currentTimelineData&#91;i].time = new Date(runningTime);\n                }\n                \n                \/\/ Calculate parallel track starting points based on target mix time\n                let mixStart = currentTimelineData&#91;2].time;\n                currentTimelineData&#91;1].time = new Date(mixStart.getTime() - steps&#91;1].duration * 60000);\n                currentTimelineData&#91;0].time = new Date(mixStart.getTime() - steps&#91;0].duration * 60000);\n            }\n\n            renderTimeline();\n        }\n\n        function renderTimeline() {\n            const container = document.getElementById('timelineSteps');\n            container.innerHTML = '';\n            \n            currentTimelineData.forEach((step, index) => {\n                const row = document.createElement('div');\n                row.className = 'step-row' + (step.isOverlap ? ' overlap' : '');\n                \n                const info = document.createElement('div');\n                info.className = 'step-info';\n                \n                let durationLabel = step.duration > 0 ? `Duration: ${formatDuration(step.duration)}` : 'Milestone';\n                info.innerHTML = `&lt;strong>${step.name}&lt;\/strong>&lt;br>&lt;small style=\"color:#666;\">${step.desc}&lt;\/small>&lt;br>&lt;small style=\"color:#999; font-style:italic;\">${durationLabel}&lt;\/small>`;\n                \n                const timeDiv = document.createElement('div');\n                timeDiv.className = 'step-time';\n                \n                const input = document.createElement('input');\n                input.type = 'datetime-local';\n                \n                const localTime = new Date(step.time.getTime() - step.time.getTimezoneOffset() * 60000);\n                input.value = localTime.toISOString().slice(0, 16);\n                \n                input.onchange = function() {\n                    recalculateFromStep(index, new Date(this.value));\n                };\n                \n                timeDiv.appendChild(input);\n                row.appendChild(info);\n                row.appendChild(timeDiv);\n                container.appendChild(row);\n            });\n            \n            document.getElementById('timelineCard').style.display = 'block';\n        }\n\n        function recalculateFromStep(changedIndex, newTime) {\n            currentTimelineData&#91;changedIndex].time = new Date(newTime);\n            const recipeKey = document.getElementById('recipeSelect').value;\n            const steps = recipes&#91;recipeKey];\n\n            if (changedIndex === 0) {\n                \/\/ Changing Levain Start recalculates everything forward\n                let levainStart = currentTimelineData&#91;0].time;\n                let mixStart = new Date(levainStart.getTime() + steps&#91;0].duration * 60000);\n                currentTimelineData&#91;1].time = new Date(mixStart.getTime() - steps&#91;1].duration * 60000);\n                currentTimelineData&#91;2].time = mixStart;\n                \n                let runningTime = new Date(mixStart);\n                for (let i = 3; i &lt; steps.length; i++) {\n                    runningTime = new Date(runningTime.getTime() + steps&#91;i-1].duration * 60000);\n                    currentTimelineData&#91;i].time = new Date(runningTime);\n                }\n            } \n            else if (changedIndex === 1) {\n                \/\/ If you shift Autolyse Start, it forces a shift in the target Mix Time\n                let autolyseStart = currentTimelineData&#91;1].time;\n                let mixStart = new Date(autolyseStart.getTime() + steps&#91;1].duration * 60000);\n                currentTimelineData&#91;2].time = mixStart;\n                \n                let runningTime = new Date(mixStart);\n                for (let i = 3; i &lt; steps.length; i++) {\n                    runningTime = new Date(runningTime.getTime() + steps&#91;i-1].duration * 60000);\n                    currentTimelineData&#91;i].time = new Date(runningTime);\n                }\n            } \n            else if (changedIndex === 2) {\n                \/\/ Crucial: Changing Mix Time (e.g. Levain took longer) shifts everything downstream\n                let mixStart = currentTimelineData&#91;2].time;\n                currentTimelineData&#91;1].time = new Date(mixStart.getTime() - steps&#91;1].duration * 60000);\n                \n                let runningTime = new Date(mixStart);\n                for (let i = 3; i &lt; steps.length; i++) {\n                    runningTime = new Date(runningTime.getTime() + steps&#91;i-1].duration * 60000);\n                    currentTimelineData&#91;i].time = new Date(runningTime);\n                }\n            } \n            else {\n                \/\/ Changing any step downstream (e.g., Bulk Fermentation ending late) ripples forward\n                let runningTime = new Date(newTime);\n                for (let i = changedIndex + 1; i &lt; steps.length; i++) {\n                    runningTime = new Date(runningTime.getTime() + steps&#91;i-1].duration * 60000);\n                    currentTimelineData&#91;i].time = new Date(runningTime);\n                }\n            }\n\n            renderTimeline();\n        }\n\n        function formatDuration(mins) {\n            if (mins &lt; 60) return `${mins} mins`;\n            const hours = Math.floor(mins \/ 60);\n            const remainingMins = mins % 60;\n            return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;\n        }\n    &lt;\/script>\n&lt;\/body>\n&lt;\/html><\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"footnotes":""},"class_list":["post-70","page","type-page","status-publish"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/pages\/70","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/comments?post=70"}],"version-history":[{"count":1,"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/pages\/70\/revisions"}],"predecessor-version":[{"id":71,"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/pages\/70\/revisions\/71"}],"wp:attachment":[{"href":"https:\/\/chrisbrans.com\/index.php\/wp-json\/wp\/v2\/media?parent=70"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}