Files
pocket-pascal/handover/reference-kueche_app.html
Steffi Müller a482bf5b09 Add Phase 1 Pocket Pascal UI: home, pantry, week plan, and plate.
Replace the hello-world shell with the elevated design system, bilingual screens, recipe/extras data, and the interactive Mein Teller view.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 12:22:32 +02:00

837 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Baukasten">
<meta name="theme-color" content="#ffffff">
<title>Pocket Pascal</title>
<style>
:root{
--bg:#ffffff; --surface:#ffffff; --ink:#111111; --ink-soft:#5c5c5c; --line:#e6e6e6;
--green:#2F5233; --green-soft:#e3ece4;
--sage:#4A6FA5; --sage-soft:#e7edf5;
--terracotta:#B33D22; --terracotta-soft:#f7e0d8; --terracotta-text:#A83B22;
--leaf:#A8C23E; --leaf-soft:#f2f5df; --leaf-text:#5C6B1E;
--gold:#E8B93A; --gold-soft:#fdf1d9; --gold-text:#8A6A1A;
--slate:#6B4C9A; --slate-soft:#efe9f5;
--pick:#16A34A; --caution:#DC2626;
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
html,body{ height:100%; }
body{
margin:0; background:var(--bg); color:var(--ink);
font-family: ui-sans-serif, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
font-variant-numeric: tabular-nums;
padding-top:env(safe-area-inset-top); padding-bottom:env(safe-area-inset-bottom);
overscroll-behavior-y: contain;
}
.display{ font-family:"Futura","Century Gothic","Avenir Next","Avenir",ui-sans-serif,sans-serif; font-weight:800; letter-spacing:-0.01em; }
/* ---------- top bar ---------- */
.topbar{
position:sticky; top:0; z-index:10; background:var(--surface);
border-bottom:1px solid var(--line);
display:flex; align-items:center; gap:8px;
padding:14px 16px calc(14px + env(safe-area-inset-top)) 16px;
margin-top:calc(-1 * env(safe-area-inset-top));
padding-top:calc(14px + env(safe-area-inset-top));
}
.backbtn{
display:none; border:none; background:var(--bg); color:var(--ink);
width:34px; height:34px; border-radius:50%; font-size:1.1rem;
align-items:center; justify-content:center; flex:none;
}
.backbtn.show{ display:flex; }
.tb-title{ flex:1; min-width:0; }
.topbar h1{ font-size:1.05rem; margin:0; font-weight:900; text-transform:uppercase; letter-spacing:0.02em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.topbar .eyebrow{ font-size:0.62rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--ink-soft); font-weight:700; display:block; }
.tb-actions{ flex:none; display:flex; align-items:center; gap:8px; }
.langpill{
display:flex; gap:2px; background:var(--bg); border:1px solid var(--line);
border-radius:999px; padding:2px;
}
.langpill button{
border:none; background:transparent; padding:5px 9px; border-radius:999px;
font-size:0.66rem; font-weight:800; letter-spacing:0.04em; color:var(--ink-soft);
}
.langpill button.active{ background:var(--green); color:#fff; }
.gearbtn{
border:none; background:var(--bg); width:30px; height:30px; border-radius:50%;
font-size:0.95rem; display:flex; align-items:center; justify-content:center; flex:none;
}
main{ padding:16px; padding-bottom:calc(32px + env(safe-area-inset-bottom)); max-width:640px; margin:0 auto; }
.view{ display:none; }
.view.active{ display:block; }
/* ---------- home grid ---------- */
.homegrid{
display:grid; grid-template-columns:repeat(2,1fr); gap:12px;
grid-auto-rows:minmax(120px,auto);
}
.tile{
border:1px solid var(--line); border-radius:14px; overflow:hidden;
display:flex; flex-direction:column; justify-content:space-between;
padding:16px; text-align:left; background:var(--surface);
-webkit-user-select:none; user-select:none;
}
.tile .ic{ font-size:2rem; line-height:1; }
.tile .t{ font-size:0.95rem; font-weight:900; text-transform:uppercase; letter-spacing:0.01em; margin-top:10px; }
.tile .d{ font-size:0.7rem; color:var(--ink-soft); margin-top:2px; }
.tile.soon{ opacity:0.4; }
.tile.teller{ border-top:4px solid var(--green); }
.tile.vorrat{ border-top:4px solid var(--terracotta); }
.tile.settings{ border-top:4px solid var(--slate); }
.tile.builder{ border-top:4px solid var(--pick); }
.tile.soon{ border-top:4px solid var(--line); }
/* ---------- plate toggle ---------- */
.segctl{
display:flex; background:var(--green-soft); border-radius:999px; padding:3px; margin-bottom:16px;
}
.segctl button{
flex:1; border:none; background:transparent; padding:9px 4px; border-radius:999px;
font-size:0.78rem; font-weight:700; color:var(--ink-soft);
}
.segctl button.active{ background:var(--surface); color:var(--ink); box-shadow:0 1px 3px rgba(0,0,0,0.12); }
.plate-wrap{ display:flex; flex-direction:column; align-items:center; gap:10px; margin-bottom:18px; }
.plate{ width:150px; height:150px; border-radius:50%; position:relative; box-shadow:inset 0 0 0 1px var(--line); }
.plate::after{ content:""; position:absolute; inset:22%; background:var(--surface); border-radius:50%; box-shadow:inset 0 0 0 1px var(--line); }
.legend{ display:flex; flex-direction:column; gap:4px; font-size:0.78rem; color:var(--ink-soft); }
.legend span{ display:flex; align-items:center; gap:6px; justify-content:center; }
.dot{ width:8px; height:8px; border-radius:50%; flex:none; }
.dot.g{background:var(--green);} .dot.t{background:var(--terracotta);} .dot.o{background:var(--gold);} .dot.s{background:var(--slate);}
/* ---------- accordion (steps + categories) ---------- */
.acc{ border:1px solid var(--line); border-radius:10px; overflow:hidden; margin-bottom:10px; }
.acc-head{
display:flex; align-items:center; gap:10px; padding:13px 14px;
background:var(--surface); border:none; width:100%; text-align:left;
}
.acc-head .num{ flex:none; width:26px; height:26px; border-radius:50%; color:#fff; font-weight:700; font-size:0.8rem; display:flex; align-items:center; justify-content:center; }
.acc-head .num.g{background:var(--green);} .acc-head .num.t{background:var(--terracotta);} .acc-head .num.o{background:var(--gold); color:#111;} .acc-head .num.s{background:var(--slate);}
.acc-head .ic{ flex:none; font-size:1.3rem; width:26px; text-align:center; }
.acc-head .title{ flex:1; font-weight:800; font-size:0.86rem; text-transform:uppercase; letter-spacing:0.02em; }
.acc-head .qty{ font-size:0.72rem; color:var(--ink-soft); font-weight:600; }
.acc-head .count{ font-size:0.7rem; color:var(--ink-soft); background:var(--bg); border-radius:20px; padding:2px 9px; flex:none; }
.acc-head .chev{ flex:none; font-size:0.9rem; color:var(--ink-soft); transition:transform 0.15s ease; }
.acc.open .chev{ transform:rotate(90deg); }
.acc-body{ display:none; padding:0 14px 14px; border-top:1px solid var(--line); }
.acc.open .acc-body{ display:block; padding-top:10px; }
.acc-body p{ margin:0; font-size:0.82rem; color:var(--ink-soft); line-height:1.5; }
.cat-head{ border-left-width:4px; border-left-style:solid; }
.acc.protein .cat-head{ border-left-color:var(--green); }
.acc.snack .cat-head{ border-left-color:var(--sage); }
.acc.obst .cat-head{ border-left-color:var(--terracotta); }
.acc.gemuese .cat-head{ border-left-color:var(--leaf); }
.acc.fett .cat-head{ border-left-color:var(--gold); }
.acc.kh .cat-head{ border-left-color:var(--slate); }
table{ width:100%; border-collapse:collapse; font-size:0.82rem; margin-top:6px; }
table td{ padding:6px 0; border-bottom:1px dashed var(--line); vertical-align:top; }
table td:last-child{ text-align:right; color:var(--ink-soft); white-space:nowrap; padding-left:8px; }
tr:last-child td{ border-bottom:none; }
.approx{ font-style:italic; }
.dotmark{ font-size:0.7em; margin-right:3px; }
.dotmark.pick{ color:var(--pick); }
.dotmark.caution{ color:var(--caution); }
.subhead{ font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--ink-soft); margin:10px 0 0; }
.rot{ margin-top:10px; padding-top:10px; border-top:1px dashed var(--line); font-size:0.78rem; line-height:1.45; }
.rot b{ text-transform:uppercase; letter-spacing:0.03em; font-size:0.7rem; }
.acc.protein .rot b{ color:var(--green); } .acc.snack .rot b{ color:var(--sage); }
.acc.obst .rot b{ color:var(--terracotta-text); } .acc.gemuese .rot b{ color:var(--leaf-text); }
.acc.fett .rot b{ color:var(--gold-text); } .acc.kh .rot b{ color:var(--slate); }
.searchbar{
display:flex; align-items:center; gap:8px; background:var(--surface); border:1px solid var(--line);
border-radius:10px; padding:10px 12px; margin-bottom:14px;
}
.searchbar input{ border:none; outline:none; font-size:0.9rem; flex:1; background:transparent; color:var(--ink); }
.searchbar .clear{ border:none; background:var(--bg); color:var(--ink-soft); border-radius:50%; width:22px; height:22px; font-size:0.75rem; display:none; }
.unitnote{ font-size:0.74rem; color:var(--ink-soft); background:var(--green-soft); border-radius:8px; padding:9px 12px; margin-bottom:14px; }
.recipe{ background:var(--sage-soft); border:1px solid var(--sage); border-radius:8px; padding:10px 12px; font-size:0.8rem; line-height:1.5; margin-top:8px; }
.recipe b{ color:var(--sage); }
.rules{ display:flex; flex-direction:column; gap:8px; border-top:1px solid var(--line); padding-top:14px; margin-top:16px; }
.rule{ display:flex; gap:8px; font-size:0.82rem; line-height:1.4; }
.rule::before{ content:"—"; color:var(--terracotta-text); flex:none; }
/* ---------- settings ---------- */
.setgroup{ border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin-bottom:12px; }
.setgroup h3{ font-size:0.72rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--ink-soft); margin:0 0 10px; font-weight:700; }
.setrow{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.setrow .label{ font-size:0.88rem; font-weight:600; }
.setnote{ font-size:0.72rem; color:var(--ink-soft); margin-top:8px; line-height:1.4; }
.setnote.ok{ color:var(--pick); }
.setnote.warn{ color:var(--caution); }
.setgroup.disabled{ opacity:0.45; }
/* ---------- builder ---------- */
.builder-row{ margin-bottom:16px; }
.builder-row .rowhead{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom:8px; }
.builder-row label{ font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--ink-soft); }
.builder-row .rcount{ font-size:0.68rem; color:var(--ink-soft); }
.chip-grid{ display:flex; flex-wrap:wrap; gap:7px; }
.chip{
border:1px solid var(--line); background:var(--surface); color:var(--ink);
padding:8px 13px; border-radius:999px; font-size:0.82rem; font-weight:600;
display:flex; align-items:center; gap:5px; -webkit-user-select:none; user-select:none;
}
.chip .dotmark{ margin-right:0; }
.builder-row.protein .chip.on{ background:var(--green); border-color:var(--green); color:#fff; }
.builder-row.obstgem .chip.on{ background:var(--terracotta); border-color:var(--terracotta); color:#fff; }
.builder-row.fett .chip.on{ background:var(--gold); border-color:var(--gold); color:#111; }
.builder-row.kh .chip.on{ background:var(--slate); border-color:var(--slate); color:#fff; }
.chip.on .dotmark.pick, .chip.on .dotmark.caution{ filter:brightness(1.4) saturate(1.4); }
.builder-result{ margin-top:6px; padding:14px 16px; border-radius:10px; border:1px solid var(--line); font-size:0.85rem; line-height:1.5; }
.builder-result .bh{ font-weight:900; font-size:0.95rem; text-transform:uppercase; letter-spacing:0.01em; margin-bottom:4px; display:flex; align-items:center; gap:8px; }
.builder-result.neutral{ background:var(--surface); }
.builder-result.solid{ background:var(--green-soft); }
.builder-result.solid .bh{ color:var(--green); }
.builder-result.pick{ background:var(--green-soft); }
.builder-result.pick .bh{ color:var(--green); }
.builder-result.caution{ background:#fdf1ec; }
.builder-result.caution .bh{ color:var(--terracotta-text); }
.builder-result ul{ margin:6px 0 0; padding-left:18px; }
.builder-result li{ margin-bottom:2px; }
</style>
<div class="topbar">
<button class="backbtn" id="backBtn" onclick="goHome()"></button>
<div class="tb-title">
<span class="eyebrow" id="topEyebrow">Body Comeback</span>
<h1 id="topTitle">Pocket Pascal</h1>
</div>
<div class="tb-actions">
<div class="langpill">
<button id="pillDe" onclick="setLang('de')">DE</button>
<button id="pillEn" onclick="setLang('en')">EN</button>
</div>
<button class="gearbtn" onclick="showView('settings')">⚙️</button>
</div>
</div>
<main>
<!-- HOME -->
<section class="view active" id="view-home">
<div class="homegrid" id="homegrid"></div>
</section>
<!-- TELLER -->
<section class="view" id="view-teller">
<div class="segctl">
<button id="segFull" class="active" onclick="setMeal('full')"></button>
<button id="segHalf" onclick="setMeal('half')"></button>
</div>
<div class="plate-wrap">
<div class="plate" id="plateFull" style="background:conic-gradient(var(--green) 0deg 120deg, var(--terracotta) 120deg 240deg, var(--gold) 240deg 300deg, var(--slate) 300deg 360deg);"></div>
<div class="plate" id="plateHalf" style="display:none; background:conic-gradient(var(--green) 0deg 200deg, var(--terracotta) 200deg 360deg);"></div>
<div class="legend" id="legendFull"></div>
<div class="legend" id="legendHalf" style="display:none;"></div>
</div>
<div id="stepsList"></div>
<div class="rules" id="tellerRules"></div>
</section>
<!-- VORRATSKAMMER -->
<section class="view" id="view-vorrat">
<div class="searchbar">
<span>🔍</span>
<input id="searchInput" oninput="onSearch()">
<button class="clear" id="clearBtn" onclick="clearSearch()"></button>
</div>
<div class="unitnote" id="unitnote"></div>
<div id="catList"></div>
</section>
<!-- BUILDER -->
<section class="view" id="view-builder">
<div class="segctl">
<button id="bSegFull" class="active" onclick="setBuilderMeal('full')"></button>
<button id="bSegHalf" onclick="setBuilderMeal('half')"></button>
</div>
<div id="builderPickers"></div>
<div class="builder-result neutral" id="builderResult"></div>
</section>
<!-- SETTINGS -->
<section class="view" id="view-settings">
<div class="setgroup">
<h3 id="setLangLabel">Sprache</h3>
<div class="setrow">
<span class="label">Deutsch / English</span>
<div class="langpill">
<button id="pillDe2" onclick="setLang('de')">DE</button>
<button id="pillEn2" onclick="setLang('en')">EN</button>
</div>
</div>
<div class="setnote" id="persistNote"></div>
</div>
<div class="setgroup disabled">
<h3 id="setMoreLabel">Weitere Einstellungen</h3>
<div class="setnote" id="setMoreNote"></div>
</div>
</section>
</main>
<script>
// ---------------------------------------------------------------------------
// i18n dictionary — all static UI strings
// ---------------------------------------------------------------------------
const UI = {
de: {
body:'Body Comeback', appTitle:'Pocket Pascal',
tileTellerT:'Dein Teller', tileTellerD:'Baukasten & Reihenfolge',
tileVorratT:'Vorratskammer', tileVorratD:'Einkauf & Kühlschrank',
tileSettingsT:'Einstellungen', tileSettingsD:'Sprache & mehr',
tileBuilderT:'Teller-Builder', tileBuilderD:'Kombi checken',
tileSugarT:'Zucker-Guide', tileSugarD:'Bald verfügbar',
tileCheckT:'Checkliste', tileCheckD:'Bald verfügbar',
card1:'Karte 1', card2:'Karte 2', cardSettings:'Einstellungen', cardBuilder:'Teller-Builder',
pickProtein:'Protein', pickObstGem:'Obst / Gemüse', pickFett:'Fett', pickKh:'Kohlenhydrate',
pickPlaceholder:'— auswählen —',
bResultMissingH:'Noch unvollständig',
bResultMissingList:'Fehlt noch:',
bResultSolidH:'Solider Teller',
bResultSolidHSnack:'Solider Snack',
bResultPickH:'Guter Teller!',
bResultPickHSnack:'Guter Snack!',
bResultPickList:'Besonders gut gewählt:',
bResultCautionH:'Mit Hinweis',
bResultCautionList:'Bewusst genießen:',
bResultCautionNote:'Kein Fehler — nur ein Punkt, den du im Blick behalten kannst.',
segFull:'Ganze Mahlzeit', segHalf:'Halbe Mahlzeit',
legFullP:'⅓ Protein', legFullVO:'⅓ Gemüse / Obst', legFullFK:'⅓ Fett + Kohlenhydrate',
legHalfP:'Protein', legHalfVO:'Gemüse / Obst',
rule1:'Jede Mahlzeit beruht auf Protein — egal ob süß, herzhaft, Snack oder Hauptmahlzeit.',
rule2:'Jede Mahlzeit enthält eine Portion Gemüse oder Obst.',
rule3:'4 Mahlzeiten am Tag, ca. alle 34 Std.',
rule4:'2 L Wasser täglich.',
searchPlaceholder:'Lebensmittel suchen…',
unitnote:'Werte je 100 g, sofern nicht anders angegeben.',
pickLabel:'besonders empfehlenswert', cautionLabel:'bewusst genießen',
noResults:'Keine Treffer.',
thumbDefault:'Daumenregel',
setLangLabel:'Sprache', setMoreLabel:'Weitere Einstellungen',
setMoreNote:'Mahlzeiten/Tag, Einkaufsrhythmus u.a. folgen in Phase 2.',
persistOk:'Spracheinstellung wird auf diesem Gerät gespeichert.',
persistWarn:'Spracheinstellung wird in dieser Ansicht nicht gespeichert — nach Neuladen wieder Deutsch.'
},
en: {
body:'Body Comeback', appTitle:'Pocket Pascal',
tileTellerT:'Your Plate', tileTellerD:'Building blocks & sequence',
tileVorratT:'The Pantry', tileVorratD:'Shopping & fridge',
tileSettingsT:'Settings', tileSettingsD:'Language & more',
tileBuilderT:'Plate Builder', tileBuilderD:'Check a combo',
tileSugarT:'Sugar Guide', tileSugarD:'Coming soon',
tileCheckT:'Checklist', tileCheckD:'Coming soon',
card1:'Card 1', card2:'Card 2', cardSettings:'Settings', cardBuilder:'Plate Builder',
pickProtein:'Protein', pickObstGem:'Veg / Fruit', pickFett:'Fat', pickKh:'Carbohydrate',
pickPlaceholder:'— choose —',
bResultMissingH:'Still incomplete',
bResultMissingList:'Still missing:',
bResultSolidH:'Solid plate',
bResultSolidHSnack:'Solid snack',
bResultPickH:'Good plate!',
bResultPickHSnack:'Good snack!',
bResultPickList:'Especially well chosen:',
bResultCautionH:'With a note',
bResultCautionList:'Enjoy mindfully:',
bResultCautionNote:'Not a mistake — just something to keep an eye on.',
segFull:'Full meal', segHalf:'Half meal',
legFullP:'⅓ Protein', legFullVO:'⅓ Veg / Fruit', legFullFK:'⅓ Fat + Carbs',
legHalfP:'Protein', legHalfVO:'Veg / Fruit',
rule1:'Every meal is built on protein — sweet, savoury, snack, or main.',
rule2:'Every meal includes a portion of vegetables or fruit.',
rule3:'4 meals a day, roughly every 34 hours.',
rule4:'2 L water daily.',
searchPlaceholder:'Search foods…',
unitnote:'Values per 100 g unless noted otherwise.',
pickLabel:'especially recommended', cautionLabel:'enjoy mindfully',
noResults:'No matches.',
thumbDefault:'Rule of thumb',
setLangLabel:'Language', setMoreLabel:'More settings',
setMoreNote:'Meals/day, shopping rhythm and more arrive in Phase 2.',
persistOk:'Language choice is saved on this device.',
persistWarn:'Language choice isnt saved in this view — reloads back to German.'
}
};
// ---------------------------------------------------------------------------
// data model — structured records, name/text as {de,en}
// ---------------------------------------------------------------------------
const STEPS = [
{n:1, cls:'g',
title:{de:'Protein', en:'Protein'},
qty:{de:'30 g pro Mahlzeit', en:'30 g per meal'},
text:{de:'Bspw. 125 g Rinderfilet, 300 g Skyr, 40 g Proteinpulver, 50 g Erbsenproteingeschnetzeltes oder 5 Eier. 150 g am Tag insgesamt.',
en:'E.g. 125 g beef fillet, 300 g Skyr, 40 g protein powder, 50 g pea-protein strips, or 5 eggs. 150 g total per day.'}},
{n:2, cls:'t',
title:{de:'Obst ODER Gemüse', en:'Fruit OR Vegetables'},
qty:{de:'eine Portion', en:'one portion'},
text:{de:'Obst = 100 g oder eine geballte Faust. Gemüse = 200 g oder 2 Handvoll.',
en:'Fruit = 100 g or one clenched fist. Vegetables = 200 g or 2 handfuls.'}},
{n:3, cls:'o',
title:{de:'Fett', en:'Fat'},
qty:{de:'', en:''},
text:{de:'15 g Samen/Kerne/Nüsse (≈20 Stück, 1 EL) · ¼ Avocado · 1 EL Öl (15 g) — Nussöl außer Erdnuss, Olivenöl, Leinsamenöl.',
en:'15 g seeds/nuts (≈20 pieces, 1 tbsp) · ¼ avocado · 1 tbsp oil (15 g) — nut oil except peanut, olive oil, linseed oil.'}},
{n:4, cls:'s',
title:{de:'Kohlenhydrate', en:'Carbohydrates'},
qty:{de:'', en:''},
text:{de:'75 g Reis/Nudeln/Brot (trocken) · ca. 250 g rohe Kartoffeln (≈3 mittelgroße) · 50 g Haferflocken (≈5 gehäufte EL).',
en:'75 g rice/pasta/bread (dry) · approx. 250 g raw potatoes (≈3 medium) · 50 g oats (≈5 heaped tbsp).'}}
];
// f: 'pick' | 'caution' | undefined
const CATS = [
{id:'protein', cls:'protein', ic:'🥚', title:{de:'Protein', en:'Protein'},
thumb:{de:'Mindestens 1520 g Protein pro 100 g, wenig Zucker & wenig Fett, kurze Zutatenliste.',
en:'At least 1520 g protein per 100 g, low sugar & low fat, short ingredient list.'},
items:[
{id:'edamame', name:{de:'Edamame', en:'Edamame'}, v:{de:'125 kcal · 14 g', en:'125 kcal · 14 g'}},
{id:'egg', name:{de:'Ei (Größe M)', en:'Egg (size M)'}, v:{de:'137 kcal · 13 g', en:'137 kcal · 13 g'}},
{id:'pea_protein_strips', name:{de:'Erbsenproteinschnetzel', en:'Pea-protein strips'}, v:{de:'367 kcal · 61 g', en:'367 kcal · 61 g'}},
{id:'feta', name:{de:'Feta (leicht)', en:'Feta (light)'}, v:{de:'161 kcal · 19 g', en:'161 kcal · 19 g'}, f:'caution'},
{id:'chicken_breast', name:{de:'Hühnerbrust', en:'Chicken breast'}, v:{de:'111 kcal · 24 g', en:'111 kcal · 24 g'}, f:'pick'},
{id:'cottage_cheese', name:{de:'Hüttenkäse', en:'Cottage cheese'}, v:{de:'87 kcal · 12,3 g', en:'87 kcal · 12.3 g'}},
{id:'like_chicken', name:{de:'„Like Chicken" (pflanzlich)', en:'"Like Chicken" (plant-based)'}, v:{de:'≈140 kcal · 18 g', en:'≈140 kcal · 18 g'}, approx:true},
{id:'low_fat_quark', name:{de:'Magerquark', en:'Low-fat quark'}, v:{de:'67 kcal · 12 g', en:'67 kcal · 12 g'}},
{id:'beef_fillet', name:{de:'Rinderfilet / Tatar', en:'Beef fillet / tartare'}, v:{de:'112 kcal · 22 g', en:'112 kcal · 22 g'}, f:'pick'},
{id:'silken_tofu', name:{de:'Seidentofu', en:'Silken tofu'}, v:{de:'≈55 kcal · 5 g', en:'≈55 kcal · 5 g'}, approx:true},
{id:'skyr', name:{de:'Skyr', en:'Skyr'}, v:{de:'54 kcal · 10,3 g', en:'54 kcal · 10.3 g'}, f:'pick'},
{id:'tempeh', name:{de:'Tempeh', en:'Tempeh'}, v:{de:'184 kcal · 19 g', en:'184 kcal · 19 g'}},
{id:'tofu', name:{de:'Tofu', en:'Tofu'}, v:{de:'129 kcal · 13 g', en:'129 kcal · 13 g'}}
],
sub:{title:{de:'Milch & Joghurt', en:'Milk & yoghurt'}, items:[
{id:'greek_yogurt_10', name:{de:'Griech. Joghurt 10 %', en:'Greek yoghurt 10%'}, v:{de:'≈115 kcal · 5 g', en:'≈115 kcal · 5 g'}, approx:true},
{id:'natural_yogurt', name:{de:'Naturjoghurt', en:'Natural yoghurt'}, v:{de:'≈61 kcal · 3,5 g', en:'≈61 kcal · 3.5 g'}, approx:true},
{id:'oatly_barista', name:{de:'Oatly Barista', en:'Oatly Barista'}, v:{de:'≈68 kcal · 1 g (kaum Protein)', en:'≈68 kcal · 1 g (barely protein)'}, approx:true, f:'caution'},
{id:'whole_milk', name:{de:'Vollmilch 3,5 %', en:'Whole milk 3.5%'}, v:{de:'≈64 kcal · 3,4 g', en:'≈64 kcal · 3.4 g'}, approx:true, f:'caution'}
]}},
{id:'snack', cls:'snack', ic:'🥤', title:{de:'Proteinsnacks für Steffi', en:'Protein snacks for Steffi'},
thumb:{de:'Mindestens 15 g Protein pro Portion, unter 5 g Zucker, Fett unter 8 g — Zutatenliste beginnt nicht mit Zucker/Sirup/Schokolade.',
en:'At least 15 g protein per serving, under 5 g sugar, fat under 8 g — ingredient list shouldnt open with sugar/syrup/chocolate.'},
items:[
{id:'three_eggs', name:{de:'3 Eier', en:'3 eggs'}, v:{de:'≈205 kcal · 19 g', en:'≈205 kcal · 19 g'}, approx:true},
{id:'cottage_berries', name:{de:'Hüttenkäse + Beeren', en:'Cottage cheese + berries'}, v:{de:'≈210 kcal · 25 g', en:'≈210 kcal · 25 g'}, approx:true},
{id:'chickpea_waffles', name:{de:'Kichererbsenwaffeln 90g', en:'Chickpea waffles 90g'}, v:{de:'≈300 kcal · 17 g', en:'≈300 kcal · 17 g'}, approx:true, f:'caution'},
{id:'pumpkin_seeds_30', name:{de:'Kürbiskerne 30g', en:'Pumpkin seeds 30g'}, v:{de:'≈171 kcal · 9 g', en:'≈171 kcal · 9 g'}, approx:true, f:'caution'},
{id:'lentil_waffles', name:{de:'Linsenwaffeln 90g', en:'Lentil waffles 90g'}, v:{de:'≈300 kcal · 18 g', en:'≈300 kcal · 18 g'}, approx:true, f:'caution'},
{id:'protein_bar', name:{de:'Magerer Proteinriegel', en:'Lean protein bar'}, v:{de:'≈180 kcal · 20 g', en:'≈180 kcal · 20 g'}, approx:true},
{id:'protein_rice_pudding', name:{de:'Proteinmilchreis (2x)', en:'Protein rice pudding (2x)'}, v:{de:'≈340 kcal · 40 g', en:'≈340 kcal · 40 g'}, approx:true},
{id:'protein_pudding', name:{de:'Proteinpudding', en:'Protein pudding'}, v:{de:'≈90 kcal · 20 g', en:'≈90 kcal · 20 g'}, approx:true, f:'pick'},
{id:'protein_shake', name:{de:'Proteinshake 40g', en:'Protein shake 40g'}, v:{de:'≈150 kcal · 30 g', en:'≈150 kcal · 30 g'}, approx:true, f:'pick'},
{id:'turkey_breast_pack', name:{de:'Putenbrust, 1 Packung', en:'Turkey breast, 1 pack'}, v:{de:'≈130 kcal · 28 g', en:'≈130 kcal · 28 g'}, approx:true, f:'pick'},
{id:'smoked_tofu_cubes', name:{de:'Räuchertofu-Würfel', en:'Smoked tofu cubes'}, v:{de:'≈225 kcal · 25 g', en:'≈225 kcal · 25 g'}, approx:true},
{id:'skyr_honey', name:{de:'Skyr/Griech. Joghurt 10% + Honig, 250g+', en:'Skyr/Greek yoghurt 10% + honey, 250g+'}, v:{de:'≈180 kcal · 25 g', en:'≈180 kcal · 25 g'}, approx:true}
],
recipe:{title:{de:'Rezept: Whey-Waffeln', en:'Recipe: Whey waffles'},
text:{de:'40 g Whey-Protein + 1 Ei + 40 g Haferflocken (fein gemahlen) + 60 ml Milch/Wasser verquirlen. ≈25 g Protein.',
en:'Whisk 40 g whey protein + 1 egg + 40 g oats (finely ground) + 60 ml milk/water. ≈25 g protein.'}}},
{id:'obst', cls:'obst', ic:'🍎', title:{de:'Obst', en:'Fruit'},
thumb:{de:'Frisch oder tiefgekühlt ohne Zuckerzusatz, Saison bevorzugen.',
en:'Fresh or frozen without added sugar, prefer whats in season.'},
items:[
{id:'apple', name:{de:'Apfel', en:'Apple'}, v:{de:'Vit. C, Kalium, Kupfer', en:'Vit. C, potassium, copper'}},
{id:'banana', name:{de:'Banane', en:'Banana'}, v:{de:'Kalium, B6, C, Magnesium', en:'Potassium, B6, C, magnesium'}, f:'caution'},
{id:'blueberry', name:{de:'Blaubeere', en:'Blueberry'}, v:{de:'Vit. K, C, Mangan', en:'Vit. K, C, manganese'}, f:'pick'},
{id:'strawberry', name:{de:'Erdbeere', en:'Strawberry'}, v:{de:'Vit. C, Mangan, Folsäure, Kalium', en:'Vit. C, manganese, folate, potassium'}},
{id:'granadilla', name:{de:'Granadilla (≈ Passionsfrucht)', en:'Granadilla (≈ passion fruit)'}, v:{de:'Vit. C, A, Kalium, B2', en:'Vit. C, A, potassium, B2'}, approx:true},
{id:'pomegranate', name:{de:'Granatapfel', en:'Pomegranate'}, v:{de:'Vit. C, K, Folsäure, Kalium, B6', en:'Vit. C, K, folate, potassium, B6'}, f:'pick'},
{id:'mango', name:{de:'Mango', en:'Mango'}, v:{de:'Vit. C, A, B6, Folsäure, Kupfer', en:'Vit. C, A, B6, folate, copper'}, f:'pick'},
{id:'nectarine', name:{de:'Nektarine', en:'Nectarine'}, v:{de:'Vit. C, A, Kalium, E, Niacin', en:'Vit. C, A, potassium, E, niacin'}},
{id:'orange', name:{de:'Orange', en:'Orange'}, v:{de:'Vit. C, Folsäure, Kalium, Calcium', en:'Vit. C, folate, potassium, calcium'}},
{id:'passion_fruit', name:{de:'Passionsfrucht', en:'Passion fruit'}, v:{de:'Vit. C, A, Kalium, Eisen', en:'Vit. C, A, potassium, iron'}, approx:true},
{id:'plum', name:{de:'Pflaume', en:'Plum'}, v:{de:'Vit. C, K, Kalium, A, Kupfer', en:'Vit. C, K, potassium, A, copper'}},
{id:'grapes', name:{de:'Trauben', en:'Grapes'}, v:{de:'Vit. K, C, Kalium, Kupfer, Mangan', en:'Vit. K, C, potassium, copper, manganese'}, f:'caution'}
]},
{id:'gemuese', cls:'gemuese', ic:'🥦', title:{de:'Gemüse', en:'Vegetables'},
thumbLabel:{de:'Tipp', en:'Tip'},
thumb:{de:'TK-Gemüse ist völlig okay, wenns schnell gehen muss — genauso nährstoffreich wie frisch, einfach ohne Salz-/Fettzusatz wählen.',
en:'Frozen veg is perfectly fine when time is short — just as nutrient-rich as fresh, just pick it without added salt/fat.'},
items:[
{id:'broccoli', name:{de:'Brokkoli', en:'Broccoli'}, v:{de:'Vit. C, K, Folsäure, Eisen', en:'Vit. C, K, folate, iron'}},
{id:'mushrooms', name:{de:'Champignons', en:'Mushrooms'}, v:{de:'Kalium, Selen, Kupfer, B2', en:'Potassium, selenium, copper, B2'}},
{id:'iceberg_lettuce', name:{de:'Eisbergsalat', en:'Iceberg lettuce'}, v:{de:'Vit. K, A, Folsäure, Kalium — nährstoffarm', en:'Vit. K, A, folate, potassium — low nutrient density'}, f:'caution'},
{id:'lambs_lettuce', name:{de:'Feldsalat', en:'Lambs lettuce'}, v:{de:'Vit. A, C, Folsäure, Eisen', en:'Vit. A, C, folate, iron'}, f:'pick'},
{id:'cucumber', name:{de:'Gurke', en:'Cucumber'}, v:{de:'Vit. K, Kalium, Mangan', en:'Vit. K, potassium, manganese'}},
{id:'carrot', name:{de:'Karotte', en:'Carrot'}, v:{de:'Vit. A, K, Kalium, B6', en:'Vit. A, K, potassium, B6'}, f:'pick'},
{id:'bell_pepper', name:{de:'Paprika', en:'Bell pepper'}, v:{de:'Vit. C, A, B6, E', en:'Vit. C, A, B6, E'}},
{id:'spinach', name:{de:'Spinat', en:'Spinach'}, v:{de:'Vit. K, A, Folsäure, Eisen', en:'Vit. K, A, folate, iron'}, f:'pick'},
{id:'tomato', name:{de:'Tomate', en:'Tomato'}, v:{de:'Vit. C, K, Kalium, Folsäure', en:'Vit. C, K, potassium, folate'}},
{id:'zucchini', name:{de:'Zucchini', en:'Zucchini'}, v:{de:'Vit. C, Kalium, B6, Mangan', en:'Vit. C, potassium, B6, manganese'}}
],
note:{de:'Hinweis: Süßkartoffel, Kochbanane und Erbsen zählen hier als Kohlenhydratquelle, Avocado als Fettquelle — siehe dort.',
en:'Note: sweet potato, plantain and peas count here as a carb source, avocado as a fat source — see those cards.'}},
{id:'fett', cls:'fett', ic:'🥑', title:{de:'Fett', en:'Fat'},
thumb:{de:'Native/kaltgepresste Öle, ungesalzene & ungeröstete Nüsse/Kerne, Zutatenliste = nur die Zutat selbst.',
en:'Native/cold-pressed oils, unsalted & unroasted nuts/seeds, ingredient list = just the ingredient itself.'},
items:[
{id:'avocado', name:{de:'Avocado', en:'Avocado'}, v:{de:'217 kcal · 12,5 g', en:'217 kcal · 12.5 g'}},
{id:'chia_seeds', name:{de:'Chiasamen', en:'Chia seeds'}, v:{de:'486 kcal · 31 g', en:'486 kcal · 31 g'}, f:'pick'},
{id:'pumpkin_seeds', name:{de:'Kürbiskerne', en:'Pumpkin seeds'}, v:{de:'570 kcal · 44 g', en:'570 kcal · 44 g'}},
{id:'linseed_oil', name:{de:'Leinöl', en:'Linseed oil'}, v:{de:'837 kcal · 93 g', en:'837 kcal · 93 g'}, f:'pick'},
{id:'almonds', name:{de:'Mandeln', en:'Almonds'}, v:{de:'612 kcal · 55 g', en:'612 kcal · 55 g'}},
{id:'olive_oil', name:{de:'Olivenöl', en:'Olive oil'}, v:{de:'857 kcal · 91,5 g', en:'857 kcal · 91.5 g'}},
{id:'walnuts', name:{de:'Walnüsse', en:'Walnuts'}, v:{de:'674 kcal · 62,5 g', en:'674 kcal · 62.5 g'}, f:'pick'}
]},
{id:'kh', cls:'kh', ic:'🌾', title:{de:'Kohlenhydrate', en:'Carbohydrates'},
thumb:{de:'Vollkorn als erste Zutat, wenig zugesetzter Zucker. Faustregel 10:1 — pro 10 g Kohlenhydrate mind. 1 g Ballaststoffe (z. B. 30 g KH → mind. 3 g Ballaststoffe).',
en:'Whole grain as the first ingredient, low added sugar. 10:1 rule of thumb — at least 1 g fibre per 10 g carbs (e.g. 30 g carbs → at least 3 g fibre).'},
items:[
{id:'brown_lentils', name:{de:'Berglinsen', en:'Brown lentils'}, v:{de:'319 kcal · ≈46 g', en:'319 kcal · ≈46 g'}, approx:true, f:'pick'},
{id:'buckwheat', name:{de:'Buchweizen', en:'Buckwheat'}, v:{de:'343 kcal · 71,5 g', en:'343 kcal · 71.5 g'}},
{id:'puffed_spelt', name:{de:'Dinkel gepufft', en:'Puffed spelt'}, v:{de:'≈338 kcal · 70 g', en:'≈338 kcal · 70 g'}, approx:true},
{id:'spelt_pasta', name:{de:'Dinkel-Vollkorn-Nudeln', en:'Whole-grain spelt pasta'}, v:{de:'≈338 kcal · 70 g', en:'≈338 kcal · 70 g'}, approx:true},
{id:'peas', name:{de:'Erbsen', en:'Peas'}, v:{de:'93 kcal · 10,4 g', en:'93 kcal · 10.4 g'}},
{id:'oats', name:{de:'Haferflocken', en:'Oats'}, v:{de:'372 kcal · 66,3 g', en:'372 kcal · 66.3 g'}},
{id:'millet', name:{de:'Hirse', en:'Millet'}, v:{de:'363 kcal · 69 g', en:'363 kcal · 69 g'}, f:'caution'},
{id:'chickpeas', name:{de:'Kichererbsen', en:'Chickpeas'}, v:{de:'108 kcal · 14,2 g', en:'108 kcal · 14.2 g'}},
{id:'chickpea_pasta', name:{de:'Kichererbsennudeln / -couscous', en:'Chickpea pasta / couscous'}, v:{de:'≈335 kcal · 50 g', en:'≈335 kcal · 50 g'}, approx:true},
{id:'plantain', name:{de:'Kochbanane', en:'Plantain'}, v:{de:'≈122 kcal · 32 g', en:'≈122 kcal · 32 g'}, approx:true, f:'caution'},
{id:'lentil_pasta', name:{de:'Linsennudeln', en:'Lentil pasta'}, v:{de:'≈335 kcal · 52 g', en:'≈335 kcal · 52 g'}, approx:true},
{id:'quinoa', name:{de:'Quinoa', en:'Quinoa'}, v:{de:'368 kcal · 64,2 g', en:'368 kcal · 64.2 g'}},
{id:'red_lentils', name:{de:'Rote Linsen', en:'Red lentils'}, v:{de:'326 kcal · ≈52 g', en:'326 kcal · ≈52 g'}, approx:true, f:'pick'},
{id:'sweet_potato', name:{de:'Süßkartoffel', en:'Sweet potato'}, v:{de:'86 kcal · 20,1 g', en:'86 kcal · 20.1 g'}, f:'caution'},
{id:'french_lentils', name:{de:'Tellerlinsen', en:'French lentils'}, v:{de:'234 kcal · ≈34 g', en:'234 kcal · ≈34 g'}, approx:true, f:'pick'}
]}
];
// ---------------------------------------------------------------------------
// state
// ---------------------------------------------------------------------------
let persistWorks = true;
function loadLang(){
try{
const v = localStorage.getItem('pp_lang');
return v === 'en' ? 'en' : 'de';
}catch(e){ persistWorks = false; return 'de'; }
}
function saveLang(l){
try{ localStorage.setItem('pp_lang', l); }catch(e){ persistWorks = false; }
}
const state = { lang: loadLang() };
const builderState = { meal:'full', sel:{ protein:[], obstgem:[], fett:[], kh:[] } };
function t(key){ return UI[state.lang][key]; }
function tr(obj){ return obj[state.lang]; }
// ---------------------------------------------------------------------------
// render helpers
// ---------------------------------------------------------------------------
function dot(f){
if(f==='pick') return `<span class="dotmark pick">●</span>`;
if(f==='caution') return `<span class="dotmark caution">●</span>`;
return '';
}
function rowsHtml(items){
return items.map(it=>`<tr><td>${tr(it.name)}</td><td class="${it.approx?'approx':''}">${dot(it.f)}${tr(it.v)}</td></tr>`).join('');
}
// ---------------------------------------------------------------------------
// builder helpers
// ---------------------------------------------------------------------------
function catItems(id){
const c = CATS.find(x=>x.id===id);
return c.sub ? c.items.concat(c.sub.items) : c.items;
}
function obstGemItems(){
return catItems('obst').concat(catItems('gemuese'));
}
function findItem(slot, id){
const list = slot==='protein' ? catItems('protein')
: slot==='obstgem' ? obstGemItems()
: slot==='fett' ? catItems('fett')
: catItems('kh');
return list.find(it=>it.id===id);
}
function renderHome(){
document.getElementById('homegrid').innerHTML = `
<button class="tile teller" onclick="showView('teller')">
<span class="ic">🍽️</span>
<div><div class="t">${t('tileTellerT')}</div><div class="d">${t('tileTellerD')}</div></div>
</button>
<button class="tile vorrat" onclick="showView('vorrat')">
<span class="ic">🧺</span>
<div><div class="t">${t('tileVorratT')}</div><div class="d">${t('tileVorratD')}</div></div>
</button>
<button class="tile builder" onclick="showView('builder')">
<span class="ic">🧩</span>
<div><div class="t">${t('tileBuilderT')}</div><div class="d">${t('tileBuilderD')}</div></div>
</button>
<button class="tile settings" onclick="showView('settings')">
<span class="ic">⚙️</span>
<div><div class="t">${t('tileSettingsT')}</div><div class="d">${t('tileSettingsD')}</div></div>
</button>
<button class="tile soon" disabled>
<span class="ic">🍬</span>
<div><div class="t">${t('tileSugarT')}</div><div class="d">${t('tileSugarD')}</div></div>
</button>
<button class="tile soon" disabled>
<span class="ic">✅</span>
<div><div class="t">${t('tileCheckT')}</div><div class="d">${t('tileCheckD')}</div></div>
</button>
`;
}
function renderSteps(){
document.getElementById('stepsList').innerHTML = STEPS.map(s=>`
<div class="acc" id="step-${s.n}">
<button class="acc-head" onclick="toggleAcc('step-${s.n}')">
<span class="num ${s.cls}">${s.n}</span>
<span class="title">${tr(s.title)}</span>
${tr(s.qty) ? `<span class="qty">${tr(s.qty)}</span>` : ''}
<span class="chev"></span>
</button>
<div class="acc-body"><p>${tr(s.text)}</p></div>
</div>
`).join('');
}
function renderTellerStatic(){
document.getElementById('segFull').textContent = t('segFull');
document.getElementById('segHalf').textContent = t('segHalf');
document.getElementById('legendFull').innerHTML = `
<span><i class="dot g"></i>${t('legFullP')}</span>
<span><i class="dot t"></i>${t('legFullVO')}</span>
<span><i class="dot o"></i><i class="dot s"></i>${t('legFullFK')}</span>
`;
document.getElementById('legendHalf').innerHTML = `
<span><i class="dot g"></i>${t('legHalfP')}</span>
<span><i class="dot t"></i>${t('legHalfVO')}</span>
`;
document.getElementById('tellerRules').innerHTML = ['rule1','rule2','rule3','rule4']
.map(k=>`<div class="rule">${t(k)}</div>`).join('');
}
function renderCats(filter){
const f = (filter||'').trim().toLowerCase();
const html = CATS.map(c=>{
let items = c.items;
let subitems = c.sub ? c.sub.items : null;
let matchCount = items.length + (subitems?subitems.length:0);
let forceOpen = false;
if(f){
items = c.items.filter(it=>tr(it.name).toLowerCase().includes(f));
subitems = subitems ? subitems.filter(it=>tr(it.name).toLowerCase().includes(f)) : null;
matchCount = items.length + (subitems?subitems.length:0);
forceOpen = matchCount>0;
if(matchCount===0) return '';
}
return `
<div class="acc ${c.cls} ${forceOpen?'open':''}" id="cat-${c.id}">
<button class="acc-head cat-head" onclick="toggleAcc('cat-${c.id}')">
<span class="ic">${c.ic}</span>
<span class="title">${tr(c.title)}</span>
<span class="count">${matchCount}</span>
<span class="chev"></span>
</button>
<div class="acc-body">
<table>${rowsHtml(items)}</table>
${subitems && subitems.length ? `<p class="subhead">${tr(c.sub.title)}</p><table>${rowsHtml(subitems)}</table>` : ''}
${c.recipe ? `<div class="recipe"><b>${tr(c.recipe.title)}</b><br>${tr(c.recipe.text)}</div>` : ''}
${c.note ? `<p class="subhead" style="text-transform:none;font-weight:400;">${tr(c.note)}</p>` : ''}
<div class="rot"><b>${c.thumbLabel ? tr(c.thumbLabel) : t('thumbDefault')}</b><br>${tr(c.thumb)}</div>
</div>
</div>`;
}).join('');
document.getElementById('catList').innerHTML = html || `<p style="color:var(--ink-soft); font-size:0.85rem;">${t('noResults')}</p>`;
}
function renderVorratStatic(){
document.getElementById('searchInput').placeholder = t('searchPlaceholder');
document.getElementById('unitnote').innerHTML =
`${t('unitnote')} <span class="dotmark pick">●</span> ${t('pickLabel')} · <span class="dotmark caution">●</span> ${t('cautionLabel')}`;
}
function renderBuilder(){
document.getElementById('bSegFull').textContent = t('segFull');
document.getElementById('bSegHalf').textContent = t('segHalf');
const slots = builderState.meal==='full'
? [['protein','pickProtein'],['obstgem','pickObstGem'],['fett','pickFett'],['kh','pickKh']]
: [['protein','pickProtein'],['obstgem','pickObstGem']];
document.getElementById('builderPickers').innerHTML = slots.map(([slot,labelKey])=>{
const list = slot==='protein' ? catItems('protein')
: slot==='obstgem' ? obstGemItems()
: slot==='fett' ? catItems('fett')
: catItems('kh');
const chips = list.map(it=>{
const on = builderState.sel[slot].includes(it.id);
return `<button class="chip ${on?'on':''}" onclick="toggleBuilderSel('${slot}','${it.id}')">${dot(it.f)}${tr(it.name)}</button>`;
}).join('');
const count = builderState.sel[slot].length;
return `
<div class="builder-row ${slot}">
<div class="rowhead">
<label>${t(labelKey)}</label>
${count ? `<span class="rcount">${count}×</span>` : ''}
</div>
<div class="chip-grid">${chips}</div>
</div>`;
}).join('');
evaluateBuilder();
}
function toggleBuilderSel(slot, id){
const arr = builderState.sel[slot];
const idx = arr.indexOf(id);
if(idx===-1) arr.push(id); else arr.splice(idx,1);
renderBuilder();
}
function setBuilderMeal(which){
builderState.meal = which;
document.getElementById('bSegFull').classList.toggle('active', which==='full');
document.getElementById('bSegHalf').classList.toggle('active', which==='half');
renderBuilder();
}
function evaluateBuilder(){
const required = builderState.meal==='full' ? ['protein','obstgem','fett','kh'] : ['protein','obstgem'];
const missing = required.filter(k=>builderState.sel[k].length===0);
const box = document.getElementById('builderResult');
const labelKeys = {protein:'pickProtein', obstgem:'pickObstGem', fett:'pickFett', kh:'pickKh'};
if(missing.length){
box.className = 'builder-result neutral';
box.innerHTML = `<div class="bh">${t('bResultMissingH')}</div>
<div>${t('bResultMissingList')} ${missing.map(k=>t(labelKeys[k])).join(', ')}</div>`;
return;
}
const items = [];
required.forEach(k=>builderState.sel[k].forEach(id=>items.push({slot:k, item:findItem(k, id)})));
const cautions = items.filter(x=>x.item.f==='caution');
const picks = items.filter(x=>x.item.f==='pick');
const isSnack = builderState.meal==='half';
if(cautions.length){
box.className = 'builder-result caution';
box.innerHTML = `<div class="bh">🧡 ${t('bResultCautionH')}</div>
<div>${t('bResultCautionList')}</div>
<ul>${cautions.map(x=>`<li>${tr(x.item.name)}</li>`).join('')}</ul>
<div style="margin-top:6px;">${t('bResultCautionNote')}</div>`;
} else if(picks.length){
box.className = 'builder-result pick';
box.innerHTML = `<div class="bh">✅ ${isSnack ? t('bResultPickHSnack') : t('bResultPickH')}</div>
<div>${t('bResultPickList')}</div>
<ul>${picks.map(x=>`<li>${tr(x.item.name)}</li>`).join('')}</ul>`;
} else {
box.className = 'builder-result solid';
box.innerHTML = `<div class="bh">👍 ${isSnack ? t('bResultSolidHSnack') : t('bResultSolidH')}</div>`;
}
}
function renderSettings(){
document.getElementById('setLangLabel').textContent = t('setLangLabel');
document.getElementById('setMoreLabel').textContent = t('setMoreLabel');
document.getElementById('setMoreNote').textContent = t('setMoreNote');
const note = document.getElementById('persistNote');
note.textContent = persistWorks ? t('persistOk') : t('persistWarn');
note.className = 'setnote ' + (persistWorks ? 'ok' : 'warn');
}
function renderLangPills(){
const de = state.lang==='de';
['pillDe','pillDe2'].forEach(id=>document.getElementById(id).classList.toggle('active', de));
['pillEn','pillEn2'].forEach(id=>document.getElementById(id).classList.toggle('active', !de));
}
function renderAll(){
renderLangPills();
renderHome();
renderSteps();
renderTellerStatic();
renderVorratStatic();
renderCats(document.getElementById('searchInput').value);
renderBuilder();
renderSettings();
refreshTopTitle();
}
function setLang(l){
if(l===state.lang) return;
state.lang = l;
saveLang(l);
renderAll();
}
// ---------------------------------------------------------------------------
// navigation
// ---------------------------------------------------------------------------
function toggleAcc(id){
document.getElementById(id).classList.toggle('open');
}
function setMeal(which){
const isFull = which==='full';
document.getElementById('segFull').classList.toggle('active', isFull);
document.getElementById('segHalf').classList.toggle('active', !isFull);
document.getElementById('plateFull').style.display = isFull?'block':'none';
document.getElementById('plateHalf').style.display = isFull?'none':'block';
document.getElementById('legendFull').style.display = isFull?'flex':'none';
document.getElementById('legendHalf').style.display = isFull?'none':'flex';
}
let currentView = 'home';
function refreshTopTitle(){
const back = document.getElementById('backBtn');
const eyebrow = document.getElementById('topEyebrow');
const title = document.getElementById('topTitle');
if(currentView==='home'){
back.classList.remove('show');
eyebrow.textContent = t('body');
title.textContent = t('appTitle');
} else {
back.classList.add('show');
if(currentView==='teller'){ eyebrow.textContent = t('card1'); title.textContent = t('tileTellerT'); }
else if(currentView==='vorrat'){ eyebrow.textContent = t('card2'); title.textContent = t('tileVorratT'); }
else if(currentView==='builder'){ eyebrow.textContent = t('appTitle'); title.textContent = t('cardBuilder'); }
else if(currentView==='settings'){ eyebrow.textContent = t('appTitle'); title.textContent = t('cardSettings'); }
}
}
function showView(name){
currentView = name;
document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));
document.getElementById('view-'+name).classList.add('active');
refreshTopTitle();
window.scrollTo(0,0);
history.pushState({view:name}, '', '#'+name);
}
function goHome(){ showView('home'); }
function onSearch(){
const val = document.getElementById('searchInput').value;
document.getElementById('clearBtn').style.display = val ? 'block' : 'none';
renderCats(val);
}
function clearSearch(){
document.getElementById('searchInput').value='';
document.getElementById('clearBtn').style.display='none';
renderCats('');
}
window.addEventListener('popstate', (e)=>{
const v = (e.state && e.state.view) || 'home';
currentView = v;
document.querySelectorAll('.view').forEach(el=>el.classList.remove('active'));
document.getElementById('view-'+v).classList.add('active');
refreshTopTitle();
});
renderAll();
</script>