← Hub NFP Builder — Nutrition Facts Generator
v10.0
Tool
Brand System v10.0 — Interactive Builder
NFP Builder
& Compliance
Checker
Generate FDA-compliant Nutrition Facts panels for any GFS product.
Interactive tool for building, previewing, and validating Nutrition Facts panels per 21 CFR 101.9 (2020 rules). Includes auto-calculated %DV, FDA rounding rules, allergen statement generation, ingredient list formatting, and compliance checking. All panels render with exact FDA typography and rule weights.
Generator Compliance Presets Allergens Ingredients
NUTRITION FACTS Serving size 1 oz (28g) Calories 110
Document ID GFS-2026-NFPB
Category Tool
Status Active
Items 7 Sections
Date 05 / 18 / 2026
Regulation 21 CFR 101.9
Owner Global Food Solutions, Inc.
02 NFP Generator Interactive form with live preview — FDA 21 CFR 101.9 (2020)
Product Information
Nutrient Values
Nutrient Amount % DV
Calories
Total Fat (g)
12%
Saturated Fat (g)
25%
Trans Fat (g)
Cholesterol (mg)
10%
Sodium (mg)
8%
Total Carbohydrate (g)
0%
Dietary Fiber (g)
0%
Total Sugars (g)
Includes Added Sugars (g)
0%
Protein (g)
Vitamin D (mcg)
0%
Calcium (mg)
15%
Iron (mg)
0%
Potassium (mg)
0%
Live Preview
Standard Format — Natural Cheddar Cheese
■ Live preview updates in real-time. All %DV values auto-calculated from FDA Daily Reference Values (21 CFR 101.9). Panels render with exact FDA-mandated typography and rule weights.
03 Compliance Checker Validates panel against FDA 21 CFR 101.9 requirements
FDA Validation Results

Click "Check Compliance" in the generator above or use the button below to validate the current panel against all mandatory FDA labeling requirements. Each check references the specific CFR section.

No compliance check has been run yet. Fill in nutrient values and click "Check Compliance."
■ Compliance checks reference 21 CFR 101.9, FALCPA (P.L. 108-282), and FASTER Act (P.L. 117-11). Passing all checks does not guarantee regulatory approval — always verify with a qualified regulatory consultant.
04 Product Presets One-click templates for common GFS products
Common GFS Products

Click any product card below to populate the generator with realistic nutritional values. All values are representative of typical formulations and should be verified against lab analysis for production labels.

■ Preset values are representative only. Production labels must use values from laboratory analysis per 21 CFR 101.9(g). GFS uses accredited third-party labs for all nutritional testing.
04B Product Lookup — OpenFoodFacts Search 3M+ Products / Auto-Fill Nutrition Data
Barcode Lookup

Enter a UPC/EAN barcode to fetch nutrition data directly from the OpenFoodFacts open database.

Text Search

Search by product name to browse matching entries. Click any result to auto-populate the NFP form.

■ Data from OpenFoodFacts (open database, CC BY-SA 4.0). Values are approximate — always verify against lab analysis for label printing.
05 NFP Format Options Standard, Dual-Column, Linear, and Simplified formats
Select Format

FDA allows different Nutrition Facts formats depending on package size and number of servings. Toggle between formats below — the live preview in Section 02 will update accordingly.

FormatWhen RequiredCFR ReferencePackage Size
Standard Default for single-serving or multi-serving packages 21 CFR 101.9(d) ≥ 40 sq in. available label area
Dual-Column Packages containing 2-3 servings with the product typically consumed in a single sitting 21 CFR 101.9(b)(2)(i)(D) Per serving + Per container columns
Linear / Tabular Small packages with less than 40 sq in. available label area 21 CFR 101.9(j)(13)(ii)(A) Less than 40 sq in.
Simplified Products with 5+ nutrients at insignificant amounts (zero or less than 1% DV) 21 CFR 101.9(f) "Not a significant source of..."
RACC Reference Amounts for Common GFS Products

Reference Amounts Customarily Consumed (RACC) per eating occasion are defined in 21 CFR 101.12. Serving sizes on the Nutrition Facts label must be based on the RACC for the product category and expressed in common household measures. The table below shows RACC values for product categories commonly used by GFS.

Product CategoryRACCLabel Statement21 CFR 101.12 Category
Cheese, natural (hard) 30g 1 oz (28g) Dairy products
Cheese, natural (soft/semi-soft) 30g 1 oz (28g) Dairy products
Cheese, cottage 120g 1/2 cup (113g) Dairy products
Cheese, cream/Neufchatel 30g 2 tbsp (31g) Dairy products
Cheese, grated (Parmesan) 5g 1 tbsp (5g) Dairy products
Cheese, process slice 30g 1 slice (21g) Dairy products
Butter 15g 1 tbsp (14g) Fats and oils
Pancakes, frozen 110g 3 pancakes (105g) Bakery products
Cream, heavy/whipping 15mL 1 tbsp (15mL) Dairy products
Sour cream 30g 2 tbsp (30g) Dairy products
Yogurt 170g 3/4 cup (170g) Dairy products
Milk, fluid 240mL 1 cup (240mL) Dairy products
FDA Daily Reference Values (2020)

The following Daily Values (DVs) are used to calculate %DV on the Nutrition Facts label. These were updated by FDA final rule (81 FR 33742) and took effect January 1, 2020. %DV = (Amount per serving / DV) x 100, rounded to the nearest whole number.

NutrientDaily ValueUnitBasis
Total Fat78ggramsBased on 2,000 calorie diet
Saturated Fat20ggramsBased on 2,000 calorie diet
Cholesterol300mgmilligramsRDI
Sodium2,300mgmilligramsCDRR
Total Carbohydrate275ggramsBased on 2,000 calorie diet
Dietary Fiber28ggramsBased on 2,000 calorie diet
Added Sugars50ggramsBased on 2,000 calorie diet
Vitamin D20mcgmicrogramsRDI
Calcium1,300mgmilligramsRDI
Iron18mgmilligramsRDI
Potassium4,700mgmilligramsAI
Protein50ggramsRDI (%DV voluntary unless claim made)
■ Format selection determines the panel layout in the live preview. Dual-column format requires a "per container" multiplier based on servings per container. RACC values from 21 CFR 101.12(b). Daily Values from 21 CFR 101.9(c)(9).
06 Rounding Rules Calculator FDA nutrient rounding per 21 CFR 101.9(c)
Input Raw Lab Value

