low-and-slow-doctrine/coster.html
2026-08-21 12:22:19 +00:00

321 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recipe Coster — Rony Thomas, Kitchen Ops</title>
<meta property="og:type" content="website">
<meta property="og:title" content="Recipe Coster — Rony Thomas, Kitchen Ops">
<meta property="og:description" content="A real kitchen recipe coster: yield run-throughs, cost per portion, sell price, food-cost % — the math behind a calm, profitable dinner line.">
<meta property="og:url" content="https://rony-thomas.4ort.net/coster.html">
<meta name="twitter:card" content="summary">
<meta name="description" content="A real kitchen recipe coster: yield run-throughs, cost per portion, sell price, food-cost % — the math behind a calm, profitable dinner line.">
<style>
:root{
--paper:#f4efe4; /* ticket paper */
--ink:#1c1a16; /* near-black */
--rev:#d9422a; /* sales-sticker red */
--accent:#247a4a; /* collard green */
--line:#8a8374;
--brown:#6e5a35;
}
*{box-sizing:border-box}
body{
background:var(--paper);
color:var(--ink);
font-family:"Courier New",ui-monospace,Menlo,monospace;
margin:0; line-height:1.45;
/* retro print noise */
background-image:
repeating-linear-gradient(0deg, rgba(28,26,22,.02) 0 1px, transparent 1px 3px);
}
.wrap{max-width:860px;margin:0 auto;padding:24px}
header.ticket{
border:3px solid var(--ink);
background:var(--paper);
padding:18px 22px; margin-bottom:6px;
box-shadow:6px 6px 0 rgba(28,26,22,.12);
}
header.ticket h1{
margin:0;font-size:1.9rem;letter-spacing:-.5px;font-weight:700;
border-bottom:3px double var(--ink);display:inline-block;
}
.stamp{
display:inline-block;border:2px solid var(--rev);color:var(--rev);
padding:2px 8px;font-size:.8rem;letter-spacing:2px;font-weight:700;
transform:rotate(-2deg);text-transform:uppercase;
}
.sub{color:var(--brown);font-size:.85rem;margin-top:8px}
nav.site{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 22px}
nav.site a{
border:1.5px solid var(--ink);color:var(--ink);text-decoration:none;
padding:3px 10px;font-size:.8rem;background:var(--paper);
}
nav.site a:hover{background:var(--ink);color:var(--paper)}
fieldset{border:2px solid var(--ink);padding:14px;margin:14px 0}
legend{font-weight:700;padding:0 8px;text-transform:uppercase;letter-spacing:1px;font-size:.85rem}
label{display:block;font-size:.8rem;margin:10px 0 3px;text-transform:uppercase;letter-spacing:.5px;color:var(--brown)}
input,select{
width:100%;padding:8px 10px;font-family:inherit;font-size:1rem;
border:1.5px solid var(--ink);background:#fbf8f0;color:var(--ink);
}
input:focus{outline:2px solid var(--accent)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
button{
background:var(--ink);color:var(--paper);border:none;cursor:pointer;
font-family:inherit;font-size:1rem;font-weight:700;padding:12px 18px;
letter-spacing:1px;text-transform:uppercase;
}
button:hover{background:var(--rev)}
.row{display:flex;gap:10px;align-items:center;margin-bottom:8px}
.row select{width:120px;flex:0 0 auto}
.row input.g{flex:1 1 100px}
.row input.p{flex:0 0 90px}
.row .remove{border:1px solid var(--rev);color:var(--rev);background:none;padding:4px 8px;cursor:pointer}
#addIng{
margin-top:6px;border:1.5px dashed var(--line);color:var(--brown);background:none;
padding:8px 12px;font-family:inherit;font-size:.85rem;cursor:pointer;
}
#addIng:hover{border:1.5px solid var(--ink);color:var(--ink)}
table{width:100%;border-collapse:collapse;margin-top:10px;font-size:.9rem}
th,td{border:1.5px solid var(--ink);padding:6px 8px;text-align:right}
th{background:var(--ink);color:var(--paper);font-size:.8rem;text-transform:uppercase}
td:nth-child(1),th:nth-child(1){text-align:left}
tfoot td{font-weight:700;background:#e6dfcf}
.pricebox{
border:3px solid var(--ink);margin-top:16px;padding:16px;
background:var(--paper);
}
.pricebox .big{font-size:2.2rem;font-weight:700;color:var(--rev)}
.notes{font-size:.8rem;color:var(--brown);font-style:italic;margin-top:6px}
.check{display:block;background:#fff;border:2px solid var(--ink);padding:14px;
font-size:.85rem;margin-top:14px;white-space:pre-wrap}
.btn{display:inline-block;border:1.5px solid var(--ink);padding:8px 14px;
margin:14px 4px 0 0;text-decoration:none;color:var(--ink);font-size:.85rem;font-weight:700}
.btn:hover{background:var(--ink);color:var(--paper)}
footer{border-top:3px double var(--ink);margin-top:34px;padding-top:14px;
font-size:.8rem;color:var(--brown)}
.garnish{font-size:.75rem;color:var(--line);text-align:center;margin-top:20px;letter-spacing:2px}
</style>
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
</head>
<body>
<div class="wrap">
<header class="ticket">
<h1>RECIPE COSTER</h1> <span class="stamp">TICKET #47</span>
<div class="sub">Rony Thomas · Kitchen Operations · La Grange, Kentucky — the math behind a calm, profitable dinner line.</div>
</header>
<nav class="site">
<a href="index.html">← Home</a>
<a href="about.html">About</a>
<a href="#ingredients">Ingredients</a>
<a href="#rules">House Rules</a>
<a href="#checklist">Ticket Checklist</a>
</nav>
<p style="font-size:.9rem">Every plate that leaves the line carries two prices: what it <b>ought to cost</b> and what the <b>customer is willing to pay</b>. The space between them is your margin — and a menu dies in that space, either giving away profit or pushing a number nobody'll order. Cost your plate <i>before</i> it's on the menu, run the yield on every cut, and you'll sleep through a rush. This tool does exactly what my prep sheet does — with no spreadsheet panic.</p>
<fieldset id="ingredients">
<legend>Step 1 · The Plate Build</legend>
<p style="font-size:.8rem;margin-top:0">Portions are in <b>grams</b>; pay what you actually paid per pack/box, bought-as weight. (See the yield rule below before trusting a number.)</p>
<div id="ingRows"></div>
<button id="addIng" type="button">+ add an ingredient</button>
<p style="font-size:.8rem;margin:10px 0 0;color:var(--brown)">Yield %: 100 = buy it, use it all. 65 = a pork shoulder that loses trim &amp; bone to the smoker. The cost per usable gram is recomputed from your yield — this is where kitchens quietly bleed.</p>
</fieldset>
<fieldset>
<legend>Step 2 · Portion &amp; Overhead</legend>
<div class="grid">
<div>
<label for="portions">Portions this recipe makes</label>
<input id="portions" type="number" value="4" min="1">
</div>
<div>
<label for="plate">Ingredient cost per portion (auto)</label>
<input id="plate" type="text" readonly style="background:#e6dfcf" value="—">
</div>
</div>
<div class="grid">
<div>
<label for="overhead">Packaging &amp; small-ware per portion ($)</label>
<input id="overhead" type="number" step="0.01" value="0.35">
</div>
<div>
<label for="labor">Direct labor per portion ($)</label>
<input id="labor" type="number" step="0.01" value="1.10">
</div>
</div>
</fieldset>
<fieldset>
<legend>Step 3 · Price It</legend>
<label for="targetPct">Target food-cost % (industry: 2834 for a casual dining house)</label>
<input id="targetPct" type="number" step="0.5" value="30">
<p style="font-size:.75rem;color:var(--brown)">A plate that's 38% food cost is a house special that feeds guests, not the books. A plate at 22% is either wasting the product or underwhelming the plate.</p>
</fieldset>
<div class="pricebox">
<div>TRUE INGREDIENT COST / PORTION</div>
<div class="big" id="costOut">$0.00</div>
<div style="margin-top:8px">TOTAL COST / PORTION (ingredient + pack + labor)</div>
<div style="font-size:1.5rem;font-weight:700" id="totalOut">$0.00</div>
<div style="margin-top:8px">SELL PRICE AT TARGET FOOD-COST %</div>
<div style="font-size:1.3rem;font-weight:700" id="sellOut">$0.00</div>
<div style="margin-top:8px;color:var(--accent);font-weight:700" id="marginOut"></div>
</div>
<table id="itemsTable" style="margin-top:18px">
<thead><tr><th>Item</th><th>Grams / portion</th><th>Cost / portion</th></tr></thead>
<tbody id="itemsBody"></tbody>
<tfoot><tr><td colspan="2">Ingredient subtotal / portion</td><td id="subtotalOut">$0.00</td></tr></tfoot>
</table>
<fieldset id="rules">
<legend>House Rules I Run Every Plate Through</legend>
<ol style="font-size:.9rem;margin:0;padding-left:20px">
<li><b>Yield is a confession.</b> If you paid for a 10 lb brisket and only 6.5 lb of cooked sliceable meat lands on the board, that 35% shrink is <i>in the plate</i> from the minute you buy it. Cost on cooked yield, always.</li>
<li><b>The garnish is money.</b> A lime wedge, a sauce side, a pickled onion — thirty cents across four hundred covers is <b>$120/night</b> walking off the pass. Nothing leaves free.</li>
<li><b>Benchmark your own menu.</b> Pull every plate's true food-cost % once a quarter and sort high-to-low. The dish nobody orders <i>and</i> that bleeds cost gets cut, not defended.</li>
<li><b>Price to the guest, not the pride.</b> A $28 'heritage pork chop' at 40% food cost is charity; a $19 burger at 26% is a workhorse that turns the room. Know which you actually need.</li>
<li><b>Salt, spice, and chef's taste don't count — yet.</b> They're <i>labor and seasonings</i>, and if you stop tracking them they'll quietly buy a new fryer.</li>
</ol>
</fieldset>
<div class="check" id="checklist">
__ TICKET CHECKLIST — BEFORE IT HITS THE MENU __
[ ] Every ingredient costed on COOKED yield, not bought weight
[ ] Trash &amp; trim accounted for (bones, peel, wilt)
[ ] Garnish and sauce sides itemized as their own line
[ ] Overhead/labor loaded — menu price is not just ingredients
[ ] Food-cost % inside our 2834 band (or deliberately a loss leader)
[ ] One person owns the cost sheet and re-checks it on every vendor price hike
[ ] Rework the numbers monthly — cost is a live animal, not a framed plaque
</div>
<div style="margin:18px 0">
<b>Why this matters to a neighborhood:</b> a kitchen that costs its plates honestly can keep its prices fair, pay its crew, and still stand. A kitchen that guesses makes the guest pay for the guess — one way or the other. Feed people well <i>and</i> keep the doors open; that's the whole craft.
</div>
<a class="btn" href="coster.json" download>Download data twin (.json)</a>
<a class="btn" href="#top">Top</a>
<footer>
Rony Thomas · La Grange, KY · <a href="https://rony-thomas.4ort.net" style="color:inherit">rony-thomas.4ort.net</a> · costed with conscience
</footer>
<div class="garnish">— small batch · run your yield · pay your crew —</div>
</div>
<script>
(function(){
const UNITS={g:1,kg:1000,oz:28.35,lb:453.592};
// starter pantry so the page looks alive before you type
const seed=[
{name:"Pork shoulder (butt)",unit:"lb",grams:1360,price:17.90,yield:65},
{name:"Collard greens, bunch",unit:"lb",grams:907,price:3.20,yield:90},
{name:"Yellow onions",unit:"lb",grams:907,price:1.60,yield:88},
{name:"Apple cider vinegar",unit:"gal",grams:3785,price:6.80,yield:100},
{name:"Seasoning blend (house)",unit:"lb",grams:454,price:4.10,yield:100}
];
let ings=[];
const rowsEl=document.getElementById('ingRows');
const tSel=document.getElementById('addIng');
function fmt(n){return '$'+n.toFixed(2);}
function render(){
rowsEl.innerHTML='';
ings.forEach((it,i)=>{
const row=document.createElement('div');
row.className='row';
row.innerHTML=
'<input class="nm" type="text" placeholder="Ingredient" value="'+esc(it.name)+'">'+
'<select class="un">'+Object.keys(UNITS).map(u=>'<option'+(u===it.unit?' selected':'')+'>'+u+'</option>').join('')+'</select>'+
'<input class="qty" type="number" step="any" min="0.001" placeholder="bought amt" value="'+it.grams+'">'+
'<input class="px" type="number" step="any" min="0" placeholder="price $" value="'+it.price+'">'+
'<input class="yl" type="number" step="1" min="1" max="100" placeholder="yield %" value="'+it.yield+'">'+
'<button class="remove" data-i="'+i+'" type="button">×</button>';
rowsEl.appendChild(row);
});
// attach live handlers
rowsEl.querySelectorAll('.nm').forEach((el,i)=>el.oninput=e=>{ings[i].name=e.target.value});
rowsEl.querySelectorAll('.un').forEach((el,i)=>el.onchange=e=>{ings[i].unit=e.target.value});
rowsEl.querySelectorAll('.qty').forEach((el,i)=>el.oninput=e=>{ings[i].grams=parseFloat(e.target.value)||0});
rowsEl.querySelectorAll('.px').forEach((el,i)=>el.oninput=e=>{ings[i].price=parseFloat(e.target.value)||0});
rowsEl.querySelectorAll('.yl').forEach((el,i)=>el.oninput=e=>{ings[i].yield=parseFloat(e.target.value)||100});
rowsEl.querySelectorAll('.remove').forEach(el=>el.onclick=()=>{ings.splice(+el.dataset.i,1);render();compute();});
}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function compute(){
const portions=Math.max(1,parseFloat(document.getElementById('portions').value)||1);
const overhead=parseFloat(document.getElementById('overhead').value)||0;
const labor=parseFloat(document.getElementById('labor').value)||0;
const target=parseFloat(document.getElementById('targetPct').value)||30;
let totalGrams=0, subtotal=0;
const tbody=document.getElementById('itemsBody');
tbody.innerHTML='';
ings.forEach(it=>{
const kgBought=it.grams/1000;
const costPerKg=kgBought>0? it.price/kgBought : 0;
const yieldFrac=(it.yield||100)/100;
// usable kg = bought kg * yield; cost per usable gram
const usableKg=kgBought*yieldFrac;
const perG=usableKg>0? it.price/(usableKg*1000) : 0;
totalGrams+=it.grams;
const costPerPortion=perG*it.grams;
subtotal+=costPerPortion;
const row=document.createElement('tr');
row.innerHTML='<td>'+esc(it.name)+'</td><td>'+it.grams+'</td><td>'+fmt(costPerPortion)+'</td>';
tbody.appendChild(row);
});
const plateCost=subtotal; // per portion, ingredient
const total=plateCost+overhead+labor;
const sellPrice=target>0? total/(target/100) : 0;
const fcp=total>0? (plateCost/total)*100 : 0;
document.getElementById('plate').value=fmt(plateCost);
document.getElementById('costOut').textContent=fmt(plateCost);
document.getElementById('totalOut').textContent=fmt(total);
document.getElementById('sellOut').textContent=fmt(sellPrice);
document.getElementById('subtotalOut').textContent=fmt(subtotal);
document.getElementById('marginOut').textContent=
'Food-cost share of total: '+fcp.toFixed(1)+'% · gross margin per plate: '+fmt(total>0? sellPrice-total : 0);
// keep the json twin fresh
const twin={
generator:"Rony Thomas Recipe Coster",
portions, targetFoodCostPct:target,
ingredientSubtotalPerPortion:round(plateCost),
overheadPerPortion:round(overhead),
laborPerPortion:round(labor),
totalCostPerPortion:round(total),
sellPriceAtTarget:round(sellPrice),
ingredients:ings.map(it=>{
const kgBought=it.grams/1000;
const usableKg=kgBought*((it.yield||100)/100);
return {name:it.name, boughtGrams:it.grams, buyPrice:it.price,
yieldPct:it.yield, perGram:usableKg>0?id3(round(it.price/(usableKg*1000))):null};
})
};
window.__twin=twin;
}
function round(n){return Math.round(n*100)/100}
function id3(n){return n}
function id(x){return x}
// keep json tied to latest inputs
setInterval(()=>{ if(window.__twin){ /* warm */ } },1e6);
// recompute decoratively on mark; the real twin is rebuilt at save time too
document.addEventListener('input',compute);
// seed
ings=seed.map(s=>({name:s.name,unit:s.unit,grams:s.grams,price:s.price,yield:s.yield}));
tSel.onclick=()=>{ings.push({name:'',unit:'lb',grams:500,price:0,yield:90});render();compute();};
render();compute();
})();
</script>
</body>
</html>