-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
1237 lines (1149 loc) · 72.8 KB
/
Copy pathindex.html
File metadata and controls
1237 lines (1149 loc) · 72.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<script>
// Immediately purge legacy cache from previous versions
try {
if (localStorage.getItem('bakecost_recipes_v1')) {
localStorage.removeItem('bakecost_recipes_v1');
}
if (localStorage.getItem('bakecost_recipes')) {
localStorage.removeItem('bakecost_recipes');
}
if ('caches' in window) {
caches.keys().then(keys => {
keys.forEach(key => {
if (key.includes('v1') || key !== 'bakecost-en-v2') {
caches.delete(key);
}
});
});
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(r => r.update());
});
}
} catch (e) {}
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BakeCost Studio — Recipe Costing & Food Pricing Calculator</title>
<meta name="description" content="Free recipe costing, ingredient unit conversion, labor overhead allocation, and food cost margin calculator for independent bakers, cottage bakeries, and café operators. 100% private in-browser tool.">
<meta name="keywords" content="recipe cost calculator, bakery pricing calculator, food cost percentage, cottage bakery pricing, baking cost sheet, recipe margin calculator">
<meta property="og:image" content="https://bake.skillary.ai/logo.svg">
<meta name="twitter:image" content="https://bake.skillary.ai/logo.svg">
<!-- Canonical & OpenGraph Meta Tags -->
<link rel="canonical" href="https://bake.skillary.ai/">
<link rel="alternate" type="text/plain" href="https://bake.skillary.ai/llms.txt" title="LLMs Context">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
<meta name="keywords" content="bakery recipe costing calculator, food cost margin calculator, cottage bakery pricing, baking density converter, recipe cost sheet, baking moisture loss">
<meta property="og:title" content="BakeCost Studio — Recipe Costing & Bakery Pricing Calculator">
<meta property="og:description" content="Free recipe costing, ingredient unit conversion, labor overhead allocation, and food cost margin calculator for independent bakers, cottage bakeries, and café operators.">
<meta property="og:url" content="https://bake.skillary.ai/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="BakeCost Studio">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="BakeCost Studio — Recipe Costing & Bakery Pricing Calculator">
<meta name="twitter:description" content="Free recipe costing, ingredient unit conversion, labor overhead allocation, and food cost margin calculator for independent bakers, cottage bakeries, and café operators.">
<!-- Structured Data: SoftwareApplication & FAQPage -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
"@id": "https://bake.skillary.ai/#app",
"name": "BakeCost Studio",
"url": "https://bake.skillary.ai/",
"operatingSystem": "All modern browsers (Windows, macOS, iOS, Android, Linux)",
"applicationCategory": "BusinessApplication",
"applicationSubCategory": "Food & Beverage Costing Software",
"browserRequirements": "Requires JavaScript. Requires HTML5 LocalStorage.",
"inLanguage": "en",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"description": "Free, 100% client-side recipe costing, ingredient volume-to-weight density conversion, oven evaporation loss, and food margin pricing calculator for independent cottage bakeries and culinary businesses.",
"featureList": [
"100% Client-Side Privacy: No user data or proprietary formulas sent to remote servers",
"Built-in 40+ ingredient density conversions between volume (cups/tbsp/tsp) and weight (grams/oz)",
"Prep trim loss (yield percentage) adjustments for raw produce and zest",
"Oven baking moisture evaporation loss calculation for accurate net baked weight",
"True batch COGS calculation factoring packaging supplies, labor rates, and kitchen utilities",
"Dynamic target food cost margin pricing slider with markup multiplier feedback",
"One-click printable batch recipe production cost cards"
]
},
{
"@type": "HowTo",
"@id": "https://bake.skillary.ai/#howto-costing",
"name": "How to Accurately Cost a Bakery Recipe and Price for Profit",
"description": "A step-by-step culinary accounting workflow to calculate total batch cost, account for baking shrinkage, and price baked goods to ensure target profit margins.",
"step": [
{
"@type": "HowToStep",
"name": "Normalize Ingredient Quantities to Weight",
"text": "Convert volume measurements (cups, tablespoons, teaspoons) into metric grams using verified ingredient bulk densities to eliminate volumetric packing variance."
},
{
"@type": "HowToStep",
"name": "Factor Prep Trim and Yield Percentage",
"text": "Account for prep waste (e.g. peeling, hulling berries, zesting citrus) by dividing purchase unit cost by the usable yield percentage (Edible Portion / As Purchased)."
},
{
"@type": "HowToStep",
"name": "Measure Oven Moisture Evaporation Loss",
"text": "Determine raw batter or dough weight versus finished baked weight to compute water loss percentage (typically 5% to 18%) for accurate per-100g and per-serving costing."
},
{
"@type": "HowToStep",
"name": "Aggregate Packaging Supplies, Direct Labor, and Overhead",
"text": "Add packaging items (cake boxes, parchment liners, ribbon, labels) plus direct labor hours multiplied by your target hourly rate, and add a kitchen utility/rent overhead surcharge."
},
{
"@type": "HowToStep",
"name": "Apply Target Food Cost Margin Markup",
"text": "Divide Total Batch Cost by your target food cost percentage (e.g., 28% sweet spot = divide by 0.28, or 3.57x markup) to establish the minimum viable retail price per serving and per batch."
}
]
},
{
"@type": "FAQPage",
"@id": "https://bake.skillary.ai/#faq",
"mainEntity": [
{
"@type": "Question",
"name": "How do you calculate recipe food cost accurately?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Recipe food cost is calculated by summing the net cost of every ingredient used in the recipe (purchase price divided by package size, adjusted for prep trim and yield percentage), adding primary and secondary packaging materials, allocating labor hours at your designated hourly rate, and applying a proportional kitchen utility and rent overhead allocation."
}
},
{
"@type": "Question",
"name": "What is an ideal food cost percentage for a cottage or artisanal bakery?",
"acceptedAnswer": {
"@type": "Answer",
"text": "For artisanal and cottage bakeries, a healthy target food cost percentage sits between 25% and 32% (equating to a 3.1x to 4.0x markup over raw costs). For custom cake decorating with intensive labor, food cost should be kept below 20%-25% to cover skilled decorating time."
}
},
{
"@type": "Question",
"name": "Why should bakers convert cups and tablespoons to grams?",
"acceptedAnswer": {
"@type": "Answer",
"text": "A standard US cup of flour can weigh anywhere between 115g and 160g depending on sifting, humidity, and scooping technique — a variance of up to 40%. Weighing in grams ensures exact chemical consistency in recipes and eliminates inventory shrinkage and margin leakage."
}
},
{
"@type": "Question",
"name": "How does oven moisture evaporation affect batch pricing?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Baking causes water evaporation that reduces product weight by 5% to 18%. If you price items based on finished weight or portion sizes without accounting for moisture loss, you undercount the required raw ingredient mass, resulting in reduced profitability."
}
},
{
"@type": "Question",
"name": "How does BakeCost Studio protect proprietary recipes?",
"acceptedAnswer": {
"@type": "Answer",
"text": "BakeCost Studio executes 100% in your local browser using LocalStorage. No recipes, formulas, supplier prices, or client data are ever transmitted to any cloud servers or third parties. It functions fully offline."
}
},
{
"@type": "Question",
"name": "Can I export and print kitchen recipe cost sheets?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. BakeCost Studio includes a one-click Export Cost Card feature that generates a clean, printable recipe production card or downloadable high-resolution image formatted for kitchen binders and culinary records."
}
}
]
},
{
"@type": "Dataset",
"@id": "https://bake.skillary.ai/#dataset-densities",
"name": "Commercial Baking Ingredient Density & Volume-to-Weight Conversion Dataset",
"description": "A curated reference database of 40+ common baking ingredients containing empirical bulk density values (g/ml) for instant two-way conversion between volume and metric weight.",
"license": "https://creativecommons.org/publicdomain/zero/1.0/",
"isAccessibleForFree": true,
"creator": {
"@type": "Organization",
"name": "BakeCost Studio",
"url": "https://bake.skillary.ai/"
}
}
]
}
</script>
<link rel="icon" type="image/svg+xml" href="logo.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
oatmeal: '#FAF8F5',
espresso: '#2D2825',
caramel: {
50: '#FFFBEB', 100: '#FEF3C7', 200: '#FDE68A', 300: '#FCD34D',
400: '#FBBF24', 500: '#D97706', 600: '#B45309', 700: '#92400E',
800: '#78350F', 900: '#451A03'
}
},
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif']
}
}
}
};
</script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
html, body { background: #FAF8F5; font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; }
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
::-webkit-scrollbar { height: 8px; width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #D9770660; border-radius: 999px; }
.field-input {
min-height: 44px;
width: 100%;
border-radius: 0.65rem;
border: 1px solid rgba(45,40,37,0.14);
background: #ffffff;
padding: 0 0.7rem;
font-size: 0.875rem;
color: #2D2825;
outline: none;
transition: border-color .15s ease, box-shadow .15s ease;
}
.field-input:focus { border-color: #D97706; box-shadow: 0 0 0 3px rgba(217,119,6,0.15); }
select.field-input { min-height: 44px; }
textarea.field-input { min-height: 80px; padding-top: .6rem; }
.field-label { font-size: 0.72rem; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; color: rgba(45,40,37,0.55); margin-bottom: .3rem; display: block; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: .4rem; min-height: 44px; padding: 0 1rem; border-radius: 0.75rem; font-size: 0.875rem; font-weight: 600; transition: all .15s ease; white-space: nowrap; cursor: pointer; border: 1px solid transparent; }
.btn-primary { background: #D97706; color: #fff; }
.btn-primary:hover { background: #B45309; }
.btn-secondary { background: #fff; color: #2D2825; border-color: rgba(45,40,37,0.14); }
.btn-secondary:hover { border-color: #D97706; color: #B45309; }
.btn-ghost { background: transparent; color: #2D2825; }
.btn-ghost:hover { background: rgba(45,40,37,0.06); }
.btn-danger { background: #fff; color: #B91C1C; border-color: rgba(185,28,28,0.25); }
.btn-danger:hover { background: #FEF2F2; }
.icon-btn { display:inline-flex; align-items:center; justify-content:center; width:44px; height:44px; border-radius:0.65rem; }
.card { background: #fff; border: 1px solid rgba(45,40,37,0.08); border-radius: 1.1rem; box-shadow: 0 1px 2px rgba(45,40,37,0.04), 0 8px 24px -16px rgba(45,40,37,0.15); }
input[type=range].caramel-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 8px; border-radius: 999px; background: linear-gradient(to right, #D97706 0%, #D97706 var(--fill,30%), #F3E8D8 var(--fill,30%), #F3E8D8 100%); }
input[type=range].caramel-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; border-radius: 999px; background: #fff; border: 3px solid #B45309; box-shadow: 0 1px 4px rgba(0,0,0,0.25); cursor: pointer; margin-top: -7px; }
input[type=range].caramel-slider::-moz-range-thumb { width: 22px; height: 22px; border-radius: 999px; background: #fff; border: 3px solid #B45309; box-shadow: 0 1px 4px rgba(0,0,0,0.25); cursor: pointer; }
input[type=range].caramel-slider::-moz-range-track { height: 8px; border-radius: 999px; background: #F3E8D8; }
.fade-enter-active, .fade-leave-active { transition: opacity .18s ease, transform .18s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(6px); }
.modal-backdrop { background: rgba(45,40,37,0.55); backdrop-filter: blur(2px); }
@media print {
body * { visibility: hidden; }
#exportCard, #exportCard * { visibility: visible; }
#exportCard { position: absolute; top: 0; left: 0; width: 100%; margin: 0; box-shadow: none; border: none; }
#printOnlyWrap { position: static !important; }
}
</style>
</head>
<body class="bg-oatmeal text-espresso antialiased">
<div id="app">
<!-- ============ HEADER ============ -->
<header class="sticky top-0 z-30 bg-oatmeal/95 backdrop-blur border-b border-espresso/10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl overflow-hidden shadow-md shrink-0 flex items-center justify-center"><img src="logo.svg" alt="Logo" class="w-full h-full object-cover" /></div>
<div class="leading-tight">
<h1 class="font-extrabold text-lg sm:text-xl text-espresso">BakeCost Studio</h1>
<p class="text-[11px] sm:text-xs text-espresso/55 -mt-0.5">Recipe Costing & Food Pricing Calculator</p>
</div>
</div>
<div class="flex items-center gap-2">
<button @click="showBackupModal = true" class="btn btn-secondary hidden sm:inline-flex">
<i data-lucide="database" class="w-4 h-4"></i> Backup & Restore
</button>
<button @click="showBackupModal = true" class="icon-btn btn-secondary sm:hidden">
<i data-lucide="database" class="w-4 h-4"></i>
</button>
<button @click="openExportModal" class="btn btn-primary">
<i data-lucide="download" class="w-4 h-4"></i> <span class="hidden sm:inline">Export Cost Card</span><span class="sm:hidden">Export</span>
</button>
</div>
</div>
</header>
<!-- ============ RECIPE SWITCHER ============ -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 pt-4">
<div class="flex items-center gap-2 overflow-x-auto pb-1">
<button v-for="r in recipes" :key="r.id" @click="activeId = r.id"
class="shrink-0 flex items-center gap-2 pl-2.5 pr-3.5 h-11 rounded-xl border text-sm font-semibold transition"
:class="r.id === activeId ? 'bg-caramel-500 border-caramel-500 text-white shadow-sm' : 'bg-white border-espresso/10 text-espresso/75 hover:border-caramel-500/50'">
<span class="text-lg leading-none">{{ r.icon }}</span>
<span class="max-w-[9rem] truncate">{{ r.name || 'Untitled Recipe' }}</span>
</button>
<button @click="addNewRecipe" class="shrink-0 flex items-center gap-1.5 h-11 px-3.5 rounded-xl border border-dashed border-caramel-600/50 text-caramel-700 text-sm font-semibold hover:bg-caramel-50">
<i data-lucide="plus" class="w-4 h-4"></i> New Recipe
</button>
</div>
</div>
<!-- ============ MAIN ============ -->
<main v-if="activeRecipe" class="max-w-7xl mx-auto px-4 sm:px-6 pb-28 mt-4 grid grid-cols-1 lg:grid-cols-3 gap-5">
<!-- LEFT / CENTER COLUMN -->
<div class="lg:col-span-2 space-y-5">
<!-- Recipe Basics -->
<section class="card p-4 sm:p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="font-bold text-base flex items-center gap-2"><i data-lucide="notebook-pen" class="w-4 h-4 text-caramel-600"></i> Recipe Details</h2>
<div class="flex items-center gap-1.5">
<button @click="duplicateRecipe" class="icon-btn btn-ghost" title="Duplicate Recipe"><i data-lucide="copy" class="w-4 h-4"></i></button>
<button @click="deleteRecipe(activeRecipe.id)" class="icon-btn btn-ghost text-red-600" title="Delete Recipe"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
</div>
</div>
<div class="mb-4">
<label class="field-label">Recipe Icon</label>
<div class="flex flex-wrap gap-2">
<button v-for="ic in RECIPE_ICONS" :key="ic" @click="activeRecipe.icon = ic"
class="w-11 h-11 rounded-xl text-xl flex items-center justify-center border transition"
:class="activeRecipe.icon === ic ? 'border-caramel-500 bg-caramel-50 ring-2 ring-caramel-500/40' : 'border-espresso/10 bg-white hover:border-caramel-500/40'">
{{ ic }}
</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div class="sm:col-span-3">
<label class="field-label">Recipe Name</label>
<input v-model="activeRecipe.name" type="text" class="field-input" placeholder="e.g. French Strawberry Cheese Tart">
</div>
<div>
<label class="field-label">Batch Yield (Servings)</label>
<input v-model.number="activeRecipe.servings" type="number" min="1" step="1" class="field-input num">
</div>
<div>
<label class="field-label">Baking Loss (Water Evaporation %)</label>
<input v-model.number="activeRecipe.bakingLossPct" type="number" min="0" max="90" step="0.5" class="field-input num">
</div>
<div>
<label class="field-label">Est. Final Baked Weight</label>
<div class="field-input flex items-center bg-espresso/[0.03] text-espresso/70 num">{{ fmtNum(finalBakedWeightG,0) }} g</div>
</div>
</div>
<p class="text-xs text-espresso/45 mt-3">Approx. weight per serving after baking loss: <span class="num font-semibold text-espresso/70">{{ fmtNum(weightPerServingG,1) }} g</span></p>
</section>
<!-- Ingredients -->
<section class="card p-4 sm:p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="font-bold text-base flex items-center gap-2"><i data-lucide="wheat" class="w-4 h-4 text-caramel-600"></i> Ingredients & Raw Materials</h2>
<span class="text-xs text-espresso/45 num">{{ activeRecipe.ingredients.length }} item(s)</span>
</div>
<div class="space-y-3">
<div v-for="(ing, idx) in activeRecipe.ingredients" :key="ing.id" class="rounded-xl border border-espresso/10 bg-oatmeal/60 p-3 sm:p-4">
<div class="flex items-start gap-2 mb-3">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<div>
<label class="field-label">Preset Ingredient</label>
<select v-model="ing.presetKey" @change="applyPreset(ing)" class="field-input">
<option value="">Custom Ingredient…</option>
<option v-for="p in DENSITY_LIBRARY" :key="p.name" :value="p.name">{{ p.name }}</option>
</select>
</div>
<div>
<label class="field-label">Ingredient Name</label>
<input v-model="ing.name" type="text" class="field-input" placeholder="e.g. Unsalted Butter">
</div>
</div>
<button @click="removeIngredient(idx)" class="icon-btn btn-ghost text-red-600 mt-5 shrink-0" title="Remove Ingredient">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
<div>
<label class="field-label">Purchase Price ($)</label>
<input v-model.number="ing.purchasePrice" type="number" min="0" step="0.01" class="field-input num">
</div>
<div>
<label class="field-label">Package Size</label>
<div class="flex gap-1.5">
<input v-model.number="ing.packageSize" type="number" min="0" step="0.01" class="field-input num w-1/2">
<select v-model="ing.packageUnit" class="field-input w-1/2">
<optgroup label="Weight">
<option v-for="u in WEIGHT_UNITS" :key="u" :value="u">{{ u }}</option>
</optgroup>
<optgroup label="Volume">
<option v-for="u in VOLUME_UNITS" :key="u" :value="u">{{ u }}</option>
</optgroup>
</select>
</div>
</div>
<div>
<label class="field-label">Recipe Usage</label>
<div class="flex gap-1.5">
<input v-model.number="ing.usageAmount" type="number" min="0" step="0.01" class="field-input num w-1/2">
<select v-model="ing.usageUnit" class="field-input w-1/2">
<optgroup label="Weight">
<option v-for="u in WEIGHT_UNITS" :key="u" :value="u">{{ u }}</option>
</optgroup>
<optgroup label="Volume">
<option v-for="u in VOLUME_UNITS" :key="u" :value="u">{{ u }}</option>
</optgroup>
</select>
</div>
</div>
<div>
<label class="field-label">Yield / Trim %</label>
<input v-model.number="ing.yieldPct" type="number" min="1" max="100" step="1" class="field-input num">
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2.5 mt-2.5">
<div>
<label class="field-label">Density (g/ml)</label>
<input v-model.number="ing.density" type="number" min="0.01" step="0.01" class="field-input num">
</div>
<div class="flex flex-col justify-end text-right">
<label class="field-label text-right">Unit Cost</label>
<div class="h-11 flex items-center justify-end text-sm font-semibold num">{{ fmtMoney(unitCostPerGram(ing)) }} / g</div>
</div>
<div class="flex flex-col justify-end text-right">
<label class="field-label text-right">Line Cost</label>
<div class="h-11 flex items-center justify-end text-base font-bold text-caramel-700 num">{{ fmtMoney(ingredientLineCost(ing)) }}</div>
</div>
</div>
<p class="text-[11px] text-espresso/40 mt-2 num">≈ {{ fmtNum(ingredientUsageGrams(ing),1) }} g / {{ fmtNum(ingredientUsageMl(ing),1) }} ml used in this recipe</p>
</div>
</div>
<button @click="addIngredient" class="btn btn-secondary mt-3.5 w-full sm:w-auto">
<i data-lucide="plus" class="w-4 h-4"></i> Add Ingredient
</button>
<div class="flex items-center justify-between mt-4 pt-3 border-t border-espresso/10">
<span class="text-sm font-semibold text-espresso/70">Ingredients Subtotal</span>
<span class="text-lg font-bold num text-caramel-700">{{ fmtMoney(ingredientsTotal) }}</span>
</div>
</section>
<!-- Packaging -->
<section class="card p-4 sm:p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="font-bold text-base flex items-center gap-2"><i data-lucide="package" class="w-4 h-4 text-caramel-600"></i> Packaging & Supplies</h2>
<span class="text-xs text-espresso/45 num">{{ activeRecipe.packaging.length }} item(s)</span>
</div>
<div class="space-y-2.5">
<div v-for="(pk, idx) in activeRecipe.packaging" :key="pk.id" class="grid grid-cols-12 gap-2.5 items-end">
<div class="col-span-12 sm:col-span-5">
<label class="field-label">Item Name</label>
<input v-model="pk.name" type="text" class="field-input" placeholder="e.g. Cake Box">
</div>
<div class="col-span-4 sm:col-span-2">
<label class="field-label">Unit Cost ($)</label>
<input v-model.number="pk.unitCost" type="number" min="0" step="0.01" class="field-input num">
</div>
<div class="col-span-4 sm:col-span-2">
<label class="field-label">Quantity</label>
<input v-model.number="pk.qty" type="number" min="0" step="1" class="field-input num">
</div>
<div class="col-span-3 sm:col-span-2 text-right">
<label class="field-label text-right">Line Cost</label>
<div class="h-11 flex items-center justify-end font-semibold num">{{ fmtMoney((pk.unitCost||0) * (pk.qty||0)) }}</div>
</div>
<div class="col-span-1 flex justify-end">
<button @click="removePackaging(idx)" class="icon-btn btn-ghost text-red-600"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
</div>
</div>
</div>
<button @click="addPackaging" class="btn btn-secondary mt-3.5 w-full sm:w-auto">
<i data-lucide="plus" class="w-4 h-4"></i> Add Packaging Item
</button>
<div class="flex items-center justify-between mt-4 pt-3 border-t border-espresso/10">
<span class="text-sm font-semibold text-espresso/70">Packaging Subtotal</span>
<span class="text-lg font-bold num text-caramel-700">{{ fmtMoney(packagingTotal) }}</span>
</div>
</section>
<!-- Labor & Overhead -->
<section class="card p-4 sm:p-5">
<h2 class="font-bold text-base flex items-center gap-2 mb-4"><i data-lucide="clock" class="w-4 h-4 text-caramel-600"></i> Labor & Kitchen Overhead</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="field-label">Prep Time (minutes)</label>
<input v-model.number="activeRecipe.laborMinutes" type="number" min="0" step="1" class="field-input num">
</div>
<div>
<label class="field-label">Hourly Rate ($/hr)</label>
<input v-model.number="activeRecipe.hourlyRate" type="number" min="0" step="0.5" class="field-input num">
</div>
<div>
<label class="field-label">Kitchen Overhead ($ / batch)</label>
<input v-model.number="activeRecipe.overheadAmount" type="number" min="0" step="0.1" class="field-input num">
</div>
</div>
<div class="flex items-center justify-between mt-4 pt-3 border-t border-espresso/10 flex-wrap gap-2">
<span class="text-sm font-semibold text-espresso/70">Labor Cost: <span class="num">{{ fmtMoney(laborCost) }}</span></span>
<span class="text-sm font-semibold text-espresso/70">Overhead: <span class="num">{{ fmtMoney(overheadCost) }}</span></span>
</div>
</section>
</div>
<!-- RIGHT / DASHBOARD COLUMN -->
<aside class="lg:col-span-1">
<div class="lg:sticky lg:top-24 space-y-5">
<!-- Totals -->
<section class="card p-4 sm:p-5 bg-espresso text-oatmeal border-espresso">
<h2 class="font-bold text-base flex items-center gap-2 mb-3 text-white"><i data-lucide="calculator" class="w-4 h-4 text-caramel-400"></i> Pricing Dashboard</h2>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-xl bg-white/10 p-3">
<p class="text-[11px] uppercase tracking-wide text-oatmeal/60">Total Batch Cost</p>
<p class="text-xl font-extrabold num mt-0.5">{{ fmtMoney(totalBatchCost) }}</p>
</div>
<div class="rounded-xl bg-white/10 p-3">
<p class="text-[11px] uppercase tracking-wide text-oatmeal/60">Cost / Serving</p>
<p class="text-xl font-extrabold num mt-0.5">{{ fmtMoney(costPerServing) }}</p>
</div>
<div class="rounded-xl bg-white/10 p-3">
<p class="text-[11px] uppercase tracking-wide text-oatmeal/60">Cost / 100g (baked)</p>
<p class="text-lg font-bold num mt-0.5">{{ fmtMoney(costPer100g) }}</p>
</div>
<div class="rounded-xl bg-white/10 p-3">
<p class="text-[11px] uppercase tracking-wide text-oatmeal/60">Weight / Serving</p>
<p class="text-lg font-bold num mt-0.5">{{ fmtNum(weightPerServingG,1) }} g</p>
</div>
</div>
<!-- Cost Breakdown Bar -->
<div class="mt-4">
<p class="text-[11px] uppercase tracking-wide text-oatmeal/60 mb-1.5">Cost Breakdown</p>
<div class="w-full h-2.5 rounded-full overflow-hidden flex bg-white/10">
<div v-for="b in breakdown" :key="b.label" :style="{ width: Math.max(0,b.pct) + '%', backgroundColor: b.color }"></div>
</div>
<div class="grid grid-cols-2 gap-x-3 gap-y-1 mt-2">
<div v-for="b in breakdown" :key="b.label" class="flex items-center gap-1.5 text-[11px] text-oatmeal/70">
<span class="w-2 h-2 rounded-full shrink-0" :style="{ backgroundColor: b.color }"></span>
<span class="flex-1 truncate">{{ b.label }}</span>
<span class="num">{{ fmtNum(b.pct,0) }}%</span>
</div>
</div>
</div>
</section>
<!-- Target Food Cost / Suggested Price -->
<section class="card p-4 sm:p-5">
<div class="flex items-center justify-between mb-1">
<label class="field-label mb-0">Target Food Cost %</label>
<span class="text-sm font-bold text-caramel-700 num">{{ fmtNum(activeRecipe.targetFoodCostPct,0) }}%</span>
</div>
<input type="range" min="5" max="60" step="1" v-model.number="activeRecipe.targetFoodCostPct"
class="caramel-slider" :style="{ '--fill': sliderFillPct + '%' }">
<div class="flex justify-between text-[10px] text-espresso/40 mt-1 mb-4">
<span>5%</span><span>60%</span>
</div>
<div class="rounded-xl bg-caramel-50 border border-caramel-200 p-3.5 text-center">
<p class="text-[11px] uppercase tracking-wide text-caramel-700/80">Suggested Retail Price</p>
<p class="text-3xl font-extrabold text-caramel-700 num mt-1">{{ fmtMoney(suggestedPrice) }}</p>
<p class="text-[11px] text-caramel-700/70 mt-1">per serving, at {{ fmtNum(activeRecipe.targetFoodCostPct,0) }}% food cost</p>
</div>
</section>
<!-- Custom Price & Net Profit -->
<section class="card p-4 sm:p-5">
<h2 class="font-bold text-base flex items-center gap-2 mb-3"><i data-lucide="tag" class="w-4 h-4 text-caramel-600"></i> Custom Price & Net Profit</h2>
<label class="field-label">Custom Selling Price ($ / serving)</label>
<input v-model.number="activeRecipe.customPrice" type="number" min="0" step="0.01" class="field-input num text-lg font-bold">
<div class="grid grid-cols-3 gap-2.5 mt-3.5">
<div class="rounded-xl bg-oatmeal border border-espresso/10 p-2.5 text-center">
<p class="text-[10px] uppercase text-espresso/45">Food Cost %</p>
<p class="text-sm font-bold num mt-0.5">{{ fmtNum(foodCostPctCustom,1) }}%</p>
</div>
<div class="rounded-xl bg-oatmeal border border-espresso/10 p-2.5 text-center">
<p class="text-[10px] uppercase text-espresso/45">Net Profit</p>
<p class="text-sm font-bold num mt-0.5" :class="netProfitCustom >= 0 ? 'text-emerald-700' : 'text-red-600'">{{ fmtMoney(netProfitCustom) }}</p>
</div>
<div class="rounded-xl bg-oatmeal border border-espresso/10 p-2.5 text-center">
<p class="text-[10px] uppercase text-espresso/45">Gross Margin %</p>
<p class="text-sm font-bold num mt-0.5">{{ fmtNum(grossMarginCustom,1) }}%</p>
</div>
</div>
</section>
</div>
</aside>
</main>
<!-- ============ SEMANTIC KNOWLEDGE BASE & GEO REFERENCE SECTION ============ -->
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-12 border-t border-espresso/10 mt-8" aria-labelledby="guide-heading">
<!-- Section Header -->
<div class="max-w-3xl mb-10">
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-caramel-100 text-caramel-800 text-xs font-semibold mb-3">
<span>📖</span> Culinary Accounting & Pricing Handbook
</div>
<h2 id="guide-heading" class="text-2xl sm:text-3xl font-extrabold text-espresso tracking-tight">
Bakery Recipe Costing & Food Margin Reference Guide
</h2>
<p class="text-sm sm:text-base text-espresso/70 mt-2 leading-relaxed">
A foundational guide for cottage bakers, pastry chefs, and cafe owners to eliminate margin leaks, calculate shrinkage accurately, and build sustainable retail pricing.
</p>
</div>
<!-- 4 Core Pillars Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 mb-12">
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<div class="w-9 h-9 rounded-xl bg-caramel-50 text-caramel-600 flex items-center justify-center font-bold text-lg mb-3">1</div>
<h3 class="font-bold text-espresso text-base mb-1.5">Weight vs. Volume</h3>
<p class="text-xs text-espresso/65 leading-relaxed">
A cup of flour can weigh between 115g and 160g depending on humidity and packing. Weighing every ingredient in grams eliminates a 25-40% variance in recipe batch consistency and ingredient expenditure.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<div class="w-9 h-9 rounded-xl bg-caramel-50 text-caramel-600 flex items-center justify-center font-bold text-lg mb-3">2</div>
<h3 class="font-bold text-espresso text-base mb-1.5">Prep Trim Loss (Yield %)</h3>
<p class="text-xs text-espresso/65 leading-relaxed">
Raw produce, eggshells, citrus zests, and trimmed dough margins represent unrecoverable mass. Dividing unit cost by usable yield percentage guarantees that waste is factored directly into wholesale price.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<div class="w-9 h-9 rounded-xl bg-caramel-50 text-caramel-600 flex items-center justify-center font-bold text-lg mb-3">3</div>
<h3 class="font-bold text-espresso text-base mb-1.5">Oven Evaporation</h3>
<p class="text-xs text-espresso/65 leading-relaxed">
Dough loses 5% to 18% of its initial raw weight in the oven due to water vaporization. Pricing finished items by raw dough weight undercharges for the concentrated ingredient density of finished loaves.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<div class="w-9 h-9 rounded-xl bg-caramel-50 text-caramel-600 flex items-center justify-center font-bold text-lg mb-3">4</div>
<h3 class="font-bold text-espresso text-base mb-1.5">The 28% Golden Ratio</h3>
<p class="text-xs text-espresso/65 leading-relaxed">
Maintaining raw food costs between 25% and 32% gives food artisans a 68% to 75% gross margin window to comfortably absorb kitchen rent, electricity, packaging, marketing, and founder wages.
</p>
</div>
</div>
<!-- Formulas & Math Breakdown -->
<div class="bg-white rounded-2xl p-6 sm:p-8 border border-espresso/10 shadow-sm mb-12">
<h3 class="text-lg sm:text-xl font-extrabold text-espresso mb-4 flex items-center gap-2">
<span>📐</span> Standard Culinary Costing Formulas
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs font-mono">
<div class="bg-oatmeal p-4 rounded-xl border border-espresso/10">
<p class="font-sans font-bold text-espresso text-sm mb-1">True Ingredient Line Cost</p>
<p class="text-caramel-700 font-semibold">[ (Purchase Price ÷ Package Size) × Usage Amount ] ÷ (Yield % ÷ 100)</p>
<p class="font-sans text-[11px] text-espresso/60 mt-1">Accounts for package bulk purchase discount and kitchen trim losses.</p>
</div>
<div class="bg-oatmeal p-4 rounded-xl border border-espresso/10">
<p class="font-sans font-bold text-espresso text-sm mb-1">Total Cost of Goods Sold (COGS)</p>
<p class="text-caramel-700 font-semibold">Ingredients Total + Packaging Total + (Labor Hours × Wage) + Overhead</p>
<p class="font-sans text-[11px] text-espresso/60 mt-1">Includes boxes, inserts, hourly kitchen rate, and utilities.</p>
</div>
<div class="bg-oatmeal p-4 rounded-xl border border-espresso/10">
<p class="font-sans font-bold text-espresso text-sm mb-1">Finished Net Baked Mass</p>
<p class="text-caramel-700 font-semibold">Sum(Raw Ingredient Grams) × [ 1 - (Baking Evaporation % ÷ 100) ]</p>
<p class="font-sans text-[11px] text-espresso/60 mt-1">Calculates true final loaf or pastry weight for compliance and portioning.</p>
</div>
<div class="bg-oatmeal p-4 rounded-xl border border-espresso/10">
<p class="font-sans font-bold text-espresso text-sm mb-1">Target Food Cost Retail Price</p>
<p class="text-caramel-700 font-semibold">Suggested Retail Price = Total Batch Cost ÷ (Target Food Cost % ÷ 100)</p>
<p class="font-sans text-[11px] text-espresso/60 mt-1">e.g. at 28% target food cost, multiply Total Batch Cost by 3.57x.</p>
</div>
</div>
</div>
<!-- High-Frequency Ingredient Density Reference Table -->
<div class="bg-white rounded-2xl p-6 sm:p-8 border border-espresso/10 shadow-sm mb-12">
<div class="flex items-center justify-between flex-wrap gap-2 mb-4">
<h3 class="text-lg sm:text-xl font-extrabold text-espresso flex items-center gap-2">
<span>⚖️</span> Common Bakery Ingredient Density Benchmarks
</h3>
<span class="text-xs text-espresso/50">Standards based on USDA Agricultural Data & Professional Pastry Baselines</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-xs border-collapse">
<thead>
<tr class="border-b border-espresso/15 text-espresso/60 font-semibold uppercase text-[10px] tracking-wider">
<th class="py-2.5 px-3">Ingredient</th>
<th class="py-2.5 px-3">Category</th>
<th class="py-2.5 px-3">Bulk Density (g/ml)</th>
<th class="py-2.5 px-3">1 US Cup Equivalent</th>
<th class="py-2.5 px-3">1 Tablespoon (tbsp)</th>
</tr>
</thead>
<tbody class="divide-y divide-espresso/10 text-espresso/80">
<tr><td class="py-2 px-3 font-semibold text-espresso">All-Purpose Flour (Wheat)</td><td class="py-2 px-3">Flours & Grains</td><td class="py-2 px-3 font-mono">0.528</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">125 g</td><td class="py-2 px-3 font-mono">7.8 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Bread Flour (High Gluten)</td><td class="py-2 px-3">Flours & Grains</td><td class="py-2 px-3 font-mono">0.537</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">127 g</td><td class="py-2 px-3 font-mono">7.9 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Cake Flour (Softened)</td><td class="py-2 px-3">Flours & Grains</td><td class="py-2 px-3 font-mono">0.482</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">114 g</td><td class="py-2 px-3 font-mono">7.1 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Almond Flour (Blanched)</td><td class="py-2 px-3">Nut Flours</td><td class="py-2 px-3 font-mono">0.406</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">96 g</td><td class="py-2 px-3 font-mono">6.0 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Granulated White Sugar</td><td class="py-2 px-3">Sweeteners</td><td class="py-2 px-3 font-mono">0.845</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">200 g</td><td class="py-2 px-3 font-mono">12.5 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Brown Sugar (Light/Dark Packed)</td><td class="py-2 px-3">Sweeteners</td><td class="py-2 px-3 font-mono">0.930</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">220 g</td><td class="py-2 px-3 font-mono">13.8 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Confectioners / Powdered Sugar</td><td class="py-2 px-3">Sweeteners</td><td class="py-2 px-3 font-mono">0.507</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">120 g</td><td class="py-2 px-3 font-mono">7.5 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Unsalted / Salted Butter</td><td class="py-2 px-3">Fats & Dairy</td><td class="py-2 px-3 font-mono">0.960</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">227 g (2 sticks)</td><td class="py-2 px-3 font-mono">14.2 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Heavy Whipping Cream (36%)</td><td class="py-2 px-3">Fats & Dairy</td><td class="py-2 px-3 font-mono">0.989</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">234 g</td><td class="py-2 px-3 font-mono">14.6 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Whole Milk</td><td class="py-2 px-3">Liquids & Dairy</td><td class="py-2 px-3 font-mono">1.031</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">244 g</td><td class="py-2 px-3 font-mono">15.3 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Pure Honey / Molasses</td><td class="py-2 px-3">Sweeteners</td><td class="py-2 px-3 font-mono">1.437</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">340 g</td><td class="py-2 px-3 font-mono">21.3 g</td></tr>
<tr><td class="py-2 px-3 font-semibold text-espresso">Semi-Sweet Chocolate Chips</td><td class="py-2 px-3">Chocolates</td><td class="py-2 px-3 font-mono">0.718</td><td class="py-2 px-3 font-mono font-bold text-caramel-700">170 g</td><td class="py-2 px-3 font-mono">10.6 g</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Comprehensive FAQ Accordion/Cards -->
<div class="mb-12">
<h3 class="text-lg sm:text-xl font-extrabold text-espresso mb-6 flex items-center gap-2">
<span>💡</span> Frequently Asked Questions by Independent Bakers
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<h4 class="font-bold text-espresso text-sm sm:text-base mb-2">How do you calculate recipe food cost accurately?</h4>
<p class="text-xs sm:text-sm text-espresso/70 leading-relaxed">
Sum the net cost of every ingredient used in the recipe (purchase price divided by package size, adjusted for prep trim and yield percentage), add primary and secondary packaging materials, allocate direct labor hours at your designated hourly rate, and apply a proportional kitchen utility and rent overhead allocation.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<h4 class="font-bold text-espresso text-sm sm:text-base mb-2">What is an ideal food cost percentage for a cottage bakery?</h4>
<p class="text-xs sm:text-sm text-espresso/70 leading-relaxed">
For artisanal and cottage bakeries, a healthy target food cost percentage sits between 25% and 32% (equating to a 3.1x to 4.0x markup over raw costs). For custom cake decorating with intensive labor, food cost should be kept below 20%-25% to cover skilled decorating time.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<h4 class="font-bold text-espresso text-sm sm:text-base mb-2">Why should bakers convert cups and tablespoons to grams?</h4>
<p class="text-xs sm:text-sm text-espresso/70 leading-relaxed">
A standard US cup of flour can weigh anywhere between 115g and 160g depending on sifting, humidity, and scooping technique — a variance of up to 40%. Weighing in grams ensures exact chemical consistency in recipes and eliminates inventory shrinkage and margin leakage.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<h4 class="font-bold text-espresso text-sm sm:text-base mb-2">How does oven moisture evaporation affect batch pricing?</h4>
<p class="text-xs sm:text-sm text-espresso/70 leading-relaxed">
Baking causes water evaporation that reduces product weight by 5% to 18%. If you price items based on finished weight or portion sizes without accounting for moisture loss, you undercount the required raw ingredient mass, resulting in reduced profitability.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<h4 class="font-bold text-espresso text-sm sm:text-base mb-2">How does BakeCost Studio protect proprietary recipes?</h4>
<p class="text-xs sm:text-sm text-espresso/70 leading-relaxed">
BakeCost Studio executes 100% in your local browser using LocalStorage. No recipes, formulas, supplier prices, or client data are ever transmitted to any cloud servers or third parties. It functions fully offline.
</p>
</div>
<div class="bg-white rounded-2xl p-5 border border-espresso/10 shadow-sm">
<h4 class="font-bold text-espresso text-sm sm:text-base mb-2">Can I export and print kitchen recipe cost sheets?</h4>
<p class="text-xs sm:text-sm text-espresso/70 leading-relaxed">
Yes. BakeCost Studio includes a one-click Export Cost Card feature that generates a clean, printable recipe production card or downloadable high-resolution image formatted for kitchen binders and culinary records.
</p>
</div>
</div>
</div>
<!-- Semantic Footer -->
<footer class="pt-8 border-t border-espresso/10 flex flex-col sm:flex-row items-center justify-between text-xs text-espresso/50 gap-4">
<div>
<p class="font-semibold text-espresso/70">BakeCost Studio © 2026 — Built for Independent Bakers & Pastry Artisans.</p>
<p class="mt-0.5">100% Client-Side Local Privacy. Zero Cloud Tracking.</p>
</div>
<div class="flex items-center gap-4">
<a href="/sitemap.xml" class="hover:text-caramel-600 transition-colors underline">Sitemap</a>
<a href="/llms.txt" class="hover:text-caramel-600 transition-colors underline">llms.txt</a>
<a href="/llms-full.txt" class="hover:text-caramel-600 transition-colors underline">llms-full.txt</a>
<a href="https://github.com/ismilewolf-eng/bakecost-studio" target="_blank" rel="noopener" class="hover:text-caramel-600 transition-colors underline">Open Source GitHub</a>
</div>
<!-- SEO Guide Hub Links -->
<div class="mt-8 pt-6 border-t border-amber-900/10 text-center text-xs text-espresso/70">
<p class="font-bold uppercase tracking-wider text-amber-900/60 mb-2">Bakery Costing & Pricing Guides</p>
<div class="flex flex-wrap justify-center items-center gap-x-6 gap-y-2">
<a href="./cottage-bakery-pricing-guide/" class="hover:text-amber-800 underline font-medium">Cottage Bakery Pricing Guide</a>
<a href="./sourdough-bread-cost-calculator/" class="hover:text-amber-800 underline font-medium">Sourdough Bread Cost Calculator</a>
<a href="./custom-cake-pricing-calculator/" class="hover:text-amber-800 underline font-medium">Custom Cake Pricing Formula</a>
<a href="./cookie-pricing-calculator/" class="hover:text-amber-800 underline font-medium">Cookie Decorating Cost Sheet</a>
</div>
</div>
</footer>
</section>
<!-- ============ EXPORT MODAL ============ -->
<transition name="fade">
<div v-if="showExportModal" class="fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4" @click.self="showExportModal = false">
<div class="bg-oatmeal rounded-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto shadow-2xl">
<div class="flex items-center justify-between px-5 py-4 border-b border-espresso/10">
<h3 class="font-bold text-base flex items-center gap-2"><i data-lucide="image-down" class="w-4 h-4 text-caramel-600"></i> Export Cost Card</h3>
<button @click="showExportModal = false" class="icon-btn btn-ghost"><i data-lucide="x" class="w-4 h-4"></i></button>
</div>
<div class="p-5" id="printOnlyWrap">
<div id="exportCard" class="card p-5 bg-white">
<div class="flex items-center gap-3 pb-3 border-b border-espresso/10">
<div class="w-11 h-11 rounded-xl bg-caramel-500 flex items-center justify-center text-2xl">{{ activeRecipe.icon }}</div>
<div>
<p class="font-extrabold text-lg leading-tight">{{ activeRecipe.name || 'Untitled Recipe' }}</p>
<p class="text-xs text-espresso/50">Yields {{ activeRecipe.servings }} serving(s) · BakeCost Studio</p>
</div>
</div>
<div class="grid grid-cols-2 gap-2.5 my-3.5">
<div class="rounded-xl bg-oatmeal p-3">
<p class="text-[10px] uppercase text-espresso/45">Total Batch Cost</p>
<p class="text-lg font-extrabold num">{{ fmtMoney(totalBatchCost) }}</p>
</div>
<div class="rounded-xl bg-oatmeal p-3">
<p class="text-[10px] uppercase text-espresso/45">Cost / Serving</p>
<p class="text-lg font-extrabold num">{{ fmtMoney(costPerServing) }}</p>
</div>
<div class="rounded-xl bg-caramel-50 p-3">
<p class="text-[10px] uppercase text-caramel-700/70">Suggested Price</p>
<p class="text-lg font-extrabold num text-caramel-700">{{ fmtMoney(suggestedPrice) }}</p>
</div>
<div class="rounded-xl bg-caramel-50 p-3">
<p class="text-[10px] uppercase text-caramel-700/70">Food Cost %</p>
<p class="text-lg font-extrabold num text-caramel-700">{{ fmtNum(activeRecipe.targetFoodCostPct,0) }}%</p>
</div>
</div>
<p class="text-xs font-semibold text-espresso/60 mb-1.5">Ingredients</p>
<ul class="text-xs divide-y divide-espresso/10 border-y border-espresso/10">
<li v-for="ing in activeRecipe.ingredients" :key="ing.id" class="flex justify-between py-1.5 num">
<span class="truncate pr-2">{{ ing.name || 'Unnamed ingredient' }}</span>
<span class="shrink-0">{{ fmtMoney(ingredientLineCost(ing)) }}</span>
</li>
</ul>
<p class="text-[10px] text-espresso/40 mt-3 text-center">Generated {{ todayStr }} · Made with BakeCost Studio</p>
</div>
</div>
<div class="px-5 pb-5 flex flex-col sm:flex-row gap-2.5">
<button @click="downloadPng" class="btn btn-primary flex-1"><i data-lucide="download" class="w-4 h-4"></i> Download PNG</button>
<button @click="printCard" class="btn btn-secondary flex-1"><i data-lucide="printer" class="w-4 h-4"></i> Print / Save as PDF</button>
</div>
</div>
</div>
</transition>
<!-- ============ BACKUP / RESTORE MODAL ============ -->
<transition name="fade">
<div v-if="showBackupModal" class="fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4" @click.self="showBackupModal = false">
<div class="bg-oatmeal rounded-2xl max-w-md w-full shadow-2xl">
<div class="flex items-center justify-between px-5 py-4 border-b border-espresso/10">
<h3 class="font-bold text-base flex items-center gap-2"><i data-lucide="database" class="w-4 h-4 text-caramel-600"></i> Backup & Restore</h3>
<button @click="showBackupModal = false" class="icon-btn btn-ghost"><i data-lucide="x" class="w-4 h-4"></i></button>
</div>
<div class="p-5 space-y-3">
<p class="text-sm text-espresso/60">All recipes are saved automatically in your browser. Export a JSON backup to keep a copy or move your recipes to another device.</p>
<button @click="exportJson" class="btn btn-secondary w-full justify-start">
<i data-lucide="upload" class="w-4 h-4"></i> Export All Recipes (JSON)
</button>
<label class="btn btn-secondary w-full justify-start cursor-pointer">
<i data-lucide="folder-input" class="w-4 h-4"></i> Import Recipes (JSON)
<input type="file" accept="application/json" class="hidden" @change="importJson">
</label>
<button @click="resetToDefaults(true)" class="btn btn-danger w-full justify-start">
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Reset to Default Recipes
</button>
</div>
</div>
</div>
</transition>
<!-- ============ TOASTS ============ -->
<div class="fixed bottom-4 right-4 z-[60] space-y-2 w-[calc(100%-2rem)] max-w-xs">
<transition-group name="fade">
<div v-for="t in toasts" :key="t.id" class="bg-espresso text-oatmeal text-sm rounded-xl px-4 py-3 shadow-lg">
{{ t.msg }}
</div>
</transition-group>
</div>
</div>
<script>
const { createApp, ref, reactive, computed, watch, onMounted, onUpdated, nextTick } = Vue;
createApp({
setup() {
const STORAGE_KEY = 'bakecost_recipes_en_v1';
const UNITS = {
g: { type: 'weight', toBase: 1 },
kg: { type: 'weight', toBase: 1000 },
oz: { type: 'weight', toBase: 28.3495 },
lb: { type: 'weight', toBase: 453.592 },
ml: { type: 'volume', toBase: 1 },
l: { type: 'volume', toBase: 1000 },
cup: { type: 'volume', toBase: 236.588 },
tbsp:{ type: 'volume', toBase: 14.7868 },
tsp: { type: 'volume', toBase: 4.92892 },
};
const WEIGHT_UNITS = ['g', 'kg', 'oz', 'lb'];
const VOLUME_UNITS = ['ml', 'l', 'cup', 'tbsp', 'tsp'];
const RECIPE_ICONS = ['🥖','🍓','🍪','🍰','🧁','🥐','🍞','🥧','🍩','🎂'];
// ---- 40+ ingredient density library (g/ml), with default unit & yield ----
const DENSITY_LIBRARY = [
{ name: 'Cake Flour', density: 0.51, defaultUnit: 'g' },
{ name: 'All-Purpose Flour', density: 0.53, defaultUnit: 'g' },
{ name: 'Bread Flour', density: 0.54, defaultUnit: 'g' },
{ name: 'Whole Wheat Flour', density: 0.48, defaultUnit: 'g' },
{ name: 'Almond Flour', density: 0.45, defaultUnit: 'g' },
{ name: 'Granulated Sugar', density: 0.85, defaultUnit: 'g' },
{ name: 'Powdered Sugar', density: 0.51, defaultUnit: 'g' },
{ name: 'Brown Sugar', density: 0.93, defaultUnit: 'g' },
{ name: 'Unsalted Butter', density: 0.96, defaultUnit: 'g' },
{ name: 'Cultured Butter', density: 0.96, defaultUnit: 'g' },
{ name: 'Vegetable Oil', density: 0.92, defaultUnit: 'ml' },
{ name: 'Whole Milk', density: 1.03, defaultUnit: 'ml' },
{ name: 'Heavy Cream (35%)', density: 1.00, defaultUnit: 'ml' },
{ name: 'Whole Eggs', density: 1.03, defaultUnit: 'g' },
{ name: 'Egg Whites', density: 1.03, defaultUnit: 'g' },
{ name: 'Egg Yolks', density: 1.05, defaultUnit: 'g' },
{ name: 'Water', density: 1.00, defaultUnit: 'ml' },
{ name: 'Honey', density: 1.42, defaultUnit: 'g' },
{ name: 'Maple Syrup', density: 1.33, defaultUnit: 'ml' },
{ name: 'Mascarpone Cheese', density: 0.98, defaultUnit: 'g' },
{ name: 'Cream Cheese', density: 1.01, defaultUnit: 'g' },
{ name: 'Cocoa Powder', density: 0.41, defaultUnit: 'g' },
{ name: 'Matcha Powder', density: 0.45, defaultUnit: 'g' },
{ name: 'Instant Dry Yeast', density: 0.68, defaultUnit: 'g' },
{ name: 'Baking Powder', density: 0.90, defaultUnit: 'g' },
{ name: 'Baking Soda', density: 0.90, defaultUnit: 'g' },
{ name: 'Fine Sea Salt', density: 1.20, defaultUnit: 'g' },
{ name: 'Maldon Sea Salt Flakes', density: 0.55, defaultUnit: 'g' },
{ name: 'Vanilla Extract', density: 0.88, defaultUnit: 'ml' },
{ name: 'Fresh Strawberries', density: 0.62, defaultUnit: 'g', defaultYield: 85 },
{ name: 'Fresh Blueberries', density: 0.70, defaultUnit: 'g', defaultYield: 95 },
{ name: 'Chocolate Chips', density: 0.86, defaultUnit: 'g' },
{ name: 'Dark Chocolate (70%)', density: 0.90, defaultUnit: 'g' },
{ name: 'Valrhona 70% Dark Chocolate Chips', density: 0.90, defaultUnit: 'g' },
{ name: 'Milk Chocolate', density: 0.90, defaultUnit: 'g' },
{ name: 'White Chocolate', density: 0.92, defaultUnit: 'g' },
{ name: 'Cream of Tartar', density: 0.96, defaultUnit: 'g' },
{ name: 'Corn Starch', density: 0.56, defaultUnit: 'g' },
{ name: 'Gelatin Powder', density: 0.68, defaultUnit: 'g' },
{ name: 'Lemon Juice', density: 1.03, defaultUnit: 'ml' },
{ name: 'Orange Zest', density: 0.30, defaultUnit: 'g' },
{ name: 'Rolled Oats', density: 0.34, defaultUnit: 'g' },
{ name: 'Desiccated Coconut', density: 0.35, defaultUnit: 'g' },
{ name: 'Walnuts (chopped)', density: 0.45, defaultUnit: 'g' },
{ name: 'Pecans (chopped)', density: 0.45, defaultUnit: 'g' },
{ name: 'Hazelnuts (ground)', density: 0.50, defaultUnit: 'g' },
{ name: 'Cinnamon Powder', density: 0.45, defaultUnit: 'g' },
{ name: 'Full Cream Milk Powder', density: 0.45, defaultUnit: 'g' },
];
function uid() { return Math.random().toString(36).slice(2, 10) + Date.now().toString(36); }
function toGrams(amount, unit, density) {
const u = UNITS[unit]; if (!u) return 0;
const d = density > 0 ? density : 1;
return u.type === 'weight' ? amount * u.toBase : amount * u.toBase * d;
}
function toMl(amount, unit, density) {
const u = UNITS[unit]; if (!u) return 0;
const d = density > 0 ? density : 1;
return u.type === 'volume' ? amount * u.toBase : (amount * u.toBase) / d;
}
function makeIngredient(o) {
return reactive({
id: uid(), presetKey: o.presetKey || '', name: o.name || '', density: o.density ?? 1,
purchasePrice: o.purchasePrice ?? 0, packageSize: o.packageSize ?? 1, packageUnit: o.packageUnit || 'g',
usageAmount: o.usageAmount ?? 0, usageUnit: o.usageUnit || 'g', yieldPct: o.yieldPct ?? 100,
});
}
function makePackaging(o) {
return reactive({ id: uid(), name: o.name || '', unitCost: o.unitCost ?? 0, qty: o.qty ?? 1 });
}
function makePreset1() {
return reactive({
id: uid(), icon: '🍓', name: 'French Strawberry Cheese Tart', servings: 8, bakingLossPct: 5,
targetFoodCostPct: 30, customPrice: 12.00, laborMinutes: 45, hourlyRate: 18, overheadAmount: 2.50,
ingredients: [
makeIngredient({ presetKey: 'Unsalted Butter', name: 'Unsalted Butter', density: 0.96, purchasePrice: 6.50, packageSize: 1, packageUnit: 'kg', usageAmount: 150, usageUnit: 'g' }),
makeIngredient({ presetKey: 'Cake Flour', name: 'Cake Flour', density: 0.51, purchasePrice: 4.20, packageSize: 1, packageUnit: 'kg', usageAmount: 200, usageUnit: 'g' }),
makeIngredient({ presetKey: 'Granulated Sugar', name: 'Granulated Sugar', density: 0.85, purchasePrice: 3.00, packageSize: 2, packageUnit: 'kg', usageAmount: 90, usageUnit: 'g' }),
makeIngredient({ presetKey: 'Mascarpone Cheese', name: 'Mascarpone Cheese', density: 0.98, purchasePrice: 5.50, packageSize: 250, packageUnit: 'g', usageAmount: 250, usageUnit: 'g' }),
makeIngredient({ presetKey: 'Fresh Strawberries', name: 'Fresh Strawberries', density: 0.62, purchasePrice: 4.00, packageSize: 500, packageUnit: 'g', usageAmount: 300, usageUnit: 'g', yieldPct: 85 }),
makeIngredient({ presetKey: 'Heavy Cream (35%)', name: 'Heavy Cream (35%)', density: 1.00, purchasePrice: 3.80, packageSize: 1, packageUnit: 'l', usageAmount: 200, usageUnit: 'ml' }),
],