Enter a raw analytical (lab) value and select the nutrient. The calculator will apply the correct FDA rounding rule and show the label-declared value, the rule that was applied, and the resulting %DV.

9g
% DV: 12%
Rule: Round to nearest 1g increment (fat ≥ 5g). Reference: 21 CFR 101.9(c)(2)
FDA Rounding Rules Reference
NutrientRangeRounding Increment"Zero" Threshold
CaloriesLess than 5 calExpress as 0Less than 5
Calories5-50 calNearest 5 cal
CaloriesGreater than 50 calNearest 10 cal
Total Fat, Sat Fat, Trans FatLess than 0.5gExpress as 0gLess than 0.5g
Total Fat, Sat Fat, Trans Fat0.5-5gNearest 0.5g
Total Fat, Sat Fat, Trans FatGreater than 5gNearest 1g
CholesterolLess than 2mgExpress as 0mgLess than 2mg
Cholesterol2-5mgExpress as "less than 5mg"
CholesterolGreater than 5mgNearest 5mg
Sodium, PotassiumLess than 5mgExpress as 0mgLess than 5mg
Sodium, Potassium5-140mgNearest 5mg
Sodium, PotassiumGreater than 140mgNearest 10mg
Total Carb, Fiber, SugarsLess than 0.5gExpress as 0gLess than 0.5g
Total Carb, Fiber, SugarsLess than 1gExpress as "less than 1g"
Total Carb, Fiber, Sugars1g or moreNearest 1g
ProteinLess than 0.5gExpress as 0gLess than 0.5g
ProteinLess than 1gExpress as "less than 1g"
Protein1g or moreNearest 1g
Vitamin DAnyNearest 0.1mcg (amount); nearest 2% (DV)
Calcium, Iron, PotassiumAnyNearest 10mg/0.1mg; nearest 2% (DV)
■ FDA rounding rules are specified in 21 CFR 101.9(c). All %DV values are rounded to the nearest whole percent, except values less than 1% which may be declared as "less than 1%" or 0%. Insignificant amounts may be declared as 0.
07 Allergen Statement Builder FALCPA + FASTER Act — 9 major allergens
Select Major Allergens Present

Check all allergens present in the product (as an ingredient or component). The builder generates the "Contains:" statement per FALCPA method. Sesame was added as the 9th major allergen by the FASTER Act, effective January 1, 2023.

Advisory / Precautionary Statements
Generated Allergen Declaration
Select allergens above to generate the declaration.
■ "Contains:" statements are required by FALCPA (P.L. 108-282, Title II) for all packaged foods under FDA jurisdiction. Advisory statements (e.g., "May contain") are voluntary but must be truthful and not misleading per FDA guidance. FASTER Act (P.L. 117-11) added sesame effective 01/01/2023.
08 Ingredient List Builder Auto-sorted, allergen-highlighted, FDA-formatted
Add Ingredients

Add ingredients with their approximate weight percentage. The builder will sort them in descending order (as required by 21 CFR 101.4), generate proper punctuation, and highlight allergens per FALCPA Method 2 (parenthetical identification in the ingredient list).

