321 lines
16 KiB
HTML
321 lines
16 KiB
HTML
<!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 & 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 & 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 & 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: 28–34 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 & 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 28–34 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
|
||
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> |