Test and deploy / test-and-deploy (push) Successful in 1m25s
Highest severity: injected "?" buttons were hijacking the implicit <label> association on 14 fields, so clicking the label text opened the help dialog instead of focusing the input and screen readers lost the input's accessible name. ensureLabelAssociation() now gives each affected input an explicit id/for pairing before the button lands. Also: milestones-ror/actuators topic placement is now text-matched with a warning fallback instead of blind array indexing; every topic warns if it resolves zero containers; dialog temperatures (milestone table, RoR budget, worked example, both charge-setting tables) now respect the °C/°F toggle instead of being stuck in °C; the print worksheet's duplicate curve chart was missing the data-tempc attributes its live counterpart had, so only its title/caption (not its axis numbers) converted; the "?" trigger's touch target grew from 15px to a WCAG-conformant ~25px without changing the visual dot; two genuine coverage gaps closed (a roast-log checklist topic, a curve-reading topic); a worked feasibility example was restored; two claims that didn't trace back to the source worksheet were corrected (an invented "±15 seconds" sweet-spot tolerance, a wrong moisture-to-time mapping); three cultivar cells regained detail lost in the original transcription; a data-transcription footnote no longer leaks into an unrelated topic; checklist items keep their list semantics under VoiceOver. Co-Authored-By: Fable <[email protected]> Co-Authored-By: Opus <[email protected]>
2516 lines
105 KiB
HTML
2516 lines
105 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Roast Planner</title>
|
||
<meta name="theme-color" content="#A8481A" />
|
||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||
<link rel="manifest" href="/manifest.webmanifest?v=__ASSET_VERSION__" />
|
||
<link rel="icon" href="/icon.svg?v=__ASSET_VERSION__" type="image/svg+xml" />
|
||
<link rel="apple-touch-icon" href="/icon.svg?v=__ASSET_VERSION__" />
|
||
<link rel="stylesheet" href="/worksheet.css" media="print" />
|
||
<link rel="stylesheet" href="/app.css?v=__ASSET_VERSION__" />
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<nav class="side-nav" id="side-nav" aria-label="Main navigation">
|
||
<div class="side-nav-brand">
|
||
<span class="brand-mark" aria-hidden="true">◐</span>
|
||
<span class="brand-name">Roast Planner</span>
|
||
</div>
|
||
<div class="nav-group">
|
||
<a class="nav-item" href="/app" aria-current="page"
|
||
><span class="nav-icon" aria-hidden="true">◐</span
|
||
><span class="nav-label">Planner</span></a
|
||
>
|
||
<button class="nav-item" type="button" id="nav-plans">
|
||
<span class="nav-icon" aria-hidden="true">▤</span
|
||
><span class="nav-label">Plans</span>
|
||
</button>
|
||
<a class="nav-item" href="/inventory"
|
||
><span class="nav-icon" aria-hidden="true">▥</span
|
||
><span class="nav-label">Inventory</span></a
|
||
>
|
||
<a class="nav-item" href="/cupping"
|
||
><span class="nav-icon" aria-hidden="true">◒</span
|
||
><span class="nav-label">Cupping</span></a
|
||
>
|
||
<a class="nav-item" href="/account"
|
||
><span class="nav-icon" aria-hidden="true">◔</span
|
||
><span class="nav-label">Account</span></a
|
||
>
|
||
<a class="nav-item hidden" id="nav-admin" href="/admin"
|
||
><span class="nav-icon" aria-hidden="true">⚙</span
|
||
><span class="nav-label">Admin</span></a
|
||
>
|
||
</div>
|
||
<div class="nav-group">
|
||
<div class="nav-group-title">Tools</div>
|
||
<button class="nav-item" type="button" id="nav-prefill">
|
||
<span class="nav-icon" aria-hidden="true">↗</span
|
||
><span class="nav-label">Prefill from URL</span>
|
||
</button>
|
||
<button class="nav-item" type="button" id="nav-alog">
|
||
<span class="nav-icon" aria-hidden="true">∿</span
|
||
><span class="nav-label">Reference curve</span>
|
||
</button>
|
||
<button class="nav-item" type="button" id="nav-import-export">
|
||
<span class="nav-icon" aria-hidden="true">⇅</span
|
||
><span class="nav-label">Import / export</span>
|
||
</button>
|
||
<button
|
||
class="nav-item"
|
||
type="button"
|
||
id="btn-toggle-fids"
|
||
aria-pressed="false"
|
||
title="Show the worksheet reference codes (e.g. 1.4) on every field"
|
||
>
|
||
<span class="nav-icon" aria-hidden="true">#</span
|
||
><span class="nav-label">Field IDs</span>
|
||
</button>
|
||
<button class="nav-item" type="button" id="nav-settings">
|
||
<span class="nav-icon" aria-hidden="true">◎</span
|
||
><span class="nav-label">Settings & sync</span>
|
||
</button>
|
||
</div>
|
||
<div class="nav-spacer"></div>
|
||
<button
|
||
class="icon-btn nav-collapse-toggle"
|
||
type="button"
|
||
id="nav-collapse"
|
||
aria-label="Collapse navigation"
|
||
title="Collapse navigation"
|
||
>
|
||
«
|
||
</button>
|
||
<div class="nav-user">
|
||
<div class="nav-user-avatar" id="nav-user-avatar" aria-hidden="true"></div>
|
||
<div class="nav-user-detail">
|
||
<span class="nav-user-email" id="account-email"></span>
|
||
<button class="nav-user-logout" type="button" id="btn-logout">
|
||
Log out
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="app-workspace" id="app-workspace">
|
||
<header class="app-header">
|
||
<div class="header-row-top">
|
||
<button
|
||
class="icon-btn nav-hamburger"
|
||
type="button"
|
||
id="nav-hamburger"
|
||
aria-label="Open navigation"
|
||
aria-expanded="false"
|
||
>
|
||
☰
|
||
</button>
|
||
<div class="brand">
|
||
<div class="brand-text">
|
||
<h1>Roast Planner</h1>
|
||
<p class="brand-sub" id="header-coffee-name">New plan</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="section-nav" id="section-nav" aria-label="Plan sections">
|
||
<a href="#sec-coffee">Plan</a>
|
||
<a href="#sec-bean">Bean</a>
|
||
<a href="#sec-machine">Machine plan</a>
|
||
<a href="#sec-roastlog">Roast log</a>
|
||
<a href="#sec-after">After</a>
|
||
</nav>
|
||
|
||
<div class="header-actions">
|
||
<span class="autosave-chip" id="autosave-status"
|
||
><span class="dot"></span
|
||
><span class="autosave-text">Not saved yet</span></span
|
||
>
|
||
<button id="btn-print" type="button" class="primary-btn">
|
||
Print
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div
|
||
class="connection-status hidden"
|
||
id="connection-status"
|
||
role="status"
|
||
aria-live="polite"
|
||
></div>
|
||
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-prefill"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="prefill-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="prefill-title">Prefill from a bean product URL</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<label class="stack-label" for="prefill-url">Product page URL</label>
|
||
<input
|
||
type="url"
|
||
id="prefill-url"
|
||
class="text-input"
|
||
placeholder="https://roaster.example/shop/some-coffee"
|
||
/>
|
||
<label class="checkbox-row"
|
||
><input type="checkbox" id="prefill-overwrite" /> Overwrite
|
||
already-filled fields</label
|
||
>
|
||
<div class="drawer-actions">
|
||
<button id="prefill-go" type="button" class="primary-btn">
|
||
Fetch & prefill
|
||
</button>
|
||
<button id="prefill-undo" type="button" class="ghost-btn" disabled>
|
||
Undo prefill
|
||
</button>
|
||
</div>
|
||
<div id="prefill-result" class="drawer-result"></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-alog"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="alog-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="alog-title">Attach an Artisan .alog reference curve</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<label class="filebtn primary-btn"
|
||
>Choose .alog file<input type="file" id="alog-file" accept=".alog"
|
||
/></label>
|
||
<button id="alog-library-refresh" type="button" class="ghost-btn">
|
||
Show local library
|
||
</button>
|
||
<ul class="lib-list hidden" id="alog-library-list"></ul>
|
||
<div id="alog-result" class="drawer-result"></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-plans"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="plans-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="plans-title">Your plans</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<button id="btn-new-plan" type="button" class="primary-btn">
|
||
New plan
|
||
</button>
|
||
<p class="drawer-result">
|
||
Choose a plan to continue. Every edit is stored on this device first
|
||
and then synced when online.
|
||
</p>
|
||
<ul class="plan-list" id="plan-list" aria-live="polite"></ul>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-import-export"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="import-export-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="import-export-title">Import / export</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<p class="stack-label">Load a plan from a JSON file</p>
|
||
<button id="btn-load" type="button" class="primary-btn">
|
||
Choose file…
|
||
</button>
|
||
<input id="file-load" type="file" accept="application/json" />
|
||
<p class="stack-label">Save the current plan</p>
|
||
<button id="btn-save" type="button" class="ghost-btn">
|
||
Download as JSON
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-settings"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="settings-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="settings-title">Settings & sync</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<p class="drawer-result">
|
||
<strong id="settings-email"></strong> is the active account. Plans are
|
||
saved locally first, then synced to your account whenever you are
|
||
online.
|
||
</p>
|
||
<div class="field">
|
||
<span class="field-label">Temperature unit</span>
|
||
<div
|
||
class="segmented small"
|
||
id="temp-unit-toggle"
|
||
role="radiogroup"
|
||
aria-label="Temperature unit"
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="tempUnit" value="C" checked /><span
|
||
>°C</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="tempUnit" value="F" /><span
|
||
>°F</span
|
||
></label
|
||
>
|
||
</div>
|
||
<p class="field-note">
|
||
Changes how Machine Plan and Roast Log temperatures are shown.
|
||
Plans are always stored in °C, so switching back is lossless.
|
||
</p>
|
||
</div>
|
||
<p class="drawer-result">
|
||
Install Roast Planner from your browser menu for a focused workspace.
|
||
Updates wait for your confirmation so an in-progress plan is never
|
||
discarded.
|
||
</p>
|
||
<button id="btn-install" type="button" class="primary-btn hidden">
|
||
Install Roast Planner
|
||
</button>
|
||
<button id="btn-refresh" type="button" class="ghost-btn hidden">
|
||
Update ready — refresh safely
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<dialog class="help-dialog" id="field-help-dialog" aria-labelledby="help-dialog-title">
|
||
<div class="help-card">
|
||
<div class="help-head">
|
||
<div>
|
||
<span class="help-kicker">Field guide</span>
|
||
<h3 id="help-dialog-title"></h3>
|
||
</div>
|
||
<button class="icon-btn" type="button" data-close-help aria-label="Close">✕</button>
|
||
</div>
|
||
<div class="help-body" id="help-dialog-body"></div>
|
||
<p class="help-attrib">
|
||
Timing framework: Rob Hoos, <em>Cultivars: A Practical Guide for Coffee Roasters</em>.
|
||
Temperatures, bands and machine settings: your own logged roasts on the Hottop
|
||
KN-8828B-2K+ — they transfer to no other machine.
|
||
</p>
|
||
</div>
|
||
</dialog>
|
||
|
||
<form id="plan-form">
|
||
<div class="workspace-grid">
|
||
<div class="form-col">
|
||
<section class="panel-card" id="sec-coffee">
|
||
<div class="panel-head"><h2>The Coffee</h2></div>
|
||
<div class="panel-body">
|
||
<details class="why-panel" data-why="sec-coffee" open>
|
||
<summary>Why cultivar sets the clock</summary>
|
||
<p>
|
||
A roast plan is three durations laid end to end: drying +
|
||
Maillard + development = the total roast. Cultivar is the
|
||
one input that sets a time you cannot negotiate — when
|
||
first crack lands. The seed's genetics fix how much sugar
|
||
and acid it carries and how heat gets into it, so each
|
||
cultivar browns and builds its aromatics on its own
|
||
schedule. A Bourbon rushed to a 7:30 first crack tastes
|
||
thin and papery no matter how well you handle the rest; a
|
||
Gesha dragged to 9:00 goes savory and loses the florals you
|
||
paid for.
|
||
</p>
|
||
<p>
|
||
That is why the FC anchor below feeds Time Ledger line 1
|
||
and everything else in the plan is a correction of seconds
|
||
around it. Treat the cultivar on the bag as your best
|
||
hypothesis, not a fact — labels are wrong often enough that
|
||
the "After the Roast" section exists to correct them.
|
||
</p>
|
||
</details>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="0.1"
|
||
><span class="field-label">Coffee / lot</span
|
||
><input
|
||
class="field-input"
|
||
name="0.1"
|
||
placeholder="e.g. Finca El Ocaso"
|
||
/></label>
|
||
<label class="field" data-fid="0.2"
|
||
><span class="field-label">Date</span
|
||
><input class="field-input" type="date" name="0.2"
|
||
/></label>
|
||
<label class="field" data-fid="0.3"
|
||
><span class="field-label">Batch #</span
|
||
><input class="field-input" name="0.3"
|
||
/></label>
|
||
<label class="field" data-fid="0.4"
|
||
><span class="field-label">Green in</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="0.4" /><span class="unit"
|
||
>g</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field" id="lot-picker-field">
|
||
<span class="field-label">From inventory lot</span>
|
||
<select class="field-input" name="inventory.lotId" id="lot-select">
|
||
<option value="">— none —</option>
|
||
</select>
|
||
<span class="field-note" id="lot-picker-note"
|
||
>Optional — link a lot and the Roast Log can draw the
|
||
green weight down when you charge.</span
|
||
>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="subhead">Cultivar</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="1.1"
|
||
><span class="field-label">Cultivar</span
|
||
><input
|
||
class="field-input"
|
||
name="1.1"
|
||
list="cultivar-list"
|
||
placeholder="e.g. Caturra"
|
||
autocomplete="off"
|
||
/></label>
|
||
<label class="field" data-fid="1.3">
|
||
<span class="field-label">Origin</span
|
||
><input
|
||
class="field-input"
|
||
name="1.3"
|
||
placeholder="e.g. Huila, Colombia"
|
||
/>
|
||
</label>
|
||
<div class="field wide" data-fid="1.2">
|
||
<span class="field-label">Group</span>
|
||
<div class="segmented" data-radio-group="1.2">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="typica" /><span
|
||
>Typica</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="bourbon" /><span
|
||
>Bourbon</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="ethiopian" /><span
|
||
>Ethiopian</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="hybrid" /><span
|
||
>Hybrid</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="1.4"
|
||
><span class="field-label">FC anchor</span
|
||
><input class="field-input" name="1.4" placeholder="8:45" />
|
||
<span class="field-note"
|
||
>Ledger line 1 — the whole plan hangs from this. Use
|
||
your cultivar's own row (autofills from the list),
|
||
never a group average.</span
|
||
></label
|
||
>
|
||
<label class="field" data-fid="1.5"
|
||
><span class="field-label">Profile mm:ss|mm:ss|mm:ss</span
|
||
><input
|
||
class="field-input"
|
||
name="1.5"
|
||
placeholder="4:15|3:15|1:30"
|
||
/></label>
|
||
<label class="field" data-fid="1.6"
|
||
><span class="field-label">± Refine</span
|
||
><input class="field-input" name="1.6" placeholder="0" />
|
||
<span class="field-note"
|
||
>0 the first time you roast this coffee. Afterwards,
|
||
carry "One change next batch" here — this is how the
|
||
plan learns.</span
|
||
></label
|
||
>
|
||
<label class="field" data-fid="1.7"
|
||
><span class="field-label">± Dev modifier</span
|
||
><input class="field-input" name="1.7" />
|
||
<span class="field-note"
|
||
>Your cultivar profile's third number minus 1:30 —
|
||
autofilled; 0 for most cultivars. Feeds ledger line
|
||
9.</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-card" id="sec-blend">
|
||
<div class="panel-head">
|
||
<h2>Blend</h2>
|
||
<div class="field" data-fid="2.1">
|
||
<div class="segmented small" data-radio-group="2.1">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.1" value="single" checked /><span
|
||
>Single-origin</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.1" value="blend" /><span
|
||
>Blend</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body" id="blend-body">
|
||
<details class="why-panel" data-why="sec-blend" open>
|
||
<summary>Why blends combine times, never temperatures</summary>
|
||
<p>
|
||
The drum has one heat setting and one clock — you cannot
|
||
give 55% of the mass a different schedule than the other
|
||
45%. Times are additive, so a weighted average of two
|
||
first-crack anchors is another valid time on the same
|
||
clock. An average of two temperatures means nothing: first
|
||
crack is a property of the chemistry, not a dial you set.
|
||
</p>
|
||
<p>
|
||
If one component is 60% or more of the batch, roast the
|
||
whole batch as that coffee and let the rest ride along. If
|
||
no component dominates, weight each anchor by its share.
|
||
And if components disagree by more than about a minute at
|
||
first crack, split-roast and blend after — one compromise
|
||
profile serves neither half.
|
||
</p>
|
||
</details>
|
||
<div class="blend-cards" id="blend-cards"></div>
|
||
<div class="blend-total" id="blend-total">
|
||
<div class="blend-total-bar">
|
||
<div class="blend-total-fill" id="blend-total-fill"></div>
|
||
</div>
|
||
<span class="blend-total-label" id="blend-total-label"
|
||
>0% of 100%</span
|
||
>
|
||
</div>
|
||
<button type="button" class="ghost-btn small" id="btn-add-blend">
|
||
+ Add component
|
||
</button>
|
||
|
||
<div class="subhead">How the components combine</div>
|
||
<div class="field wide" data-fid="2.3">
|
||
<div class="segmented" data-radio-group="2.3">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.3" value="dominant" /><span
|
||
>Dominant (≥60%)</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.3" value="weighted" /><span
|
||
>Weighted average</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.3" value="split" /><span
|
||
>Split-roast, blend after</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="2.4"
|
||
><span class="field-label">Resulting FC anchor</span
|
||
><input class="field-input" name="2.4" />
|
||
<span class="field-note"
|
||
>Replaces 1.4 as ledger line 1 while "Blend" is
|
||
selected.</span
|
||
></label
|
||
>
|
||
<label class="field" data-fid="2.5"
|
||
><span class="field-label">Resulting processing modifier</span
|
||
><input class="field-input" name="2.5"
|
||
/></label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-card" id="sec-process">
|
||
<div class="panel-head"><h2>Roast Target</h2></div>
|
||
<div class="panel-body">
|
||
<details class="why-panel" data-why="sec-process" open>
|
||
<summary>Why processing moves development, not first crack</summary>
|
||
<p>
|
||
Processing changed the chemistry that browning starts
|
||
from. Naturals and dark honeys keep far more free
|
||
fructose, and fructose browns faster than sucrose — the
|
||
same profile lands visibly darker. That is why natural and
|
||
honey lots take 15–30 seconds <em>less</em> development
|
||
and want a slightly earlier drop, and why processing never
|
||
touches the first-crack anchor. Change development and
|
||
drop, hold everything else, and the experiment stays
|
||
readable.
|
||
</p>
|
||
<p>
|
||
Roast level is the one input that is purely your decision
|
||
— and development is a bell curve, not a slider. Shorter
|
||
development pushes acidity up, longer pulls it down; but
|
||
too short reads sharp and astringent with no fruit behind
|
||
it, and too long goes flat and dull. The pleasant region is
|
||
in the middle and narrower than the acidity curve
|
||
suggests, which is why the fix is always a 15-second step
|
||
with a cupping in between, never a big swing.
|
||
</p>
|
||
</details>
|
||
<div class="field wide" data-fid="3.1">
|
||
<span class="field-label">Process</span>
|
||
<div class="chip-group" data-radio-group="3.1">
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="washed" /><span
|
||
>Washed</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="yellow-honey" /><span
|
||
>Yellow / white honey</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input
|
||
type="radio"
|
||
name="3.1"
|
||
value="red-black-honey"
|
||
/><span>Red / black honey</span></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="natural" /><span
|
||
>Natural</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="anaerobic" /><span
|
||
>Anaerobic / CM</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="3.2"
|
||
><span class="field-label">Dev modifier</span
|
||
><input class="field-input" name="3.2" />
|
||
<span class="field-note"
|
||
>Washed 0 · natural / dark honey −0:15 to −0:30 ·
|
||
anaerobic a further −0:10 to −0:15. Feeds ledger line
|
||
8.</span
|
||
></label
|
||
>
|
||
</div>
|
||
|
||
<div class="subhead">Roast level</div>
|
||
<div class="field wide" data-fid="4.1">
|
||
<div class="chip-group" data-radio-group="4.1">
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="light" /><span
|
||
>Light</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="light-medium" /><span
|
||
>Light-medium</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="medium" /><span
|
||
>Medium</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="dark" /><span
|
||
>Dark</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="very-dark" /><span
|
||
>Very dark</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="4.2"
|
||
><span class="field-label">Target weight loss</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="4.2" /><span class="unit"
|
||
>%</span
|
||
>
|
||
</div>
|
||
<span class="field-note"
|
||
>Light 11–13% · medium 14–16% · dark 17–18%. Your
|
||
after-roast scale check against this is the honest
|
||
measure of roast degree.</span
|
||
></label
|
||
>
|
||
<label class="field" data-fid="4.3"
|
||
><span class="field-label">Dev base</span
|
||
><input class="field-input" name="4.3" />
|
||
<span class="field-note"
|
||
>Ledger line 7. Light 1:15–2:00 · medium 2:00–3:00 ·
|
||
dark 3:00–4:00. Development past 4:30 mutes any
|
||
roast.</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-card" id="sec-bean">
|
||
<div class="panel-head"><h2>Bean Condition</h2></div>
|
||
<div class="panel-body">
|
||
<details class="why-panel" data-why="sec-bean" open>
|
||
<summary>Why bean condition is worth seconds, not a new plan</summary>
|
||
<p>
|
||
Moisture, density and screen size change how much energy the
|
||
plan costs, not its shape. Wet greens (over 11.5%) stall as
|
||
they near 100 °C while water boils off — expect the flat
|
||
spot and don't panic-add heat. Dense, high-grown seed
|
||
tolerates a hard early push; soft, low-grown seed gives you
|
||
defects instead of speed. Very large beans scorch outside
|
||
while the core lags — the safe way to go faster is more
|
||
airflow at a lower inlet temperature, never more burner.
|
||
</p>
|
||
<p>
|
||
None of these auto-adjust the ledger, and that is
|
||
deliberate: they inform the ±0:15 judgment in Net correction
|
||
and your heat/fan choices, nothing more. Don't have lab
|
||
numbers? Leave moisture and density blank — record only
|
||
evidence you actually have.
|
||
</p>
|
||
</details>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="5.1"
|
||
><span class="field-label">Moisture</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="5.1" /><span class="unit"
|
||
>%</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field" data-fid="5.2"
|
||
><span class="field-label">Density</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="5.2" /><span class="unit"
|
||
>g/L</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field" data-fid="5.3"
|
||
><span class="field-label">Altitude</span
|
||
><input class="field-input" name="5.3"
|
||
/></label>
|
||
<label class="field" data-fid="5.4"
|
||
><span class="field-label">Screen</span
|
||
><input class="field-input" name="5.4"
|
||
/></label>
|
||
</div>
|
||
<div class="field-grid">
|
||
<div class="field" data-fid="5.5">
|
||
<span class="field-label">Sweet spot</span>
|
||
<div class="segmented" data-radio-group="5.5">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="5.5" value="narrow" /><span
|
||
>Narrow</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="5.5" value="med" /><span
|
||
>Medium</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="5.5" value="wide" /><span
|
||
>Wide</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<label class="field" data-fid="5.6"
|
||
><span class="field-label">± Net correction</span
|
||
><input class="field-input" name="5.6" />
|
||
<span class="field-note"
|
||
>Rarely more than ±0:15, and only for an observation
|
||
you can name — moisture and density never create this
|
||
number automatically. Feeds ledger line 3.</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-card" id="sec-machine">
|
||
<div class="panel-head"><h2>Machine Plan</h2></div>
|
||
<div class="panel-body">
|
||
<details class="why-panel" data-why="sec-machine" open>
|
||
<summary>
|
||
Why the rate of rise must fall, and never touch zero
|
||
</summary>
|
||
<p>
|
||
A bean that is still gaining heat, but gaining it more
|
||
slowly, is moving through its reactions in order. Two
|
||
shapes break that. The crash: RoR falls off a cliff just
|
||
after first crack, when the exotherm ends and an oversized
|
||
heat cut lands at the same moment. The bake: RoR flattens
|
||
toward zero, the bean sits at temperature without
|
||
progressing, and the cup goes flat, papery and
|
||
sweetness-free — nothing after the drop rescues a bake.
|
||
</p>
|
||
<p>
|
||
So the rule is: decline steadily from the
|
||
post-turning-point peak, through small frequent heat cuts
|
||
rather than a few big ones, and stay ahead of the
|
||
machine's roughly 60-second lag — what you change now
|
||
shows up a minute from now, so cut <em>before</em> the
|
||
exotherm, gently. If a phase would need a rate outside the
|
||
proven bands to hit its milestone, the plan is not
|
||
reachable — fix the ledger, not the roaster.
|
||
</p>
|
||
</details>
|
||
<div class="subhead">Temperatures & rate-of-rise</div>
|
||
<div class="milestone-list">
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="133"
|
||
data-band-hi="195"
|
||
>
|
||
<div class="milestone-label">Charge</div>
|
||
<output class="milestone-plan" data-out="t-charge"
|
||
>0:00</output
|
||
>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.charge.tempC"
|
||
placeholder="°C"
|
||
aria-label="Charge planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="77"
|
||
data-band-hi="100"
|
||
>
|
||
<div class="milestone-label">Turning point</div>
|
||
<input
|
||
class="field-input sm milestone-time"
|
||
name="temps.tp.time"
|
||
placeholder="m:ss"
|
||
aria-label="Turning point planned time"
|
||
/>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.tp.tempC"
|
||
placeholder="°C"
|
||
aria-label="Turning point planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="148"
|
||
data-band-hi="166"
|
||
>
|
||
<div class="milestone-label">Yellow</div>
|
||
<output class="milestone-plan" data-out="t-yellow">—</output>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.yellow.tempC"
|
||
placeholder="°C"
|
||
aria-label="Yellow planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="176"
|
||
data-band-hi="187"
|
||
>
|
||
<div class="milestone-label">First crack</div>
|
||
<output class="milestone-plan" data-out="t-fc">—</output>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.fc.tempC"
|
||
placeholder="°C"
|
||
aria-label="First crack planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="183"
|
||
data-band-hi="197"
|
||
>
|
||
<div class="milestone-label">Drop</div>
|
||
<output class="milestone-plan" data-out="t-drop">—</output>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.drop.tempC"
|
||
placeholder="°C"
|
||
aria-label="Drop planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="subhead">Actuator schedule (planned)</div>
|
||
<p class="field-note">
|
||
The timing of your first heat cut is the main first-crack
|
||
lever — later cut, earlier crack. Say why you make each
|
||
change; that's what makes this log readable next week.
|
||
</p>
|
||
<div class="actuator-timeline" id="actuator-timeline"></div>
|
||
<button
|
||
type="button"
|
||
class="ghost-btn small"
|
||
id="btn-add-actuator"
|
||
>
|
||
+ Add step
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="phase-divider" role="separator" aria-label="Roast day begins">
|
||
<span class="phase-divider-label">Roast day</span>
|
||
<span class="phase-divider-text"
|
||
>Everything above is decided before you charge. Fill in from
|
||
here down at the machine, then after cupping. Print takes the
|
||
plan with you.</span
|
||
>
|
||
</div>
|
||
|
||
<section class="panel-card phase-later" id="sec-roastlog">
|
||
<div class="panel-head">
|
||
<h2>Roast Log — Plan vs. Actual</h2>
|
||
<span class="phase-chip">during the roast</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<details class="why-panel" data-why="sec-roastlog" open>
|
||
<summary>Record what you observed, never what you planned</summary>
|
||
<p>
|
||
A planned milestone written into the log as though it
|
||
happened destroys the only evidence you have. If you never
|
||
actually heard first crack, write that down — "not heard,
|
||
called at 8:10 by smell" is a useful record. "8:00"
|
||
because that was the plan is worse than nothing, because
|
||
next batch you will trust it. The plan column stays fixed
|
||
beside your entries precisely so the gap is visible. The
|
||
gap is the data.
|
||
</p>
|
||
</details>
|
||
<div class="milestone-list">
|
||
<div class="milestone-row log-row">
|
||
<div class="milestone-label">Charge</div>
|
||
<output class="milestone-plan" data-out="pa-charge"
|
||
>0:00</output
|
||
>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.charge.actualTime"
|
||
placeholder="0:00"
|
||
aria-label="Charge actual time"
|
||
/>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.charge.actualBt"
|
||
placeholder="°C"
|
||
aria-label="Charge actual temperature"
|
||
/>
|
||
<input
|
||
class="field-input note"
|
||
name="planActual.charge.note"
|
||
placeholder="Note"
|
||
aria-label="Charge note"
|
||
/>
|
||
</div>
|
||
<div class="milestone-row log-row">
|
||
<div class="milestone-label">Turning point</div>
|
||
<output class="milestone-plan" data-out="pa-tp">—</output>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.tp.actualTime"
|
||
placeholder="0:00"
|
||
aria-label="Turning point actual time"
|
||
/>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.tp.actualBt"
|
||
placeholder="°C"
|
||
aria-label="Turning point actual temperature"
|
||
/>
|
||
<input
|
||
class="field-input note"
|
||
name="planActual.tp.note"
|
||
placeholder="Note"
|
||
aria-label="Turning point note"
|
||
/>
|
||
</div>
|
||
<div class="milestone-row log-row">
|
||
<div class="milestone-label">Yellow</div>
|
||
<output class="milestone-plan" data-out="pa-yellow">—</output>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.yellow.actualTime"
|
||
placeholder="0:00"
|
||
aria-label="Yellow actual time"
|
||
/>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.yellow.actualBt"
|
||
placeholder="°C"
|
||
aria-label="Yellow actual temperature"
|
||
/>
|
||
<input
|
||
class="field-input note"
|
||
name="planActual.yellow.note"
|
||
placeholder="Note"
|
||
aria-label="Yellow note"
|
||
/>
|
||
</div>
|
||
<div class="milestone-row log-row">
|
||
<div class="milestone-label">First crack</div>
|
||
<output class="milestone-plan" data-out="pa-fc">—</output>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.fc.actualTime"
|
||
placeholder="0:00"
|
||
aria-label="First crack actual time"
|
||
/>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.fc.actualBt"
|
||
placeholder="°C"
|
||
aria-label="First crack actual temperature"
|
||
/>
|
||
<input
|
||
class="field-input note"
|
||
name="planActual.fc.note"
|
||
placeholder="Note"
|
||
aria-label="First crack note"
|
||
/>
|
||
</div>
|
||
<div class="milestone-row log-row">
|
||
<div class="milestone-label">Drop</div>
|
||
<output class="milestone-plan" data-out="pa-drop">—</output>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.drop.actualTime"
|
||
placeholder="0:00"
|
||
aria-label="Drop actual time"
|
||
/>
|
||
<input
|
||
class="field-input sm"
|
||
name="planActual.drop.actualBt"
|
||
placeholder="°C"
|
||
aria-label="Drop actual temperature"
|
||
/>
|
||
<input
|
||
class="field-input note"
|
||
name="planActual.drop.note"
|
||
placeholder="Note"
|
||
aria-label="Drop note"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="inventory-consume hidden" id="inventory-consume">
|
||
<span class="inventory-consume-label" id="inventory-consume-label"></span>
|
||
<button type="button" class="ghost-btn small" id="btn-consume">
|
||
Draw from lot
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-card phase-later" id="sec-after">
|
||
<div class="panel-head">
|
||
<h2>After the Roast</h2>
|
||
<span class="phase-chip">after cupping</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<details class="why-panel" data-why="sec-after" open>
|
||
<summary>One change per batch — arithmetic, not patience</summary>
|
||
<p>
|
||
Move two variables and there are four possible
|
||
explanations for what the cup does; move three and there
|
||
are eight, and you can't separate them without more
|
||
batches than you have coffee for. This worksheet makes
|
||
single-variable experiments cheap: every number has one
|
||
owner.
|
||
</p>
|
||
<p>
|
||
Weight loss is the honest instrument — (green − roasted) ÷
|
||
green × 100 rolls total time, development, and drop
|
||
temperature into one number your scale can measure. Then
|
||
match the cup to a symptom: papery or thin → first crack
|
||
+0:30. Savory, florals gone → first crack −0:30. Sharp
|
||
acidity with no fruit → development +0:15 (a DTR under 12%
|
||
confirms it). Flat and dull → development −0:15. Flat with
|
||
no clear defect → fix the RoR shape, not the times. Write
|
||
the winner into "One change next batch" — and next time
|
||
you roast this coffee, carry it into ± Refine (1.6). That
|
||
handoff is how the plan learns.
|
||
</p>
|
||
</details>
|
||
<div class="field-grid">
|
||
<label class="field"
|
||
><span class="field-label">Green in</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="afterRoast.greenIn" /><span
|
||
class="unit"
|
||
>g</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field"
|
||
><span class="field-label">Out</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="afterRoast.out" /><span
|
||
class="unit"
|
||
>g</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field"
|
||
><span class="field-label">Weight loss</span>
|
||
<div class="unit-input">
|
||
<input
|
||
class="field-input"
|
||
name="afterRoast.weightLossPct"
|
||
/><span class="unit">%</span>
|
||
</div></label
|
||
>
|
||
<label class="field"
|
||
><span class="field-label">Actual DTR</span>
|
||
<div class="unit-input">
|
||
<input
|
||
class="field-input"
|
||
name="afterRoast.actualDtrPct"
|
||
/><span class="unit">%</span>
|
||
</div></label
|
||
>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field"
|
||
><span class="field-label">Colour WB/GR</span
|
||
><input class="field-input" name="afterRoast.colour"
|
||
/></label>
|
||
<label class="field"
|
||
><span class="field-label">Rested</span>
|
||
<div class="unit-input">
|
||
<input
|
||
class="field-input"
|
||
name="afterRoast.restedDays"
|
||
/><span class="unit">days</span>
|
||
</div></label
|
||
>
|
||
<label class="field"
|
||
><span class="field-label">Brew ratio</span
|
||
><input class="field-input" name="afterRoast.brewRatio"
|
||
/></label>
|
||
<label class="field"
|
||
><span class="field-label">Method</span
|
||
><input class="field-input" name="afterRoast.method"
|
||
/></label>
|
||
</div>
|
||
<label class="field"
|
||
><span class="field-label">Cup notes</span
|
||
><input class="field-input" name="afterRoast.cupNotes"
|
||
/></label>
|
||
<div class="field-grid">
|
||
<label class="field accent"
|
||
><span class="field-label">One change next batch</span
|
||
><input class="field-input" name="afterRoast.oneChange"
|
||
/></label>
|
||
<label class="field"
|
||
><span class="field-label">What would prove it wrong</span
|
||
><input class="field-input" name="afterRoast.disproof"
|
||
/></label>
|
||
</div>
|
||
<div class="cupping-link">
|
||
<button type="button" class="ghost-btn" id="btn-open-cupping">
|
||
Open cupping session
|
||
</button>
|
||
<span class="field-note" id="cupping-link-note"></span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<aside class="dock" aria-label="Live plan feedback">
|
||
<div class="dock-card ledger-card" id="plan-now">
|
||
<div class="dock-card-head"><h2>Time Ledger</h2></div>
|
||
<p class="dock-note">
|
||
Drying + Maillard + Development = the whole roast. Cultivar
|
||
sets first crack; process and level set development.
|
||
</p>
|
||
|
||
<div class="ledger-group">
|
||
<div class="ledger-row">
|
||
<span class="l-fid">1.4</span
|
||
><span class="l-label">Cultivar / blend anchor</span
|
||
><output class="l-val" data-out="l1">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">1.6</span
|
||
><span class="l-label">± Refinement</span
|
||
><output class="l-val" data-out="l2">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">5.6</span
|
||
><span class="l-label">± Bean condition</span
|
||
><output class="l-val" data-out="l3">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">6.4</span
|
||
><span class="l-label">± Batch size</span
|
||
><input
|
||
class="ledger-input"
|
||
name="6.4"
|
||
placeholder="0"
|
||
aria-label="Batch-size correction"
|
||
title="225 g lands first crack ~1:30–2:00 later than 200 g"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="ledger-total hero">
|
||
<span>First crack</span><output data-out="A">—</output>
|
||
</div>
|
||
|
||
<div class="ledger-info">
|
||
<div class="info-pill">
|
||
<span>Yellow</span><output data-out="yellow">—</output>
|
||
</div>
|
||
<div class="info-pill">
|
||
<span>Maillard</span><output data-out="maillard">—</output>
|
||
</div>
|
||
<span class="info-note"
|
||
>informational — not summed into development</span
|
||
>
|
||
</div>
|
||
|
||
<div class="ledger-group">
|
||
<div class="ledger-row">
|
||
<span class="l-fid">4.3</span
|
||
><span class="l-label">Development base</span
|
||
><output class="l-val" data-out="l7">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">3.2</span
|
||
><span class="l-label">± Processing modifier</span
|
||
><output class="l-val" data-out="l8">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">1.7</span
|
||
><span class="l-label">± Cultivar dev modifier</span
|
||
><output class="l-val" data-out="l9">—</output>
|
||
</div>
|
||
</div>
|
||
<div class="ledger-total hero">
|
||
<span>Development</span><output data-out="C">—</output>
|
||
</div>
|
||
<div class="ledger-total hero drop">
|
||
<span>Drop (end of roast)</span><output data-out="D">—</output>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="ledger-warnings hidden"
|
||
id="ledger-warnings"
|
||
role="status"
|
||
aria-live="polite"
|
||
></div>
|
||
|
||
<div class="dock-card checks-card">
|
||
<div class="dock-card-head"><h2>Sanity Checks</h2></div>
|
||
<div class="checks-grid">
|
||
<div class="check-tile unknown" data-pass="drying">
|
||
<span class="check-name">Drying share</span>
|
||
<output class="check-value" data-out="check-drying">—</output>
|
||
<span class="check-band">target 43–51%</span>
|
||
<span class="check-why"
|
||
>high = late yellow, browning starved · low = rushed
|
||
before internal pressure built</span
|
||
>
|
||
</div>
|
||
<div class="check-tile unknown" data-pass="maillard">
|
||
<span class="check-name">Maillard share</span>
|
||
<output class="check-value" data-out="check-maillard">—</output>
|
||
<span class="check-band">target 33–39%</span>
|
||
<span class="check-why"
|
||
>low = too small a browning window for sweetness and
|
||
complexity</span
|
||
>
|
||
</div>
|
||
<div class="check-tile unknown" data-pass="dtr">
|
||
<span class="check-name">Development ratio</span>
|
||
<output class="check-value" data-out="check-dtr">—</output>
|
||
<span class="check-band">target 12–20%</span>
|
||
<span class="check-why"
|
||
>under 12% = under-developed — sharp, astringent acidity
|
||
with no fruit behind it</span
|
||
>
|
||
</div>
|
||
<div class="check-tile unknown" data-pass="ceiling">
|
||
<span class="check-name">Development ceiling</span>
|
||
<output class="check-value" data-out="check-ceiling">—</output>
|
||
<span class="check-band">under 4:30</span>
|
||
<span class="check-why"
|
||
>over 4:30 mutes any roast — nothing good is past this
|
||
line</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dock-card curve-card">
|
||
<div class="dock-card-head">
|
||
<h2>The Curve</h2>
|
||
<span class="curve-legend"
|
||
><i class="sw sw-plan"></i>plan
|
||
<i class="sw sw-ref"></i>reference</span
|
||
>
|
||
</div>
|
||
<div class="curve-wrap">
|
||
<svg
|
||
id="curve-svg-live"
|
||
viewBox="0 0 714 334"
|
||
width="100%"
|
||
role="img"
|
||
aria-label="Roast curve, live plan"
|
||
>
|
||
<defs>
|
||
<style>
|
||
#curve-svg-live .maj {
|
||
stroke: var(--line);
|
||
stroke-width: 0.7;
|
||
}
|
||
#curve-svg-live .min {
|
||
stroke: var(--surface-sunken);
|
||
stroke-width: 0.6;
|
||
}
|
||
#curve-svg-live .ax {
|
||
stroke: var(--ink-3);
|
||
stroke-width: 1.1;
|
||
}
|
||
#curve-svg-live .al {
|
||
font-size: 8px;
|
||
fill: var(--ink-3);
|
||
}
|
||
#curve-svg-live .band {
|
||
fill: var(--ember);
|
||
opacity: 0.08;
|
||
}
|
||
#curve-svg-live .bandline {
|
||
font-size: 7.5px;
|
||
fill: var(--ember);
|
||
}
|
||
</style>
|
||
</defs>
|
||
<rect
|
||
x="52"
|
||
y="24"
|
||
width="600"
|
||
height="270"
|
||
fill="var(--surface-raised)"
|
||
/>
|
||
<rect class="band" x="52" y="65.4" width="600" height="19.8" />
|
||
<text
|
||
x="58"
|
||
y="62.4"
|
||
class="bandline"
|
||
data-band-lo="176"
|
||
data-band-hi="187"
|
||
data-band-prefix="first-crack band, "
|
||
>
|
||
first-crack band, 176–187 °C
|
||
</text>
|
||
<g class="min">
|
||
<line x1="72" y1="24" x2="72" y2="294" />
|
||
<line x1="112" y1="24" x2="112" y2="294" />
|
||
<line x1="152" y1="24" x2="152" y2="294" />
|
||
<line x1="192" y1="24" x2="192" y2="294" />
|
||
<line x1="232" y1="24" x2="232" y2="294" />
|
||
<line x1="272" y1="24" x2="272" y2="294" />
|
||
<line x1="312" y1="24" x2="312" y2="294" />
|
||
<line x1="352" y1="24" x2="352" y2="294" />
|
||
<line x1="392" y1="24" x2="392" y2="294" />
|
||
<line x1="432" y1="24" x2="432" y2="294" />
|
||
<line x1="472" y1="24" x2="472" y2="294" />
|
||
<line x1="512" y1="24" x2="512" y2="294" />
|
||
<line x1="552" y1="24" x2="552" y2="294" />
|
||
<line x1="592" y1="24" x2="592" y2="294" />
|
||
<line x1="632" y1="24" x2="632" y2="294" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="92" y1="24" x2="92" y2="294" />
|
||
<line x1="132" y1="24" x2="132" y2="294" />
|
||
<line x1="172" y1="24" x2="172" y2="294" />
|
||
<line x1="212" y1="24" x2="212" y2="294" />
|
||
<line x1="252" y1="24" x2="252" y2="294" />
|
||
<line x1="292" y1="24" x2="292" y2="294" />
|
||
<line x1="332" y1="24" x2="332" y2="294" />
|
||
<line x1="372" y1="24" x2="372" y2="294" />
|
||
<line x1="412" y1="24" x2="412" y2="294" />
|
||
<line x1="452" y1="24" x2="452" y2="294" />
|
||
<line x1="492" y1="24" x2="492" y2="294" />
|
||
<line x1="532" y1="24" x2="532" y2="294" />
|
||
<line x1="572" y1="24" x2="572" y2="294" />
|
||
<line x1="612" y1="24" x2="612" y2="294" />
|
||
</g>
|
||
<g class="min">
|
||
<line x1="52" y1="276" x2="652" y2="276" />
|
||
<line x1="52" y1="240" x2="652" y2="240" />
|
||
<line x1="52" y1="204" x2="652" y2="204" />
|
||
<line x1="52" y1="168" x2="652" y2="168" />
|
||
<line x1="52" y1="132" x2="652" y2="132" />
|
||
<line x1="52" y1="96" x2="652" y2="96" />
|
||
<line x1="52" y1="60" x2="652" y2="60" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="52" y1="258" x2="652" y2="258" />
|
||
<line x1="52" y1="222" x2="652" y2="222" />
|
||
<line x1="52" y1="186" x2="652" y2="186" />
|
||
<line x1="52" y1="150" x2="652" y2="150" />
|
||
<line x1="52" y1="114" x2="652" y2="114" />
|
||
<line x1="52" y1="78" x2="652" y2="78" />
|
||
<line x1="52" y1="42" x2="652" y2="42" />
|
||
</g>
|
||
<line class="ax" x1="52" y1="24" x2="52" y2="294" />
|
||
<line class="ax" x1="52" y1="294" x2="652" y2="294" />
|
||
<line class="ax" x1="652" y1="24" x2="652" y2="294" />
|
||
<g class="al curve-axis-temp" text-anchor="end">
|
||
<text data-tempc="60" x="47" y="296.8">60</text>
|
||
<text data-tempc="80" x="47" y="260.8">80</text>
|
||
<text data-tempc="100" x="47" y="224.8">100</text>
|
||
<text data-tempc="120" x="47" y="188.8">120</text>
|
||
<text data-tempc="140" x="47" y="152.8">140</text>
|
||
<text data-tempc="160" x="47" y="116.8">160</text>
|
||
<text data-tempc="180" x="47" y="80.8">180</text>
|
||
<text data-tempc="200" x="47" y="44.8">200</text>
|
||
</g>
|
||
<g class="al" text-anchor="middle">
|
||
<text x="52" y="304">0:00</text>
|
||
<text x="92" y="304">1:00</text>
|
||
<text x="132" y="304">2:00</text>
|
||
<text x="172" y="304">3:00</text>
|
||
<text x="212" y="304">4:00</text>
|
||
<text x="252" y="304">5:00</text>
|
||
<text x="292" y="304">6:00</text>
|
||
<text x="332" y="304">7:00</text>
|
||
<text x="372" y="304">8:00</text>
|
||
<text x="412" y="304">9:00</text>
|
||
<text x="452" y="304">10:00</text>
|
||
<text x="492" y="304">11:00</text>
|
||
<text x="532" y="304">12:00</text>
|
||
<text x="572" y="304">13:00</text>
|
||
<text x="612" y="304">14:00</text>
|
||
<text x="652" y="304">15:00</text>
|
||
</g>
|
||
<g id="plan-curve-live"></g>
|
||
<g id="ref-curve-live"></g>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<!-- /app-workspace -->
|
||
</div>
|
||
<!-- /app-shell -->
|
||
|
||
<!-- Kept OUTSIDE #plan-form and OUTSIDE .app-shell on purpose: identically-named radios here must not fight for
|
||
mutual exclusivity with the screen form's radios (same name + same form owner would
|
||
silently uncheck one another). This whole block is display:none except when printing;
|
||
main.js re-syncs it from state.plan on beforeprint. -->
|
||
<div id="print-sheet">
|
||
<div class="sheet-wrap">
|
||
<div class="sheet">
|
||
<!-- ══════════════════ FRONT ══════════════════ -->
|
||
<section class="first">
|
||
<div class="wsbar">
|
||
<span class="t">Manual Roast Plan — Worksheet</span>
|
||
<span class="r">Sheet 1 of 2 · Plan | Hottop KN-8828B-2K+</span>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="id">THE COFFEE</span
|
||
><span class="ref"
|
||
>[n.n] = field ID · cited everywhere it feeds a number</span
|
||
>
|
||
</div>
|
||
<div class="bd wsline tightln">
|
||
<span class="fid">0.1</span><span class="lb">Coffee / lot</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 44mm"
|
||
name="0.1"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="fid">0.2</span><span class="lb">Date</span>
|
||
<input class="f n ws-input" type="date" name="0.2" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">0.3</span
|
||
><span class="lb">Batch #</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 10mm"
|
||
name="0.3"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="fid">0.4</span><span class="lb">Green in</span>
|
||
<input class="f n ws-input" name="0.4" />g<span
|
||
class="pv"
|
||
></span
|
||
><br />
|
||
|
||
<span class="stepdot">1</span><span class="fid">1.1</span
|
||
><span class="lb">Cultivar</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 30mm"
|
||
name="1.1"
|
||
list="cultivar-list"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="fid">1.2</span><span class="lb">Group</span>
|
||
<span class="circ" data-radio-group="1.2">
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="typica" /><span
|
||
>Typica</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="bourbon" /><span
|
||
>Bourbon</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="ethiopian" /><span
|
||
>Ethiopian</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="hybrid" /><span
|
||
>Hybrid</span
|
||
></label
|
||
>
|
||
</span>
|
||
<span class="sep">|</span><span class="fid">1.3</span
|
||
><span class="lb">Origin</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 20mm"
|
||
name="1.3"
|
||
/><span class="pv"></span><br />
|
||
|
||
<span class="fid" style="margin-left: 4.6mm">1.4</span
|
||
><span class="lb">FC anchor</span>
|
||
<input class="f n ws-input" name="1.4" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">1.5</span
|
||
><span class="lb">Profile mm:ss|mm:ss|mm:ss</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 22mm"
|
||
name="1.5"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><small>your cultivar's row, Step 1 — not a group number</small
|
||
><br />
|
||
|
||
<span class="fid" style="margin-left: 4.6mm">1.6</span
|
||
><span class="lb">± Refine → ledger 2</span>
|
||
<input class="f n ws-input" name="1.6" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>0 first cook, else Step 12</small>
|
||
<span class="sep">|</span><span class="fid">1.7</span
|
||
><span class="lb">± Dev mod → ledger 9</span>
|
||
<input class="f n ws-input" name="1.7" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>Step 1's profile 3rd # −1:30</small><br />
|
||
|
||
<span class="stepdot">3</span><span class="fid">3.1</span
|
||
><span class="lb">Process</span>
|
||
<span class="circ" data-radio-group="3.1">
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="washed" /><span
|
||
>washed</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="yellow-honey" /><span
|
||
>yellow/white honey</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input
|
||
type="radio"
|
||
name="3.1"
|
||
value="red-black-honey"
|
||
/><span>red/black honey</span></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="natural" /><span
|
||
>natural</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="anaerobic" /><span
|
||
>anaerobic/CM</span
|
||
></label
|
||
>
|
||
</span>
|
||
<span class="sep">|</span><span class="fid">3.2</span
|
||
><span class="lb">Dev mod → ledger 8</span>
|
||
<input class="f n ws-input" name="3.2" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>Step 3 table</small><br />
|
||
|
||
<span class="stepdot">4</span><span class="fid">4.1</span
|
||
><span class="lb">Roast level</span>
|
||
<span class="circ" data-radio-group="4.1">
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="light" /><span
|
||
>light</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="light-medium" /><span
|
||
>light-med</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="medium" /><span
|
||
>medium</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="dark" /><span
|
||
>dark</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="very-dark" /><span
|
||
>very dark</span
|
||
></label
|
||
>
|
||
</span>
|
||
<span class="sep">|</span><span class="fid">4.2</span
|
||
><span class="lb">Target WL</span>
|
||
<input class="f n ws-input" name="4.2" />%<span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">4.3</span
|
||
><span class="lb">Dev base → ledger 7</span>
|
||
<input
|
||
class="f n ws-input"
|
||
style="min-width: 12mm"
|
||
name="4.3"
|
||
/><span class="pv"></span><br />
|
||
|
||
<span class="stepdot">5</span><span class="fid">5.1</span
|
||
><span class="lb">Moisture</span>
|
||
<input class="f n ws-input" name="5.1" />%<span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.2</span
|
||
><span class="lb">Density</span>
|
||
<input class="f n ws-input" name="5.2" />g/L<span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.3</span
|
||
><span class="lb">Altitude</span>
|
||
<input class="f n ws-input" name="5.3" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.4</span
|
||
><span class="lb">Screen</span>
|
||
<input class="f n ws-input" name="5.4" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.5</span
|
||
><span class="lb">Sweet</span>
|
||
<span class="circ" data-radio-group="5.5">
|
||
<label class="opt"
|
||
><input type="radio" name="5.5" value="narrow" /><span
|
||
>narrow</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="5.5" value="med" /><span
|
||
>med</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="5.5" value="wide" /><span
|
||
>wide</span
|
||
></label
|
||
> </span
|
||
><br />
|
||
<span class="fid" style="margin-left: 4.6mm">5.6</span
|
||
><span class="lb">± Net correction → ledger 3</span>
|
||
<input class="f n ws-input" name="5.6" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>judgment, rarely >±0:15</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">2 · BLEND</span
|
||
><span class="ref">skip if single-origin → Step 2</span>
|
||
</div>
|
||
<div class="bd">
|
||
<div class="wsline tightln">
|
||
<span class="fid">2.1</span>This lot is:
|
||
<span class="circ" data-radio-group="2.1">
|
||
<label class="opt"
|
||
><input
|
||
type="radio"
|
||
name="2.1"
|
||
value="single"
|
||
checked
|
||
/><span>single-origin — skip this box</span></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="2.1" value="blend" /><span
|
||
>blend or community lot</span
|
||
></label
|
||
>
|
||
</span>
|
||
</div>
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th><span class="fid">2.2</span>Component</th>
|
||
<th style="width: 22mm">Group</th>
|
||
<th style="width: 24mm">Process</th>
|
||
<th class="num" style="width: 14mm">Share %</th>
|
||
<th class="num" style="width: 20mm">FC anchor</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="blend-rows"></tbody>
|
||
</table>
|
||
<div class="wsline tightln">
|
||
<span class="fid">2.3</span>Method:
|
||
<span class="circ" data-radio-group="2.3">
|
||
<label class="opt"
|
||
><input type="radio" name="2.3" value="dominant" /><span
|
||
>dominant (≥60%)</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="2.3" value="weighted" /><span
|
||
>weighted average</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="2.3" value="split" /><span
|
||
>split-roast, blend after</span
|
||
></label
|
||
>
|
||
</span>
|
||
</div>
|
||
<div class="wsline tightln">
|
||
<span class="fid">2.4</span
|
||
><span class="lb">Resulting FC anchor → ledger line 1</span>
|
||
<input class="f n ws-input" name="2.4" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">2.5</span
|
||
><span class="lb"
|
||
>Resulting processing modifier → ledger line 8</span
|
||
>
|
||
<input class="f n ws-input" name="2.5" /><span
|
||
class="pv"
|
||
></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">6 · TIME LEDGER</span
|
||
><span class="ref">method & why → Step 6</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="ledger">
|
||
<tr class="row">
|
||
<td class="ln">1</td>
|
||
<td class="desc">
|
||
Cultivar/blend anchor
|
||
<span class="sub">from 1.4, or 2.4 if blend</span>
|
||
</td>
|
||
<td class="val"><output data-out="l1">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">2</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Refinement — from 1.6
|
||
<span class="sub">0 first cook, else Step 12</span>
|
||
</td>
|
||
<td class="val"><output data-out="l2">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">3</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Bean condition — from 5.6
|
||
</td>
|
||
<td class="val"><output data-out="l3">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">4</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Batch size vs. Step 9 reference
|
||
weights
|
||
<span class="sub"
|
||
>225 g ≈ 1:30–2:00 later than 200 g</span
|
||
>
|
||
</td>
|
||
<td class="val">
|
||
<input
|
||
class="f n ws-input"
|
||
name="6.4"
|
||
placeholder="0"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr class="total">
|
||
<td class="ln">A</td>
|
||
<td class="desc"><strong>FIRST CRACK</strong></td>
|
||
<td class="val"><output data-out="A">—</output></td>
|
||
</tr>
|
||
<tr class="grp">
|
||
<td class="ln"></td>
|
||
<td class="desc" colspan="2">
|
||
B · YELLOW / MAILLARD — informational only. NOT summed
|
||
into C.
|
||
</td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">5</td>
|
||
<td class="desc"><strong>YELLOW</strong> = A × 0.56</td>
|
||
<td class="val"><output data-out="yellow">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">6</td>
|
||
<td class="desc"><strong>MAILLARD</strong> = A − line 5</td>
|
||
<td class="val"><output data-out="maillard">—</output></td>
|
||
</tr>
|
||
<tr class="grp">
|
||
<td class="ln"></td>
|
||
<td class="desc" colspan="2">
|
||
C · DEVELOPMENT — sum ONLY rows 7 + 8 + 9
|
||
</td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">7</td>
|
||
<td class="desc">Development base — from 4.3</td>
|
||
<td class="val"><output data-out="l7">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">8</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Processing modifier — from 3.2,
|
||
or 2.5 if blend
|
||
</td>
|
||
<td class="val"><output data-out="l8">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">9</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Cultivar dev modifier — from 1.7
|
||
</td>
|
||
<td class="val"><output data-out="l9">—</output></td>
|
||
</tr>
|
||
<tr class="total">
|
||
<td class="ln">C</td>
|
||
<td class="desc">
|
||
<strong>DEVELOPMENT</strong> = 7 + 8 + 9 only
|
||
</td>
|
||
<td class="val"><output data-out="C">—</output></td>
|
||
</tr>
|
||
<tr class="total">
|
||
<td class="ln">D</td>
|
||
<td class="desc">
|
||
<strong>DROP (end of roast)</strong> = A + C
|
||
</td>
|
||
<td class="val"><output data-out="D">—</output></td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">7 · SANITY CHECKS</span
|
||
><span class="ref"
|
||
>a fail sends you back to box 6 → Step 7</span
|
||
>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 44mm">Check</th>
|
||
<th class="num" style="width: 24mm">Should be</th>
|
||
<th class="num" style="width: 24mm">Yours</th>
|
||
<th class="num" style="width: 16mm">Pass?</th>
|
||
<th>Failure means</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Drying share</td>
|
||
<td class="num">43–51 %</td>
|
||
<td class="num">
|
||
<output data-out="check-drying">—</output>
|
||
</td>
|
||
<td class="num passcell unknown" data-pass="drying"></td>
|
||
<td>high = late yellow · low = rushed</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Maillard share</td>
|
||
<td class="num">33–39 %</td>
|
||
<td class="num">
|
||
<output data-out="check-maillard">—</output>
|
||
</td>
|
||
<td
|
||
class="num passcell unknown"
|
||
data-pass="maillard"
|
||
></td>
|
||
<td>low = too little browning</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Development ratio</td>
|
||
<td class="num">12–20 %</td>
|
||
<td class="num">
|
||
<output data-out="check-dtr">—</output>
|
||
</td>
|
||
<td class="num passcell unknown" data-pass="dtr"></td>
|
||
<td>under 12 % = under-developed</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Development ceiling</td>
|
||
<td class="num">< 4:30</td>
|
||
<td class="num">
|
||
<output data-out="check-ceiling">—</output>
|
||
</td>
|
||
<td class="num passcell unknown" data-pass="ceiling"></td>
|
||
<td>over 4:30 mutes any roast</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════════════ BACK ══════════════════ -->
|
||
<section>
|
||
<div class="wsbar">
|
||
<span class="t">Roast & Record</span>
|
||
<span class="r"
|
||
>Sheet 2 of 2 | coffee: <span id="back-coffee-name"></span
|
||
></span>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">8 · TEMPERATURES & RATE-OF-RISE</span
|
||
><span class="ref">→ Step 8</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 28mm">Milestone</th>
|
||
<th class="num" style="width: 20mm">Time</th>
|
||
<th class="num" style="width: 18mm" id="print-machine-temp-th">°C</th>
|
||
<th class="num" style="width: 36mm">Your logged band</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Charge</td>
|
||
<td class="num">
|
||
<output data-out="t-charge">0:00</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.charge.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="133" data-band-hi="195">133–195 °C</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Turning point</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.tp.time"
|
||
placeholder="m:ss"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.tp.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="77" data-band-hi="100" data-band-suffix=" @ 0:47–1:15">77–100 °C @ 0:47–1:15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Yellow</td>
|
||
<td class="num">
|
||
<output data-out="t-yellow">—</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.yellow.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="148" data-band-hi="166">148–166 °C</td>
|
||
</tr>
|
||
<tr>
|
||
<td>First crack</td>
|
||
<td class="num"><output data-out="t-fc">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.fc.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="176" data-band-hi="187">176–187 °C</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Drop</td>
|
||
<td class="num"><output data-out="t-drop">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.drop.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="183" data-band-hi="197" data-band-suffix=" light">183–197 °C light</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">9 · ACTUATOR SCHEDULE (planned)</span
|
||
><span class="ref">→ Step 9</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th class="num" style="width: 20mm">Time</th>
|
||
<th class="num" style="width: 18mm">Heat %</th>
|
||
<th class="num" style="width: 18mm">Fan %</th>
|
||
<th class="num" style="width: 26mm">Expected BT</th>
|
||
<th>Why this change</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="actuator-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox wsgrid">
|
||
<div class="hd">
|
||
<span class="n">10 · THE CURVE</span
|
||
><span class="ref"
|
||
>plan drawn live · reference from an .alog if attached → Step
|
||
10</span
|
||
>
|
||
</div>
|
||
<div class="bd">
|
||
<div class="grid-wrap">
|
||
<svg
|
||
id="curve-svg"
|
||
viewBox="0 0 714 334"
|
||
width="100%"
|
||
role="img"
|
||
aria-label="Roast curve plotting grid"
|
||
>
|
||
<defs>
|
||
<style>
|
||
.maj {
|
||
stroke: #8b8078;
|
||
stroke-width: 0.7;
|
||
}
|
||
.min {
|
||
stroke: #e2dad2;
|
||
stroke-width: 0.45;
|
||
}
|
||
.ax {
|
||
stroke: #1a1512;
|
||
stroke-width: 1.3;
|
||
}
|
||
.al {
|
||
font-size: 8px;
|
||
fill: #5a5048;
|
||
}
|
||
.alr {
|
||
font-size: 8px;
|
||
fill: #7a3b12;
|
||
}
|
||
.band {
|
||
fill: #7a3b12;
|
||
opacity: 0.05;
|
||
}
|
||
.ttl {
|
||
font-size: 8px;
|
||
font-weight: 700;
|
||
fill: #1a1512;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
</style>
|
||
</defs>
|
||
<rect x="52" y="24" width="600" height="270" fill="#fff" />
|
||
<rect
|
||
class="band"
|
||
x="52"
|
||
y="65.4"
|
||
width="600"
|
||
height="19.8"
|
||
/>
|
||
<text
|
||
x="58"
|
||
y="62.4"
|
||
class="alr"
|
||
style="font-size: 7px"
|
||
data-band-lo="176"
|
||
data-band-hi="187"
|
||
data-band-prefix="your first-crack band, "
|
||
>
|
||
your first-crack band, 176–187 °C
|
||
</text>
|
||
<g class="min">
|
||
<line x1="72" y1="24" x2="72" y2="294" />
|
||
<line x1="112" y1="24" x2="112" y2="294" />
|
||
<line x1="152" y1="24" x2="152" y2="294" />
|
||
<line x1="192" y1="24" x2="192" y2="294" />
|
||
<line x1="232" y1="24" x2="232" y2="294" />
|
||
<line x1="272" y1="24" x2="272" y2="294" />
|
||
<line x1="312" y1="24" x2="312" y2="294" />
|
||
<line x1="352" y1="24" x2="352" y2="294" />
|
||
<line x1="392" y1="24" x2="392" y2="294" />
|
||
<line x1="432" y1="24" x2="432" y2="294" />
|
||
<line x1="472" y1="24" x2="472" y2="294" />
|
||
<line x1="512" y1="24" x2="512" y2="294" />
|
||
<line x1="552" y1="24" x2="552" y2="294" />
|
||
<line x1="592" y1="24" x2="592" y2="294" />
|
||
<line x1="632" y1="24" x2="632" y2="294" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="92" y1="24" x2="92" y2="294" />
|
||
<line x1="132" y1="24" x2="132" y2="294" />
|
||
<line x1="172" y1="24" x2="172" y2="294" />
|
||
<line x1="212" y1="24" x2="212" y2="294" />
|
||
<line x1="252" y1="24" x2="252" y2="294" />
|
||
<line x1="292" y1="24" x2="292" y2="294" />
|
||
<line x1="332" y1="24" x2="332" y2="294" />
|
||
<line x1="372" y1="24" x2="372" y2="294" />
|
||
<line x1="412" y1="24" x2="412" y2="294" />
|
||
<line x1="452" y1="24" x2="452" y2="294" />
|
||
<line x1="492" y1="24" x2="492" y2="294" />
|
||
<line x1="532" y1="24" x2="532" y2="294" />
|
||
<line x1="572" y1="24" x2="572" y2="294" />
|
||
<line x1="612" y1="24" x2="612" y2="294" />
|
||
</g>
|
||
<g class="min">
|
||
<line x1="52" y1="276" x2="652" y2="276" />
|
||
<line x1="52" y1="240" x2="652" y2="240" />
|
||
<line x1="52" y1="204" x2="652" y2="204" />
|
||
<line x1="52" y1="168" x2="652" y2="168" />
|
||
<line x1="52" y1="132" x2="652" y2="132" />
|
||
<line x1="52" y1="96" x2="652" y2="96" />
|
||
<line x1="52" y1="60" x2="652" y2="60" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="52" y1="258" x2="652" y2="258" />
|
||
<line x1="52" y1="222" x2="652" y2="222" />
|
||
<line x1="52" y1="186" x2="652" y2="186" />
|
||
<line x1="52" y1="150" x2="652" y2="150" />
|
||
<line x1="52" y1="114" x2="652" y2="114" />
|
||
<line x1="52" y1="78" x2="652" y2="78" />
|
||
<line x1="52" y1="42" x2="652" y2="42" />
|
||
</g>
|
||
<line class="ax" x1="52" y1="24" x2="52" y2="294" />
|
||
<line class="ax" x1="52" y1="294" x2="652" y2="294" />
|
||
<line class="ax" x1="652" y1="24" x2="652" y2="294" />
|
||
<g class="al curve-axis-temp" text-anchor="end">
|
||
<text data-tempc="60" x="47" y="296.8">60</text>
|
||
<text data-tempc="80" x="47" y="260.8">80</text>
|
||
<text data-tempc="100" x="47" y="224.8">100</text>
|
||
<text data-tempc="120" x="47" y="188.8">120</text>
|
||
<text data-tempc="140" x="47" y="152.8">140</text>
|
||
<text data-tempc="160" x="47" y="116.8">160</text>
|
||
<text data-tempc="180" x="47" y="80.8">180</text>
|
||
<text data-tempc="200" x="47" y="44.8">200</text>
|
||
</g>
|
||
<text
|
||
x="14"
|
||
y="159"
|
||
class="ttl"
|
||
transform="rotate(-90 14 159)"
|
||
text-anchor="middle"
|
||
id="print-curve-axis-title"
|
||
>
|
||
BEAN TEMP °C
|
||
</text>
|
||
<g class="al" text-anchor="middle">
|
||
<text x="52" y="304">0:00</text>
|
||
<text x="92" y="304">1:00</text>
|
||
<text x="132" y="304">2:00</text>
|
||
<text x="172" y="304">3:00</text>
|
||
<text x="212" y="304">4:00</text>
|
||
<text x="252" y="304">5:00</text>
|
||
<text x="292" y="304">6:00</text>
|
||
<text x="332" y="304">7:00</text>
|
||
<text x="372" y="304">8:00</text>
|
||
<text x="412" y="304">9:00</text>
|
||
<text x="452" y="304">10:00</text>
|
||
<text x="492" y="304">11:00</text>
|
||
<text x="532" y="304">12:00</text>
|
||
<text x="572" y="304">13:00</text>
|
||
<text x="612" y="304">14:00</text>
|
||
<text x="652" y="304">15:00</text>
|
||
</g>
|
||
<text x="368" y="320" style="font-size: 8px; fill: #5a5048">
|
||
TIME FROM CHARGE · solid = plan · dashed = reference
|
||
(.alog)
|
||
</text>
|
||
<g id="plan-curve"></g>
|
||
<g id="ref-curve"></g>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">11 · AT THE MACHINE — PLAN vs ACTUAL</span
|
||
><span class="ref">→ Step 11</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 28mm">Milestone</th>
|
||
<th class="num" style="width: 20mm">Plan time</th>
|
||
<th class="num" style="width: 22mm">Actual time</th>
|
||
<th class="num" style="width: 18mm">Actual BT</th>
|
||
<th>Note</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Charge</td>
|
||
<td class="num">
|
||
<output data-out="pa-charge">0:00</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.charge.actualTime"
|
||
placeholder="0:00"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.charge.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.charge.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Turning point</td>
|
||
<td class="num"><output data-out="pa-tp">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.tp.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.tp.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.tp.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Yellow</td>
|
||
<td class="num">
|
||
<output data-out="pa-yellow">—</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.yellow.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.yellow.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.yellow.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>First crack</td>
|
||
<td class="num"><output data-out="pa-fc">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.fc.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.fc.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.fc.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Drop</td>
|
||
<td class="num"><output data-out="pa-drop">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.drop.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.drop.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.drop.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">12 · AFTER THE ROAST</span
|
||
><span class="ref">→ Step 12</span>
|
||
</div>
|
||
<div class="bd wsline tightln">
|
||
<span class="lb">Green in</span>
|
||
<input class="f n ws-input" name="afterRoast.greenIn" /><span
|
||
class="pv"
|
||
></span
|
||
>g <span class="sep">|</span><span class="lb">Out</span>
|
||
<input class="f n ws-input" name="afterRoast.out" /><span
|
||
class="pv"
|
||
></span
|
||
>g <span class="sep">|</span><span class="lb">Weight loss</span>
|
||
<input
|
||
class="f n ws-input"
|
||
name="afterRoast.weightLossPct"
|
||
/><span class="pv"></span>% <span class="sep">|</span
|
||
><span class="lb">Actual DTR</span>
|
||
<input
|
||
class="f n ws-input"
|
||
name="afterRoast.actualDtrPct"
|
||
/><span class="pv"></span>%<br />
|
||
<span class="lb">Colour WB/GR</span>
|
||
<input class="f ws-input" name="afterRoast.colour" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="lb">Rested</span>
|
||
<input class="f n ws-input" name="afterRoast.restedDays" /><span
|
||
class="pv"
|
||
></span
|
||
>days <span class="sep">|</span
|
||
><span class="lb">Brew ratio</span>
|
||
<input class="f ws-input" name="afterRoast.brewRatio" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="lb">Method</span>
|
||
<input class="f w ws-input" name="afterRoast.method" /><span
|
||
class="pv"
|
||
></span
|
||
><br />
|
||
<span class="lb">Cup notes</span>
|
||
<input
|
||
class="f xw ws-input"
|
||
style="min-width: 152mm"
|
||
name="afterRoast.cupNotes"
|
||
/><span class="pv"></span><br />
|
||
<span class="lb" style="color: var(--accent)"
|
||
>One change next batch</span
|
||
>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 58mm"
|
||
name="afterRoast.oneChange"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="lb">What would prove it wrong</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 34mm"
|
||
name="afterRoast.disproof"
|
||
/><span class="pv"></span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<datalist id="cultivar-list"></datalist>
|
||
|
||
<script type="module" src="/js/main.js?v=__ASSET_VERSION__"></script>
|
||
</body>
|
||
</html>
|