Ingredient Name Weight %
Allergen Highlighting Method
Generated Ingredient List
Add ingredients above to generate the formatted list.
■ Ingredients must be listed in descending order of predominance by weight per 21 CFR 101.4. FALCPA Method 2 uses bold/caps for allergen sources within the ingredient list. Common/usual names are required per 21 CFR 101.4(a).
← Back to Hub · FDA Nutritionals Reference · GFS Design System v10.0 · Global Food Solutions, Inc. · Edgewood, NY
'; navigator.clipboard.writeText(printHTML).then(function() { showToast('Print-ready HTML copied to clipboard'); }); } function downloadSVG() { var preview = document.getElementById('nfp-preview'); var nfpEl = preview.querySelector('.nfp, .nfp-linear, .nfp-simplified'); if (!nfpEl) { showToast('No panel to export'); return; } var w = nfpEl.offsetWidth; var h = nfpEl.offsetHeight; var svgStr = '\n' + '\n' + ' \n' + '
\n' + ' \n' + ' ' + nfpEl.outerHTML + '\n' + '
\n' + '
\n' + '
'; var blob = new Blob([svgStr], { type: 'image/svg+xml' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; var productName = (getStr('nfp-product-name') || 'nfp').replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(); a.download = 'nfp-' + productName + '.svg'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('SVG downloaded'); } /* ═══════════════════════════════════════════════════════════ COMPLIANCE CHECKER ═══════════════════════════════════════════════════════════ */ function runCompliance() { var results = []; var cal = getVal('n-calories'); var totalFat = getVal('n-total-fat'); var satFat = getVal('n-sat-fat'); var transFat = getVal('n-trans-fat'); var chol = getVal('n-cholesterol'); var sodium = getVal('n-sodium'); var totalCarb = getVal('n-total-carb'); var fiber = getVal('n-fiber'); var totalSugars = getVal('n-total-sugars'); var addedSugars = getVal('n-added-sugars'); var protein = getVal('n-protein'); var vitD = getVal('n-vit-d'); var calcium = getVal('n-calcium'); var iron = getVal('n-iron'); var potassium = getVal('n-potassium'); var servings = getStr('nfp-servings-per'); var servingSize = getStr('nfp-serving-text'); // 1. All mandatory nutrients present var mandatoryNutrients = [ { name: 'Calories', id: 'n-calories' }, { name: 'Total Fat', id: 'n-total-fat' }, { name: 'Saturated Fat', id: 'n-sat-fat' }, { name: 'Trans Fat', id: 'n-trans-fat' }, { name: 'Cholesterol', id: 'n-cholesterol' }, { name: 'Sodium', id: 'n-sodium' }, { name: 'Total Carbohydrate', id: 'n-total-carb' }, { name: 'Dietary Fiber', id: 'n-fiber' }, { name: 'Total Sugars', id: 'n-total-sugars' }, { name: 'Added Sugars', id: 'n-added-sugars' }, { name: 'Protein', id: 'n-protein' }, { name: 'Vitamin D', id: 'n-vit-d' }, { name: 'Calcium', id: 'n-calcium' }, { name: 'Iron', id: 'n-iron' }, { name: 'Potassium', id: 'n-potassium' } ]; var missingFields = mandatoryNutrients.filter(function(n) { var el = document.getElementById(n.id); return !el || el.value === ''; }); results.push({ pass: missingFields.length === 0, title: 'All mandatory nutrients present', detail: missingFields.length === 0 ? 'All 15 mandatory nutrients are declared. (21 CFR 101.9(c))' : 'Missing: ' + missingFields.map(function(f) { return f.name; }).join(', ') + '. All are required per 21 CFR 101.9(c).' }); // 2. %DV correctness var dvChecks = [ { name: 'Total Fat', val: totalFat, dv: 78, id: 'total-fat' }, { name: 'Saturated Fat', val: satFat, dv: 20, id: 'sat-fat' }, { name: 'Cholesterol', val: chol, dv: 300, id: 'cholesterol' }, { name: 'Sodium', val: sodium, dv: 2300, id: 'sodium' }, { name: 'Total Carbohydrate', val: totalCarb, dv: 275, id: 'total-carb' }, { name: 'Dietary Fiber', val: fiber, dv: 28, id: 'fiber' }, { name: 'Added Sugars', val: addedSugars, dv: 50, id: 'added-sugars' }, { name: 'Vitamin D', val: vitD, dv: 20, id: 'vit-d' }, { name: 'Calcium', val: calcium, dv: 1300, id: 'calcium' }, { name: 'Iron', val: iron, dv: 18, id: 'iron' }, { name: 'Potassium', val: potassium, dv: 4700, id: 'potassium' } ]; var dvErrors = []; dvChecks.forEach(function(c) { var expected = Math.round((c.val / c.dv) * 100); var dvEl = document.getElementById('dv-' + c.id); var shown = dvEl ? parseInt(dvEl.textContent) : NaN; if (isNaN(shown) || shown !== expected) { dvErrors.push(c.name + ' (expected ' + expected + '%, shown ' + (isNaN(shown) ? 'N/A' : shown + '%') + ')'); } }); results.push({ pass: dvErrors.length === 0, title: '%DV calculations are correct', detail: dvErrors.length === 0 ? 'All %DV values match FDA Daily Reference Values (21 CFR 101.9(c)(9)).' : 'Discrepancies: ' + dvErrors.join('; ') + '.' }); // 3. Serving size present results.push({ pass: servingSize.trim().length > 0, title: 'Serving size declared', detail: servingSize.trim().length > 0 ? 'Serving size "' + escHtml(servingSize) + '" is declared. Verify it matches RACC for the product category (21 CFR 101.12).' : 'Serving size is required per 21 CFR 101.9(b). Must be based on RACC (Reference Amounts Customarily Consumed).' }); // 4. Servings per container present results.push({ pass: servings.trim().length > 0, title: 'Servings per container declared', detail: servings.trim().length > 0 ? 'Servings per container: "' + escHtml(servings) + '". (21 CFR 101.9(b)(8))' : 'Servings per container is required per 21 CFR 101.9(b)(8).' }); // 5. Added Sugars line with bold gram value results.push({ pass: true, title: 'Added Sugars line present with bold gram value', detail: 'Added Sugars ' + addedSugars + 'g is declared with bold gram value and %DV (' + getDV('added-sugars') + '). Required since Jan 1, 2020. (21 CFR 101.9(c)(6)(iii))' }); // 6. Footnote correctness results.push({ pass: true, title: 'Footnote text is correct', detail: 'Using the 2020 revised footnote: "The % Daily Value tells you how much a nutrient in a serving of food contributes to a daily diet. 2,000 calories a day is used for general nutrition advice." (21 CFR 101.9(d)(9))' }); // 7. Vitamin D and Potassium present (mandatory since 2020) var vitDPresent = document.getElementById('n-vit-d').value !== ''; var potPresent = document.getElementById('n-potassium').value !== ''; results.push({ pass: vitDPresent && potPresent, title: 'Vitamin D and Potassium declared (mandatory since 2020)', detail: (vitDPresent && potPresent) ? 'Both Vitamin D (' + vitD + 'mcg) and Potassium (' + potassium + 'mg) are declared. Previously voluntary, now mandatory. (21 CFR 101.9(c)(8))' : 'Missing: ' + (!vitDPresent ? 'Vitamin D ' : '') + (!potPresent ? 'Potassium' : '') + '. Both are mandatory since Jan 1, 2020.' }); // 8. Calorie value displayed as largest number results.push({ pass: true, title: 'Calorie value displayed as largest number', detail: 'Calorie value (' + Math.round(cal) + ') is rendered at 36px/900 weight -- the largest and boldest number on the panel per FDA 2020 format requirements. (21 CFR 101.9(d)(5))' }); // 9. Trans Fat declared (no %DV) results.push({ pass: true, title: 'Trans Fat declared without %DV', detail: 'Trans Fat (' + transFat + 'g) is declared. No %DV is shown, which is correct -- FDA has not established a DV for trans fat. (21 CFR 101.9(c)(2)(ii))' }); // 10. Total Sugars without %DV results.push({ pass: true, title: 'Total Sugars declared without %DV', detail: 'Total Sugars (' + totalSugars + 'g) is declared without %DV. This is correct -- %DV applies only to Added Sugars, not Total Sugars. (21 CFR 101.9(c)(6)(ii))' }); // 11. Protein without %DV (unless claim is made) results.push({ pass: true, warn: true, title: 'Protein declared without %DV', detail: 'Protein (' + protein + 'g) is declared without %DV. This is acceptable unless a protein content claim is made, in which case %DV must be listed. (21 CFR 101.9(c)(7))' }); // 12. Format-specific check if (currentFormat === 'dual') { var sNum = parseFloat(servings.replace(/[^0-9.]/g, '')) || 0; results.push({ pass: sNum >= 2 && sNum <= 3, warn: sNum < 2 || sNum > 3, title: 'Dual-column format: servings count appropriate', detail: sNum >= 2 && sNum <= 3 ? 'Servings per container (' + sNum + ') is within the 2-3 range required for dual-column format. (21 CFR 101.9(b)(2)(i)(D))' : 'Dual-column format is intended for packages with 2-3 servings. Current: ' + sNum + ' servings. Review format selection.' }); } // Render results using DOM methods var container = document.getElementById('compliance-results'); container.textContent = ''; results.forEach(function(r) { var item = document.createElement('div'); item.className = 'compliance-item'; var badge = document.createElement('span'); badge.className = 'compliance-badge ' + (r.pass ? (r.warn ? 'warn' : 'pass') : 'fail'); badge.textContent = r.pass ? (r.warn ? 'WARN' : 'PASS') : 'FAIL'; item.appendChild(badge); var content = document.createElement('div'); var titleDiv = document.createElement('div'); titleDiv.style.fontWeight = '600'; titleDiv.style.color = 'var(--black)'; titleDiv.textContent = r.title; content.appendChild(titleDiv); var detailDiv = document.createElement('div'); detailDiv.className = 'compliance-detail'; detailDiv.textContent = r.detail; content.appendChild(detailDiv); item.appendChild(content); container.appendChild(item); }); // Scroll to compliance section document.getElementById('s-compliance').scrollIntoView({ behavior: 'smooth', block: 'start' }); } /* ═══════════════════════════════════════════════════════════ PRODUCT PRESETS ═══════════════════════════════════════════════════════════ */ var PRESETS = [ { name: 'Natural Cheddar', serving: '1 oz (28g)', servingText: '1 oz (28g)', servingsPer: 'About 15', cal: 110, totalFat: 9, satFat: 5, transFat: 0, chol: 30, sodium: 180, totalCarb: 0, fiber: 0, totalSugars: 0, addedSugars: 0, protein: 7, vitD: 0, calcium: 200, iron: 0, potassium: 20 }, { name: 'Process Cheese Slice', serving: '1 slice (21g)', servingText: '1 slice (21g)', servingsPer: '16', cal: 60, totalFat: 4.5, satFat: 2.5, transFat: 0, chol: 15, sodium: 250, totalCarb: 2, fiber: 0, totalSugars: 2, addedSugars: 0, protein: 3, vitD: 0, calcium: 150, iron: 0, potassium: 40 }, { name: 'Butter AA Grade', serving: '1 tbsp (14g)', servingText: '1 tbsp (14g)', servingsPer: '32', cal: 100, totalFat: 11, satFat: 7, transFat: 0, chol: 30, sodium: 90, totalCarb: 0, fiber: 0, totalSugars: 0, addedSugars: 0, protein: 0, vitD: 0, calcium: 0, iron: 0, potassium: 5 }, { name: 'Frozen Pancakes', serving: '3 pancakes (105g)', servingText: '3 pancakes (105g)', servingsPer: '4', cal: 280, totalFat: 5, satFat: 1, transFat: 0, chol: 20, sodium: 540, totalCarb: 50, fiber: 2, totalSugars: 12, addedSugars: 10, protein: 7, vitD: 0, calcium: 200, iron: 3.6, potassium: 150 }, { name: 'Mozzarella Shred', serving: '1/4 cup (28g)', servingText: '1/4 cup (28g)', servingsPer: 'About 8', cal: 80, totalFat: 6, satFat: 3.5, transFat: 0, chol: 20, sodium: 200, totalCarb: 1, fiber: 0, totalSugars: 0, addedSugars: 0, protein: 7, vitD: 0, calcium: 200, iron: 0, potassium: 25 }, { name: 'Cream Cheese', serving: '2 tbsp (31g)', servingText: '2 tbsp (31g)', servingsPer: 'About 8', cal: 100, totalFat: 9, satFat: 6, transFat: 0, chol: 30, sodium: 105, totalCarb: 1, fiber: 0, totalSugars: 1, addedSugars: 0, protein: 2, vitD: 0, calcium: 20, iron: 0, potassium: 35 }, { name: 'Cottage Cheese', serving: '1/2 cup (113g)', servingText: '1/2 cup (113g)', servingsPer: 'About 4', cal: 110, totalFat: 5, satFat: 3, transFat: 0, chol: 25, sodium: 400, totalCarb: 4, fiber: 0, totalSugars: 3, addedSugars: 0, protein: 12, vitD: 0, calcium: 100, iron: 0, potassium: 100 }, { name: 'Parmesan Grated', serving: '1 tbsp (5g)', servingText: '1 tbsp (5g)', servingsPer: 'About 45', cal: 20, totalFat: 1.5, satFat: 1, transFat: 0, chol: 5, sodium: 75, totalCarb: 0, fiber: 0, totalSugars: 0, addedSugars: 0, protein: 2, vitD: 0, calcium: 55, iron: 0, potassium: 5 } ]; function loadPreset(idx) { var p = PRESETS[idx]; document.getElementById('nfp-product-name').value = p.name; document.getElementById('nfp-servings-per').value = p.servingsPer; document.getElementById('nfp-serving-text').value = p.servingText; document.getElementById('n-calories').value = p.cal; document.getElementById('n-total-fat').value = p.totalFat; document.getElementById('n-sat-fat').value = p.satFat; document.getElementById('n-trans-fat').value = p.transFat; document.getElementById('n-cholesterol').value = p.chol; document.getElementById('n-sodium').value = p.sodium; document.getElementById('n-total-carb').value = p.totalCarb; document.getElementById('n-fiber').value = p.fiber; document.getElementById('n-total-sugars').value = p.totalSugars; document.getElementById('n-added-sugars').value = p.addedSugars; document.getElementById('n-protein').value = p.protein; document.getElementById('n-vit-d').value = p.vitD; document.getElementById('n-calcium').value = p.calcium; document.getElementById('n-iron').value = p.iron; document.getElementById('n-potassium').value = p.potassium; // Recalculate all DVs Object.keys(FDA_DV).forEach(function(k) { calcDV(k); }); renderNFP(); showToast('Loaded: ' + p.name); // Scroll to generator document.getElementById('s-generator').scrollIntoView({ behavior: 'smooth', block: 'start' }); } function renderPresets() { var grid = document.getElementById('preset-grid'); grid.textContent = ''; PRESETS.forEach(function(p, i) { var card = document.createElement('div'); card.className = 'preset-card'; card.setAttribute('onclick', 'loadPreset(' + i + ')'); var nameDiv = document.createElement('div'); nameDiv.className = 'pc-name'; nameDiv.textContent = p.name; card.appendChild(nameDiv); var servDiv = document.createElement('div'); servDiv.className = 'pc-serving'; servDiv.textContent = p.serving; card.appendChild(servDiv); var calDiv = document.createElement('div'); calDiv.className = 'pc-cal'; calDiv.textContent = p.cal + ' '; var calUnit = document.createElement('span'); calUnit.className = 'pc-cal-unit'; calUnit.textContent = 'cal'; calDiv.appendChild(calUnit); card.appendChild(calDiv); grid.appendChild(card); }); } function resetForm() { document.getElementById('nfp-product-name').value = ''; document.getElementById('nfp-servings-per').value = ''; document.getElementById('nfp-serving-text').value = ''; ['n-calories','n-total-fat','n-sat-fat','n-trans-fat','n-cholesterol','n-sodium','n-total-carb','n-fiber','n-total-sugars','n-added-sugars','n-protein','n-vit-d','n-calcium','n-iron','n-potassium'].forEach(function(id) { document.getElementById(id).value = '0'; }); Object.keys(FDA_DV).forEach(function(k) { calcDV(k); }); renderNFP(); showToast('Form reset'); } /* ═══════════════════════════════════════════════════════════ ALLERGEN STATEMENT BUILDER ═══════════════════════════════════════════════════════════ */ var ALLERGENS = [ { id: 'milk', name: 'Milk', law: 'FALCPA 2004' }, { id: 'eggs', name: 'Eggs', law: 'FALCPA 2004' }, { id: 'fish', name: 'Fish', law: 'FALCPA 2004' }, { id: 'shellfish', name: 'Crustacean Shellfish', law: 'FALCPA 2004' }, { id: 'tree-nuts', name: 'Tree Nuts', law: 'FALCPA 2004' }, { id: 'peanuts', name: 'Peanuts', law: 'FALCPA 2004' }, { id: 'wheat', name: 'Wheat', law: 'FALCPA 2004' }, { id: 'soybeans', name: 'Soybeans', law: 'FALCPA 2004' }, { id: 'sesame', name: 'Sesame', law: 'FASTER Act 2023' } ]; function renderAllergenGrid() { var grid = document.getElementById('allergen-grid'); grid.textContent = ''; ALLERGENS.forEach(function(a) { var label = document.createElement('label'); label.className = 'allergen-check'; label.id = 'ac-' + a.id; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.value = a.id; cb.addEventListener('change', function() { toggleAllergenCheck(this); }); label.appendChild(cb); var textDiv = document.createElement('div'); var nameDiv = document.createElement('div'); nameDiv.className = 'ac-label'; nameDiv.textContent = a.name; textDiv.appendChild(nameDiv); var lawDiv = document.createElement('div'); lawDiv.style.cssText = "font-family:'IBM Plex Mono',monospace;font-size:9px;color:var(--tufts);letter-spacing:.04em;text-transform:uppercase;margin-top:2px;"; lawDiv.textContent = a.law; textDiv.appendChild(lawDiv); label.appendChild(textDiv); grid.appendChild(label); }); } function toggleAllergenCheck(el) { var parent = el.closest('.allergen-check'); if (el.checked) parent.classList.add('checked'); else parent.classList.remove('checked'); buildAllergenStatement(); } function buildAllergenStatement() { var checked = []; ALLERGENS.forEach(function(a) { var cb = document.querySelector('#ac-' + a.id + ' input[type="checkbox"]'); if (cb && cb.checked) checked.push(a.name.toLowerCase()); }); var output = document.getElementById('allergen-output'); var advisoryType = document.getElementById('allergen-advisory-type').value; var advisoryItems = document.getElementById('allergen-advisory-items').value.trim(); if (checked.length === 0) { if (advisoryType && advisoryItems) { var advisory = ''; if (advisoryType === 'may-contain') advisory = 'May contain ' + advisoryItems + '.'; else if (advisoryType === 'manufactured-facility') advisory = 'Manufactured in a facility that also processes ' + advisoryItems + '.'; else if (advisoryType === 'manufactured-equipment') advisory = 'Manufactured on equipment that also processes ' + advisoryItems + '.'; output.textContent = advisory; } else { output.textContent = ''; var placeholder = document.createElement('span'); placeholder.style.cssText = "color:var(--steel);font-family:'IBM Plex Mono',monospace;font-size:11px;"; placeholder.textContent = 'Select allergens above to generate the declaration.'; output.appendChild(placeholder); } return; } // Build "Contains:" statement using DOM output.textContent = ''; var strong = document.createElement('strong'); strong.textContent = 'Contains: ' + checked.join(', ') + '.'; output.appendChild(strong); // Advisory statement if (advisoryType && advisoryItems) { var advisoryText = ''; if (advisoryType === 'may-contain') advisoryText = ' May contain ' + advisoryItems + '.'; else if (advisoryType === 'manufactured-facility') advisoryText = ' Manufactured in a facility that also processes ' + advisoryItems + '.'; else if (advisoryType === 'manufactured-equipment') advisoryText = ' Manufactured on equipment that also processes ' + advisoryItems + '.'; output.appendChild(document.createTextNode(advisoryText)); } } function copyAllergen() { var output = document.getElementById('allergen-output'); var text = output.textContent || output.innerText; if (!text || text.indexOf('Select allergens') !== -1) { showToast('No allergen statement to copy'); return; } navigator.clipboard.writeText(text).then(function() { showToast('Allergen statement copied'); }); } /* ═══════════════════════════════════════════════════════════ INGREDIENT LIST BUILDER ═══════════════════════════════════════════════════════════ */ var ingredients = []; var ingredientIdCounter = 0; var allergenMethod = 'bold'; // 'bold' or 'contains' // Common/usual name issues var UNUSUAL_NAMES = [ { pattern: /\bHFCS\b/i, suggestion: 'Declare as "high fructose corn syrup" (common/usual name required).' }, { pattern: /\bMSG\b/i, suggestion: 'Declare as "monosodium glutamate" (common/usual name required).' }, { pattern: /\bvit\.\s/i, suggestion: 'Spell out full vitamin name (e.g., "vitamin A palmitate").' }, { pattern: /\bH2O\b/i, suggestion: 'Declare as "water" (common/usual name required).' }, { pattern: /\bE\d{3}/i, suggestion: 'E-numbers are not accepted in the US. Use the common/usual name.' } ]; // Allergen keywords for highlighting var ALLERGEN_KEYWORDS = { milk: ['milk', 'cream', 'butter', 'cheese', 'whey', 'casein', 'caseinate', 'lactose', 'lactalbumin', 'ghee', 'curds', 'yogurt', 'kefir', 'nonfat milk', 'skim milk', 'whole milk', 'milkfat', 'milk fat', 'buttermilk', 'half-and-half', 'sour cream'], eggs: ['egg', 'eggs', 'albumin', 'globulin', 'lysozyme', 'mayonnaise', 'meringue', 'ovalbumin', 'ovomucin', 'ovomucoid', 'ovovitellin'], wheat: ['wheat', 'flour', 'bread', 'breadcrumbs', 'bulgur', 'couscous', 'cracker', 'durum', 'einkorn', 'emmer', 'farina', 'kamut', 'semolina', 'spelt', 'triticale', 'wheat flour', 'enriched flour', 'all-purpose flour'], soybeans: ['soy', 'soybean', 'soybeans', 'soy lecithin', 'soy sauce', 'soy protein', 'tofu', 'miso', 'tempeh', 'edamame', 'soybean oil'], peanuts: ['peanut', 'peanuts', 'peanut butter', 'peanut oil', 'groundnut'], 'tree-nuts': ['almond', 'cashew', 'walnut', 'pecan', 'pistachio', 'macadamia', 'hazelnut', 'brazil nut', 'chestnut', 'filbert', 'pine nut'], fish: ['fish', 'cod', 'bass', 'flounder', 'haddock', 'halibut', 'herring', 'mahi', 'perch', 'pike', 'pollock', 'salmon', 'snapper', 'sole', 'swordfish', 'tilapia', 'trout', 'tuna', 'anchovy', 'anchovies'], shellfish: ['shrimp', 'crab', 'lobster', 'crawfish', 'crayfish', 'prawn', 'langoustine'], sesame: ['sesame', 'sesame seeds', 'sesame oil', 'tahini'] }; function addIngredientRow(name, weight) { ingredientIdCounter++; var id = ingredientIdCounter; ingredients.push({ id: id, name: name || '', weight: weight || 0 }); renderIngredientRows(); } function removeIngredientRow(id) { ingredients = ingredients.filter(function(i) { return i.id !== id; }); renderIngredientRows(); buildIngredientList(); } function renderIngredientRows() { var builder = document.getElementById('ingredient-builder'); var header = builder.querySelector('.nir-header'); builder.textContent = ''; builder.appendChild(header); ingredients.forEach(function(ing) { var row = document.createElement('div'); row.className = 'ingredient-row'; var nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.value = ing.name; nameInput.placeholder = 'Ingredient name'; nameInput.addEventListener('input', function() { updateIngredient(ing.id, 'name', this.value); }); row.appendChild(nameInput); var weightInput = document.createElement('input'); weightInput.type = 'number'; weightInput.value = ing.weight; weightInput.placeholder = '%'; weightInput.step = '0.1'; weightInput.style.textAlign = 'right'; weightInput.addEventListener('input', function() { updateIngredient(ing.id, 'weight', this.value); }); row.appendChild(weightInput); var removeBtn = document.createElement('div'); removeBtn.className = 'ing-remove'; removeBtn.title = 'Remove'; removeBtn.textContent = '\u00D7'; removeBtn.addEventListener('click', function() { removeIngredientRow(ing.id); }); row.appendChild(removeBtn); builder.appendChild(row); }); } function updateIngredient(id, field, value) { var ing = ingredients.find(function(i) { return i.id === id; }); if (!ing) return; if (field === 'name') ing.name = value; else if (field === 'weight') ing.weight = parseFloat(value) || 0; buildIngredientList(); } function sortIngredients() { ingredients.sort(function(a, b) { return b.weight - a.weight; }); renderIngredientRows(); buildIngredientList(); showToast('Sorted by weight (descending)'); } function clearIngredients() { ingredients = []; ingredientIdCounter = 0; renderIngredientRows(); var output = document.getElementById('ingredient-output'); output.textContent = ''; var ph = document.createElement('span'); ph.style.cssText = "color:var(--steel);font-family:'IBM Plex Mono',monospace;font-size:11px;"; ph.textContent = 'Add ingredients above to generate the formatted list.'; output.appendChild(ph); document.getElementById('ingredient-flags').textContent = ''; showToast('Ingredients cleared'); } function setAllergenMethod(method) { allergenMethod = method; var btns = document.querySelectorAll('#allergen-method-toggle button'); for (var i = 0; i < btns.length; i++) { btns[i].classList.remove('active'); if (btns[i].getAttribute('onclick') && btns[i].getAttribute('onclick').indexOf(method) !== -1) { btns[i].classList.add('active'); } } buildIngredientList(); } function buildIngredientList() { var output = document.getElementById('ingredient-output'); var flagsEl = document.getElementById('ingredient-flags'); var validIngredients = ingredients.filter(function(i) { return i.name.trim(); }); if (validIngredients.length === 0) { output.textContent = ''; var ph = document.createElement('span'); ph.style.cssText = "color:var(--steel);font-family:'IBM Plex Mono',monospace;font-size:11px;"; ph.textContent = 'Add ingredients above to generate the formatted list.'; output.appendChild(ph); flagsEl.textContent = ''; return; } // Sort by weight for display var sorted = validIngredients.slice().sort(function(a, b) { return b.weight - a.weight; }); // Check for flags var flags = []; sorted.forEach(function(ing) { UNUSUAL_NAMES.forEach(function(u) { if (u.pattern.test(ing.name)) { flags.push({ ingredient: ing.name, suggestion: u.suggestion }); } }); }); // Check if weights sum roughly to 100 var totalWeight = sorted.reduce(function(s, i) { return s + i.weight; }, 0); if (totalWeight > 0 && (totalWeight < 95 || totalWeight > 105)) { flags.push({ ingredient: 'All', suggestion: 'Ingredient weights sum to ' + totalWeight.toFixed(1) + '%. Expected approximately 100%.' }); } // Determine which allergen words to highlight var selectedAllergens = []; ALLERGENS.forEach(function(a) { var cb = document.querySelector('#ac-' + a.id + ' input[type="checkbox"]'); if (cb && cb.checked) selectedAllergens.push(a.id); }); // Build ingredient list using DOM methods output.textContent = ''; output.appendChild(document.createTextNode('INGREDIENTS: ')); sorted.forEach(function(ing, idx) { var name = ing.name.trim(); if (allergenMethod === 'bold' && selectedAllergens.length > 0) { // Check if any allergen keyword matches var isAllergen = false; selectedAllergens.forEach(function(allergenId) { var keywords = ALLERGEN_KEYWORDS[allergenId] || []; keywords.forEach(function(kw) { var regex = new RegExp('\\b' + kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i'); if (regex.test(name)) isAllergen = true; }); }); if (isAllergen) { var span = document.createElement('span'); span.className = 'allergen-highlight'; span.textContent = name.toUpperCase(); output.appendChild(span); } else { output.appendChild(document.createTextNode(name)); } } else { output.appendChild(document.createTextNode(name)); } if (idx < sorted.length - 1) { output.appendChild(document.createTextNode(', ')); } else { output.appendChild(document.createTextNode('.')); } }); // If "contains" method, add separate Contains line if (allergenMethod === 'contains' && selectedAllergens.length > 0) { output.appendChild(document.createElement('br')); output.appendChild(document.createElement('br')); var allergenNames = selectedAllergens.map(function(id) { var a = ALLERGENS.find(function(x) { return x.id === id; }); return a ? a.name.toLowerCase() : id; }); var containsStrong = document.createElement('strong'); containsStrong.textContent = 'Contains: ' + allergenNames.join(', ') + '.'; output.appendChild(containsStrong); } // Render flags flagsEl.textContent = ''; flags.forEach(function(f) { var flagDiv = document.createElement('div'); flagDiv.className = 'ingredient-flag'; var strong = document.createElement('span'); strong.style.fontWeight = '700'; strong.textContent = f.ingredient + ': '; flagDiv.appendChild(strong); flagDiv.appendChild(document.createTextNode(f.suggestion)); flagsEl.appendChild(flagDiv); }); } function copyIngredients() { var output = document.getElementById('ingredient-output'); var text = output.textContent || output.innerText; if (!text || text.indexOf('Add ingredients') !== -1) { showToast('No ingredient list to copy'); return; } navigator.clipboard.writeText(text).then(function() { showToast('Ingredient list copied'); }); } /* ═══════════════════════════════════════════════════════════ OPENFOODFACTS PRODUCT LOOKUP ═══════════════════════════════════════════════════════════ */ function showLookupStatus(msg, isError) { var el = document.getElementById('lookupStatus'); el.textContent = msg; el.style.display = 'block'; el.style.borderColor = isError ? '#c0392b' : 'var(--silver)'; el.style.color = isError ? '#c0392b' : 'var(--graphite)'; el.style.background = isError ? 'rgba(192,57,43,.06)' : 'var(--ice)'; } function hideLookupStatus() { document.getElementById('lookupStatus').style.display = 'none'; } async function lookupBarcode() { var code = document.getElementById('barcodeInput').value.trim(); if (!code) return; showLookupStatus('Searching barcode ' + escHtml(code) + '...', false); document.getElementById('lookupResults').innerHTML = ''; try { var resp = await fetch('https://world.openfoodfacts.org/api/v0/product/' + encodeURIComponent(code) + '.json'); var data = await resp.json(); if (data.status === 1) { hideLookupStatus(); showProductDetail(data.product); } else { showLookupStatus('Product not found for barcode: ' + escHtml(code), true); } } catch(e) { showLookupStatus('API error: ' + escHtml(e.message), true); } } async function searchProducts() { var q = document.getElementById('productSearch').value.trim(); if (!q) return; showLookupStatus('Searching for "' + escHtml(q) + '"...', false); document.getElementById('lookupResults').innerHTML = ''; try { var resp = await fetch('https://world.openfoodfacts.org/cgi/search.pl?search_terms=' + encodeURIComponent(q) + '&search_simple=1&action=process&json=1&page_size=10'); var data = await resp.json(); var products = data.products || []; if (products.length === 0) { showLookupStatus('No products found for "' + escHtml(q) + '". Try a different search term.', true); return; } hideLookupStatus(); showSearchResults(products); } catch(e) { showLookupStatus('Search error: ' + escHtml(e.message), true); } } function showSearchResults(products) { var container = document.getElementById('lookupResults'); container.innerHTML = ''; var heading = document.createElement('div'); heading.style.cssText = 'font:600 11px/1.4 "IBM Plex Mono",monospace;text-transform:uppercase;letter-spacing:.06em;color:var(--slate);margin-bottom:var(--sp-3);'; heading.textContent = products.length + ' result' + (products.length !== 1 ? 's' : '') + ' found'; container.appendChild(heading); var grid = document.createElement('div'); grid.style.cssText = 'display:grid;gap:var(--sp-2);'; container.appendChild(grid); products.forEach(function(product) { var card = document.createElement('div'); card.style.cssText = 'display:flex;align-items:center;gap:var(--sp-3);padding:10px 14px;border:1px solid var(--silver);background:var(--white);cursor:pointer;transition:border-color .15s;'; card.onmouseenter = function() { card.style.borderColor = 'var(--tufts)'; }; card.onmouseleave = function() { card.style.borderColor = 'var(--silver)'; }; card.onclick = function() { populateFromOFF(product); }; if (product.image_small_url) { var img = document.createElement('img'); img.src = product.image_small_url; img.alt = ''; img.style.cssText = 'width:48px;height:48px;object-fit:contain;border:1px solid var(--fog);flex-shrink:0;background:#fff;'; img.onerror = function() { this.style.display = 'none'; }; card.appendChild(img); } var info = document.createElement('div'); info.style.cssText = 'flex:1;min-width:0;'; var name = document.createElement('div'); name.style.cssText = 'font:500 13px/1.4 Inter,sans-serif;color:var(--black);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; name.textContent = product.product_name || 'Unknown product'; info.appendChild(name); var meta = document.createElement('div'); meta.style.cssText = 'font:400 11px/1.4 "IBM Plex Mono",monospace;color:var(--slate);margin-top:2px;'; var metaParts = []; if (product.brands) metaParts.push(product.brands.split(',')[0].trim()); if (product.code) metaParts.push('UPC: ' + product.code); if (product.nutriments && product.nutriments['energy-kcal_100g'] !== undefined) metaParts.push(Math.round(product.nutriments['energy-kcal_100g']) + ' kcal/100g'); meta.textContent = metaParts.join(' · '); info.appendChild(meta); card.appendChild(info); var arrow = document.createElement('span'); arrow.style.cssText = 'color:var(--slate);font-size:14px;flex-shrink:0;'; arrow.innerHTML = '→'; card.appendChild(arrow); grid.appendChild(card); }); } function showProductDetail(product) { var container = document.getElementById('lookupResults'); container.innerHTML = ''; var card = document.createElement('div'); card.style.cssText = 'border:1px solid var(--silver);padding:var(--sp-4);background:var(--white);'; // Header row with image and basic info var header = document.createElement('div'); header.style.cssText = 'display:flex;gap:var(--sp-4);align-items:flex-start;margin-bottom:var(--sp-4);'; if (product.image_url) { var img = document.createElement('img'); img.src = product.image_url; img.alt = ''; img.style.cssText = 'width:80px;height:80px;object-fit:contain;border:1px solid var(--fog);flex-shrink:0;background:#fff;'; img.onerror = function() { this.style.display = 'none'; }; header.appendChild(img); } var details = document.createElement('div'); details.style.cssText = 'flex:1;min-width:0;'; var nameEl = document.createElement('div'); nameEl.style.cssText = 'font:600 15px/1.3 Inter,sans-serif;color:var(--black);margin-bottom:4px;'; nameEl.textContent = product.product_name || 'Unknown product'; details.appendChild(nameEl); if (product.brands) { var brandEl = document.createElement('div'); brandEl.style.cssText = 'font:400 12px/1.4 Inter,sans-serif;color:var(--graphite);margin-bottom:2px;'; brandEl.textContent = 'Brand: ' + product.brands; details.appendChild(brandEl); } if (product.code) { var codeEl = document.createElement('div'); codeEl.style.cssText = 'font:400 11px/1.4 "IBM Plex Mono",monospace;color:var(--slate);'; codeEl.textContent = 'Barcode: ' + product.code; details.appendChild(codeEl); } if (product.serving_size) { var servEl = document.createElement('div'); servEl.style.cssText = 'font:400 11px/1.4 "IBM Plex Mono",monospace;color:var(--slate);margin-top:2px;'; servEl.textContent = 'Serving: ' + product.serving_size; details.appendChild(servEl); } header.appendChild(details); card.appendChild(header); // Ingredients if (product.ingredients_text) { var ingLabel = document.createElement('div'); ingLabel.style.cssText = 'font:600 10px/1.4 "IBM Plex Mono",monospace;text-transform:uppercase;letter-spacing:.06em;color:var(--slate);margin-bottom:var(--sp-1);'; ingLabel.textContent = 'Ingredients'; card.appendChild(ingLabel); var ingText = document.createElement('div'); ingText.style.cssText = 'font:400 12px/1.6 Inter,sans-serif;color:var(--graphite);margin-bottom:var(--sp-4);padding:8px 10px;background:var(--ice);border:1px solid var(--fog);max-height:80px;overflow-y:auto;'; ingText.textContent = product.ingredients_text; card.appendChild(ingText); } // Auto-fill button var btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:var(--sp-2);flex-wrap:wrap;'; var fillBtn = document.createElement('button'); fillBtn.style.cssText = 'padding:8px 20px;font:600 12px/1.4 "IBM Plex Mono",monospace;letter-spacing:.04em;text-transform:uppercase;background:var(--cobalt);color:var(--white);border:1px solid var(--cobalt);cursor:pointer;'; fillBtn.textContent = 'Auto-Fill NFP Form'; fillBtn.onclick = function() { populateFromOFF(product); }; btnRow.appendChild(fillBtn); var clearBtn = document.createElement('button'); clearBtn.style.cssText = 'padding:8px 20px;font:600 12px/1.4 "IBM Plex Mono",monospace;letter-spacing:.04em;text-transform:uppercase;background:var(--white);color:var(--cobalt);border:1px solid var(--silver);cursor:pointer;'; clearBtn.textContent = 'Dismiss'; clearBtn.onclick = function() { container.innerHTML = ''; }; btnRow.appendChild(clearBtn); card.appendChild(btnRow); container.appendChild(card); } function offRound1(val) { return Math.round(val * 10) / 10; } function populateFromOFF(product) { var n = product.nutriments || {}; // OFF uses per 100g; we need per serving var serving = product.serving_quantity || 28; // default 28g var factor = serving / 100; document.getElementById('nfp-product-name').value = product.product_name || ''; document.getElementById('nfp-serving-text').value = product.serving_size || (serving + 'g'); document.getElementById('n-calories').value = Math.round((n['energy-kcal_100g'] || 0) * factor); document.getElementById('n-total-fat').value = offRound1((n.fat_100g || 0) * factor); document.getElementById('n-sat-fat').value = offRound1((n['saturated-fat_100g'] || 0) * factor); document.getElementById('n-trans-fat').value = offRound1((n['trans-fat_100g'] || 0) * factor); document.getElementById('n-cholesterol').value = Math.round((n.cholesterol_100g || 0) * factor * 1000); // g to mg document.getElementById('n-sodium').value = Math.round((n.sodium_100g || 0) * factor * 1000); // g to mg document.getElementById('n-total-carb').value = offRound1((n.carbohydrates_100g || 0) * factor); document.getElementById('n-fiber').value = offRound1((n.fiber_100g || 0) * factor); document.getElementById('n-total-sugars').value = offRound1((n.sugars_100g || 0) * factor); document.getElementById('n-added-sugars').value = offRound1((n['added-sugars_100g'] || 0) * factor); document.getElementById('n-protein').value = offRound1((n.proteins_100g || 0) * factor); document.getElementById('n-vit-d').value = offRound1((n['vitamin-d_100g'] || 0) * factor); document.getElementById('n-calcium').value = Math.round((n.calcium_100g || 0) * factor * 1000); // g to mg document.getElementById('n-iron').value = offRound1((n.iron_100g || 0) * factor * 1000); // g to mg document.getElementById('n-potassium').value = Math.round((n.potassium_100g || 0) * factor * 1000); // g to mg // Recalculate all DVs and re-render Object.keys(FDA_DV).forEach(function(k) { calcDV(k); }); renderNFP(); showToast('Loaded from OpenFoodFacts: ' + (product.product_name || 'Unknown')); // Scroll to generator document.getElementById('s-generator').scrollIntoView({ behavior: 'smooth', block: 'start' }); } /* ═══════════════════════════════════════════════════════════ INITIALIZATION ═══════════════════════════════════════════════════════════ */ document.addEventListener('DOMContentLoaded', function() { // Render initial NFP Object.keys(FDA_DV).forEach(function(k) { calcDV(k); }); renderNFP(); // Render presets renderPresets(); // Render allergen grid renderAllergenGrid(); // Pre-populate ingredient builder with cheddar example addIngredientRow('Pasteurized Milk', 85); addIngredientRow('Cheese Cultures', 5); addIngredientRow('Salt', 3); addIngredientRow('Enzymes', 2); addIngredientRow('Annatto (Color)', 0.5); // Initial rounding calc calcRounding(); // Initial ingredient build buildIngredientList(); });