feat(recipes): add image upload, fix save 500, seed HelloFresh data

- Store hero image as base64 data URI in text column (V023 migration)
- Add file upload UI to RecipeForm with FileReader preview
- Remove isChildFriendly from RecipeCreateRequest (no form field)
- Fix 500 on save: effort values now lowercase, serves/cookTimeMin changed
  from primitive short to nullable Integer to survive omitted fields
- Fix empty categories panel: removed stale tagType=category filter
- Group category tags by type with German headings in recipe form
- Split SuggestionResponse.SuggestionRecipe (no image) from SlotRecipe
- Seed 11 HelloFresh recipes with ingredients, steps and tags (V101)
- Add frontend e2e scaffold, specs and dev yml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 20:23:28 +02:00
parent 116e400a91
commit 520dae5adf
34 changed files with 9862 additions and 84 deletions

View File

@@ -153,7 +153,7 @@ public class PlanningService {
plan, candidate, slotDate, config, recentlyCookedIds); plan, candidate, slotDate, config, recentlyCookedIds);
double scoreDelta = simulatedScore - currentScore; double scoreDelta = simulatedScore - currentScore;
boolean hasConflict = scoreDelta < 0; boolean hasConflict = scoreDelta < 0;
return new SuggestionResponse.SuggestionItem(toSlotRecipe(candidate), scoreDelta, hasConflict); return new SuggestionResponse.SuggestionItem(toSuggestionRecipe(candidate), scoreDelta, hasConflict);
}) })
.sorted((a, b) -> Double.compare(b.scoreDelta(), a.scoreDelta())) .sorted((a, b) -> Double.compare(b.scoreDelta(), a.scoreDelta()))
.limit(limit) .limit(limit)
@@ -422,6 +422,11 @@ public class PlanningService {
recipe.getCookTimeMin(), recipe.getHeroImageUrl()); recipe.getCookTimeMin(), recipe.getHeroImageUrl());
} }
private SuggestionResponse.SuggestionRecipe toSuggestionRecipe(Recipe recipe) {
return new SuggestionResponse.SuggestionRecipe(recipe.getId(), recipe.getName(),
recipe.getEffort(), recipe.getCookTimeMin());
}
private boolean hasConsecutiveDays(List<LocalDate> days) { private boolean hasConsecutiveDays(List<LocalDate> days) {
if (days.size() < 2) return false; if (days.size() < 2) return false;
List<LocalDate> sorted = days.stream().sorted().toList(); List<LocalDate> sorted = days.stream().sorted().toList();

View File

@@ -1,11 +1,14 @@
package com.recipeapp.planning.dto; package com.recipeapp.planning.dto;
import java.util.List; import java.util.List;
import java.util.UUID;
public record SuggestionResponse(List<SuggestionItem> suggestions) { public record SuggestionResponse(List<SuggestionItem> suggestions) {
public record SuggestionRecipe(UUID id, String name, String effort, short cookTimeMin) {}
public record SuggestionItem( public record SuggestionItem(
SlotResponse.SlotRecipe recipe, SuggestionRecipe recipe,
double scoreDelta, double scoreDelta,
boolean hasConflict boolean hasConflict
) {} ) {}

View File

@@ -60,8 +60,10 @@ public class RecipeService {
Household household = householdRepository.findById(householdId) Household household = householdRepository.findById(householdId)
.orElseThrow(() -> new ResourceNotFoundException("Household not found")); .orElseThrow(() -> new ResourceNotFoundException("Household not found"));
Recipe recipe = new Recipe(household, request.name(), request.serves(), Recipe recipe = new Recipe(household, request.name(),
request.cookTimeMin(), request.effort(), request.isChildFriendly()); request.serves() != null ? request.serves().shortValue() : 0,
request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0,
request.effort(), false);
recipe.setHeroImageUrl(request.heroImageUrl()); recipe.setHeroImageUrl(request.heroImageUrl());
addIngredients(recipe, household, request.ingredients()); addIngredients(recipe, household, request.ingredients());
@@ -78,10 +80,9 @@ public class RecipeService {
Household household = recipe.getHousehold(); Household household = recipe.getHousehold();
recipe.setName(request.name()); recipe.setName(request.name());
recipe.setServes(request.serves()); recipe.setServes(request.serves() != null ? request.serves().shortValue() : 0);
recipe.setCookTimeMin(request.cookTimeMin()); recipe.setCookTimeMin(request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0);
recipe.setEffort(request.effort()); recipe.setEffort(request.effort());
recipe.setChildFriendly(request.isChildFriendly());
recipe.setHeroImageUrl(request.heroImageUrl()); recipe.setHeroImageUrl(request.heroImageUrl());
recipe.getIngredients().clear(); recipe.getIngredients().clear();

View File

@@ -6,13 +6,13 @@ import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public record RecipeCreateRequest( public record RecipeCreateRequest(
@NotBlank @Size(max = 200) String name, @NotBlank @Size(max = 200) String name,
@Min(1) @Max(20) short serves, Integer serves,
@Min(0) short cookTimeMin, Integer cookTimeMin,
@NotBlank @Pattern(regexp = "easy|medium|hard") String effort, @NotBlank @Pattern(regexp = "easy|medium|hard") String effort,
boolean isChildFriendly, String heroImageUrl,
@Size(max = 500) String heroImageUrl,
@NotEmpty @Valid List<IngredientEntry> ingredients, @NotEmpty @Valid List<IngredientEntry> ingredients,
@Valid List<StepEntry> steps, @Valid List<StepEntry> steps,
@NotEmpty List<UUID> tagIds @NotEmpty List<UUID> tagIds

View File

@@ -36,7 +36,7 @@ public class Recipe {
@Column(name = "is_child_friendly", nullable = false) @Column(name = "is_child_friendly", nullable = false)
private boolean isChildFriendly; private boolean isChildFriendly;
@Column(name = "hero_image_url", length = 500) @Column(name = "hero_image_url", columnDefinition = "text")
private String heroImageUrl; private String heroImageUrl;
@Column(name = "deleted_at") @Column(name = "deleted_at")

View File

@@ -0,0 +1,3 @@
spring:
flyway:
locations: classpath:db/migration,classpath:db/seed

View File

@@ -0,0 +1 @@
ALTER TABLE recipe ALTER COLUMN hero_image_url TYPE text;

View File

@@ -0,0 +1,434 @@
-- Dev seed: 11 HelloFresh vegetarian recipes (4 persons)
-- Fixed UUIDs so the migration is idempotent and references are stable.
-- Ingredients use dd000002-prefix, tags ee000001-prefix, recipes ff000002-prefix.
-- ─── Tags ────────────────────────────────────────────────────────────────────
INSERT INTO tag (id, household_id, name, tag_type) VALUES
('ee000001-0000-0000-0000-000000000001', 'bbbbbbbb-0000-0000-0000-000000000001', 'Vegetarisch', 'dietary'),
('ee000001-0000-0000-0000-000000000002', 'bbbbbbbb-0000-0000-0000-000000000001', 'Glutenfrei', 'dietary'),
('ee000001-0000-0000-0000-000000000003', 'bbbbbbbb-0000-0000-0000-000000000001', 'Deutsch', 'cuisine'),
('ee000001-0000-0000-0000-000000000004', 'bbbbbbbb-0000-0000-0000-000000000001', 'Mediterran', 'cuisine'),
('ee000001-0000-0000-0000-000000000005', 'bbbbbbbb-0000-0000-0000-000000000001', 'Asiatisch', 'cuisine'),
('ee000001-0000-0000-0000-000000000006', 'bbbbbbbb-0000-0000-0000-000000000001', 'Mexikanisch', 'cuisine'),
('ee000001-0000-0000-0000-000000000007', 'bbbbbbbb-0000-0000-0000-000000000001', 'Käse', 'protein'),
('ee000001-0000-0000-0000-000000000008', 'bbbbbbbb-0000-0000-0000-000000000001', 'Hülsenfrüchte', 'protein'),
('ee000001-0000-0000-0000-000000000009', 'bbbbbbbb-0000-0000-0000-000000000001', 'Eier', 'protein'),
('ee000001-0000-0000-0000-000000000010', 'bbbbbbbb-0000-0000-0000-000000000001', 'Auflauf', 'other'),
('ee000001-0000-0000-0000-000000000011', 'bbbbbbbb-0000-0000-0000-000000000001', 'Nudeln', 'other'),
('ee000001-0000-0000-0000-000000000012', 'bbbbbbbb-0000-0000-0000-000000000001', 'Reis', 'other'),
('ee000001-0000-0000-0000-000000000013', 'bbbbbbbb-0000-0000-0000-000000000001', 'Schnell', 'other'),
('ee000001-0000-0000-0000-000000000014', 'bbbbbbbb-0000-0000-0000-000000000001', 'Ofengericht', 'other'),
('ee000001-0000-0000-0000-000000000015', 'bbbbbbbb-0000-0000-0000-000000000001', 'Flammkuchen', 'other')
ON CONFLICT ON CONSTRAINT uq_tag_name DO NOTHING;
-- ─── Additional Ingredients ──────────────────────────────────────────────────
INSERT INTO ingredient (id, household_id, name, is_staple, category_id) VALUES
-- Gemüse
('dd000002-0000-0000-0000-000000000001', 'bbbbbbbb-0000-0000-0000-000000000001', 'Rucola', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000002', 'bbbbbbbb-0000-0000-0000-000000000001', 'Kirschtomaten', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000003', 'bbbbbbbb-0000-0000-0000-000000000001', 'Chilischote', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000004', 'bbbbbbbb-0000-0000-0000-000000000001', 'Gurke', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000005', 'bbbbbbbb-0000-0000-0000-000000000001', 'Radieschen', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000006', 'bbbbbbbb-0000-0000-0000-000000000001', 'Rote Zwiebeln', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000007', 'bbbbbbbb-0000-0000-0000-000000000001', 'Rote Spitzpaprika', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000008', 'bbbbbbbb-0000-0000-0000-000000000001', 'Gelbe Paprika', false, 'cc000001-0000-0000-0000-000000000001'),
('dd000002-0000-0000-0000-000000000009', 'bbbbbbbb-0000-0000-0000-000000000001', 'Feldsalat', false, 'cc000001-0000-0000-0000-000000000001'),
-- Obst
('dd000002-0000-0000-0000-000000000010', 'bbbbbbbb-0000-0000-0000-000000000001', 'Avocado', false, 'cc000001-0000-0000-0000-000000000002'),
('dd000002-0000-0000-0000-000000000011', 'bbbbbbbb-0000-0000-0000-000000000001', 'Äpfel', false, 'cc000001-0000-0000-0000-000000000002'),
-- Milchprodukte & Eier
('dd000002-0000-0000-0000-000000000012', 'bbbbbbbb-0000-0000-0000-000000000001', 'Hartkäse ital. Art', false, 'cc000001-0000-0000-0000-000000000004'),
('dd000002-0000-0000-0000-000000000013', 'bbbbbbbb-0000-0000-0000-000000000001', 'Cheddar (gerieben)', false, 'cc000001-0000-0000-0000-000000000004'),
('dd000002-0000-0000-0000-000000000014', 'bbbbbbbb-0000-0000-0000-000000000001', 'Frischkäse', false, 'cc000001-0000-0000-0000-000000000004'),
('dd000002-0000-0000-0000-000000000015', 'bbbbbbbb-0000-0000-0000-000000000001', 'Joghurt', false, 'cc000001-0000-0000-0000-000000000004'),
('dd000002-0000-0000-0000-000000000016', 'bbbbbbbb-0000-0000-0000-000000000001', 'Halloumi', false, 'cc000001-0000-0000-0000-000000000004'),
('dd000002-0000-0000-0000-000000000017', 'bbbbbbbb-0000-0000-0000-000000000001', 'Tex-Mex-Käsemischung', false, 'cc000001-0000-0000-0000-000000000004'),
-- Getreide & Nudeln
('dd000002-0000-0000-0000-000000000018', 'bbbbbbbb-0000-0000-0000-000000000001', 'Orzonudeln', false, 'cc000001-0000-0000-0000-000000000005'),
('dd000002-0000-0000-0000-000000000019', 'bbbbbbbb-0000-0000-0000-000000000001', 'Tortellini', false, 'cc000001-0000-0000-0000-000000000005'),
('dd000002-0000-0000-0000-000000000020', 'bbbbbbbb-0000-0000-0000-000000000001', 'Jasminreis', true, 'cc000001-0000-0000-0000-000000000005'),
('dd000002-0000-0000-0000-000000000021', 'bbbbbbbb-0000-0000-0000-000000000001', 'Gnocchi (frisch)', false, 'cc000001-0000-0000-0000-000000000005'),
('dd000002-0000-0000-0000-000000000022', 'bbbbbbbb-0000-0000-0000-000000000001', 'Fladenbrot', false, 'cc000001-0000-0000-0000-000000000005'),
-- Hülsenfrüchte
('dd000002-0000-0000-0000-000000000023', 'bbbbbbbb-0000-0000-0000-000000000001', 'Schwarze Bohnen (Dose)', true, 'cc000001-0000-0000-0000-000000000006'),
-- Konserven
('dd000002-0000-0000-0000-000000000024', 'bbbbbbbb-0000-0000-0000-000000000001', 'Tomaten-Polpa', true, 'cc000001-0000-0000-0000-000000000007'),
('dd000002-0000-0000-0000-000000000025', 'bbbbbbbb-0000-0000-0000-000000000001', 'Chilipolpa', true, 'cc000001-0000-0000-0000-000000000007'),
('dd000002-0000-0000-0000-000000000026', 'bbbbbbbb-0000-0000-0000-000000000001', 'Getrocknete Tomaten', true, 'cc000001-0000-0000-0000-000000000007'),
('dd000002-0000-0000-0000-000000000027', 'bbbbbbbb-0000-0000-0000-000000000001', 'Grüne Oliven', true, 'cc000001-0000-0000-0000-000000000007'),
-- Gewürze & Kräuter
('dd000002-0000-0000-0000-000000000028', 'bbbbbbbb-0000-0000-0000-000000000001', 'Petersilie (frisch)', false, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000029', 'bbbbbbbb-0000-0000-0000-000000000001', 'Basilikum (frisch)', false, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000030', 'bbbbbbbb-0000-0000-0000-000000000001', 'Schnittlauch', false, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000031', 'bbbbbbbb-0000-0000-0000-000000000001', 'Koriander (frisch)', false, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000032', 'bbbbbbbb-0000-0000-0000-000000000001', 'Scharfes Currypulver', true, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000033', 'bbbbbbbb-0000-0000-0000-000000000001', 'Kumin (gemahlen)', true, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000034', 'bbbbbbbb-0000-0000-0000-000000000001', 'Koriander & Kumin', true, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000035', 'bbbbbbbb-0000-0000-0000-000000000001', 'Gewürzmischung HelloMexico', true, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000036', 'bbbbbbbb-0000-0000-0000-000000000001', 'Gewürzmischung Kartoffelknaller', true, 'cc000001-0000-0000-0000-000000000008'),
('dd000002-0000-0000-0000-000000000037', 'bbbbbbbb-0000-0000-0000-000000000001', 'Gewürzmischung HelloMediterraneo',true, 'cc000001-0000-0000-0000-000000000008'),
-- Tiefkühl
('dd000002-0000-0000-0000-000000000038', 'bbbbbbbb-0000-0000-0000-000000000001', 'Flammkuchenteig', false, 'cc000001-0000-0000-0000-000000000013'),
-- Saucen & Pasten
('dd000002-0000-0000-0000-000000000039', 'bbbbbbbb-0000-0000-0000-000000000001', 'Balsamicocreme', true, 'cc000001-0000-0000-0000-000000000010'),
('dd000002-0000-0000-0000-000000000040', 'bbbbbbbb-0000-0000-0000-000000000001', 'Basilikumpaste', true, 'cc000001-0000-0000-0000-000000000010'),
('dd000002-0000-0000-0000-000000000041', 'bbbbbbbb-0000-0000-0000-000000000001', 'Mayonnaise', true, 'cc000001-0000-0000-0000-000000000010'),
-- Nüsse & Samen
('dd000002-0000-0000-0000-000000000042', 'bbbbbbbb-0000-0000-0000-000000000001', 'Haselnusskerne', true, 'cc000001-0000-0000-0000-000000000011')
ON CONFLICT (id) DO NOTHING;
-- ─── Recipes ─────────────────────────────────────────────────────────────────
INSERT INTO recipe (id, household_id, name, serves, cook_time_min, effort, is_child_friendly) VALUES
('ff000002-0000-0000-0000-000000000001', 'bbbbbbbb-0000-0000-0000-000000000001',
'Scharfer Auflauf mit Orzonudeln', 4, 30, 'easy', false),
('ff000002-0000-0000-0000-000000000002', 'bbbbbbbb-0000-0000-0000-000000000001',
'Tortellini mit Ricotta-Füllung', 4, 25, 'easy', true),
('ff000002-0000-0000-0000-000000000003', 'bbbbbbbb-0000-0000-0000-000000000001',
'Knuspriger Flammkuchen mit Mozzarella', 4, 35, 'easy', false),
('ff000002-0000-0000-0000-000000000004', 'bbbbbbbb-0000-0000-0000-000000000001',
'Fruchtiges Tomatenrisotto mit Zitrone', 4, 30, 'medium', false),
('ff000002-0000-0000-0000-000000000005', 'bbbbbbbb-0000-0000-0000-000000000001',
'Karotten-Hafer-Puffer', 4, 40, 'medium', false),
('ff000002-0000-0000-0000-000000000006', 'bbbbbbbb-0000-0000-0000-000000000001',
'Überbackene Penne mit getrockneten Tomaten', 4, 50, 'easy', false),
('ff000002-0000-0000-0000-000000000007', 'bbbbbbbb-0000-0000-0000-000000000001',
'Chili sin Carne', 4, 40, 'easy', false),
('ff000002-0000-0000-0000-000000000008', 'bbbbbbbb-0000-0000-0000-000000000001',
'Gebratene Gnocchi mit Ofenzucchini', 4, 35, 'easy', false),
('ff000002-0000-0000-0000-000000000009', 'bbbbbbbb-0000-0000-0000-000000000001',
'Pasta nach Art Caponata', 4, 45, 'easy', false),
('ff000002-0000-0000-0000-000000000010', 'bbbbbbbb-0000-0000-0000-000000000001',
'Auflauf mit Halloumi und Aubergine', 4, 40, 'medium', false),
('ff000002-0000-0000-0000-000000000011', 'bbbbbbbb-0000-0000-0000-000000000001',
'Buntes Ofengemüse mit Halloumi', 4, 30, 'easy', false)
ON CONFLICT (id) DO NOTHING;
-- ─── Recipe Ingredients ──────────────────────────────────────────────────────
-- V100 ingredients referenced by name via subquery (gen_random_uuid IDs).
-- New dd000002 ingredients referenced by fixed UUID.
-- 01 Scharfer Auflauf mit Orzonudeln
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zwiebeln'), 2, 'Stück', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 2, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Aubergine'), 1, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 'dd000002-0000-0000-0000-000000000003', 2, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 'dd000002-0000-0000-0000-000000000024', 2, 'Dose', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zucchini'), 2, 'Stück', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Oliven (schwarz)'), 100, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 'dd000002-0000-0000-0000-000000000018', 300, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 'dd000002-0000-0000-0000-000000000012', 40, 'g', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 'dd000002-0000-0000-0000-000000000013', 100, 'g', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 'dd000002-0000-0000-0000-000000000001', 75, 'g', 11),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Gemüsebrühe'), 800, 'ml', 12);
-- 02 Tortellini mit Ricotta-Füllung
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 2, 'Stück', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zucchini'), 2, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 'dd000002-0000-0000-0000-000000000002', 400, 'g', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 'dd000002-0000-0000-0000-000000000029', 5, 'g', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 'dd000002-0000-0000-0000-000000000028', 3, 'g', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 'dd000002-0000-0000-0000-000000000030', 2, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 'dd000002-0000-0000-0000-000000000019', 800, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 'dd000002-0000-0000-0000-000000000014', 400, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Sonnenblumenkerne'), 10, 'g', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 4, 'EL', 10);
-- 03 Knuspriger Flammkuchen mit Mozzarella
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000028', 5, 'g', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000030', 5, 'g', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000014', 200, 'g', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000038', 2, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000006', 2, 'Stück', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Mozzarella'), 250, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000004', 2, 'Stück', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000005', 200, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000015', 200, 'g', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Senf (mittelscharf)'), 20, 'ml', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 'dd000002-0000-0000-0000-000000000001', 200, 'g', 11),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 3, 'EL', 12),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Weißweinessig'), 2, 'EL', 13);
-- 04 Fruchtiges Tomatenrisotto mit Zitrone
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Karotten'), 2, 'Stück', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zwiebeln'), 2, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 1, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 'dd000002-0000-0000-0000-000000000012', 80, 'g', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zitronen'), 2, 'Stück', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Gemüsebrühe'), 8, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Risottoreis (Arborio)'), 600, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Mozzarella'), 2, 'Stück', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 'dd000002-0000-0000-0000-000000000040', 24, 'ml', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 'dd000002-0000-0000-0000-000000000002', 400, 'g', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 2, 'EL', 11),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Weißweinessig'), 1, 'EL', 12);
-- 05 Karotten-Hafer-Puffer
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Kartoffeln'), 1200,'g', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000036', 4, 'g', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000004', 2, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000011', 2, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Senf (mittelscharf)'), 20, 'ml', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000031', 20, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000015', 150, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000041', 4, 'EL', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Karotten'), 2, 'Stück', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Haferflocken'), 50, 'g', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000017', 200, 'g', 11),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000032', 2, 'g', 12),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 'dd000002-0000-0000-0000-000000000009', 150, 'g', 13),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Eier'), 2, 'Stück', 14),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Balsamico-Essig'), 2, 'EL', 15);
-- 06 Überbackene Penne mit getrockneten Tomaten
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Penne'), 500, 'g', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 1, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 'dd000002-0000-0000-0000-000000000030', 10, 'g', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 'dd000002-0000-0000-0000-000000000002', 300, 'g', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 'dd000002-0000-0000-0000-000000000026', 100, 'g', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 'dd000002-0000-0000-0000-000000000014', 400, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Senf (mittelscharf)'), 20, 'ml', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 'dd000002-0000-0000-0000-000000000013', 200, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Butter'), 5, 'g', 9);
-- 07 Chili sin Carne
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000020', 300, 'g', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 3, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000006', 2, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000007', 2, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000008', 2, 'Stück', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000023', 2, 'Dose', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000035', 8, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Gemüsebrühe'), 8, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000025', 2, 'Dose', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000039', 24, 'ml', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000031', 20, 'g', 11),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000003', 2, 'Stück', 12),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Schmand'), 150, 'g', 13),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000028', 10, 'g', 14),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 'dd000002-0000-0000-0000-000000000010', 2, 'Stück', 15);
-- 08 Gebratene Gnocchi mit Ofenzucchini
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zucchini'), 2, 'Stück', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 'dd000002-0000-0000-0000-000000000037', 6, 'g', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 'dd000002-0000-0000-0000-000000000042', 40, 'g', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 2, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 'dd000002-0000-0000-0000-000000000026', 100, 'g', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 'dd000002-0000-0000-0000-000000000014', 400, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 'dd000002-0000-0000-0000-000000000012', 40, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 'dd000002-0000-0000-0000-000000000021', 800, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 2, 'EL', 9);
-- 09 Pasta nach Art Caponata
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 'dd000002-0000-0000-0000-000000000026', 100, 'g', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 'dd000002-0000-0000-0000-000000000027', 120, 'g', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 2, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Aubergine'), 1, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 'dd000002-0000-0000-0000-000000000006', 2, 'Stück', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 'dd000002-0000-0000-0000-000000000025', 2, 'Dose', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Penne'), 500, 'g', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zitronen'), 1, 'Stück', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 'dd000002-0000-0000-0000-000000000012', 80, 'g', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 'dd000002-0000-0000-0000-000000000001', 75, 'g', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 2, 'EL', 11);
-- 10 Auflauf mit Halloumi und Aubergine
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 4, 'Stück', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zwiebeln'), 4, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Tomaten'), 6, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Aubergine'), 2, 'Stück', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 'dd000002-0000-0000-0000-000000000034', 4, 'g', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Tomatenmark'), 70, 'g', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 'dd000002-0000-0000-0000-000000000039', 24, 'ml', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 'dd000002-0000-0000-0000-000000000016', 400, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 'dd000002-0000-0000-0000-000000000029', 20, 'g', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 'dd000002-0000-0000-0000-000000000022', 1, 'Stück', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 5, 'EL', 11);
-- 11 Buntes Ofengemüse mit Halloumi
INSERT INTO recipe_ingredient (id, recipe_id, ingredient_id, quantity, unit, sort_order) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Süßkartoffeln'), 2, 'Stück', 1),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 'dd000002-0000-0000-0000-000000000006', 2, 'Stück', 2),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Tomaten'), 2, 'Stück', 3),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 'dd000002-0000-0000-0000-000000000028', 20, 'g', 4),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 'dd000002-0000-0000-0000-000000000010', 2, 'Stück', 5),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Knoblauch'), 1, 'Stück', 6),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Zitronen'), 1, 'Stück', 7),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 'dd000002-0000-0000-0000-000000000033', 2, 'g', 8),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 'dd000002-0000-0000-0000-000000000003', 1, 'Stück', 9),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 'dd000002-0000-0000-0000-000000000016', 500, 'g', 10),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', (SELECT id FROM ingredient WHERE household_id = 'bbbbbbbb-0000-0000-0000-000000000001' AND name = 'Olivenöl (extra vergine)'), 2, 'EL', 11);
-- ─── Recipe Steps ─────────────────────────────────────────────────────────────
-- 01 Scharfer Auflauf mit Orzonudeln
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 1, 'Backofen auf 200 °C (Grillfunktion) vorheizen. Zwiebeln und Knoblauch abziehen und fein hacken. Aubergine in ca. 2 cm große Würfel schneiden. Heiße Gemüsebrühe vorbereiten. Chilischote halbieren, Kerne entfernen und in feine Streifen schneiden (Achtung: scharf!).'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 2, 'Öl in einer großen Pfanne erhitzen. Zwiebeln und Knoblauch darin 23 Min. glasig andünsten. Orzonudeln und Aubergine zugeben und anbraten, bis das Öl vollständig aufgenommen ist. Brühe, Tomaten-Polpa und Chili zugeben, verrühren und ca. 10 Min. bei mittlerer Hitze köcheln lassen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 3, 'Zucchini längs halbieren und in 0,5 cm Scheiben schneiden. Oliven in Ringe schneiden. Zucchini und die Hälfte der Oliven zum Orzo geben und ca. 3 Min. mitkochen. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 4, 'Hartkäse fein reiben. Cheddar unter den Orzo heben und alles in eine Auflaufform füllen. Mit Hartkäse bestreuen und im Backofen 510 Min. gratinieren, bis der Käse goldbraun ist.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 5, 'Öl, Salz und Pfeffer in einer großen Schüssel vermengen. Rucola und restliche Oliven unterheben.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000001', 6, 'Orzoauflauf auf Teller verteilen und mit dem Rucola-Oliven-Salat servieren.');
-- 02 Tortellini mit Ricotta-Füllung
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 1, 'Backofen auf 220 °C Ober-/Unterhitze (200 °C Umluft) vorheizen. Knoblauch abziehen. Zucchini in 0,5 cm dünne Scheiben schneiden. Kirschtomaten halbieren. Gemüse in eine große Schüssel geben, Knoblauch hinzupressen, mit Olivenöl, Salz und Pfeffer vermengen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 2, 'Gemüse auf einem mit Backpapier belegten Blech verteilen und 1820 Min. backen, bis die Zucchini leicht bräunt und die Tomaten fast geschmolzen sind. Währenddessen Kräuter abzupfen, Basilikum und Petersilie fein hacken, Schnittlauch in Röllchen schneiden.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 3, 'Großen Topf mit gesalzenem Wasser zum Kochen bringen. Frischkäse mit den gehackten Kräutern verrühren, mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 4, 'Sonnenblumenkerne in einer kleinen Pfanne ohne Fett bei mittlerer Hitze goldbraun rösten.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 5, 'Tortellini in den letzten 34 Min. der Gemüse-Backzeit in das kochende Wasser geben und al dente garen. Abgießen, zurück in den Topf geben. Gebackenes Gemüse und 4 EL Kräuterfrischkäse unterheben.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000002', 6, 'Tortellini auf Teller verteilen. Restlichen Kräuterfrischkäse als Kleckse darauf verteilen, mit Sonnenblumenkernen bestreuen und mit Basilikum dekorieren.');
-- 03 Knuspriger Flammkuchen mit Mozzarella
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 1, 'Backofen auf 200 °C Ober-/Unterhitze (180 °C Umluft) vorheizen. Schnittlauch und Petersilie fein hacken und unter den Frischkäse heben. Flammkuchenteig auf einem mit Backpapier belegten Blech ausrollen und gleichmäßig mit dem Kräuterfrischkäse bestreichen (ca. 1 cm Rand frei lassen). Mit Salz und Pfeffer bestreuen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 2, 'Rote Zwiebeln abziehen, halbieren und in feine Streifen schneiden. Mozzarella in kleine Stücke zupfen. Flammkuchen mit Zwiebelstreifen belegen und Mozzarellastücke darauf verteilen. Auf der mittleren Schiene 1315 Min. knusprig backen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 3, 'Gurke in lange, dünne Scheiben hobeln oder schneiden. Radieschen vierteln.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 4, 'Joghurt, Senf, Olivenöl, Weißweinessig, Salz und Pfeffer in einer großen Schüssel zu einem Dressing verrühren.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 5, 'Rucola, Gurkenstreifen und Radieschen in die Schüssel geben und unterheben. Bis zum Anrichten ziehen lassen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000003', 6, 'Flammkuchen in Stücke schneiden und auf Teller verteilen. Mit dem Salat servieren.');
-- 04 Fruchtiges Tomatenrisotto mit Zitrone
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 1, '1200 ml Wasser erhitzen. Karotten schälen und grob reiben. Zwiebeln fein würfeln. Knoblauch in dünne Scheiben schneiden. Hartkäse fein reiben. Zitronenschale abreiben, Zitronen halbieren und entsaften. Gemüsebrühe im heißen Wasser auflösen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 2, 'Öl in einem großen Topf erhitzen. Zwiebeln und Knoblauch darin 23 Min. glasig andünsten.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 3, 'Risottoreis zugeben und unter Rühren erhitzen, bis das Öl vollständig aufgenommen ist. Karotten und ein Drittel der Brühe zugeben und gut verrühren. Restliche Brühe nach und nach einrühren. Insgesamt ca. 20 Min. köcheln lassen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 4, 'Mozzarella in mundgerechte Stücke schneiden. Mit Basilikumpaste, Olivenöl, Weißweinessig, Salz, Pfeffer und 1 Prise Zucker marinieren und ziehen lassen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 5, 'Kirschtomaten und Hartkäse in den Risotto einrühren. Mit Zitronenabrieb und Zitronensaft abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000004', 6, 'Risotto auf Teller verteilen und mit dem marinierten Basilikummozzarella toppen.');
-- 05 Karotten-Hafer-Puffer
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 1, 'Backofen auf 200 °C Ober-/Unterhitze (180 °C Umluft) vorheizen. Kartoffeln ungeschält in Spalten (Wedges) schneiden. Auf einem mit Backpapier belegten Blech verteilen, mit Öl beträufeln, mit Gewürzmischung Kartoffelknaller, Salz und Pfeffer würzen. 2025 Min. backen, bis die Wedges innen weich und außen knusprig sind.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 2, 'Gurke längs halbieren und in Halbmondscheiben schneiden. Äpfel entkernen und in dünne Halbmonde schneiden. Balsamicoessig, Öl und Senf zu einem Dressing verrühren, mit Salz und Pfeffer abschmecken. Gurke und Apfel unterheben und marinieren lassen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 3, 'Koriander fein hacken und mit Joghurt und Mayonnaise verrühren. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 4, 'Karotten schälen und grob raspeln. In einer großen Schüssel Karotten, Haferflocken, Eier, Tex-Mex-Käsemischung und Currypulver vermischen. Mit Salz und Pfeffer würzen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 5, 'Öl in einer großen Pfanne erhitzen. Karottenmischung mithilfe eines Esslöffels zu Puffern formen und leicht flach drücken. Von beiden Seiten je ca. 3 Min. goldbraun braten.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000005', 6, 'Feldsalat unter den Apfel-Gurken-Salat heben. Auf Tellern anrichten, Karottenpuffer und Kartoffelwedges dazu platzieren. Mit Korianderdip beträufeln und genießen.');
-- 06 Überbackene Penne mit getrockneten Tomaten
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 1, 'Backofen auf 200 °C Ober-/Unterhitze (180 °C Umluft) vorheizen. Reichlich gesalzenes Wasser zum Kochen bringen. Penne 79 Min. bissfest garen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 2, 'Knoblauch abziehen und fein würfeln. Schnittlauch in Röllchen schneiden. Kirschtomaten halbieren. Getrocknete Tomaten grob zerkleinern.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 3, 'Frischkäse mit Knoblauch, Senf und dem Großteil des Schnittlauchs verrühren. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 4, 'Penne abgießen, dabei 100 ml Kochwasser auffangen. Penne zurück in den Topf geben. Frischkäsemischung und getrocknete Tomaten einrühren, bei Bedarf Kochwasser zugeben, bis eine cremige Konsistenz entsteht. Kirschtomaten unterheben.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 5, 'Penne-Mischung in eine mit Butter eingefettete Auflaufform füllen. Mit Cheddar bestreuen und im Backofen 67 Min. gratinieren.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000006', 6, 'Auflauf auf Teller verteilen und mit restlichem Schnittlauch bestreuen.');
-- 07 Chili sin Carne
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 1, 'Jasminreis mit 600 ml heißem Wasser in einem kleinen Topf aufkochen. Bei niedriger Hitze ca. 10 Min. köcheln lassen, vom Herd nehmen und abgedeckt 10 Min. quellen lassen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 2, 'Knoblauch abziehen und in feine Streifen schneiden. Rote Zwiebeln halbieren und in Streifen schneiden. Paprika halbieren, entkernen und in Streifen schneiden. Schwarze Bohnen abgießen und kalt abspülen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 3, 'Öl in einem großen Topf erhitzen. Zwiebeln und Paprika 23 Min. anbraten. Knoblauch und Gewürzmischung HelloMexico zugeben und 1 Min. mitbraten. Mit Salz und Pfeffer würzen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 4, 'Schwarze Bohnen, Chilipolpa, Gemüsebrühe und Balsamicocreme zugeben. Chili 2530 Min. bei niedriger Hitze köcheln lassen, bis die Paprika weich und das Chili cremig ist. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 5, 'Koriander und Petersilie fein hacken. Chilischote entkernen und in Streifen schneiden (Achtung: scharf!). Avocado halbieren, Stein entfernen und in Streifen schneiden. Schmand mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000007', 6, 'Reis mit einer Gabel auflockern, Koriander unterheben und auf Teller verteilen. Chili daneben anrichten, mit Chili und Petersilie bestreuen. Mit Avocado und einem Klecks Schmand servieren.');
-- 08 Gebratene Gnocchi mit Ofenzucchini
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 1, 'Backofen auf 220 °C Ober-/Unterhitze (200 °C Umluft) vorheizen. Zucchini in 0,5 cm dünne Scheiben schneiden. Auf einem mit Backpapier belegten Blech verteilen, mit Gewürzmischung HelloMediterraneo, Öl, Salz und Pfeffer würzen. Ca. 15 Min. backen, bis die Zucchini weich ist.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 2, 'Haselnusskerne in einer großen Pfanne ohne Fett bei mittlerer Hitze rösten, bis sie duften. Herausnehmen, abkühlen lassen und grob hacken. Pfanne beiseite stellen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 3, 'Knoblauch abziehen. Getrocknete Tomaten grob hacken und mit Frischkäse, Knoblauch und 200 ml Wasser in ein hohes Gefäß geben. Mit einem Pürierstab zu einer glatten Soße mixen. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 4, 'Öl in der Pfanne bei mittlerer Hitze erhitzen. Gnocchi darin 89 Min. anbraten, bis sie knusprig und leicht gebräunt sind.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 5, 'Soße zu den Gnocchi geben, alles vermengen und ca. 2 Min. einkochen lassen. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000008', 6, 'Gnocchi auf Teller verteilen, mit gebackener Zucchini, geriebenem Hartkäse und Haselnusskernen toppen.');
-- 09 Pasta nach Art Caponata
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 1, 'Reichlich gesalzenes Wasser für die Pasta aufkochen. Getrocknete Tomaten und grüne Oliven grob hacken (Öl der Oliven auffangen). Knoblauch abziehen und fein hacken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 2, 'Aubergine in 12 cm Würfel schneiden. Rote Zwiebeln halbieren und in Streifen schneiden. Olivenöl in einer großen Pfanne stark erhitzen. Aubergine 34 Min. scharf anbraten. Zwiebeln, getrocknete Tomaten, Oliven und Knoblauch zugeben und 2 Min. mitbraten.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 3, 'Hitze reduzieren, Chilipolpa zugeben und alles 1012 Min. köcheln lassen, bis die Soße eingedickt und das Gemüse weich ist. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 4, 'Penne ca. 10 Min. bissfest garen und abgießen. Zitrone heiß abwaschen, Schale abreiben und in Spalten schneiden.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 5, 'Penne zur Soße in die Pfanne geben und gut vermengen. Mit Zitronenabrieb und Zitronensaft abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000009', 6, 'Pasta in tiefen Tellern anrichten, mit geriebenem Hartkäse und Rucola bestreuen.');
-- 10 Auflauf mit Halloumi und Aubergine
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 1, 'Backofen auf 200 °C Ober-/Unterhitze (180 °C Umluft) vorheizen. 2 Knoblauchzehen mit dem Messerrücken andrücken und 15 Min. im Ofen rösten. Restlichen Knoblauch abziehen und fein hacken. Zwiebeln in Streifen schneiden. Tomaten und Auberginen in ca. 2 cm Würfel schneiden.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 2, 'Öl in einer großen Pfanne erhitzen. Zwiebeln und gehackten Knoblauch 3 Min. andünsten. Aubergine, Tomatenwürfel sowie Koriander & Kumin zugeben und kurz anbraten.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 3, 'Gemüse mit 200 ml Wasser ablöschen. Tomatenmark und Balsamicocreme einrühren und ca. 10 Min. köcheln lassen. Mit Salz und Pfeffer abschmecken. Währenddessen Halloumi in 0,5 cm Scheiben schneiden und Basilikumblätter abzupfen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 4, 'Soße in eine große Auflaufform geben und Halloumischeiben darüber verteilen. Ca. 20 Min. im Ofen backen. Fladenbrot in 2 cm Scheiben schneiden.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 5, 'Geröstete Knoblauchzehen abziehen und fein hacken. Mit Olivenöl, Salz und Pfeffer verrühren. Knoblauchöl auf die Brotscheiben träufeln, auf ein Backblech legen und 510 Min. knusprig aufbacken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000010', 6, 'Auflauf 2 Min. unter dem Grill bräunen, bis der Halloumi goldbraun ist. Mit Basilikumblättern bestreuen und mit dem Knoblauchbrot servieren.');
-- 11 Buntes Ofengemüse mit Halloumi
INSERT INTO recipe_step (id, recipe_id, step_number, instruction) VALUES
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 1, 'Backofen auf 220 °C Ober-/Unterhitze (200 °C Umluft) vorheizen. Süßkartoffeln schälen und in 2 cm Würfel schneiden. Rote Zwiebeln halbieren und in ca. 1 cm Spalten schneiden.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 2, 'Süßkartoffelwürfel und Zwiebelspalten auf einem mit Backpapier belegten Blech verteilen, mit Salz und Pfeffer würzen. Ca. 25 Min. backen, bis die Süßkartoffeln weich sind.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 3, 'Tomaten halbieren und in Spalten schneiden. Petersilie fein hacken. Avocado würfeln. Die Hälfte der Petersilie und die Avocadowürfel zu den Tomaten geben und beiseitestellen.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 4, 'Knoblauch und Chilischote fein hacken. Restliche Petersilie mit Kumin, Knoblauch, Chili (Achtung: scharf!), Zitronensaft und Olivenöl zu einem Chimichurri verrühren. Mit Salz und Pfeffer abschmecken.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 5, 'Halloumi in ca. 3 cm Würfel schneiden. In einer Pfanne mit etwas Öl bei mittlerer Hitze rundherum 34 Min. goldbraun braten.'),
(gen_random_uuid(), 'ff000002-0000-0000-0000-000000000011', 6, 'Geröstetes Gemüse und Zwiebeln in die Schüssel mit Tomaten und Avocado geben, vorsichtig vermengen. Auf Teller verteilen, mit Halloumiwürfeln toppen und Petersilien-Chimichurri darüberträufeln.');
-- ─── Recipe Tags ──────────────────────────────────────────────────────────────
INSERT INTO recipe_tag (recipe_id, tag_id) VALUES
-- 01 Scharfer Auflauf
('ff000002-0000-0000-0000-000000000001', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000001', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000001', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000001', 'ee000001-0000-0000-0000-000000000010'), -- Auflauf
-- 02 Tortellini
('ff000002-0000-0000-0000-000000000002', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000002', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000002', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000002', 'ee000001-0000-0000-0000-000000000011'), -- Nudeln
('ff000002-0000-0000-0000-000000000002', 'ee000001-0000-0000-0000-000000000013'), -- Schnell
-- 03 Flammkuchen
('ff000002-0000-0000-0000-000000000003', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000003', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000003', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000003', 'ee000001-0000-0000-0000-000000000015'), -- Flammkuchen
-- 04 Tomatenrisotto
('ff000002-0000-0000-0000-000000000004', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000004', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000004', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000004', 'ee000001-0000-0000-0000-000000000012'), -- Reis
('ff000002-0000-0000-0000-000000000004', 'ee000001-0000-0000-0000-000000000013'), -- Schnell
-- 05 Karotten-Hafer-Puffer
('ff000002-0000-0000-0000-000000000005', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000005', 'ee000001-0000-0000-0000-000000000003'), -- Deutsch
('ff000002-0000-0000-0000-000000000005', 'ee000001-0000-0000-0000-000000000009'), -- Eier
-- 06 Überbackene Penne
('ff000002-0000-0000-0000-000000000006', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000006', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000006', 'ee000001-0000-0000-0000-000000000010'), -- Auflauf
('ff000002-0000-0000-0000-000000000006', 'ee000001-0000-0000-0000-000000000011'), -- Nudeln
-- 07 Chili sin Carne
('ff000002-0000-0000-0000-000000000007', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000007', 'ee000001-0000-0000-0000-000000000006'), -- Mexikanisch
('ff000002-0000-0000-0000-000000000007', 'ee000001-0000-0000-0000-000000000008'), -- Hülsenfrüchte
('ff000002-0000-0000-0000-000000000007', 'ee000001-0000-0000-0000-000000000012'), -- Reis
-- 08 Gnocchi
('ff000002-0000-0000-0000-000000000008', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000008', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000008', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000008', 'ee000001-0000-0000-0000-000000000013'), -- Schnell
-- 09 Pasta Caponata
('ff000002-0000-0000-0000-000000000009', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000009', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000009', 'ee000001-0000-0000-0000-000000000011'), -- Nudeln
-- 10 Auflauf Halloumi
('ff000002-0000-0000-0000-000000000010', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000010', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000010', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000010', 'ee000001-0000-0000-0000-000000000010'), -- Auflauf
-- 11 Buntes Ofengemüse
('ff000002-0000-0000-0000-000000000011', 'ee000001-0000-0000-0000-000000000001'), -- Vegetarisch
('ff000002-0000-0000-0000-000000000011', 'ee000001-0000-0000-0000-000000000002'), -- Glutenfrei
('ff000002-0000-0000-0000-000000000011', 'ee000001-0000-0000-0000-000000000004'), -- Mediterran
('ff000002-0000-0000-0000-000000000011', 'ee000001-0000-0000-0000-000000000007'), -- Käse
('ff000002-0000-0000-0000-000000000011', 'ee000001-0000-0000-0000-000000000014') -- Ofengericht
ON CONFLICT DO NOTHING;

View File

@@ -161,7 +161,7 @@ class WeekPlanControllerTest {
@Test @Test
void getSuggestionsShouldReturn200() throws Exception { void getSuggestionsShouldReturn200() throws Exception {
var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null); var recipe = new SuggestionResponse.SuggestionRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15);
var item = new SuggestionResponse.SuggestionItem(recipe, 1.5, false); var item = new SuggestionResponse.SuggestionItem(recipe, 1.5, false);
var response = new SuggestionResponse(List.of(item)); var response = new SuggestionResponse(List.of(item));

View File

@@ -165,7 +165,7 @@ class RecipeControllerTest {
private RecipeCreateRequest sampleCreateRequest() { private RecipeCreateRequest sampleCreateRequest() {
var ingredientId = UUID.randomUUID(); var ingredientId = UUID.randomUUID();
return new RecipeCreateRequest( return new RecipeCreateRequest(
"Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null, "Spaghetti Bolognese", 4, 45, "medium", null,
List.of(new RecipeCreateRequest.IngredientEntry( List.of(new RecipeCreateRequest.IngredientEntry(
ingredientId, null, new BigDecimal("400"), "g", (short) 1)), ingredientId, null, new BigDecimal("400"), "g", (short) 1)),
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")), List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),

View File

@@ -126,7 +126,7 @@ class RecipeServiceTest {
}); });
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null, "Spaghetti Bolognese", 4, 45, "medium", null,
List.of(new RecipeCreateRequest.IngredientEntry( List.of(new RecipeCreateRequest.IngredientEntry(
ingredient.getId(), null, new BigDecimal("400"), "g", (short) 1)), ingredient.getId(), null, new BigDecimal("400"), "g", (short) 1)),
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")), List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),
@@ -166,7 +166,7 @@ class RecipeServiceTest {
}); });
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Carbonara", (short) 2, (short) 30, "medium", false, null, "Carbonara", 2, 30, "medium", null,
List.of(new RecipeCreateRequest.IngredientEntry( List.of(new RecipeCreateRequest.IngredientEntry(
null, "pancetta", new BigDecimal("100"), "g", (short) 1)), null, "pancetta", new BigDecimal("100"), "g", (short) 1)),
List.of(), List.of(),
@@ -192,7 +192,7 @@ class RecipeServiceTest {
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> i.getArgument(0)); when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> i.getArgument(0));
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Chicken Rice", (short) 3, (short) 25, "easy", true, null, "Chicken Rice", 3, 25, "easy", null,
List.of(new RecipeCreateRequest.IngredientEntry( List.of(new RecipeCreateRequest.IngredientEntry(
ingredient.getId(), null, new BigDecimal("300"), "g", (short) 1)), ingredient.getId(), null, new BigDecimal("300"), "g", (short) 1)),
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Cook rice.")), List.of(new RecipeCreateRequest.StepEntry((short) 1, "Cook rice.")),
@@ -450,7 +450,7 @@ class RecipeServiceTest {
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty()); when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Test", (short) 2, (short) 15, "easy", false, null, "Test", 2, 15, "easy", null,
List.of(), List.of(), List.of()); List.of(), List.of(), List.of());
assertThatThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request)) assertThatThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request))
@@ -466,7 +466,7 @@ class RecipeServiceTest {
when(ingredientRepository.findById(ingredientId)).thenReturn(Optional.empty()); when(ingredientRepository.findById(ingredientId)).thenReturn(Optional.empty());
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Test", (short) 2, (short) 15, "easy", false, null, "Test", 2, 15, "easy", null,
List.of(new RecipeCreateRequest.IngredientEntry( List.of(new RecipeCreateRequest.IngredientEntry(
ingredientId, null, new BigDecimal("100"), "g", (short) 1)), ingredientId, null, new BigDecimal("100"), "g", (short) 1)),
List.of(), List.of()); List.of(), List.of());
@@ -491,7 +491,7 @@ class RecipeServiceTest {
}); });
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Simple", (short) 1, (short) 5, "easy", false, null, "Simple", 1, 5, "easy", null,
null, null, null); null, null, null);
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request); RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
@@ -518,7 +518,7 @@ class RecipeServiceTest {
.thenReturn(Optional.empty()); .thenReturn(Optional.empty());
var request = new RecipeCreateRequest( var request = new RecipeCreateRequest(
"Updated", (short) 2, (short) 20, "easy", false, null, "Updated", 2, 20, "easy", null,
List.of(), List.of(), List.of()); List.of(), List.of(), List.of());
assertThatThrownBy(() -> recipeService.updateRecipe(HOUSEHOLD_ID, id, request)) assertThatThrownBy(() -> recipeService.updateRecipe(HOUSEHOLD_ID, id, request))

23
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('Startseite lädt korrekt', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Willkommen bei Mealprep' })).toBeVisible();
});

View File

@@ -0,0 +1,12 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'e2e',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -1,22 +1,51 @@
<script lang="ts"> <script lang="ts">
interface Warning { interface WarningItem {
title: string; dayShort: string;
explanation: string; recipeName: string;
slotId: number;
} }
let { warnings }: { warnings: Warning[] } = $props(); interface ActionWarning {
title: string;
items: WarningItem[];
}
let { warnings, weekStart }: { warnings: ActionWarning[]; weekStart: string } = $props();
</script> </script>
{#each warnings as warning} {#each warnings as warning (warning.title)}
<div <div
data-testid="warning-card" data-testid="warning-card"
class="rounded-[var(--radius-lg)] border border-[var(--yellow-light)] bg-[var(--yellow-tint)] px-4 py-3" class="rounded-[var(--radius-lg)] border border-[var(--yellow-light)] bg-[var(--yellow-tint)] overflow-hidden"
> >
<!-- Header row -->
<div class="px-4 py-2.5 border-b border-[var(--yellow-light)]">
<p class="font-[var(--font-sans)] text-[13px] font-medium text-[var(--yellow-text)]"> <p class="font-[var(--font-sans)] text-[13px] font-medium text-[var(--yellow-text)]">
{warning.title} {warning.title}
</p> </p>
<p class="mt-1 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]"> </div>
{warning.explanation}
</p> <!-- Item rows -->
{#each warning.items as item (item.slotId)}
<div class="flex items-center justify-between gap-3 px-4 py-2.5 border-b border-[var(--yellow-light)] last:border-b-0">
<!-- Left: day label + recipe name -->
<div class="flex items-center gap-2 min-w-0">
<span class="font-[var(--font-sans)] text-[11px] font-medium text-[var(--yellow-text)] w-6 flex-shrink-0">
{item.dayShort}
</span>
<span class="font-[var(--font-sans)] text-[13px] text-[var(--color-text)] truncate">
{item.recipeName}
</span>
</div>
<!-- Right: swap link -->
<a
href="/planner?week={weekStart}&swap={item.slotId}"
class="font-[var(--font-sans)] text-[12px] font-medium text-[var(--yellow-text)] flex-shrink-0 hover:underline"
>
Tauschen →
</a>
</div>
{/each}
</div> </div>
{/each} {/each}

View File

@@ -23,13 +23,30 @@
} = $props(); } = $props();
const effortOptions = [ const effortOptions = [
{ label: 'Leicht', value: 'Easy' }, { label: 'Leicht', value: 'easy' },
{ label: 'Mittel', value: 'Medium' }, { label: 'Mittel', value: 'medium' },
{ label: 'Schwer', value: 'Hard' } { label: 'Schwer', value: 'hard' }
]; ];
const initial = (() => $state.snapshot(recipe))(); const initial = (() => $state.snapshot(recipe))();
const TAG_TYPE_LABELS: Record<string, string> = {
dietary: 'Ernährung',
cuisine: 'Küche',
protein: 'Protein',
other: 'Sonstiges'
};
const groupedCategories = $derived(
Object.entries(
categories.reduce<Record<string, typeof categories>>((acc, cat) => {
const type = cat.tagType ?? 'other';
(acc[type] ??= []).push(cat);
return acc;
}, {})
)
);
let name = $state(initial?.name ?? ''); let name = $state(initial?.name ?? '');
let serves = $state<number | ''>(initial?.serves ?? ''); let serves = $state<number | ''>(initial?.serves ?? '');
let cookTimeMin = $state<number | ''>(initial?.cookTimeMin ?? ''); let cookTimeMin = $state<number | ''>(initial?.cookTimeMin ?? '');
@@ -43,6 +60,17 @@
})) ?? [{ name: '', quantity: '' as number | '', unit: '' }] })) ?? [{ name: '', quantity: '' as number | '', unit: '' }]
); );
let steps = $state(initial?.steps.map((s) => s.instruction) ?? ['']); let steps = $state(initial?.steps.map((s) => s.instruction) ?? ['']);
let heroImageUrl = $state<string | null>(initial?.heroImageUrl ?? null);
function handleImageChange(e: Event) {
const file = (e.currentTarget as HTMLInputElement).files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
heroImageUrl = reader.result as string;
};
reader.readAsDataURL(file);
}
</script> </script>
<form method="POST" {action} use:enhance> <form method="POST" {action} use:enhance>
@@ -140,6 +168,37 @@
</div> </div>
</div> </div>
<!-- Image -->
<div class="mb-[24px]">
<p class="mb-[12px] text-[13px] font-medium text-[var(--color-text)]">Bild</p>
{#if heroImageUrl}
<img
src={heroImageUrl}
alt=""
class="mb-[8px] max-h-[200px] w-full rounded-[var(--radius-md)] object-cover"
/>
<button
type="button"
onclick={() => (heroImageUrl = null)}
class="mb-[8px] text-[12px] text-[var(--color-text-muted)] hover:text-[var(--color-error)] cursor-pointer"
>
Bild entfernen
</button>
{/if}
<label
class="block w-full cursor-pointer rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-page)] px-[12px] py-[10px] text-center text-[13px] text-[var(--color-text-muted)]"
>
<input
type="file"
accept="image/*"
onchange={handleImageChange}
class="sr-only"
/>
{heroImageUrl ? 'Bild ändern' : 'Bild hochladen'}
</label>
<input type="hidden" name="heroImageUrl" value={heroImageUrl ?? ''} />
</div>
<!-- Ingredients --> <!-- Ingredients -->
<div class="mb-[24px]"> <div class="mb-[24px]">
<p class="mb-[12px] text-[13px] font-medium text-[var(--color-text)]">Zutaten</p> <p class="mb-[12px] text-[13px] font-medium text-[var(--color-text)]">Zutaten</p>
@@ -227,9 +286,14 @@
<div <div
class="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-[20px]" class="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-[20px]"
> >
<p class="mb-[12px] text-[13px] font-medium text-[var(--color-text)]">Kategorien</p> <p class="mb-[16px] text-[13px] font-medium text-[var(--color-text)]">Kategorien</p>
{#each groupedCategories as [type, tags] (type)}
<div class="mb-[16px] last:mb-0">
<p class="mb-[8px] text-[11px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
{TAG_TYPE_LABELS[type] ?? type}
</p>
<div class="flex flex-wrap gap-[8px]"> <div class="flex flex-wrap gap-[8px]">
{#each categories as cat (cat.id)} {#each tags as cat (cat.id)}
<label <label
class={[ class={[
'cursor-pointer rounded-[var(--radius-full)] border px-[12px] py-[6px] text-[13px] font-medium', 'cursor-pointer rounded-[var(--radius-full)] border px-[12px] py-[6px] text-[13px] font-medium',
@@ -257,6 +321,8 @@
{/each} {/each}
</div> </div>
</div> </div>
{/each}
</div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { error, redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { apiClient } from '$lib/server/api'; import { apiClient } from '$lib/server/api';
const VALID_EFFORTS = ['Easy', 'Medium', 'Hard']; const VALID_EFFORTS = ['easy', 'medium', 'hard'];
export const load: PageServerLoad = async ({ fetch, params }) => { export const load: PageServerLoad = async ({ fetch, params }) => {
const api = apiClient(fetch); const api = apiClient(fetch);
@@ -17,9 +17,7 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
const recipe = recipeResult.data; const recipe = recipeResult.data;
const allTags = tagsResult.data ?? []; const allTags = tagsResult.data ?? [];
const categories = allTags const categories = allTags.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
.filter((t) => t.tagType === 'category')
.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
return { return {
recipe: { recipe: {
@@ -50,6 +48,7 @@ export const actions: Actions = {
const serves = formData.get('serves'); const serves = formData.get('serves');
const cookTimeMin = formData.get('cookTimeMin'); const cookTimeMin = formData.get('cookTimeMin');
const effort = formData.get('effort') as string; const effort = formData.get('effort') as string;
const heroImageUrl = (formData.get('heroImageUrl') as string) || null;
const ingredientsJson = formData.get('ingredientsJson') as string; const ingredientsJson = formData.get('ingredientsJson') as string;
const stepsJson = formData.get('stepsJson') as string; const stepsJson = formData.get('stepsJson') as string;
const tagIds = formData.getAll('tagIds') as string[]; const tagIds = formData.getAll('tagIds') as string[];
@@ -76,6 +75,7 @@ export const actions: Actions = {
serves: serves ? Number(serves) || undefined : undefined, serves: serves ? Number(serves) || undefined : undefined,
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || undefined : undefined, cookTimeMin: cookTimeMin ? Number(cookTimeMin) || undefined : undefined,
effort, effort,
heroImageUrl,
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[]) ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
.filter((ing) => ing.name?.trim()) .filter((ing) => ing.name?.trim())
.map((ing, i) => ({ .map((ing, i) => ({

View File

@@ -26,7 +26,7 @@ describe('edit recipe page — load', () => {
name: 'Spaghetti Bolognese', name: 'Spaghetti Bolognese',
serves: 4, serves: 4,
cookTimeMin: 30, cookTimeMin: 30,
effort: 'Easy', effort: 'easy',
ingredients: [{ ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g' }], ingredients: [{ ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g' }],
steps: [{ stepNumber: 1, instruction: 'Kochen' }], steps: [{ stepNumber: 1, instruction: 'Kochen' }],
tags: [{ id: 't1', name: 'Pasta', tagType: 'category' }] tags: [{ id: 't1', name: 'Pasta', tagType: 'category' }]
@@ -53,7 +53,7 @@ describe('edit recipe page — load', () => {
}); });
const result = await load({ fetch: vi.fn(), params: { id: 'r1' } } as any); const result = await load({ fetch: vi.fn(), params: { id: 'r1' } } as any);
expect(result.recipe.name).toBe('Spaghetti Bolognese'); expect(result.recipe.name).toBe('Spaghetti Bolognese');
expect(result.recipe.effort).toBe('Easy'); expect(result.recipe.effort).toBe('easy');
}); });
it('returns categories from tags', async () => { it('returns categories from tags', async () => {
@@ -82,7 +82,7 @@ describe('edit recipe page — update action', () => {
const makeFormData = (overrides: Record<string, string | string[]> = {}) => { const makeFormData = (overrides: Record<string, string | string[]> = {}) => {
const base: Record<string, string | string[]> = { const base: Record<string, string | string[]> = {
name: 'Test Rezept', name: 'Test Rezept',
effort: 'Easy', effort: 'easy',
tagIds: ['t1'], tagIds: ['t1'],
ingredientsJson: '[]', ingredientsJson: '[]',
stepsJson: '[]', stepsJson: '[]',
@@ -174,7 +174,33 @@ describe('edit recipe page — update action', () => {
} as any).catch(() => {}); } as any).catch(() => {});
expect(mockPut).toHaveBeenCalledWith('/v1/recipes/{id}', expect.objectContaining({ expect(mockPut).toHaveBeenCalledWith('/v1/recipes/{id}', expect.objectContaining({
params: { path: { id: 'r1' } }, params: { path: { id: 'r1' } },
body: expect.objectContaining({ name: 'Test Rezept', effort: 'Easy' }) body: expect.objectContaining({ name: 'Test Rezept', effort: 'easy' })
}));
});
it('sends heroImageUrl in PUT body when provided', async () => {
mockPut.mockResolvedValue({ error: undefined });
const fd = makeFormData({ heroImageUrl: 'data:image/jpeg;base64,abc123' });
await actions.update({
request: { formData: async () => fd },
fetch: vi.fn(),
params: { id: 'r1' }
} as any).catch(() => {});
expect(mockPut).toHaveBeenCalledWith('/v1/recipes/{id}', expect.objectContaining({
body: expect.objectContaining({ heroImageUrl: 'data:image/jpeg;base64,abc123' })
}));
});
it('sends null heroImageUrl when field is empty', async () => {
mockPut.mockResolvedValue({ error: undefined });
const fd = makeFormData({ heroImageUrl: '' });
await actions.update({
request: { formData: async () => fd },
fetch: vi.fn(),
params: { id: 'r1' }
} as any).catch(() => {});
expect(mockPut).toHaveBeenCalledWith('/v1/recipes/{id}', expect.objectContaining({
body: expect.objectContaining({ heroImageUrl: null })
})); }));
}); });

View File

@@ -2,16 +2,14 @@ import { redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { apiClient } from '$lib/server/api'; import { apiClient } from '$lib/server/api';
const VALID_EFFORTS = ['Easy', 'Medium', 'Hard']; const VALID_EFFORTS = ['easy', 'medium', 'hard'];
export const load: PageServerLoad = async ({ fetch }) => { export const load: PageServerLoad = async ({ fetch }) => {
const api = apiClient(fetch); const api = apiClient(fetch);
const { data, error } = await api.GET('/v1/tags', {}); const { data, error } = await api.GET('/v1/tags', {});
const allTags = error || !data ? [] : data; const allTags = error || !data ? [] : data;
const categories = allTags const categories = allTags.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
.filter((t) => t.tagType === 'category')
.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
return { recipe: null, categories }; return { recipe: null, categories };
}; };
@@ -23,6 +21,7 @@ export const actions: Actions = {
const serves = formData.get('serves'); const serves = formData.get('serves');
const cookTimeMin = formData.get('cookTimeMin'); const cookTimeMin = formData.get('cookTimeMin');
const effort = formData.get('effort') as string; const effort = formData.get('effort') as string;
const heroImageUrl = (formData.get('heroImageUrl') as string) || null;
const ingredientsJson = formData.get('ingredientsJson') as string; const ingredientsJson = formData.get('ingredientsJson') as string;
const stepsJson = formData.get('stepsJson') as string; const stepsJson = formData.get('stepsJson') as string;
const tagIds = formData.getAll('tagIds') as string[]; const tagIds = formData.getAll('tagIds') as string[];
@@ -48,6 +47,7 @@ export const actions: Actions = {
serves: serves ? Number(serves) || undefined : undefined, serves: serves ? Number(serves) || undefined : undefined,
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || undefined : undefined, cookTimeMin: cookTimeMin ? Number(cookTimeMin) || undefined : undefined,
effort, effort,
heroImageUrl,
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[]) ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
.filter((ing) => ing.name?.trim()) .filter((ing) => ing.name?.trim())
.map((ing, i) => ({ .map((ing, i) => ({

View File

@@ -22,8 +22,10 @@ describe('new recipe page — load', () => {
}); });
const mockTags = [ const mockTags = [
{ id: 't1', name: 'Pasta', tagType: 'category' }, { id: 't1', name: 'Vegetarisch', tagType: 'dietary' },
{ id: 't2', name: 'Fleisch', tagType: 'category' } { id: 't2', name: 'Mediterran', tagType: 'cuisine' },
{ id: 't3', name: 'Käse', tagType: 'protein' },
{ id: 't4', name: 'Auflauf', tagType: 'other' }
]; ];
it('fetches tags from GET /v1/tags', async () => { it('fetches tags from GET /v1/tags', async () => {
@@ -32,11 +34,11 @@ describe('new recipe page — load', () => {
expect(mockGet).toHaveBeenCalledWith('/v1/tags', expect.anything()); expect(mockGet).toHaveBeenCalledWith('/v1/tags', expect.anything());
}); });
it('returns categories filtered from tags', async () => { it('returns all tags as categories regardless of tagType', async () => {
mockGet.mockResolvedValue({ data: mockTags, error: undefined }); mockGet.mockResolvedValue({ data: mockTags, error: undefined });
const result = await load({ fetch: vi.fn() } as any); const result = await load({ fetch: vi.fn() } as any);
expect(result.categories).toHaveLength(2); expect(result.categories).toHaveLength(4);
expect(result.categories[0].name).toBe('Pasta'); expect(result.categories[0].name).toBe('Vegetarisch');
}); });
it('returns empty categories when API fails', async () => { it('returns empty categories when API fails', async () => {
@@ -58,7 +60,7 @@ describe('new recipe page — create action', () => {
const makeFormData = (overrides: Record<string, string | string[]> = {}) => { const makeFormData = (overrides: Record<string, string | string[]> = {}) => {
const base: Record<string, string | string[]> = { const base: Record<string, string | string[]> = {
name: 'Test Rezept', name: 'Test Rezept',
effort: 'Easy', effort: 'easy',
tagIds: ['t1'], tagIds: ['t1'],
ingredientsJson: '[]', ingredientsJson: '[]',
stepsJson: '[]', stepsJson: '[]',
@@ -140,7 +142,25 @@ describe('new recipe page — create action', () => {
await actions.create({ request: { formData: async () => fd }, fetch: vi.fn() } as any).catch( await actions.create({ request: { formData: async () => fd }, fetch: vi.fn() } as any).catch(
() => {} () => {}
); );
expect(mockPost).toHaveBeenCalledWith('/v1/recipes', expect.objectContaining({ body: expect.objectContaining({ name: 'Test Rezept', effort: 'Easy' }) })); expect(mockPost).toHaveBeenCalledWith('/v1/recipes', expect.objectContaining({ body: expect.objectContaining({ name: 'Test Rezept', effort: 'easy' }) }));
});
it('sends heroImageUrl in POST body when provided', async () => {
mockPost.mockResolvedValue({ error: undefined });
const fd = makeFormData({ heroImageUrl: 'data:image/jpeg;base64,abc123' });
await actions.create({ request: { formData: async () => fd }, fetch: vi.fn() } as any).catch(() => {});
expect(mockPost).toHaveBeenCalledWith('/v1/recipes', expect.objectContaining({
body: expect.objectContaining({ heroImageUrl: 'data:image/jpeg;base64,abc123' })
}));
});
it('sends null heroImageUrl when field is empty', async () => {
mockPost.mockResolvedValue({ error: undefined });
const fd = makeFormData({ heroImageUrl: '' });
await actions.create({ request: { formData: async () => fd }, fetch: vi.fn() } as any).catch(() => {});
expect(mockPost).toHaveBeenCalledWith('/v1/recipes', expect.objectContaining({
body: expect.objectContaining({ heroImageUrl: null })
}));
}); });
it('returns fail(500) when API returns error', async () => { it('returns fail(500) when API returns error', async () => {

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}

View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,896 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Recipe App — Data Model v1.1</title>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<style>
:root {
--color-page:#FAFAF7;--color-surface:#F5F4EE;--color-subtle:#EDECEA;
--color-border:#D8D7D0;--color-text-muted:#6B6A63;--color-text:#1C1C18;
--green-tint:#E8F5EA;--green-light:#AEDCB0;--green:#3D8C4A;
--green-dark:#2E6E39;--green-deeper:#1E4A26;
--yellow-tint:#FDF6D8;--yellow-light:#F9E08A;--yellow:#F2C12E;
--yellow-dark:#C49610;--yellow-text:#8A6800;
--blue-tint:#E6F1FB;--blue-light:#A4CFF4;--blue:#2D7DD2;--blue-dark:#185FA5;
--purple-tint:#EEEDFE;--purple:#534AB7;--purple-dark:#3C3489;
--orange-tint:#FEF0E6;--orange:#E8862A;--orange-dark:#B46820;
--red-tint:#FDEAEA;--red:#DC4C3E;--red-dark:#B03020;
--font-display:'Fraunces',Georgia,serif;
--font-sans:'DM Sans',system-ui,sans-serif;
--font-mono:'DM Mono',monospace;
--radius-sm:4px;--radius-md:6px;--radius-lg:10px;--radius-xl:16px;
--shadow-card:0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--font-sans);background:var(--color-page);color:var(--color-text);font-size:14px;line-height:1.6;}
.doc{max-width:1100px;margin:0 auto;padding:48px 40px 120px;}
.doc-header{display:flex;justify-content:space-between;align-items:flex-end;padding-bottom:28px;border-bottom:1px solid var(--color-border);margin-bottom:48px;}
.doc-header h1{font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
.doc-header p{font-size:13px;color:var(--color-text-muted);}
.doc-meta{font-family:var(--font-mono);font-size:11px;color:var(--color-text-muted);text-align:right;line-height:1.9;}
.pill{display:inline-block;padding:2px 8px;border-radius:var(--radius-sm);font-size:10px;font-weight:500;letter-spacing:.05em;}
.pill-v{background:var(--green-tint);color:var(--green-dark);}
.section{margin-bottom:56px;}
.section-title{font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--color-text-muted);padding-bottom:10px;border-bottom:1px solid var(--color-border);margin-bottom:24px;}
.prose{font-size:13px;color:var(--color-text-muted);line-height:1.65;max-width:720px;margin-bottom:20px;}
/* Changelog */
.changelog{background:var(--red-tint);border:1px solid #F4ACA4;border-radius:var(--radius-xl);padding:20px 24px;margin-bottom:32px;}
.changelog h3{font-size:12px;font-weight:500;color:var(--red-dark);margin-bottom:8px;}
.changelog ul{list-style:none;padding:0;}
.changelog li{font-size:12px;color:var(--red-dark);padding:3px 0;line-height:1.5;}
.changelog li::before{content:'△ ';font-weight:700;}
/* ERD */
.erd-canvas{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:32px;overflow-x:auto;margin-bottom:24px;}
.erd-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;min-width:880px;}
.erd-entity{background:var(--color-page);border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;font-size:11px;margin-bottom:12px;}
.erd-entity:last-child{margin-bottom:0;}
.erd-entity-head{padding:8px 12px;font-weight:500;font-size:11px;letter-spacing:.04em;border-bottom:1px solid var(--color-border);}
.erd-entity-head.domain-auth{background:var(--purple-tint);color:var(--purple-dark);}
.erd-entity-head.domain-recipe{background:var(--green-tint);color:var(--green-dark);}
.erd-entity-head.domain-plan{background:var(--yellow-tint);color:var(--yellow-text);}
.erd-entity-head.domain-shop{background:var(--blue-tint);color:var(--blue-dark);}
.erd-entity-head.domain-pantry{background:var(--orange-tint);color:var(--orange-dark);}
.erd-entity-head.domain-admin{background:var(--red-tint);color:var(--red-dark);}
.erd-cols{padding:6px 12px;}
.erd-col{display:flex;justify-content:space-between;align-items:center;padding:2px 0;font-family:var(--font-mono);font-size:10px;color:var(--color-text-muted);}
.erd-col .n{color:var(--color-text);}
.erd-col .pk{color:var(--purple);font-weight:500;}
.erd-col .fk{color:var(--blue);font-weight:500;}
.erd-col .t{color:var(--color-text-muted);font-size:9px;}
.erd-col .new{background:var(--red-tint);color:var(--red-dark);font-size:8px;padding:0 4px;border-radius:2px;margin-left:4px;}
/* Table spec */
.table-spec{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);overflow:hidden;margin-bottom:24px;}
.table-spec-head{padding:14px 20px;border-bottom:1px solid var(--color-border);display:flex;justify-content:space-between;align-items:center;}
.table-spec-head h3{font-family:var(--font-mono);font-size:14px;font-weight:500;color:var(--color-text);}
.table-spec-head .domain{font-size:10px;font-weight:500;padding:2px 8px;border-radius:var(--radius-sm);}
.table-spec-purpose{padding:12px 20px;font-size:12px;color:var(--color-text-muted);border-bottom:1px solid var(--color-border);background:var(--color-page);}
.col-table{width:100%;border-collapse:collapse;font-size:11px;}
.col-table thead{background:var(--color-subtle);}
.col-table th{text-align:left;padding:8px 12px;font-size:9px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border);}
.col-table td{padding:7px 12px;border-bottom:1px solid var(--color-subtle);vertical-align:top;font-family:var(--font-mono);font-size:10px;}
.col-table td:first-child{color:var(--color-text);font-weight:500;}
.col-table td:nth-child(2){color:var(--purple);}
.col-table td:nth-child(3){color:var(--color-text-muted);font-family:var(--font-sans);}
.col-table td:nth-child(4){color:var(--color-text-muted);font-family:var(--font-sans);font-size:10px;}
.col-table tr:last-child td{border-bottom:none;}
.col-table .pk-row td{background:var(--purple-tint);}
.col-table .fk-row td:first-child{color:var(--blue);}
.spec-footer{padding:12px 20px;border-top:1px solid var(--color-border);background:var(--color-page);}
.spec-footer h4{font-size:9px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:6px;}
.spec-footer p,.spec-footer li{font-size:11px;color:var(--color-text-muted);line-height:1.55;}
.spec-footer ul{list-style:none;padding:0;}
.spec-footer li{padding:2px 0;}
.spec-footer li::before{content:'→ ';color:var(--color-border);}
.spec-footer + .spec-footer{border-top:1px dashed var(--color-border);}
.example-table{width:100%;border-collapse:collapse;font-size:10px;font-family:var(--font-mono);margin-top:8px;}
.example-table th{text-align:left;padding:5px 8px;font-size:9px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);background:var(--color-subtle);border-bottom:1px solid var(--color-border);}
.example-table td{padding:4px 8px;border-bottom:1px solid var(--color-subtle);color:var(--color-text-muted);}
.callout{background:var(--yellow-tint);border:1px solid var(--yellow-light);border-radius:var(--radius-lg);padding:14px 18px;margin-bottom:20px;}
.callout h4{font-size:11px;font-weight:500;color:var(--yellow-text);margin-bottom:4px;}
.callout p{font-size:12px;color:var(--yellow-text);line-height:1.5;}
.callout.green{background:var(--green-tint);border-color:var(--green-light);}
.callout.green h4{color:var(--green-dark);}
.callout.green p{color:var(--green-deeper);}
.callout.blue{background:var(--blue-tint);border-color:var(--blue-light);}
.callout.blue h4{color:var(--blue-dark);}
.callout.blue p{color:var(--blue-dark);}
.callout.red{background:var(--red-tint);border-color:#F4ACA4;}
.callout.red h4{color:var(--red-dark);}
.callout.red p{color:var(--red-dark);}
.query-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:16px 20px;margin-bottom:10px;}
.query-card h4{font-size:12px;font-weight:500;color:var(--color-text);margin-bottom:4px;}
.query-card .meta{font-size:10px;color:var(--color-text-muted);margin-bottom:8px;}
.query-card pre{font-family:var(--font-mono);font-size:10px;background:var(--color-text);color:#AEDCB0;padding:12px 16px;border-radius:var(--radius-md);overflow-x:auto;line-height:1.6;white-space:pre;}
.d-auth{background:var(--purple-tint);color:var(--purple-dark);}
.d-recipe{background:var(--green-tint);color:var(--green-dark);}
.d-plan{background:var(--yellow-tint);color:var(--yellow-text);}
.d-shop{background:var(--blue-tint);color:var(--blue-dark);}
.d-pantry{background:var(--orange-tint);color:var(--orange-dark);}
.d-admin{background:var(--red-tint);color:var(--red-dark);}
.summary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px;margin-bottom:24px;}
.summary-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:14px 16px;}
.summary-card .num{font-family:var(--font-display);font-size:28px;font-weight:300;line-height:1;}
.summary-card .label{font-size:10px;color:var(--color-text-muted);margin-top:4px;}
@media(max-width:900px){
.erd-grid{grid-template-columns:1fr 1fr;}
.doc{padding:24px 16px 80px;}
}
</style>
</head>
<body>
<div class="doc">
<!-- ═══ HEADER ═══ -->
<div class="doc-header">
<div>
<h1>Data model</h1>
<p>Recipe app · PostgreSQL 16 · Normalized schema with audit trails</p>
</div>
<div class="doc-meta">
<span class="pill pill-v">v1.1</span><br>
Engine: PostgreSQL 16<br>
Tables: 18<br>
Domains: 6<br>
Designed by: Atlas
</div>
</div>
<!-- ═══ CHANGELOG ═══ -->
<div class="changelog">
<h3>v1.1 changes from v1.0</h3>
<ul>
<li><strong>Tag model fixed → proper M:N.</strong> Added <code>tag</code> reference table. <code>recipe_tag</code> is now a pure junction table with recipe_id FK + tag_id FK. Tags are reusable, renameable, and queryable from both directions.</li>
<li><strong>Ingredient category normalized → 1:N.</strong> Added <code>ingredient_category</code> reference table. The <code>category</code> string on <code>ingredient</code> is replaced with <code>category_id FK</code>. Rename once, applies to all ingredients. Canonical list of aisle categories for shopping grouping.</li>
<li><strong>Admin user management added.</strong> New <code>system_role</code> column on <code>user_account</code> (admin vs user). New <code>admin_audit_log</code> table tracks admin actions: account creation, updates, password resets.</li>
<li>Table count: 16 → 18. Domain count: 5 → 6 (added Admin domain).</li>
</ul>
</div>
<!-- ═══ OVERVIEW ═══ -->
<div class="section">
<div class="section-title">Overview</div>
<div class="prose">This schema covers all six user journeys (J1J6), the suggestion/variety engine, the lightweight pantry tracker, recipe hero images, and platform-level admin user management. It is normalized by default, with computed fields (variety score) calculated at query time rather than stored. Every mutable table carries audit timestamps. Tags use a proper M:N relationship via a reference table + junction table.</div>
<div class="summary-grid">
<div class="summary-card"><div class="num" style="color:var(--purple);">4</div><div class="label">Auth &amp; household</div></div>
<div class="summary-card"><div class="num" style="color:var(--green);">7</div><div class="label">Recipe domain</div></div>
<div class="summary-card"><div class="num" style="color:var(--yellow-text);">3</div><div class="label">Planning domain</div></div>
<div class="summary-card"><div class="num" style="color:var(--blue);">2</div><div class="label">Shopping domain</div></div>
<div class="summary-card"><div class="num" style="color:var(--orange);">1</div><div class="label">Pantry domain</div></div>
<div class="summary-card"><div class="num" style="color:var(--red);">1</div><div class="label">Admin domain</div></div>
</div>
<div class="callout green">
<h4>Design decisions</h4>
<p><strong>Variety score</strong> is computed, not stored — it's derived from cooking_log + recipe_ingredient + week_plan_slot.<br>
<strong>Ingredients</strong> are a normalized reference table — enables merging, repetition tracking, and staple filtering.<br>
<strong>Tags</strong> are a proper M:N: a <code>tag</code> reference table + <code>recipe_tag</code> junction. One recipe → many tags, one tag → many recipes. Rename once, applies everywhere.<br>
<strong>Ingredient categories</strong> are a normalized 1:N reference table — one ingredient belongs to one category (e.g. "Produce", "Fish &amp; Meat"). Rename a category once, applies to all ingredients. Powers the aisle-grouped shopping list (J5 variant V2).<br>
<strong>Hero images</strong> store a URL/path reference to object storage (S3/R2).<br>
<strong>Admin</strong> uses a system_role on user_account (not the household role). Admin actions are audit-logged in a dedicated table.<br>
<strong>Pantry items</strong> link to the shared ingredient reference with best-before dates.</p>
</div>
</div>
<!-- ═══ ERD ═══ -->
<div class="section">
<div class="section-title">Entity-relationship diagram</div>
<div class="prose">Entities grouped by domain. Purple = auth, green = recipe, yellow = planning, blue = shopping, orange = pantry, red = admin. <span class="new" style="background:var(--red-tint);color:var(--red-dark);font-size:10px;padding:1px 6px;border-radius:3px;">NEW</span> marks v1.1 additions/changes.</div>
<div class="erd-canvas">
<div class="erd-grid">
<!-- COLUMN 1: AUTH -->
<div>
<div class="erd-entity">
<div class="erd-entity-head domain-auth">user_account <span class="new">CHANGED</span></div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="n">email</span><span class="t">citext UNIQUE</span></div>
<div class="erd-col"><span class="n">display_name</span><span class="t">varchar(100)</span></div>
<div class="erd-col"><span class="n">password_hash</span><span class="t">varchar(255)</span></div>
<div class="erd-col"><span class="n">system_role</span><span class="t">enum(admin,user) <span class="new">NEW</span></span></div>
<div class="erd-col"><span class="n">is_active</span><span class="t">boolean <span class="new">NEW</span></span></div>
<div class="erd-col"><span class="n">created_at</span><span class="t">timestamptz</span></div>
<div class="erd-col"><span class="n">updated_at</span><span class="t">timestamptz</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-auth">household</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="n">name</span><span class="t">varchar(100)</span></div>
<div class="erd-col"><span class="fk">FK created_by</span><span class="t">→ user_account</span></div>
<div class="erd-col"><span class="n">created_at</span><span class="t">timestamptz</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-auth">household_member</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="fk">FK user_id</span><span class="t">→ user_account UNIQUE</span></div>
<div class="erd-col"><span class="n">role</span><span class="t">enum(planner,member)</span></div>
<div class="erd-col"><span class="n">joined_at</span><span class="t">timestamptz</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-auth">household_invite</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">invite_code</span><span class="t">varchar(20) UNIQUE</span></div>
<div class="erd-col"><span class="n">status</span><span class="t">enum(pending,used,expired)</span></div>
<div class="erd-col"><span class="n">expires_at</span><span class="t">timestamptz</span></div>
</div>
</div>
</div>
<!-- COLUMN 2: RECIPE -->
<div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">recipe</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">name</span><span class="t">varchar(200)</span></div>
<div class="erd-col"><span class="n">serves</span><span class="t">smallint</span></div>
<div class="erd-col"><span class="n">cook_time_min</span><span class="t">smallint</span></div>
<div class="erd-col"><span class="n">effort</span><span class="t">enum(easy,medium,hard)</span></div>
<div class="erd-col"><span class="n">is_child_friendly</span><span class="t">boolean</span></div>
<div class="erd-col"><span class="n">hero_image_url</span><span class="t">varchar(500) NULL</span></div>
<div class="erd-col"><span class="n">deleted_at</span><span class="t">timestamptz NULL</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">ingredient <span class="new">CHANGED</span></div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">name</span><span class="t">citext</span></div>
<div class="erd-col"><span class="n">is_staple</span><span class="t">boolean</span></div>
<div class="erd-col"><span class="fk">FK category_id</span><span class="t">→ ingredient_category NULL <span class="new">NEW</span></span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">ingredient_category <span class="new">NEW</span></div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">name</span><span class="t">citext</span></div>
<div class="erd-col"><span class="n">sort_order</span><span class="t">smallint</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">recipe_ingredient</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK recipe_id</span><span class="t">→ recipe</span></div>
<div class="erd-col"><span class="fk">FK ingredient_id</span><span class="t">→ ingredient</span></div>
<div class="erd-col"><span class="n">quantity</span><span class="t">numeric(8,2)</span></div>
<div class="erd-col"><span class="n">unit</span><span class="t">varchar(20)</span></div>
<div class="erd-col"><span class="n">sort_order</span><span class="t">smallint</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">recipe_step</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK recipe_id</span><span class="t">→ recipe</span></div>
<div class="erd-col"><span class="n">step_number</span><span class="t">smallint</span></div>
<div class="erd-col"><span class="n">instruction</span><span class="t">text</span></div>
</div>
</div>
</div>
<!-- COLUMN 3: TAG (M:N) + PLAN -->
<div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">tag <span class="new">NEW</span></div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">name</span><span class="t">citext</span></div>
<div class="erd-col"><span class="n">tag_type</span><span class="t">varchar(20)</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-recipe">recipe_tag <span class="new">CHANGED</span></div>
<div class="erd-cols">
<div class="erd-col"><span class="fk">FK recipe_id</span><span class="t">→ recipe</span></div>
<div class="erd-col"><span class="fk">FK tag_id</span><span class="t">→ tag <span class="new">NEW</span></span></div>
<div class="erd-col"><span class="pk">PK (recipe_id, tag_id)</span><span class="t">composite</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-plan">week_plan</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">week_start</span><span class="t">date (Monday)</span></div>
<div class="erd-col"><span class="n">status</span><span class="t">enum(draft,confirmed)</span></div>
<div class="erd-col"><span class="n">confirmed_at</span><span class="t">timestamptz NULL</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-plan">week_plan_slot</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK week_plan_id</span><span class="t">→ week_plan</span></div>
<div class="erd-col"><span class="fk">FK recipe_id</span><span class="t">→ recipe</span></div>
<div class="erd-col"><span class="n">slot_date</span><span class="t">date</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-plan">cooking_log</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK recipe_id</span><span class="t">→ recipe</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="n">cooked_on</span><span class="t">date</span></div>
<div class="erd-col"><span class="fk">FK cooked_by</span><span class="t">→ user_account</span></div>
</div>
</div>
</div>
<!-- COLUMN 4: SHOPPING + PANTRY + ADMIN -->
<div>
<div class="erd-entity">
<div class="erd-entity-head domain-shop">shopping_list</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="fk">FK week_plan_id</span><span class="t">→ week_plan</span></div>
<div class="erd-col"><span class="n">status</span><span class="t">enum(draft,published,done)</span></div>
<div class="erd-col"><span class="n">published_at</span><span class="t">timestamptz NULL</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-shop">shopping_list_item</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK shopping_list_id</span><span class="t">→ shopping_list</span></div>
<div class="erd-col"><span class="fk">FK ingredient_id</span><span class="t">→ ingredient NULL</span></div>
<div class="erd-col"><span class="n">custom_name</span><span class="t">varchar(200) NULL</span></div>
<div class="erd-col"><span class="n">quantity / unit</span><span class="t">numeric / varchar</span></div>
<div class="erd-col"><span class="n">is_checked</span><span class="t">boolean</span></div>
<div class="erd-col"><span class="n">source_recipes</span><span class="t">uuid[]</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-pantry">pantry_item</div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK household_id</span><span class="t">→ household</span></div>
<div class="erd-col"><span class="fk">FK ingredient_id</span><span class="t">→ ingredient NULL</span></div>
<div class="erd-col"><span class="n">custom_name</span><span class="t">varchar(200) NULL</span></div>
<div class="erd-col"><span class="n">quantity / unit</span><span class="t">numeric / varchar</span></div>
<div class="erd-col"><span class="n">best_before</span><span class="t">date NULL</span></div>
<div class="erd-col"><span class="n">opened_on</span><span class="t">date NULL</span></div>
</div>
</div>
<div class="erd-entity">
<div class="erd-entity-head domain-admin">admin_audit_log <span class="new">NEW</span></div>
<div class="erd-cols">
<div class="erd-col"><span class="pk">PK id</span><span class="t">uuid</span></div>
<div class="erd-col"><span class="fk">FK admin_id</span><span class="t">→ user_account</span></div>
<div class="erd-col"><span class="fk">FK target_user_id</span><span class="t">→ user_account</span></div>
<div class="erd-col"><span class="n">action</span><span class="t">varchar(30)</span></div>
<div class="erd-col"><span class="n">detail</span><span class="t">jsonb NULL</span></div>
<div class="erd-col"><span class="n">performed_at</span><span class="t">timestamptz</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ TAG MODEL EXPLANATION ═══ -->
<div class="section">
<div class="section-title">Tag model — M:N via reference table</div>
<div class="prose">v1.0 stored tags as raw strings in recipe_tag. v1.1 fixes this to a proper many-to-many relationship. One recipe can have many tags. One tag can appear on many recipes. Tags are owned by a household and typed (protein, dietary, cuisine) to enable structured filtering.</div>
<div class="callout green">
<h4>What changed and why</h4>
<p><strong>Before (v1.0):</strong> <code>recipe_tag(recipe_id, tag varchar(50))</code> — tag name stored as raw string per row. 30 recipes tagged "chicken" = 30 copies of the string "chicken". Renaming requires updating every row. No canonical list of available tags. No typed categorization.<br><br>
<strong>After (v1.1):</strong> <code>tag(id, household_id, name, tag_type)</code> + <code>recipe_tag(recipe_id, tag_id)</code> — pure M:N junction. Rename a tag in one UPDATE. List available tags with a simple SELECT. Filter by tag_type (protein, dietary, cuisine) for the J2 suggestion engine. The junction PK is composite <code>(recipe_id, tag_id)</code> — no surrogate key needed.</p>
</div>
<!-- tag table spec -->
<div class="table-spec">
<div class="table-spec-head">
<h3>tag <span style="font-size:10px;color:var(--red);margin-left:8px;">NEW in v1.1</span></h3>
<span class="domain pill d-recipe">Recipe</span>
</div>
<div class="table-spec-purpose">Reference table for category tags. Scoped per household — each household can have its own tag vocabulary. tag_type classifies tags for the suggestion engine: "protein" tags trigger consecutive-day avoidance, "dietary" tags are informational.</div>
<table class="col-table">
<thead><tr><th>Column</th><th>Type</th><th>Constraints</th><th>Purpose</th></tr></thead>
<tbody>
<tr class="pk-row"><td>id</td><td>uuid</td><td>PK, gen_random_uuid()</td><td>Surrogate PK</td></tr>
<tr class="fk-row"><td>household_id</td><td>uuid</td><td>NOT NULL, FK → household ON DELETE CASCADE</td><td>Tags belong to a household</td></tr>
<tr><td>name</td><td>citext</td><td>NOT NULL</td><td>"Chicken", "Fish", "Vegetarian", "Pasta"</td></tr>
<tr><td>tag_type</td><td>varchar(20)</td><td>NOT NULL, CHECK(tag_type IN ('protein','dietary','cuisine','other'))</td><td>Classification. "protein" powers J2 consecutive-day filter.</td></tr>
<tr><td>created_at</td><td>timestamptz</td><td>NOT NULL, DEFAULT now()</td><td>Creation time</td></tr>
</tbody>
</table>
<div class="spec-footer">
<h4>Indexes</h4>
<ul>
<li><code>UNIQUE(household_id, name)</code> — no duplicate tag names per household</li>
<li><code>idx_tag_type</code> on <code>(household_id, tag_type)</code> — filter by type (e.g. all protein tags for J2)</li>
</ul>
</div>
<div class="spec-footer">
<h4>Seed data (created on household setup)</h4>
<table class="example-table">
<thead><tr><th>name</th><th>tag_type</th></tr></thead>
<tbody>
<tr><td>Chicken</td><td>protein</td></tr>
<tr><td>Fish</td><td>protein</td></tr>
<tr><td>Beef</td><td>protein</td></tr>
<tr><td>Pork</td><td>protein</td></tr>
<tr><td>Vegetarian</td><td>dietary</td></tr>
<tr><td>Vegan</td><td>dietary</td></tr>
<tr><td>Pasta</td><td>cuisine</td></tr>
<tr><td>Quick meal</td><td>other</td></tr>
<tr><td>Child-friendly</td><td>other</td></tr>
</tbody>
</table>
</div>
</div>
<!-- recipe_tag junction spec -->
<div class="table-spec">
<div class="table-spec-head">
<h3>recipe_tag <span style="font-size:10px;color:var(--red);margin-left:8px;">CHANGED in v1.1</span></h3>
<span class="domain pill d-recipe">Recipe</span>
</div>
<div class="table-spec-purpose">Pure M:N junction table. No surrogate key — the composite PK (recipe_id, tag_id) is the natural key. A recipe can have many tags; a tag can appear on many recipes. Both directions are indexed.</div>
<table class="col-table">
<thead><tr><th>Column</th><th>Type</th><th>Constraints</th><th>Purpose</th></tr></thead>
<tbody>
<tr class="pk-row fk-row"><td>recipe_id</td><td>uuid</td><td>NOT NULL, FK → recipe ON DELETE CASCADE, part of composite PK</td><td>Which recipe</td></tr>
<tr class="pk-row fk-row"><td>tag_id</td><td>uuid</td><td>NOT NULL, FK → tag ON DELETE CASCADE, part of composite PK</td><td>Which tag</td></tr>
</tbody>
</table>
<div class="spec-footer">
<h4>Indexes</h4>
<ul>
<li><code>PRIMARY KEY (recipe_id, tag_id)</code> — composite PK enforces uniqueness + covers "tags for recipe X"</li>
<li><code>idx_rt_tag</code> on <code>(tag_id)</code> — covers the reverse: "recipes with tag Y"</li>
</ul>
</div>
<div class="spec-footer">
<h4>Why composite PK instead of surrogate?</h4>
<p>This is a pure junction table with no attributes of its own. A surrogate id column adds nothing — the composite (recipe_id, tag_id) is the natural key, guarantees uniqueness, and serves as the clustered index for the most common access pattern ("get all tags for this recipe"). Adding a uuid PK would waste 16 bytes per row and require an extra unique index anyway.</p>
</div>
</div>
<!-- Updated J2 query -->
<div class="query-card">
<h4>J2 — Same-protein consecutive day check (updated for M:N)</h4>
<div class="meta">Uses tag.tag_type = 'protein' to filter only protein tags from the M:N join</div>
<pre>-- What protein tags are on adjacent planned days?
SELECT wps.slot_date, t.name AS protein
FROM week_plan_slot wps
JOIN recipe_tag rt ON rt.recipe_id = wps.recipe_id
JOIN tag t ON t.id = rt.tag_id
WHERE wps.week_plan_id = $1
AND t.tag_type = 'protein'
ORDER BY wps.slot_date;</pre>
</div>
</div>
<!-- ═══ INGREDIENT CATEGORY ═══ -->
<div class="section">
<div class="section-title">Ingredient category — 1:N reference table</div>
<div class="prose">v1.0 stored category as a raw string on ingredient. v1.1 normalizes this to a proper 1:N relationship. One ingredient belongs to one category. One category contains many ingredients. Categories are owned per household and ordered for shopping list grouping.</div>
<div class="callout green">
<h4>What changed and why</h4>
<p><strong>Before:</strong> <code>ingredient.category varchar(30)</code> — raw string. 15 ingredients labelled "Produce" = 15 copies. Rename requires updating every row. No canonical list. No display ordering for the aisle-grouped shopping list.<br><br>
<strong>After:</strong> <code>ingredient_category(id, household_id, name, sort_order)</code> + <code>ingredient.category_id FK</code>. Rename once, applies everywhere. sort_order controls the display order on the aisle-grouped shopping list (J5 V2). Category is nullable on ingredient — uncategorized ingredients fall into an "Other" group in the UI.</p>
</div>
<div class="table-spec">
<div class="table-spec-head">
<h3>ingredient_category <span style="font-size:10px;color:var(--red);margin-left:8px;">NEW in v1.1</span></h3>
<span class="domain pill d-recipe">Recipe</span>
</div>
<div class="table-spec-purpose">Reference table for ingredient aisle categories. Scoped per household — each household can customize their store layout. sort_order controls the display sequence in the aisle-grouped shopping list view (J5 screen D1 variant V2).</div>
<table class="col-table">
<thead><tr><th>Column</th><th>Type</th><th>Constraints</th><th>Purpose</th></tr></thead>
<tbody>
<tr class="pk-row"><td>id</td><td>uuid</td><td>PK, gen_random_uuid()</td><td>Surrogate PK</td></tr>
<tr class="fk-row"><td>household_id</td><td>uuid</td><td>NOT NULL, FK → household ON DELETE CASCADE</td><td>Categories are per-household</td></tr>
<tr><td>name</td><td>citext</td><td>NOT NULL</td><td>"Produce", "Fish &amp; Meat", "Dry Goods", "Dairy", "Sauces &amp; Condiments"</td></tr>
<tr><td>sort_order</td><td>smallint</td><td>NOT NULL, DEFAULT 0</td><td>Display order — matches supermarket aisle flow</td></tr>
<tr><td>created_at</td><td>timestamptz</td><td>NOT NULL, DEFAULT now()</td><td>Creation time</td></tr>
</tbody>
</table>
<div class="spec-footer">
<h4>Indexes</h4>
<ul>
<li><code>UNIQUE(household_id, name)</code> — no duplicate category names per household</li>
<li><code>idx_ic_sort</code> on <code>(household_id, sort_order)</code> — ordered display in shopping list grouping</li>
</ul>
</div>
<div class="spec-footer">
<h4>Seed data (created on household setup)</h4>
<table class="example-table">
<thead><tr><th>name</th><th>sort_order</th></tr></thead>
<tbody>
<tr><td>Produce</td><td>1</td></tr>
<tr><td>Fish &amp; Meat</td><td>2</td></tr>
<tr><td>Dairy &amp; Eggs</td><td>3</td></tr>
<tr><td>Dry Goods &amp; Pasta</td><td>4</td></tr>
<tr><td>Canned &amp; Jarred</td><td>5</td></tr>
<tr><td>Sauces &amp; Condiments</td><td>6</td></tr>
<tr><td>Frozen</td><td>7</td></tr>
<tr><td>Bakery &amp; Bread</td><td>8</td></tr>
</tbody>
</table>
</div>
<div class="spec-footer">
<h4>Shopping list grouping query (J5 · D1 aisle view)</h4>
<pre style="font-family:var(--font-mono);font-size:10px;background:var(--color-text);color:#AEDCB0;padding:12px 16px;border-radius:var(--radius-md);overflow-x:auto;line-height:1.6;white-space:pre;">SELECT
COALESCE(ic.name, 'Other') AS aisle,
COALESCE(ic.sort_order, 999) AS aisle_order,
sli.id, COALESCE(i.name, sli.custom_name) AS item_name,
sli.quantity, sli.unit, sli.is_checked
FROM shopping_list_item sli
LEFT JOIN ingredient i ON i.id = sli.ingredient_id
LEFT JOIN ingredient_category ic ON ic.id = i.category_id
WHERE sli.shopping_list_id = $1
ORDER BY aisle_order, item_name;</pre>
</div>
</div>
</div>
<!-- ═══ ADMIN USER MANAGEMENT ═══ -->
<div class="section">
<div class="section-title">Admin user management — new in v1.1</div>
<div class="prose">The system needs a platform-level admin who can create user accounts, update them, and reset passwords. This is separate from the household "planner" role — a planner manages meal plans, an admin manages the platform. The two role systems are orthogonal: system_role (admin vs user) lives on user_account; household_role (planner vs member) lives on household_member.</div>
<div class="callout red">
<h4>Two role systems — don't confuse them</h4>
<p><strong>system_role</strong> on <code>user_account</code>: platform-level. "admin" can manage all user accounts. "user" is a normal user. This is about platform administration.<br><br>
<strong>household role</strong> on <code>household_member</code>: app-level. "planner" has full access to 18 screens. "member" sees C1 read-only + D1 collaborative. This is about what you can do within a household.<br><br>
An admin can also be a planner in their own household. The roles are independent.</p>
</div>
<!-- user_account updated spec -->
<div class="table-spec">
<div class="table-spec-head">
<h3>user_account <span style="font-size:10px;color:var(--red);margin-left:8px;">CHANGED in v1.1</span></h3>
<span class="domain pill d-auth">Auth</span>
</div>
<div class="table-spec-purpose">User identity for both app users and platform admins. system_role determines platform-level access. is_active allows admins to deactivate accounts without deleting them. Authentication handled here; household authorization in household_member.</div>
<table class="col-table">
<thead><tr><th>Column</th><th>Type</th><th>Constraints</th><th>Purpose</th></tr></thead>
<tbody>
<tr class="pk-row"><td>id</td><td>uuid</td><td>PK, gen_random_uuid()</td><td>Surrogate PK</td></tr>
<tr><td>email</td><td>citext</td><td>NOT NULL, UNIQUE</td><td>Login identifier, case-insensitive</td></tr>
<tr><td>display_name</td><td>varchar(100)</td><td>NOT NULL</td><td>Shown in UI (sidebar avatar initials)</td></tr>
<tr><td>password_hash</td><td>varchar(255)</td><td>NOT NULL</td><td>bcrypt/argon2 hash — never exposed via API</td></tr>
<tr><td>system_role</td><td>varchar(10)</td><td>NOT NULL, DEFAULT 'user', CHECK(system_role IN ('admin','user'))</td><td style="color:var(--red-dark);">NEW — platform role. Admin can manage all accounts.</td></tr>
<tr><td>is_active</td><td>boolean</td><td>NOT NULL, DEFAULT true</td><td style="color:var(--red-dark);">NEW — admin can deactivate accounts. Inactive users cannot log in.</td></tr>
<tr><td>created_at</td><td>timestamptz</td><td>NOT NULL, DEFAULT now()</td><td>Account creation time</td></tr>
<tr><td>updated_at</td><td>timestamptz</td><td>NOT NULL, DEFAULT now()</td><td>Last profile edit</td></tr>
</tbody>
</table>
<div class="spec-footer">
<h4>Indexes</h4>
<ul>
<li><code>UNIQUE(email)</code> — login lookup</li>
<li><code>idx_user_active</code> on <code>(is_active) WHERE is_active = true</code> — filter active users on login</li>
<li><code>idx_user_system_role</code> on <code>(system_role) WHERE system_role = 'admin'</code> — fast admin check (tiny result set)</li>
</ul>
</div>
<div class="spec-footer">
<h4>Admin workflows</h4>
<p><strong>Create account:</strong> Admin INSERTs into user_account with a temporary password_hash. Admin can set system_role to 'admin' or 'user'.<br>
<strong>Update account:</strong> Admin UPDATEs display_name, email, is_active, or system_role. All changes logged to admin_audit_log.<br>
<strong>Reset password:</strong> Admin UPDATEs password_hash to a new temporary hash. User must change on next login (enforced at app layer). Logged to admin_audit_log.</p>
</div>
</div>
<!-- admin_audit_log spec -->
<div class="table-spec">
<div class="table-spec-head">
<h3>admin_audit_log <span style="font-size:10px;color:var(--red);margin-left:8px;">NEW in v1.1</span></h3>
<span class="domain pill d-admin">Admin</span>
</div>
<div class="table-spec-purpose">Immutable audit trail for all admin actions on user accounts. Every account creation, update, or password reset by an admin is logged here. Never updated or deleted. Used for compliance, debugging, and accountability.</div>
<table class="col-table">
<thead><tr><th>Column</th><th>Type</th><th>Constraints</th><th>Purpose</th></tr></thead>
<tbody>
<tr class="pk-row"><td>id</td><td>uuid</td><td>PK, gen_random_uuid()</td><td>Surrogate PK</td></tr>
<tr class="fk-row"><td>admin_id</td><td>uuid</td><td>NOT NULL, FK → user_account ON DELETE RESTRICT</td><td>Which admin performed the action</td></tr>
<tr class="fk-row"><td>target_user_id</td><td>uuid</td><td>NOT NULL, FK → user_account ON DELETE RESTRICT</td><td>Which user was affected</td></tr>
<tr><td>action</td><td>varchar(30)</td><td>NOT NULL, CHECK(action IN ('create_account','update_account','reset_password','deactivate_account','reactivate_account','change_system_role'))</td><td>What happened</td></tr>
<tr><td>detail</td><td>jsonb</td><td>NULL</td><td>Changed fields snapshot: {"field":"email","old":"a@x.com","new":"b@x.com"}</td></tr>
<tr><td>ip_address</td><td>inet</td><td>NULL</td><td>Admin's IP for security audit</td></tr>
<tr><td>performed_at</td><td>timestamptz</td><td>NOT NULL, DEFAULT now()</td><td>When the action occurred</td></tr>
</tbody>
</table>
<div class="spec-footer">
<h4>Indexes</h4>
<ul>
<li><code>idx_aal_target</code> on <code>(target_user_id, performed_at DESC)</code> — "show me all admin actions on this user"</li>
<li><code>idx_aal_admin</code> on <code>(admin_id, performed_at DESC)</code> — "show me everything this admin did"</li>
<li><code>idx_aal_action</code> on <code>(action, performed_at DESC)</code> — "show me all password resets this month"</li>
</ul>
</div>
<div class="spec-footer">
<h4>Why JSONB for detail?</h4>
<p>This is justified JSONB usage. The detail column stores variable-shape change records — a password reset has no "old" value, an email change has old + new, a deactivation has a reason string. Normalizing this into typed columns would require a different schema per action type. The JSONB is write-once, rarely queried for filtering (only read for display), and genuinely schemaless data. This is the right use case for JSON.</p>
</div>
<div class="spec-footer">
<h4>Example rows</h4>
<table class="example-table">
<thead><tr><th>admin</th><th>target</th><th>action</th><th>detail</th><th>performed_at</th></tr></thead>
<tbody>
<tr><td>admin@app.com</td><td>jane@home.com</td><td>create_account</td><td>{"display_name":"Jane Smith","system_role":"user"}</td><td>2026-04-01 10:00</td></tr>
<tr><td>admin@app.com</td><td>jane@home.com</td><td>reset_password</td><td>{"reason":"user requested via support"}</td><td>2026-04-01 10:05</td></tr>
<tr><td>admin@app.com</td><td>bob@home.com</td><td>deactivate_account</td><td>{"reason":"inactive for 12 months"}</td><td>2026-04-01 11:00</td></tr>
</tbody>
</table>
</div>
<div class="spec-footer">
<h4>Immutability enforcement</h4>
<p>Application layer must never issue UPDATE or DELETE on this table. As a safety net, a RULE or trigger can reject UPDATEs:<br>
<code>CREATE RULE no_update_audit AS ON UPDATE TO admin_audit_log DO INSTEAD NOTHING;</code><br>
<code>CREATE RULE no_delete_audit AS ON DELETE TO admin_audit_log DO INSTEAD NOTHING;</code></p>
</div>
</div>
</div>
<!-- ═══ UPDATED FK MAP ═══ -->
<div class="section">
<div class="section-title">Foreign key map (updated v1.1)</div>
<div class="table-spec">
<table class="col-table">
<thead><tr><th>From table</th><th>Column</th><th>References</th><th>Cardinality</th><th>On delete</th></tr></thead>
<tbody>
<tr><td>household</td><td>created_by</td><td>user_account.id</td><td>N:1</td><td>RESTRICT</td></tr>
<tr><td>household_member</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>household_member</td><td>user_id</td><td>user_account.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>household_invite</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>recipe</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>ingredient</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr style="background:var(--red-tint);"><td>ingredient_category</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr style="background:var(--red-tint);"><td>ingredient</td><td>category_id</td><td>ingredient_category.id</td><td>N:1 (nullable)</td><td>SET NULL</td></tr>
<tr style="background:var(--red-tint);"><td>tag</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>recipe_ingredient</td><td>recipe_id</td><td>recipe.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>recipe_ingredient</td><td>ingredient_id</td><td>ingredient.id</td><td>N:1</td><td>RESTRICT</td></tr>
<tr><td>recipe_step</td><td>recipe_id</td><td>recipe.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr style="background:var(--red-tint);"><td>recipe_tag</td><td>recipe_id</td><td>recipe.id</td><td>M:N junction</td><td>CASCADE</td></tr>
<tr style="background:var(--red-tint);"><td>recipe_tag</td><td>tag_id</td><td>tag.id</td><td>M:N junction</td><td>CASCADE</td></tr>
<tr><td>week_plan</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>week_plan_slot</td><td>week_plan_id</td><td>week_plan.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>week_plan_slot</td><td>recipe_id</td><td>recipe.id</td><td>N:1</td><td>RESTRICT</td></tr>
<tr><td>cooking_log</td><td>recipe_id</td><td>recipe.id</td><td>N:1</td><td>RESTRICT</td></tr>
<tr><td>cooking_log</td><td>week_plan_slot_id</td><td>week_plan_slot.id</td><td>N:1 (nullable)</td><td>SET NULL</td></tr>
<tr><td>shopping_list</td><td>week_plan_id</td><td>week_plan.id</td><td>N:1</td><td>RESTRICT</td></tr>
<tr><td>shopping_list_item</td><td>shopping_list_id</td><td>shopping_list.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>shopping_list_item</td><td>ingredient_id</td><td>ingredient.id</td><td>N:1 (nullable)</td><td>SET NULL</td></tr>
<tr><td>pantry_item</td><td>household_id</td><td>household.id</td><td>N:1</td><td>CASCADE</td></tr>
<tr><td>pantry_item</td><td>ingredient_id</td><td>ingredient.id</td><td>N:1 (nullable)</td><td>SET NULL</td></tr>
<tr style="background:var(--red-tint);"><td>admin_audit_log</td><td>admin_id</td><td>user_account.id</td><td>N:1</td><td>RESTRICT</td></tr>
<tr style="background:var(--red-tint);"><td>admin_audit_log</td><td>target_user_id</td><td>user_account.id</td><td>N:1</td><td>RESTRICT</td></tr>
</tbody>
</table>
</div>
<div class="prose" style="font-size:11px;"><span style="display:inline-block;width:12px;height:12px;background:var(--red-tint);border:1px solid #F4ACA4;border-radius:2px;vertical-align:middle;margin-right:4px;"></span> = new or changed in v1.1</div>
</div>
<!-- ═══ QUERY PATTERNS ═══ -->
<div class="section">
<div class="section-title">Key query patterns</div>
<div class="query-card">
<h4>J2 — Ingredient repetition check (last 3 days)</h4>
<div class="meta">Frequency: ~10×/week · Target: &lt;50ms</div>
<pre>WITH recent_meals AS (
SELECT recipe_id, cooked_on
FROM cooking_log
WHERE household_id = $1
AND cooked_on >= CURRENT_DATE - INTERVAL '3 days'
)
SELECT DISTINCT i.id, i.name
FROM recent_meals rm
JOIN recipe_ingredient ri ON ri.recipe_id = rm.recipe_id
JOIN ingredient i ON i.id = ri.ingredient_id;</pre>
</div>
<div class="query-card">
<h4>J2 — Protein tags on adjacent days (M:N join)</h4>
<div class="meta">Frequency: ~10×/week · Target: &lt;30ms</div>
<pre>SELECT wps.slot_date, t.name AS protein
FROM week_plan_slot wps
JOIN recipe_tag rt ON rt.recipe_id = wps.recipe_id
JOIN tag t ON t.id = rt.tag_id
WHERE wps.week_plan_id = $1
AND t.tag_type = 'protein'
ORDER BY wps.slot_date;</pre>
</div>
<div class="query-card">
<h4>J5 — Shopping list generation (merged + staples filtered)</h4>
<div class="meta">Frequency: 1×/week · Target: &lt;200ms</div>
<pre>SELECT i.id, i.name,
SUM(ri.quantity) AS total_qty, ri.unit,
ARRAY_AGG(DISTINCT r.id) AS source_recipe_ids
FROM week_plan_slot wps
JOIN recipe r ON r.id = wps.recipe_id
JOIN recipe_ingredient ri ON ri.recipe_id = r.id
JOIN ingredient i ON i.id = ri.ingredient_id
WHERE wps.week_plan_id = $1
AND i.is_staple = false
GROUP BY i.id, i.name, ri.unit
ORDER BY i.name;</pre>
</div>
<div class="query-card">
<h4>Pantry — Items expiring within 3 days</h4>
<div class="meta">Frequency: daily · Target: &lt;20ms</div>
<pre>SELECT pi.id, COALESCE(i.name, pi.custom_name) AS name,
pi.best_before, pi.quantity, pi.unit
FROM pantry_item pi
LEFT JOIN ingredient i ON i.id = pi.ingredient_id
WHERE pi.household_id = $1
AND pi.best_before IS NOT NULL
AND pi.best_before &lt;= CURRENT_DATE + INTERVAL '3 days'
ORDER BY pi.best_before;</pre>
</div>
<div class="query-card">
<h4>Admin — All actions on a user (audit trail)</h4>
<div class="meta">Frequency: on-demand · Target: &lt;50ms</div>
<pre>SELECT aal.action, aal.detail, aal.performed_at,
admin.display_name AS admin_name, admin.email AS admin_email
FROM admin_audit_log aal
JOIN user_account admin ON admin.id = aal.admin_id
WHERE aal.target_user_id = $1
ORDER BY aal.performed_at DESC;</pre>
</div>
<div class="query-card">
<h4>All tags for a recipe (M:N forward lookup)</h4>
<div class="meta">Frequency: every recipe detail load · Target: &lt;10ms</div>
<pre>SELECT t.id, t.name, t.tag_type
FROM recipe_tag rt
JOIN tag t ON t.id = rt.tag_id
WHERE rt.recipe_id = $1
ORDER BY t.tag_type, t.name;</pre>
</div>
<div class="query-card">
<h4>All recipes with a specific tag (M:N reverse lookup)</h4>
<div class="meta">Frequency: J2 suggestion filter, B1 filter chips · Target: &lt;30ms</div>
<pre>SELECT r.id, r.name, r.effort, r.cook_time_min
FROM recipe_tag rt
JOIN recipe r ON r.id = rt.recipe_id
WHERE rt.tag_id = $1
AND r.deleted_at IS NULL
ORDER BY r.name;</pre>
</div>
</div>
<!-- ═══ MIGRATION ORDER ═══ -->
<div class="section">
<div class="section-title">Migration order (v1.1)</div>
<div class="query-card">
<h4>Migration 001 — Extensions &amp; triggers</h4>
<div class="meta">Run once before any table creation</div>
<pre>CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "citext"; -- case-insensitive text
CREATE OR REPLACE FUNCTION trigger_set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;</pre>
</div>
<div class="callout green">
<h4>Table creation order (respects FK dependencies)</h4>
<p>1. user_account → 2. household → 3. household_member → 4. household_invite → 5. ingredient_category → 6. ingredient → 7. tag → 8. recipe → 9. recipe_ingredient → 10. recipe_step → 11. recipe_tag → 12. week_plan → 13. week_plan_slot → 14. cooking_log → 15. shopping_list → 16. shopping_list_item → 17. pantry_item → 18. admin_audit_log</p>
</div>
<div class="query-card">
<h4>Immutability rules for audit tables</h4>
<div class="meta">Apply after admin_audit_log creation</div>
<pre>-- Prevent accidental updates/deletes on audit log
CREATE RULE no_update_audit AS ON UPDATE TO admin_audit_log
DO INSTEAD NOTHING;
CREATE RULE no_delete_audit AS ON DELETE TO admin_audit_log
DO INSTEAD NOTHING;</pre>
</div>
</div>
<!-- ═══ JOURNEY COVERAGE ═══ -->
<div class="section">
<div class="section-title">Journey → table coverage matrix (v1.1)</div>
<div class="table-spec">
<table class="col-table">
<thead><tr><th>Journey</th><th>Reads</th><th>Writes</th><th>Critical path</th></tr></thead>
<tbody>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--green-dark);">J1 · Add recipe</td><td>ingredient, tag (autocomplete)</td><td>recipe, recipe_ingredient, recipe_step, recipe_tag, ingredient, tag</td><td>Recipe INSERT + child rows + tag associations in one transaction</td></tr>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--yellow-text);">J2 · Plan week</td><td>recipe, recipe_ingredient, recipe_tag, tag, cooking_log, ingredient</td><td>week_plan, week_plan_slot</td><td>Variety CTE joins tag (type=protein) for consecutive-day check</td></tr>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--green-dark);">J3 · Cook tonight</td><td>week_plan_slot, recipe, recipe_ingredient, recipe_step</td><td>cooking_log</td><td>cooking_log INSERT (immutable event)</td></tr>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--orange-dark);">J4 · Adapt on fly</td><td>recipe, recipe_tag, tag, cooking_log</td><td>week_plan_slot (UPDATE recipe_id)</td><td>Slot UPDATE + variety recompute ≤ 3 taps</td></tr>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--blue-dark);">J5 · Shopping list</td><td>week_plan_slot, recipe_ingredient, ingredient, ingredient_category</td><td>shopping_list, shopping_list_item</td><td>Merge query (GROUP BY ingredient, SUM quantity) + aisle grouping via category</td></tr>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--purple-dark);">J6 · Household setup</td><td></td><td>user_account, household, household_member, household_invite, ingredient (staples), tag (seed data), ingredient_category (seed data)</td><td>Household creation + seed data in one transaction</td></tr>
<tr><td style="font-family:var(--font-sans);font-weight:500;color:var(--orange-dark);">Pantry</td><td>pantry_item, ingredient</td><td>pantry_item</td><td>Expiry notification query (daily)</td></tr>
<tr style="background:var(--red-tint);"><td style="font-family:var(--font-sans);font-weight:500;color:var(--red-dark);">Admin</td><td>user_account, admin_audit_log</td><td>user_account, admin_audit_log</td><td>Every admin action → audit log INSERT in same transaction</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ PUSHBACK LOG ═══ -->
<div class="section">
<div class="section-title">Pushback &amp; trade-off log</div>
<div class="callout blue">
<h4>v1.0 bug: recipe_tag was 1:N, not M:N</h4>
<p><strong>Fixed in v1.1.</strong> v1.0 stored tags as raw strings, making recipe_tag structurally a 1:N (one recipe → many string rows) rather than a true M:N. The tag string had no identity — "chicken" on recipe A and "chicken" on recipe B were unrelated rows. This prevented tag renaming, tag listing, and structured filtering. v1.1 adds a <code>tag</code> reference table, making recipe_tag a proper M:N junction with FK integrity in both directions.</p>
</div>
<div class="callout blue">
<h4>v1.0 bug: ingredient.category was a raw string, not a FK</h4>
<p><strong>Fixed in v1.1.</strong> Same anti-pattern as the tag issue. 15 ingredients all storing the string "Produce" independently — no shared identity, no rename capability, no canonical list, no sort order. v1.1 extracts this to <code>ingredient_category</code> as a reference table with a 1:N FK from ingredient. The sort_order column enables the aisle-grouped shopping list (J5 D1 V2) to match supermarket layout. Category is nullable — uncategorized ingredients group into "Other."</p>
</div>
<div class="callout">
<h4>source_recipes as uuid[] — trade-off accepted</h4>
<p>Violates 1NF. But it's write-once display metadata, never joined against. A junction table would add ~60 rows/week for zero query benefit. Sunset plan: migrate to junction table if future requirements need "which list items came from recipe X?"</p>
</div>
<div class="callout">
<h4>Variety score is computed, not materialized</h4>
<p>At ~50 recipes × 7 slots, the CTE runs in &lt;100ms. Materialized view adds staleness risk. At 100× scale, we add materialized view with refresh-on-mutation triggers.</p>
</div>
<div class="callout">
<h4>admin_audit_log.detail uses JSONB — justified</h4>
<p>This is the right use case for schemaless data. Each action type has a different shape (password reset has a "reason", email change has "old"+"new", creation has full profile). The data is write-once, append-only, and queried for display only. Normalizing it into typed columns would require a different schema per action type for no benefit.</p>
</div>
<div class="callout blue">
<h4>Rejected: separate admin_user table</h4>
<p>Having a separate table for admins would split identity across two tables. Login would need to check both. An admin who is also a household planner would need two accounts. Instead, system_role on user_account keeps one identity per person. The role check is a single column read.</p>
</div>
</div>
</div>
</body>
</html>

764
specs/e1-settings.html Normal file
View File

@@ -0,0 +1,764 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>E1 / D3 — Einstellungen &amp; Vorräte · 5 Variationen</title>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<!--
spec:agent
document: E1 Settings Hub + D3 Staples — Design Variations
version: 1.0
journey: J8 Edit pantry staples
routes: /settings (E1 hub) → /household/staples?ctx=settings (D3)
screens: E1, D3
variations: V1 Section rows | V2 Card sections | V3 Accordion | V4 Settings sub-nav | V5 Dashboard tiles
last-updated: 2026-04
note: D3 = A3 (same StaplesManager component). settings context removes onboarding chrome.
note: StaplesManager renders categories as chip grids (StapleChip), not checkbox lists.
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--green-deeper: #1E4A26;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow-text: #8A6800;
--color-error: #DC4C3E;
--blue-tint: #E6F1FB;
--blue: #185FA5;
--blue-dark: #0C447C;
--font-display: 'Fraunces', Georgia, serif;
--font-sans: 'DM Sans', system-ui, sans-serif;
--font-mono: 'DM Mono', monospace;
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px; --radius-xl: 16px; --radius-full: 9999px;
--shadow-card: 0 1px 3px rgba(28,28,24,.06), 0 1px 2px rgba(28,28,24,.04);
--shadow-raised: 0 4px 12px rgba(28,28,24,.08), 0 2px 4px rgba(28,28,24,.04);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
/* ── Doc layout ── */
.doc { max-width: 960px; margin: 0 auto; padding: 48px 40px 96px; }
.doc-header { display: flex; justify-content: space-between; align-items: flex-end; padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
.doc-header p { font-size: 13px; color: var(--color-text-muted); }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
.intro { font-size: 14px; line-height: 1.75; color: var(--color-text); max-width: 640px; margin-bottom: 48px; }
.section-label { font-size: 10px; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 36px; }
/* ── Variation sections ── */
.variation { margin-bottom: 72px; }
.var-header { display: flex; align-items: flex-start; gap: 20px; margin-bottom: 20px; }
.var-num { font-family: var(--font-display); font-size: 44px; font-weight: 300; color: var(--yellow-light); line-height: 1; flex-shrink: 0; width: 56px; letter-spacing: -0.03em; }
.var-meta { flex: 1; padding-top: 4px; }
.var-title { font-size: 18px; font-weight: 500; letter-spacing: -0.01em; margin-bottom: 4px; }
.var-desc { font-size: 13px; color: var(--color-text-muted); line-height: 1.5; max-width: 540px; }
.var-tag { font-size: 10px; font-weight: 500; letter-spacing: 0.07em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-sm); background: var(--color-subtle); color: var(--color-text-muted); white-space: nowrap; margin-top: 6px; display: inline-block; }
.var-tag.rec { background: var(--green-tint); color: var(--green-dark); }
/* ── Preview containers ── */
.preview-pair { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
/* ── Notes ── */
.var-notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 16px 20px; }
.var-notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
.var-notes ul { list-style: none; display: flex; flex-direction: column; gap: 5px; }
.var-notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
.var-notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
/* ── Agent section ── */
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
.agent-table tr:last-child td { border-bottom: none; }
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
.agent-table td:nth-child(3) { color: #5A5A55; }
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
</style>
</head>
<body>
<div class="doc">
<div class="doc-header">
<div>
<h1>E1 / D3 — Einstellungen &amp; Vorräte</h1>
<p>5 Design-Variationen · Desktop-first · Routes /settings + /household/staples · Journey J8</p>
</div>
<div class="doc-meta">
Version: 1.0<br>
Screens: E1, D3<br>
Journey: J8<br>
Actor: Planner<br>
Last updated: 2026-04
</div>
</div>
<p class="intro">Two pages, one journey. <strong style="font-weight:500">E1</strong> is the settings hub at <code style="font-family:var(--font-mono);font-size:12px;background:var(--color-subtle);padding:1px 5px;border-radius:3px;">/settings</code> — currently a placeholder. <strong style="font-weight:500">D3</strong> is the StaplesManager component at <code style="font-family:var(--font-mono);font-size:12px;background:var(--color-subtle);padding:1px 5px;border-radius:3px;">/household/staples</code>, rendered with <code style="font-family:var(--font-mono);font-size:12px;background:var(--color-subtle);padding:1px 5px;border-radius:3px;">context="settings"</code>. The component uses StapleChip — ingredient pills in flex-wrap grids per category, not a checklist. Five variations explore how the hub and staples editing relate: from navigating to a sub-page, to showing staples inline on the settings page itself. The recommended variation for v1 is <strong style="font-weight:500">V3 (Accordion)</strong> — one page, no navigation, staples always one tap away.</p>
<div class="section-label">Five design variations</div>
<!-- ══════════════════════════════════════
V1 — SECTION ROWS (LINKED HUB)
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V1</div>
<div class="var-meta">
<div class="var-title">Verknüpfte Abschnitte</div>
<div class="var-desc">Settings als Hub-Seite mit klickbaren Abschnitt-Zeilen. Jeder Bereich (Profil, Vorräte, Haushalt) ist eine Zeile mit Titel, Beschreibung, Stat und Pfeil. Tippen navigiert zur jeweiligen Unterseite. Klassisches iOS-Settings-Muster.</div>
<span class="var-tag">Maximal erweiterbar für künftige Settings</span>
</div>
</div>
<div class="preview-pair">
<!-- Desktop: Hub view -->
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px — Hub-Ansicht</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<!-- Sidebar -->
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;">
<div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div>
<nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav>
</aside>
<!-- Hub content -->
<main style="flex:1;padding:40px 56px;">
<h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;margin-bottom:32px;">Einstellungen</h1>
<div style="max-width:600px;display:flex;flex-direction:column;gap:8px;">
<!-- Vorräte row (highlighted as primary) -->
<a style="display:flex;align-items:center;gap:16px;padding:20px 20px;background:var(--color-surface);border:1px solid var(--color-border);border-left:3px solid var(--green-dark);border-radius:var(--radius-lg);text-decoration:none;cursor:pointer;box-shadow:var(--shadow-card);">
<div style="flex:1;">
<div style="font-size:15px;font-weight:500;color:var(--color-text);margin-bottom:3px;">Vorräte</div>
<div style="font-size:12px;color:var(--color-text-muted);">Zutaten die immer im Haushalt vorhanden sind — werden aus Einkaufslisten gefiltert</div>
</div>
<div style="text-align:right;flex-shrink:0;">
<div style="font-size:20px;font-weight:300;font-family:var(--font-display);color:var(--green-dark);letter-spacing:-0.02em;">14</div>
<div style="font-size:10px;color:var(--color-text-muted);">aktiv</div>
</div>
<div style="color:var(--color-text-muted);font-size:18px;"></div>
</a>
<a style="display:flex;align-items:center;gap:16px;padding:20px 20px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);text-decoration:none;cursor:pointer;">
<div style="flex:1;">
<div style="font-size:15px;font-weight:500;color:var(--color-text);margin-bottom:3px;">Profil</div>
<div style="font-size:12px;color:var(--color-text-muted);">Marcel Raddatz · marcel@email.com</div>
</div>
<div style="color:var(--color-text-muted);font-size:18px;"></div>
</a>
<a style="display:flex;align-items:center;gap:16px;padding:20px 20px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);text-decoration:none;cursor:pointer;">
<div style="flex:1;">
<div style="font-size:15px;font-weight:500;color:var(--color-text);margin-bottom:3px;">Haushalt</div>
<div style="font-size:12px;color:var(--color-text-muted);">Familie Raddatz · 3 Mitglieder</div>
</div>
<div style="color:var(--color-text-muted);font-size:18px;"></div>
</a>
</div>
</main>
</div>
</div>
</div>
</div>
<!-- Mobile -->
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 16px;border-bottom:1px solid var(--color-border);background:var(--color-surface);"><div style="font-family:var(--font-display);font-size:20px;font-weight:500;">Einstellungen</div></div>
<div style="flex:1;padding:16px 20px;display:flex;flex-direction:column;gap:8px;">
<a style="display:flex;align-items:center;gap:14px;padding:16px;background:var(--color-surface);border:1px solid var(--color-border);border-left:3px solid var(--green-dark);border-radius:var(--radius-lg);text-decoration:none;cursor:pointer;">
<div style="flex:1;"><div style="font-size:15px;font-weight:500;color:var(--color-text);">Vorräte</div><div style="font-size:12px;color:var(--color-text-muted);margin-top:2px;">14 aktiv · Einkaufsliste-Filter</div></div>
<div style="color:var(--color-text-muted);font-size:16px;"></div>
</a>
<a style="display:flex;align-items:center;gap:14px;padding:16px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);text-decoration:none;cursor:pointer;">
<div style="flex:1;"><div style="font-size:15px;font-weight:500;color:var(--color-text);">Profil</div><div style="font-size:12px;color:var(--color-text-muted);margin-top:2px;">Marcel Raddatz</div></div>
<div style="color:var(--color-text-muted);font-size:16px;"></div>
</a>
<a style="display:flex;align-items:center;gap:14px;padding:16px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);text-decoration:none;cursor:pointer;">
<div style="flex:1;"><div style="font-size:15px;font-weight:500;color:var(--color-text);">Haushalt</div><div style="font-size:12px;color:var(--color-text-muted);margin-top:2px;">Familie Raddatz · 3 Mitglieder</div></div>
<div style="color:var(--color-text-muted);font-size:16px;"></div>
</a>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<!-- Second preview: staples sub-page (D3 in settings context) -->
<div style="margin-bottom:20px;">
<div class="preview-label" style="margin-bottom:6px;">Desktop · D3 Vorräte-Unterseite (nach Navigation)</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<main style="flex:1;padding:40px 56px;overflow:hidden;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">
<a style="font-size:13px;color:var(--color-text-muted);text-decoration:none;">← Einstellungen</a>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;">
<div><h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;">Vorräte</h1><div style="font-size:13px;color:var(--color-text-muted);margin-top:4px;">14 von 32 Zutaten als Vorrat markiert</div></div>
</div>
<!-- StaplesManager: category + chip grid -->
<div style="display:flex;flex-direction:column;gap:24px;">
<div>
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Gemüse</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;">
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Karotten</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Zwiebeln</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Lauch</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Knoblauch</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Fenchel</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Paprika</span>
</div>
</div>
<div>
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Getreide &amp; Hülsenfrüchte</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;">
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Pasta</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Reis</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Couscous</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Linsen</span>
<span style="padding:7px 14px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Kichererbsen</span>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Vorräte-Zeile hat einen grünen linken Rand (border-left: 3px solid --green-dark) — signalisiert "primäre Aktion" ohne ihn visuell zu überfrachten.</li>
<li>Die aktive Zutat-Zahl ("14") wird als Display-Schrift-Zahl dargestellt — schafft eine Verbindung zum Wert-Versprechen der App (Varietät-Scores sind ebenfalls Zahlen).</li>
<li>D3 Unterseite im settings-Kontext: kein Onboarding-Sidebar, kein "Weiter"-Button — nur die Kategorie-Chip-Liste. "← Einstellungen" Breadcrumb für die Rücknavigation.</li>
<li>StapleChip: Ausgewählt = --green-dark Hintergrund + weißer Text. Nicht ausgewählt = transparenter Hintergrund + --color-border Rahmen + --color-text-muted Text. Kein separater Speicher-Button — Auto-Save auf Toggle.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V2 — CARD SECTIONS
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V2</div>
<div class="var-meta">
<div class="var-title">Einstellungs-Kacheln</div>
<div class="var-desc">Jeder Einstellungsbereich als Kachel mit Titel, Beschreibung, Schlüsselstatistik und direktem Aktions-Button. Die Vorräte-Kachel zeigt "14 von 32 aktiv" und einen "Bearbeiten"-Button der direkt in den D3-Kontext führt.</div>
<span class="var-tag">Gute Übersicht auf großen Bildschirmen</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<main style="flex:1;padding:40px 56px;">
<h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;margin-bottom:32px;">Einstellungen</h1>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:820px;">
<!-- Vorräte (primary, spans or just prominent) -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:28px;display:flex;flex-direction:column;gap:12px;box-shadow:var(--shadow-card);">
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div style="font-size:16px;font-weight:500;">Vorräte</div>
<div style="text-align:right;"><div style="font-family:var(--font-display);font-size:28px;font-weight:300;color:var(--green-dark);letter-spacing:-0.02em;line-height:1;">14</div><div style="font-size:10px;color:var(--color-text-muted);">von 32 aktiv</div></div>
</div>
<div style="font-size:13px;color:var(--color-text-muted);line-height:1.5;flex:1;">Zutaten, die immer im Haushalt vorhanden sind. Sie werden beim Generieren der Einkaufsliste automatisch herausgefiltert.</div>
<button style="align-self:flex-start;background:var(--green-dark);color:#fff;font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:9px 18px;border-radius:var(--radius-md);border:none;cursor:pointer;">Vorräte bearbeiten</button>
</div>
<!-- Haushalt -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:28px;display:flex;flex-direction:column;gap:12px;box-shadow:var(--shadow-card);">
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div style="font-size:16px;font-weight:500;">Haushalt</div>
<div style="text-align:right;"><div style="font-family:var(--font-display);font-size:28px;font-weight:300;color:var(--color-text-muted);letter-spacing:-0.02em;line-height:1;">3</div><div style="font-size:10px;color:var(--color-text-muted);">Mitglieder</div></div>
</div>
<div style="font-size:13px;color:var(--color-text-muted);line-height:1.5;flex:1;">Familie Raddatz. Haushaltsname und Mitgliederverwaltung.</div>
<button style="align-self:flex-start;background:var(--color-page);color:var(--color-text);font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:9px 18px;border-radius:var(--radius-md);border:1px solid var(--color-border);cursor:pointer;">Verwalten</button>
</div>
<!-- Profil -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:28px;display:flex;flex-direction:column;gap:12px;box-shadow:var(--shadow-card);">
<div style="font-size:16px;font-weight:500;">Profil</div>
<div style="font-size:13px;color:var(--color-text-muted);flex:1;">Marcel Raddatz<br>marcel@email.com</div>
<button style="align-self:flex-start;background:var(--color-page);color:var(--color-text);font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:9px 18px;border-radius:var(--radius-md);border:1px solid var(--color-border);cursor:pointer;">Bearbeiten</button>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px;border-bottom:1px solid var(--color-border);background:var(--color-surface);"><div style="font-family:var(--font-display);font-size:20px;font-weight:500;">Einstellungen</div></div>
<div style="flex:1;padding:16px 20px;display:flex;flex-direction:column;gap:12px;">
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px;"><div style="font-size:15px;font-weight:500;">Vorräte</div><div><div style="font-family:var(--font-display);font-size:24px;font-weight:300;color:var(--green-dark);line-height:1;">14</div><div style="font-size:10px;color:var(--color-text-muted);">von 32</div></div></div>
<div style="font-size:12px;color:var(--color-text-muted);margin-bottom:14px;">Immer vorhandene Zutaten, automatisch aus Einkaufslisten gefiltert.</div>
<button style="width:100%;background:var(--green-dark);color:#fff;font-size:13px;font-weight:500;padding:10px;border-radius:var(--radius-md);border:none;">Vorräte bearbeiten</button>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"><div style="font-size:15px;font-weight:500;">Haushalt</div><div style="font-size:12px;color:var(--color-text-muted);">3 Mitglieder</div></div>
<div style="font-size:12px;color:var(--color-text-muted);margin-bottom:14px;">Familie Raddatz</div>
<button style="width:100%;background:var(--color-page);color:var(--color-text);font-size:13px;font-weight:500;padding:10px;border-radius:var(--radius-md);border:1px solid var(--color-border);">Verwalten</button>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:20px;">
<div style="font-size:15px;font-weight:500;margin-bottom:8px;">Profil</div>
<div style="font-size:12px;color:var(--color-text-muted);margin-bottom:14px;">Marcel Raddatz · marcel@email.com</div>
<button style="width:100%;background:var(--color-page);color:var(--color-text);font-size:13px;font-weight:500;padding:10px;border-radius:var(--radius-md);border:1px solid var(--color-border);">Bearbeiten</button>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Kacheln mit Display-Schrift-Zahlen (14, 3) als Schlüsselmetriken — konsistentes Designmuster mit der Varietätspunktzahl im Planer.</li>
<li>Der "Vorräte bearbeiten"-Button auf der Kachel führt direkt zur D3-Unterseite. Keine Zwischennavigation.</li>
<li>Nachteil gegenüber V3: erfordert einen Navigationswechsel für die häufigste Aufgabe (Vorräte bearbeiten). V3 macht das inline möglich.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V3 — ACCORDION (INLINE STAPLES)
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V3</div>
<div class="var-meta">
<div class="var-title">Akkordeon mit inline Vorräten</div>
<div class="var-desc">Die Settings-Seite zeigt alle Bereiche als aufklappbare Abschnitte. Der Vorräte-Bereich ist beim Öffnen der Seite standardmäßig aufgeklappt und zeigt direkt die StapleChip-Kategorien. Kein Seitenwechsel nötig.</div>
<span class="var-tag rec">Empfohlen · v1</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px — Vorräte aufgeklappt (Standard)</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<main style="flex:1;padding:40px 56px;overflow:hidden;">
<h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;margin-bottom:28px;">Einstellungen</h1>
<div style="max-width:680px;display:flex;flex-direction:column;gap:4px;">
<!-- Vorräte — expanded (default) -->
<div style="border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;">
<button style="width:100%;display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--color-surface);border:none;cursor:pointer;text-align:left;">
<span style="font-size:15px;font-weight:500;flex:1;color:var(--color-text);">Vorräte</span>
<span style="font-size:12px;color:var(--color-text-muted);margin-right:8px;">14 aktiv</span>
<span style="font-size:14px;color:var(--color-text-muted);"></span>
</button>
<div style="padding:20px;border-top:1px solid var(--color-border);background:var(--color-page);">
<div style="font-size:13px;color:var(--color-text-muted);margin-bottom:16px;">Tippe eine Zutat an um sie als Vorrat zu markieren oder zu entfernen. Änderungen werden sofort gespeichert.</div>
<div style="margin-bottom:16px;">
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Gemüse</div>
<div style="display:flex;flex-wrap:wrap;gap:7px;">
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Karotten</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Zwiebeln</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Lauch</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Knoblauch</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Fenchel</span>
</div>
</div>
<div>
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Getreide</div>
<div style="display:flex;flex-wrap:wrap;gap:7px;">
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Pasta</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Reis</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Couscous</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Mehl</span>
</div>
</div>
</div>
</div>
<!-- Profil — collapsed -->
<div style="border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;">
<button style="width:100%;display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--color-surface);border:none;cursor:pointer;text-align:left;">
<span style="font-size:15px;font-weight:500;flex:1;color:var(--color-text);">Profil</span>
<span style="font-size:12px;color:var(--color-text-muted);margin-right:8px;">Marcel Raddatz</span>
<span style="font-size:14px;color:var(--color-text-muted);"></span>
</button>
</div>
<!-- Haushalt — collapsed -->
<div style="border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;">
<button style="width:100%;display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--color-surface);border:none;cursor:pointer;text-align:left;">
<span style="font-size:15px;font-weight:500;flex:1;color:var(--color-text);">Haushalt</span>
<span style="font-size:12px;color:var(--color-text-muted);margin-right:8px;">3 Mitglieder</span>
<span style="font-size:14px;color:var(--color-text-muted);"></span>
</button>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px;border-bottom:1px solid var(--color-border);background:var(--color-surface);"><div style="font-family:var(--font-display);font-size:20px;font-weight:500;">Einstellungen</div></div>
<div style="flex:1;padding:12px 20px;display:flex;flex-direction:column;gap:4px;">
<!-- Vorräte expanded -->
<div style="border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;">
<button style="width:100%;display:flex;align-items:center;padding:14px 16px;background:var(--color-surface);border:none;cursor:pointer;text-align:left;gap:10px;"><span style="font-size:14px;font-weight:500;flex:1;">Vorräte</span><span style="font-size:11px;color:var(--color-text-muted);">14 aktiv</span><span style="font-size:12px;color:var(--color-text-muted);"></span></button>
<div style="padding:14px 16px;border-top:1px solid var(--color-border);background:var(--color-page);">
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Gemüse</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px;">
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Karotten</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Zwiebeln</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);">Lauch</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Knoblauch</span>
</div>
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Getreide</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;">
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Pasta</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Reis</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);">Couscous</span>
</div>
</div>
</div>
<div style="border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;"><button style="width:100%;display:flex;align-items:center;padding:14px 16px;background:var(--color-surface);border:none;cursor:pointer;text-align:left;gap:10px;"><span style="font-size:14px;font-weight:500;flex:1;">Profil</span><span style="font-size:11px;color:var(--color-text-muted);">Marcel R.</span><span style="font-size:12px;color:var(--color-text-muted);"></span></button></div>
<div style="border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;"><button style="width:100%;display:flex;align-items:center;padding:14px 16px;background:var(--color-surface);border:none;cursor:pointer;text-align:left;gap:10px;"><span style="font-size:14px;font-weight:500;flex:1;">Haushalt</span><span style="font-size:11px;color:var(--color-text-muted);">3 Mitglieder</span><span style="font-size:12px;color:var(--color-text-muted);"></span></button></div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Vorräte-Abschnitt ist beim Öffnen der Seite standardmäßig aufgeklappt (per URL-Hash oder initial state) — der häufigste Grund für diesen Seitenaufruf ist das Bearbeiten von Vorräten.</li>
<li>Kein Seitenwechsel zu /household/staples nötig — die StaplesManager-Komponente rendert direkt im Akkordeon-Bereich. Routing-Vorteil: die /settings URL bleibt beim Bearbeiten erhalten.</li>
<li>Akkordeon-Trigger zeigt den aktuellen Wert im eingeklappten Zustand (z. B. "14 aktiv", "Marcel Raddatz") — der Nutzer kann den Status scannen ohne aufzuklappen.</li>
<li>Auf Mobile perfekt: kein separater "Zurück"-Button nötig. Ein langer Scroll kann mit einem "Nach oben"-Link oder sticky Akkordeon-Headern optimiert werden.</li>
<li>Implementierungshinweis: Der aufgeklappte Bereich enthält direkt die StaplesManager-Komponente (<code style="font-family:var(--font-mono);font-size:11px;background:var(--color-subtle);padding:1px 4px;border-radius:2px;">&lt;StaplesManager categories={data.categories} context="settings" /&gt;</code>). Keine Seiten-Navigation erforderlich.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V4 — SETTINGS SUB-NAV
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V4</div>
<div class="var-meta">
<div class="var-title">Einstellungs-Sub-Navigation</div>
<div class="var-desc">Auf Desktop: eine zweite Navigationsleiste links neben dem Inhalt (Profil · Vorräte · Haushalt). Der Vorräte-Abschnitt ist der Standard-Inhalt. Auf Mobile: flache Liste als Einstieg, dann Drill-down.</div>
<span class="var-tag">Skaliert gut bei vielen Settings-Bereichen</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px — Vorräte als Standardansicht</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<!-- Main sidebar -->
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<!-- Settings sub-nav + content -->
<div style="flex:1;display:flex;">
<!-- Sub-nav -->
<div style="width:180px;flex-shrink:0;border-right:1px solid var(--color-border);background:var(--color-surface);padding:32px 0;">
<div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 12px;">Einstellungen</div>
<a style="display:block;padding:8px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px 2px;">Vorräte</a>
<a style="display:block;padding:8px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px 2px;">Profil</a>
<a style="display:block;padding:8px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px 2px;">Haushalt</a>
</div>
<!-- Content: Staples -->
<main style="flex:1;padding:32px 40px;overflow:hidden;">
<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:24px;">
<h2 style="font-family:var(--font-display);font-size:22px;font-weight:500;letter-spacing:-0.02em;">Vorräte</h2>
<span style="font-size:13px;color:var(--color-text-muted);">14 von 32 aktiv</span>
</div>
<div style="font-size:13px;color:var(--color-text-muted);margin-bottom:20px;line-height:1.5;">Tippe eine Zutat an um den Vorrats-Status zu ändern. Automatisch gespeichert.</div>
<div style="margin-bottom:18px;">
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Gemüse</div>
<div style="display:flex;flex-wrap:wrap;gap:7px;">
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Karotten</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Zwiebeln</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Lauch</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Knoblauch</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Fenchel</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Paprika</span>
</div>
</div>
<div>
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Getreide</div>
<div style="display:flex;flex-wrap:wrap;gap:7px;">
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Pasta</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Reis</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);cursor:pointer;">Couscous</span>
<span style="padding:6px 13px;border-radius:var(--radius-full);font-size:13px;font-weight:500;background:var(--green-dark);color:#fff;cursor:pointer;">Mehl</span>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px (Drill-down)</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 12px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--color-border);background:var(--color-surface);">
<div style="font-size:18px;color:var(--color-text-muted);"></div>
<div style="font-family:var(--font-display);font-size:18px;font-weight:500;">Vorräte</div>
<div style="flex:1;text-align:right;font-size:12px;color:var(--color-text-muted);">14 aktiv</div>
</div>
<div style="flex:1;padding:16px 20px;">
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Gemüse</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;">
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Karotten</span>
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Zwiebeln</span>
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);">Lauch</span>
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Knoblauch</span>
</div>
<div style="font-size:10px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Getreide</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;">
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Pasta</span>
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Reis</span>
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:400;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);">Couscous</span>
<span style="padding:6px 12px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Mehl</span>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Dreifache Navigationsstruktur auf Desktop (App-Sidebar → Settings-Sub-Nav → Inhalt) ist für 3 Einstellungsbereiche Overkill. Skaliert erst ab 6+ Bereichen.</li>
<li>Vorrat als Default-Selektion in der Sub-Nav sinnvoll — direktester Einstieg für J8. Verhindert eine "leere Hub"-Seite.</li>
<li>Mobile-Drill-down: Sub-Nav-Liste → Unterseite. Entspricht dem Standard-Mobile-Pattern (z. B. iOS Systemeinstellungen). Klare Navigation, aber erfordert eine Zurück-Navigation.</li>
<li>Nicht empfohlen für v1: zu viel Struktur für zu wenig Inhalt. V3 (Akkordeon) ist einfacher und erreicht dasselbe Ergebnis ohne Sub-Navigation.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V5 — DASHBOARD WITH SHORTCUT
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V5</div>
<div class="var-meta">
<div class="var-title">Schnellzugriff-Dashboard</div>
<div class="var-desc">Settings als kleines Dashboard: Haushalt-Zusammenfassung oben (Name + Mitgliederzahl), darunter ein prominenter "Vorräte bearbeiten" Einstieg mit aktuellem Zählstand und zuletzt bearbeiteten Chips als Vorschau. Alles auf einer Seite — kein Drill-down.</div>
<span class="var-tag">Informationsdicht · klarer Schwerpunkt</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<main style="flex:1;padding:40px 56px;">
<!-- Profile strip -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:32px;padding-bottom:24px;border-bottom:1px solid var(--color-border);">
<div style="display:flex;align-items:center;gap:14px;">
<div style="width:44px;height:44px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:600;color:var(--green-dark);">MR</div>
<div><div style="font-size:16px;font-weight:500;">Marcel Raddatz</div><div style="font-size:13px;color:var(--color-text-muted);">marcel@email.com · Planner</div></div>
</div>
<button style="font-family:var(--font-sans);font-size:12px;font-weight:500;padding:7px 14px;border-radius:var(--radius-md);background:var(--color-surface);border:1px solid var(--color-border);cursor:pointer;">Profil bearbeiten</button>
</div>
<!-- Main area: Staples primary + Household secondary -->
<div style="display:grid;grid-template-columns:2fr 1fr;gap:20px;max-width:820px;">
<!-- Staples primary block -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:28px;box-shadow:var(--shadow-card);">
<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:6px;">
<div style="font-size:16px;font-weight:500;">Vorräte</div>
<div style="font-family:var(--font-display);font-size:28px;font-weight:300;color:var(--green-dark);letter-spacing:-0.02em;line-height:1;">14</div>
<div style="font-size:12px;color:var(--color-text-muted);">von 32 aktiv</div>
</div>
<div style="font-size:13px;color:var(--color-text-muted);margin-bottom:18px;">Immer vorhandene Zutaten werden automatisch aus Einkaufslisten gefiltert.</div>
<!-- Chip preview -->
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px;">
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Karotten</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Pasta</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Zwiebeln</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;font-weight:500;background:var(--green-dark);color:#fff;">Reis</span>
<span style="padding:5px 11px;border-radius:var(--radius-full);font-size:12px;color:var(--color-text-muted);border:1px solid var(--color-border);">+10 weitere</span>
</div>
<button style="background:var(--green-dark);color:#fff;font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:10px 20px;border-radius:var(--radius-md);border:none;cursor:pointer;">Alle Vorräte bearbeiten</button>
</div>
<!-- Household secondary -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:24px;display:flex;flex-direction:column;gap:12px;">
<div style="font-size:15px;font-weight:500;">Haushalt</div>
<div style="font-family:var(--font-display);font-size:32px;font-weight:300;color:var(--color-text-muted);line-height:1;">3</div>
<div style="font-size:12px;color:var(--color-text-muted);">Mitglieder · Familie Raddatz</div>
<div style="flex:1;"></div>
<button style="font-family:var(--font-sans);font-size:12px;font-weight:500;padding:8px 14px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;">Verwalten</button>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<!-- Profile strip -->
<div style="padding:16px 20px;border-bottom:1px solid var(--color-border);background:var(--color-surface);display:flex;align-items:center;gap:12px;">
<div style="width:36px;height:36px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Planner · Familie Raddatz</div></div>
</div>
<div style="flex:1;padding:16px 20px;display:flex;flex-direction:column;gap:12px;">
<!-- Staples block -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:18px;">
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:10px;"><div style="font-size:15px;font-weight:500;">Vorräte</div><div style="font-family:var(--font-display);font-size:24px;font-weight:300;color:var(--green-dark);line-height:1;">14</div><div style="font-size:11px;color:var(--color-text-muted);">von 32</div></div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:14px;">
<span style="padding:4px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-dark);color:#fff;">Karotten</span>
<span style="padding:4px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-dark);color:#fff;">Pasta</span>
<span style="padding:4px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-dark);color:#fff;">Zwiebeln</span>
<span style="padding:4px 10px;border-radius:var(--radius-full);font-size:11px;color:var(--color-text-muted);border:1px solid var(--color-border);">+11</span>
</div>
<button style="width:100%;background:var(--green-dark);color:#fff;font-size:13px;font-weight:500;padding:10px;border-radius:var(--radius-md);border:none;">Vorräte bearbeiten</button>
</div>
<!-- Household -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:18px;display:flex;align-items:center;justify-content:space-between;">
<div><div style="font-size:14px;font-weight:500;">Haushalt</div><div style="font-size:12px;color:var(--color-text-muted);">Familie Raddatz · 3 Mitglieder</div></div>
<button style="font-size:12px;font-weight:500;padding:7px 14px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;"></button>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Chip-Vorschau auf der Hub-Seite (4 aktive Vorräte + "+10 weitere") gibt dem Nutzer sofort Kontext über den aktuellen Zustand — kein Klick nötig um zu verstehen, was konfiguriert ist.</li>
<li>Profil-Leiste oben als Identitätsanker: Name, E-Mail, Rolle. Schafft eine klare "Wer bin ich in diesem Haushalt"-Aussage ohne eigene Profil-Seite besuchen zu müssen.</li>
<li>2:1 Grid-Layout auf Desktop betont Vorräte als primäre Einstellung — genau das richtige Gewicht für J8.</li>
<li>Nachteil: Der "Alle Vorräte bearbeiten"-Button navigiert trotzdem zur D3-Unterseite (oder öffnet ein Modal). Keine wirklich inline-Bearbeitung wie bei V3. Funktioniert aber gut als Schnellzugriff-Übersicht.</li>
</ul>
</div>
</div>
</div><!-- /doc -->
<!-- ═══════════════════════════════════════════════════════
MACHINE-READABLE SPEC (LLM AGENT REGION)
════════════════════════════════════════════════════════ -->
<!--
spec:agent:start
document: E1 Settings + D3 Staples
version: 1.0
-->
<div class="agent-section">
<h2>Machine-readable spec — E1 Einstellungen / D3 Vorräte</h2>
<p>Authoritative implementation reference for /settings and /household/staples?ctx=settings. Use before building either page.</p>
<pre class="spec-comment">/* E1 Settings + D3 Staples — implementation rules
* 1. Recommended variation: V3 (Accordion). Vorräte section is open by default. No navigation to sub-page required.
* 2. D3 = A3: StaplesManager is the same component. context="settings" removes the onboarding sidebar and "Weiter" button.
* 3. StapleChip renders as a pill button, NOT a checkbox. Selected = --green-dark bg + white text. Unselected = transparent bg + --color-border border + --color-text-muted text.
* 4. Auto-save on toggle. No explicit save button. The component already implements debounced PATCH to /household/staples.
* 5. Category label: font-size 10px · font-weight 500 · letter-spacing 0.08em · text-transform uppercase · color --color-text-muted.
* 6. Staple count display ("14 von 32 aktiv"): derive from categories prop — count isStaple=true vs total ingredients.
* 7. Sidebar active item: "Einstellungen" (not "Vorräte" — there is no separate Vorräte sidebar item). Active style: --green-tint bg + --green-dark text.
* 8. Mobile bottom nav active tab: "Einstellungen". Same for both /settings and /household/staples routes.
* 9. Accordion trigger shows current stat in collapsed state: "14 aktiv" for Vorräte. Stat updates reactively as user toggles chips.
* 10. Changes to staples (J8) do NOT retroactively update an already-generated shopping list. If the current list should reflect changes, the planner must regenerate it via J5. Consider a note in the UI: "Gilt ab der nächsten Einkaufsliste."
* 11. Profile section: show name + email. Edit action navigates to /profile or opens an inline form. Not in scope for J8 — implement minimally.
*/</pre>
<table class="agent-table">
<thead>
<tr><th>Element</th><th>Value / Rule</th><th>Notes</th></tr>
</thead>
<tbody>
<tr class="group-row"><td colspan="3">StapleChip</td></tr>
<tr><td>Shape</td><td>border-radius: --radius-full · padding: 6px 14px</td><td>font-size: 13px (desktop) · 12px (mobile)</td></tr>
<tr><td>Selected state</td><td>background: --green-dark · color: #fff · font-weight: 500</td><td>Toggle off: PATCH ingredient isStaple=false</td></tr>
<tr><td>Unselected state</td><td>background: transparent · border: 1px solid --color-border · color: --color-text-muted · font-weight: 400</td><td>Toggle on: PATCH ingredient isStaple=true</td></tr>
<tr><td>Debounce</td><td>300ms after last toggle before PATCH fires</td><td>Already implemented in StaplesManager. Do not add extra debounce layers.</td></tr>
<tr><td>Error state</td><td>Revert chip to previous state · show inline error message</td><td>StaplesManager already handles rollback on API error</td></tr>
<tr class="group-row"><td colspan="3">Category section</td></tr>
<tr><td>Label</td><td>10px · weight 500 · tracking 0.08em · uppercase · --color-text-muted</td><td>German category names from API</td></tr>
<tr><td>Chip grid</td><td>display: flex · flex-wrap: wrap · gap: 7px (desktop) · 6px (mobile)</td><td>No fixed column count — chips wrap naturally</td></tr>
<tr class="group-row"><td colspan="3">Settings page (E1) — V3 Accordion</td></tr>
<tr><td>Vorräte section</td><td>Open by default on page load</td><td>Use Svelte derived state or URL hash to control. Default open state.</td></tr>
<tr><td>Collapsed stat</td><td>Show "N aktiv" reactively next to chevron</td><td>Derive from stapleState in StaplesManager — count true values</td></tr>
<tr><td>Accordion trigger min-height</td><td>48px (desktop) · 44px (mobile)</td><td>WCAG: interactive controls must have 44px min touch target</td></tr>
<tr><td>Accordion chevron</td><td>▲ (open) / ▼ (closed) · color: --color-text-muted</td><td>Or use CSS transform on a single chevron SVG</td></tr>
<tr class="group-row"><td colspan="3">Responsive</td></tr>
<tr><td>Desktop (≥1024px)</td><td>224px app sidebar + content area (max-width ~680px centered)</td><td>Active sidebar: "Einstellungen" (Haushalt section)</td></tr>
<tr><td>Mobile (&lt;768px)</td><td>No sidebar · bottom nav "Einstellungen" active · accordion stacks full-width</td><td>Chips wrap to multiple lines — no truncation</td></tr>
</tbody>
</table>
</div>
<!--
spec:agent:end
-->
</body>
</html>

761
specs/e2-members.html Normal file
View File

@@ -0,0 +1,761 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>E2 — Mitglieder · 5 Variationen</title>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<!--
spec:agent
document: E2 Members Page — Design Variations
version: 1.0
journey: J7 Manage household members
route: /members
screen: E2
variations: V1 Roster list | V2 Card grid | V3 Split panel | V4 Dense table | V5 Expandable rows
last-updated: 2026-04
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--green-deeper: #1E4A26;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow-text: #8A6800;
--color-error: #DC4C3E;
--blue-tint: #E6F1FB;
--blue: #185FA5;
--blue-dark: #0C447C;
--font-display: 'Fraunces', Georgia, serif;
--font-sans: 'DM Sans', system-ui, sans-serif;
--font-mono: 'DM Mono', monospace;
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px; --radius-xl: 16px; --radius-full: 9999px;
--shadow-card: 0 1px 3px rgba(28,28,24,.06), 0 1px 2px rgba(28,28,24,.04);
--shadow-raised: 0 4px 12px rgba(28,28,24,.08), 0 2px 4px rgba(28,28,24,.04);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
/* ── Doc layout ── */
.doc { max-width: 960px; margin: 0 auto; padding: 48px 40px 96px; }
.doc-header { display: flex; justify-content: space-between; align-items: flex-end; padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
.doc-header p { font-size: 13px; color: var(--color-text-muted); }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
.intro { font-size: 14px; line-height: 1.75; color: var(--color-text); max-width: 640px; margin-bottom: 48px; }
.section-label { font-size: 10px; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 36px; }
/* ── Variation sections ── */
.variation { margin-bottom: 72px; }
.var-header { display: flex; align-items: flex-start; gap: 20px; margin-bottom: 20px; }
.var-num { font-family: var(--font-display); font-size: 44px; font-weight: 300; color: var(--green-light); line-height: 1; flex-shrink: 0; width: 56px; letter-spacing: -0.03em; }
.var-meta { flex: 1; padding-top: 4px; }
.var-title { font-size: 18px; font-weight: 500; letter-spacing: -0.01em; margin-bottom: 4px; }
.var-desc { font-size: 13px; color: var(--color-text-muted); line-height: 1.5; max-width: 540px; }
.var-tag { font-size: 10px; font-weight: 500; letter-spacing: 0.07em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-sm); background: var(--color-subtle); color: var(--color-text-muted); white-space: nowrap; margin-top: 6px; display: inline-block; }
.var-tag.rec { background: var(--green-tint); color: var(--green-dark); }
/* ── Preview containers ── */
.preview-pair { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
/* ── Notes ── */
.var-notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 16px 20px; }
.var-notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
.var-notes ul { list-style: none; display: flex; flex-direction: column; gap: 5px; }
.var-notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
.var-notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
/* ── Agent section ── */
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
.agent-table tr:last-child td { border-bottom: none; }
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
.agent-table td:nth-child(3) { color: #5A5A55; }
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
</style>
</head>
<body>
<div class="doc">
<div class="doc-header">
<div>
<h1>E2 — Mitglieder</h1>
<p>5 Design-Variationen · Desktop-first · Route /members · Journey J7</p>
</div>
<div class="doc-meta">
Version: 1.0<br>
Screen: E2<br>
Journey: J7<br>
Actor: Planner<br>
Last updated: 2026-04
</div>
</div>
<p class="intro">The members page is a rarely-visited, high-trust page. The planner opens it when the household changes — a new partner joins, a family member needs access removed. The household is typically 24 people. Five variations explore the range from a simple list to a panel-based layout. The recommended variation for v1 is <strong style="font-weight:500">V1 (Roster list)</strong> — fewest moving parts, matches the access frequency, household size, and task urgency.</p>
<div class="section-label">Five design variations</div>
<!-- ══════════════════════════════════════
V1 — ROSTER LIST
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V1</div>
<div class="var-meta">
<div class="var-title">Roster-Liste</div>
<div class="var-desc">Lineares Listenformat. Alle Mitglieder als Zeilen mit Avatar, Name, Rolle und Beitrittsdatum. Ausstehende Einladungen darunter. Minimale kognitive Last für eine seltene Aufgabe.</div>
<span class="var-tag rec">Empfohlen · v1</span>
</div>
</div>
<div class="preview-pair">
<!-- Desktop -->
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<!-- Sidebar -->
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;">
<div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;">
<div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div>
<div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);letter-spacing:-0.01em;">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);letter-spacing:0.01em;">Familie Raddatz</div></div>
</div>
<nav style="padding:16px 0;flex:1;">
<div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div>
<a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a>
<a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a>
<a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a>
<div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div>
<a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a>
<a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a>
</nav>
</aside>
<!-- Content -->
<main style="flex:1;padding:40px 56px;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:32px;">
<div style="display:flex;align-items:center;gap:12px;">
<h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;color:var(--color-text);">Mitglieder</h1>
<span style="background:var(--color-subtle);color:var(--color-text-muted);font-size:12px;font-weight:500;padding:2px 9px;border-radius:var(--radius-full);">3</span>
</div>
<button style="background:var(--green-dark);color:#fff;font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:10px 20px;border-radius:var(--radius-md);border:none;cursor:pointer;">Mitglied einladen</button>
</div>
<!-- Members -->
<div style="margin-bottom:32px;">
<div style="display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:40px;height:40px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:var(--green-dark);flex-shrink:0;font-family:var(--font-sans);">MR</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Marcel Raddatz</div>
<div style="font-size:12px;color:var(--color-text-muted);">Beigetreten 14. Januar 2026</div>
</div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-tint);color:var(--green-dark);">Planner</span>
<div style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:18px;cursor:pointer;"></div>
</div>
<div style="display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:40px;height:40px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:var(--blue-dark);flex-shrink:0;font-family:var(--font-sans);">SR</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Sarah Raddatz</div>
<div style="font-size:12px;color:var(--color-text-muted);">Beigetreten 15. Januar 2026</div>
</div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
<div style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:18px;cursor:pointer;"></div>
</div>
<div style="display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:40px;height:40px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:var(--blue-dark);flex-shrink:0;font-family:var(--font-sans);">TM</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Tom Meier</div>
<div style="font-size:12px;color:var(--color-text-muted);">Beigetreten 3. März 2026</div>
</div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
<div style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:18px;cursor:pointer;"></div>
</div>
</div>
<!-- Pending invites -->
<div style="font-size:11px;font-weight:500;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:12px;">Ausstehende Einladungen · 1</div>
<div style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);">
<div style="font-family:var(--font-mono);font-size:13px;color:var(--color-text);flex:1;">inv_x8K2j</div>
<div style="font-size:12px;color:var(--yellow-text);background:var(--yellow-tint);padding:2px 8px;border-radius:var(--radius-sm);">Läuft ab in 2 Tagen</div>
<button style="font-family:var(--font-sans);font-size:12px;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;color:var(--color-text);">Kopieren</button>
<button style="font-family:var(--font-sans);font-size:12px;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;color:var(--color-text);">Neu generieren</button>
</div>
</main>
</div>
</div>
</div>
</div>
<!-- Mobile -->
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--color-border);background:var(--color-surface);">
<div style="font-family:var(--font-display);font-size:20px;font-weight:500;letter-spacing:-0.02em;">Mitglieder</div>
<button style="background:var(--green-dark);color:#fff;font-size:12px;font-weight:500;padding:8px 14px;border-radius:var(--radius-md);border:none;">+ Einladen</button>
</div>
<div style="flex:1;overflow:hidden;padding:0 20px;">
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Planner · Seit 14.1.26</div></div>
<div style="font-size:18px;color:var(--color-text-muted);"></div>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Sarah Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied · Seit 15.1.26</div></div>
<div style="font-size:18px;color:var(--color-text-muted);"></div>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Tom Meier</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied · Seit 3.3.26</div></div>
<div style="font-size:18px;color:var(--color-text-muted);"></div>
</div>
<div style="padding:16px 0 12px;"><div style="font-size:10px;font-weight:500;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Einladungen · 1</div>
<div style="padding:12px 14px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;"><span style="font-family:var(--font-mono);font-size:12px;">inv_x8K2j</span><span style="font-size:11px;color:var(--yellow-text);">2 Tage</span></div>
<div style="display:flex;gap:8px;"><button style="flex:1;font-size:12px;font-weight:500;padding:6px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;">Kopieren</button><button style="flex:1;font-size:12px;font-weight:500;padding:6px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;">Neu</button></div>
</div>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;">
<a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a>
<a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a>
<a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a>
<a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a>
</nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Planner-Avatar in Grün (--green-tint), Mitglieder-Avatar in Blau (--blue-tint) — Rollenfarben konsistent mit den Rollenbadges in der Spec.</li>
<li>Kebab-Menü (⋯) öffnet ein Kontextmenü mit "Zugang entziehen". Destructive action erfordert Bestätigung mit Mitgliedsnamen.</li>
<li>Ausstehende Einladungen haben einen gelben Ablauf-Badge wenn ≤ 3 Tage verbleiben, grau wenn mehr Zeit bleibt.</li>
<li>Auf Mobile: "Einladen"-Button als Kompakt-CTA im Seitenkopf (kein großes Hero-Element) — die Seite ist keine Onboarding-Seite, sondern eine Management-Seite.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V2 — CARD GRID
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V2</div>
<div class="var-meta">
<div class="var-title">Kachelraster</div>
<div class="var-desc">Jedes Mitglied als eigenständige Kachel mit großem Avatar-Kreis, Name, Rolle und Datum. Eine "+" Kachel mit gestricheltem Rand fungiert als Invite-CTA. Visuelle, scannable Übersicht.</div>
<span class="var-tag">Geeignet ab 4+ Mitgliedern</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;">
<div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div>
<nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav>
</aside>
<main style="flex:1;padding:40px 56px;">
<h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;margin-bottom:32px;">Mitglieder</h1>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:16px;">
<!-- Member card -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:24px 20px;display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;box-shadow:var(--shadow-card);">
<div style="width:56px;height:56px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:var(--green-dark);">MR</div>
<div><div style="font-size:14px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);margin-top:2px;">14. Jan 2026</div></div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-tint);color:var(--green-dark);">Planner</span>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:24px 20px;display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;box-shadow:var(--shadow-card);">
<div style="width:56px;height:56px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:var(--blue-dark);">SR</div>
<div><div style="font-size:14px;font-weight:500;">Sarah Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);margin-top:2px;">15. Jan 2026</div></div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:24px 20px;display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;box-shadow:var(--shadow-card);">
<div style="width:56px;height:56px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:var(--blue-dark);">TM</div>
<div><div style="font-size:14px;font-weight:500;">Tom Meier</div><div style="font-size:11px;color:var(--color-text-muted);margin-top:2px;">3. Mär 2026</div></div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
</div>
<!-- Invite tile -->
<div style="border:2px dashed var(--color-border);border-radius:var(--radius-xl);padding:24px 20px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;cursor:pointer;">
<div style="width:56px;height:56px;border-radius:50%;background:var(--color-subtle);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--color-text-muted);">+</div>
<div style="font-size:13px;font-weight:500;color:var(--color-text-muted);">Einladen</div>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface);"><div style="font-family:var(--font-display);font-size:20px;font-weight:500;">Mitglieder</div></div>
<div style="flex:1;padding:16px 20px;display:grid;grid-template-columns:1fr 1fr;gap:12px;align-content:start;">
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;">
<div style="width:44px;height:44px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:var(--green-dark);">MR</div>
<div style="font-size:13px;font-weight:500;">Marcel R.</div>
<span style="font-size:10px;font-weight:500;padding:2px 8px;border-radius:var(--radius-full);background:var(--green-tint);color:var(--green-dark);">Planner</span>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;">
<div style="width:44px;height:44px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:var(--blue-dark);">SR</div>
<div style="font-size:13px;font-weight:500;">Sarah R.</div>
<span style="font-size:10px;font-weight:500;padding:2px 8px;border-radius:var(--radius-full);background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;">
<div style="width:44px;height:44px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:var(--blue-dark);">TM</div>
<div style="font-size:13px;font-weight:500;">Tom M.</div>
<span style="font-size:10px;font-weight:500;padding:2px 8px;border-radius:var(--radius-full);background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
</div>
<div style="border:2px dashed var(--color-border);border-radius:var(--radius-lg);padding:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center;cursor:pointer;">
<div style="width:44px;height:44px;border-radius:50%;background:var(--color-subtle);display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--color-text-muted);">+</div>
<div style="font-size:12px;font-weight:500;color:var(--color-text-muted);">Einladen</div>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Kachelgröße funktioniert nur bis ca. 6 Mitgliedern — ab 7+ muss auf Liste zurückgefallen werden. Für die typische Haushaltsgröße (24) unnötig visuell.</li>
<li>Die "+" Einladen-Kachel mit gestricheltem Rahmen ist ein etabliertes Muster für "leerer Slot zum Hinzufügen" — sofort verständlich ohne Label-Erklärung.</li>
<li>Schlechter als V1 für Remove-Aktionen: Die Entfernen-Aktion ist auf der Kachel nicht sichtbar — man muss sie irgendwo verstecken (Hover-State, Kontextmenü). Auf Mobile nicht erreichbar ohne Tap-Geste.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V3 — SPLIT PANEL
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V3</div>
<div class="var-meta">
<div class="var-title">Split-Panel</div>
<div class="var-desc">Geteilte Ansicht: links die Mitgliederliste, rechts das Einladungs-Management-Panel. Auf Desktop zeigt das rechte Panel immer den aktuellen Invite-Status. Auf Mobile: Tab-Navigation zwischen den beiden Bereichen.</div>
<span class="var-tag">Für Haushalte mit häufigen Mitgliederwechseln</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<!-- Split content -->
<div style="flex:1;display:flex;overflow:hidden;">
<!-- Left: member list -->
<div style="width:340px;flex-shrink:0;border-right:1px solid var(--color-border);padding:32px 28px;background:var(--color-surface);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:24px;">
<span style="font-size:16px;font-weight:500;color:var(--color-text);">Mitglieder</span>
<span style="background:var(--color-subtle);color:var(--color-text-muted);font-size:11px;font-weight:500;padding:1px 7px;border-radius:var(--radius-full);">3</span>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Planner</div></div>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">Sarah Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied</div></div>
<button style="font-size:11px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px;">Entfernen</button>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">Tom Meier</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied</div></div>
<button style="font-size:11px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px;">Entfernen</button>
</div>
</div>
<!-- Right: invite panel -->
<div style="flex:1;padding:32px 36px;">
<div style="font-size:16px;font-weight:500;margin-bottom:20px;">Mitglied einladen</div>
<div style="font-size:13px;color:var(--color-text-muted);margin-bottom:16px;line-height:1.5;">Teile diesen Link per WhatsApp, SMS oder E-Mail. Der Empfänger erstellt ein Konto und tritt automatisch dem Haushalt bei.</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:12px 14px;display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<span style="font-family:var(--font-mono);font-size:12px;color:var(--color-text-muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">https://mealprep.app/join/inv_x8K2j</span>
<button style="font-size:12px;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--green-dark);color:#fff;border:none;cursor:pointer;white-space:nowrap;">Kopieren</button>
</div>
<button style="width:100%;font-size:13px;font-weight:500;padding:10px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;margin-bottom:28px;">Neuen Link generieren</button>
<div style="font-size:11px;font-weight:500;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:12px;">Ausstehende Einladungen · 1</div>
<div style="display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);">
<span style="font-family:var(--font-mono);font-size:12px;flex:1;">inv_x8K2j</span>
<span style="font-size:11px;color:var(--yellow-text);background:var(--yellow-tint);padding:2px 7px;border-radius:var(--radius-sm);">2 Tage</span>
<button style="font-size:18px;color:var(--color-text-muted);background:none;border:none;cursor:pointer;"></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px (Tab-Navigation)</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 0;background:var(--color-surface);border-bottom:1px solid var(--color-border);">
<div style="font-family:var(--font-display);font-size:20px;font-weight:500;margin-bottom:12px;">Mitglieder</div>
<div style="display:flex;gap:0;">
<button style="flex:1;padding:8px;font-size:13px;font-weight:500;border:none;border-bottom:2px solid var(--green-dark);background:none;color:var(--green-dark);cursor:pointer;">Mitglieder (3)</button>
<button style="flex:1;padding:8px;font-size:13px;font-weight:500;border:none;border-bottom:2px solid transparent;background:none;color:var(--color-text-muted);cursor:pointer;">Einladen</button>
</div>
</div>
<div style="flex:1;padding:0 20px;">
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Planner</div></div>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Sarah Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied</div></div>
<button style="font-size:11px;color:var(--color-error);background:none;border:none;cursor:pointer;">Entfernen</button>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Tom Meier</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied</div></div>
<button style="font-size:11px;color:var(--color-error);background:none;border:none;cursor:pointer;">Entfernen</button>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Das rechte Panel zeigt immer den Invite-Status — kein Modal, kein Navigationsschritt. Gut für Haushalte, die regelmäßig neue Mitglieder onboarden (z. B. Wohngemeinschaften).</li>
<li>"Entfernen" ist im linken Panel direkt sichtbar als Text-Link in Fehlerfarbe — kein verstecktes Kebab-Menü. Spart einen Schritt, erhöht aber das Risiko eines versehentlichen Taps auf Mobile.</li>
<li>Mobile Tab-Navigation: Tabs mit Unterstrich-Indikator. Die "Mitglieder"-Seite ist der Default. "Einladen"-Tab zeigt das Invite-Panel mit Link-Generator und ausstehendem Invite.</li>
<li>Nachteil: Mehr UI-Fläche für eine seltene Funktion (Invite). Für Haushalte mit 2 Personen wirkt das Panel überdimensioniert.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V4 — DENSE TABLE
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V4</div>
<div class="var-meta">
<div class="var-title">Datentabelle</div>
<div class="var-desc">Tabellarisches Format mit sortierbaren Spalten: Name, Rolle, Beigetreten, Status, Aktionen. Inline-Aktionen statt Kontextmenü. Kompakt und informationsdicht.</div>
<span class="var-tag">Für Power-User · Viele Mitglieder</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<main style="flex:1;padding:40px 56px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;">
<h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;">Mitglieder</h1>
<button style="background:var(--green-dark);color:#fff;font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:10px 20px;border-radius:var(--radius-md);border:none;cursor:pointer;">Mitglied einladen</button>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="border-bottom:1px solid var(--color-border);">
<th style="text-align:left;padding:8px 12px;font-size:10px;font-weight:500;letter-spacing:0.07em;text-transform:uppercase;color:var(--color-text-muted);cursor:pointer;">Name ↕</th>
<th style="text-align:left;padding:8px 12px;font-size:10px;font-weight:500;letter-spacing:0.07em;text-transform:uppercase;color:var(--color-text-muted);">Rolle</th>
<th style="text-align:left;padding:8px 12px;font-size:10px;font-weight:500;letter-spacing:0.07em;text-transform:uppercase;color:var(--color-text-muted);cursor:pointer;">Beigetreten ↕</th>
<th style="text-align:left;padding:8px 12px;font-size:10px;font-weight:500;letter-spacing:0.07em;text-transform:uppercase;color:var(--color-text-muted);">Status</th>
<th style="text-align:right;padding:8px 12px;font-size:10px;font-weight:500;letter-spacing:0.07em;text-transform:uppercase;color:var(--color-text-muted);">Aktionen</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid var(--color-subtle);">
<td style="padding:12px 12px;"><div style="display:flex;align-items:center;gap:10px;"><div style="width:30px;height:30px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div><span style="font-weight:500;">Marcel Raddatz</span></div></td>
<td style="padding:12px 12px;"><span style="padding:2px 8px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-tint);color:var(--green-dark);">Planner</span></td>
<td style="padding:12px 12px;color:var(--color-text-muted);">14. Jan 2026</td>
<td style="padding:12px 12px;"><span style="font-size:11px;color:var(--green-dark);">Aktiv</span></td>
<td style="padding:12px 12px;text-align:right;color:var(--color-text-muted);font-size:12px;"></td>
</tr>
<tr style="border-bottom:1px solid var(--color-subtle);">
<td style="padding:12px 12px;"><div style="display:flex;align-items:center;gap:10px;"><div style="width:30px;height:30px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div><span style="font-weight:500;">Sarah Raddatz</span></div></td>
<td style="padding:12px 12px;"><span style="padding:2px 8px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span></td>
<td style="padding:12px 12px;color:var(--color-text-muted);">15. Jan 2026</td>
<td style="padding:12px 12px;"><span style="font-size:11px;color:var(--green-dark);">Aktiv</span></td>
<td style="padding:12px 12px;text-align:right;"><button style="font-size:12px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px 8px;">Entfernen</button></td>
</tr>
<tr style="border-bottom:1px solid var(--color-subtle);">
<td style="padding:12px 12px;"><div style="display:flex;align-items:center;gap:10px;"><div style="width:30px;height:30px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div><span style="font-weight:500;">Tom Meier</span></div></td>
<td style="padding:12px 12px;"><span style="padding:2px 8px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span></td>
<td style="padding:12px 12px;color:var(--color-text-muted);">3. Mär 2026</td>
<td style="padding:12px 12px;"><span style="font-size:11px;color:var(--green-dark);">Aktiv</span></td>
<td style="padding:12px 12px;text-align:right;"><button style="font-size:12px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px 8px;">Entfernen</button></td>
</tr>
<!-- Pending invite row -->
<tr style="background:var(--color-subtle);">
<td style="padding:12px 12px;"><div style="display:flex;align-items:center;gap:10px;"><div style="width:30px;height:30px;border-radius:50%;background:var(--color-border);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--color-text-muted);flex-shrink:0;">?</div><span style="font-family:var(--font-mono);font-size:12px;color:var(--color-text-muted);">inv_x8K2j</span></div></td>
<td style="padding:12px 12px;color:var(--color-text-muted);font-size:12px;"></td>
<td style="padding:12px 12px;color:var(--color-text-muted);font-size:12px;"></td>
<td style="padding:12px 12px;"><span style="font-size:11px;color:var(--yellow-text);background:var(--yellow-tint);padding:2px 7px;border-radius:var(--radius-sm);">Ausstehend · 2 Tage</span></td>
<td style="padding:12px 12px;text-align:right;display:flex;justify-content:flex-end;gap:8px;"><button style="font-size:12px;color:var(--color-text-muted);background:none;border:none;cursor:pointer;padding:4px 8px;">Kopieren</button><button style="font-size:12px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px 8px;">Ablehnen</button></td>
</tr>
</tbody>
</table>
</main>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px (Karten-Fallback)</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--color-border);background:var(--color-surface);"><div style="font-family:var(--font-display);font-size:20px;font-weight:500;">Mitglieder</div><button style="background:var(--green-dark);color:#fff;font-size:12px;font-weight:500;padding:7px 14px;border-radius:var(--radius-md);border:none;">+ Einladen</button></div>
<div style="flex:1;padding:12px 20px;display:flex;flex-direction:column;gap:8px;">
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:12px 14px;display:flex;align-items:center;gap:12px;">
<div style="width:36px;height:36px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Planner · 14. Jan 2026</div></div>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:12px 14px;display:flex;align-items:center;gap:12px;">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">Sarah Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied · 15. Jan 2026</div></div>
<button style="font-size:11px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px;">Entfernen</button>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:12px 14px;display:flex;align-items:center;gap:12px;">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">Tom Meier</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied · 3. Mär 2026</div></div>
<button style="font-size:11px;color:var(--color-error);background:none;border:none;cursor:pointer;padding:4px;">Entfernen</button>
</div>
<div style="background:var(--color-subtle);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:12px 14px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;"><span style="font-family:var(--font-mono);font-size:12px;color:var(--color-text-muted);">inv_x8K2j</span><span style="font-size:11px;color:var(--yellow-text);">Ausstehend · 2 Tage</span></div>
<div style="display:flex;gap:8px;"><button style="flex:1;font-size:11px;font-weight:500;padding:5px;border-radius:var(--radius-sm);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;">Kopieren</button><button style="font-size:11px;font-weight:500;padding:5px 10px;border-radius:var(--radius-sm);background:none;border:none;cursor:pointer;color:var(--color-error);">Ablehnen</button></div>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Ausstehende Einladungen als Tabellenzeilen mit grauem Hintergrund und "?" Avatar — visuell klar von aktiven Mitgliedern getrennt, ohne eigenen Abschnitt zu benötigen.</li>
<li>Sortierbare Spalten (↕) nur auf Desktop sinnvoll — bei 24 Mitgliedern keine echte Notwendigkeit. Für Wohngemeinschaften mit 6+ Personen relevant.</li>
<li>Mobile Fallback: Karten statt Tabelle. Tabellen-Layouts kollabieren auf kleinen Bildschirmen schlecht — Karten sind das korrekte Muster für den selben Inhalt auf Mobile.</li>
</ul>
</div>
</div>
<!-- ══════════════════════════════════════
V5 — EXPANDABLE ROWS
══════════════════════════════════════ -->
<div class="variation">
<div class="var-header">
<div class="var-num">V5</div>
<div class="var-meta">
<div class="var-title">Erweiterbare Zeilen</div>
<div class="var-desc">Liste mit progressiver Offenlegung. Jede Mitgliederzeile lässt sich aufklappen, um Detailinfos und Aktionen zu zeigen. Kompakte Standardansicht, Details bei Bedarf — kein Seitennavigation nötig.</div>
<span class="var-tag">Gute Balance zwischen V1 und V4</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop · 1200px (Sarah-Zeile aufgeklappt)</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="display:flex;width:1200px;min-height:680px;background:var(--color-page);">
<aside style="width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);padding:0;display:flex;flex-direction:column;min-height:680px;"><div style="padding:18px 16px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;"><div style="width:22px;height:22px;background:var(--green);border-radius:5px;flex-shrink:0;"></div><div><div style="font-family:var(--font-display);font-size:15px;font-weight:500;color:var(--color-text);">Mealprep</div><div style="font-size:10px;color:var(--color-text-muted);">Familie Raddatz</div></div></div><nav style="padding:16px 0;flex:1;"><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 16px 6px;">Plan</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Planer</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Rezepte</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einkauf</a><div style="font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-text-muted);padding:16px 16px 6px;">Haushalt</div><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;background:var(--green-tint);color:var(--green-dark);font-weight:500;text-decoration:none;border-radius:var(--radius-md);margin:0 8px;"><span style="width:16px;height:16px;background:var(--green-light);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Mitglieder</a><a style="display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:13px;color:var(--color-text-muted);text-decoration:none;margin:0 8px;"><span style="width:16px;height:16px;background:var(--color-border);border-radius:3px;flex-shrink:0;display:inline-block;"></span>Einstellungen</a></nav></aside>
<main style="flex:1;padding:40px 56px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;">
<div style="display:flex;align-items:center;gap:12px;"><h1 style="font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-0.02em;">Mitglieder</h1><span style="background:var(--color-subtle);color:var(--color-text-muted);font-size:12px;font-weight:500;padding:2px 9px;border-radius:var(--radius-full);">3</span></div>
<button style="background:var(--green-dark);color:#fff;font-family:var(--font-sans);font-size:13px;font-weight:500;letter-spacing:0.04em;padding:10px 20px;border-radius:var(--radius-md);border:none;cursor:pointer;">Mitglied einladen</button>
</div>
<!-- Collapsed row -->
<div style="display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:40px;height:40px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Marcel Raddatz</div></div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--green-tint);color:var(--green-dark);">Planner</span>
<div style="font-size:12px;color:var(--color-text-muted);cursor:pointer;padding:4px 8px;">Details ▼</div>
</div>
<!-- Expanded row -->
<div style="border-bottom:1px solid var(--color-subtle);">
<div style="display:flex;align-items:center;gap:14px;padding:14px 0;background:var(--color-surface);border-radius:var(--radius-md) var(--radius-md) 0 0;padding:14px 16px;">
<div style="width:40px;height:40px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Sarah Raddatz</div></div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
<div style="font-size:12px;color:var(--color-text-muted);cursor:pointer;padding:4px 8px;">Details ▲</div>
</div>
<div style="background:var(--color-surface);padding:0 16px 16px;border-radius:0 0 var(--radius-md) var(--radius-md);display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:13px;color:var(--color-text-muted);">Beigetreten 15. Januar 2026 · Zugang zu Planer (Lesen) und Einkauf</div>
<button style="font-size:13px;font-weight:500;color:var(--color-error);background:none;border:1px solid var(--color-error);padding:7px 16px;border-radius:var(--radius-md);cursor:pointer;">Zugang entziehen</button>
</div>
</div>
<!-- Collapsed row -->
<div style="display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:40px;height:40px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Tom Meier</div></div>
<span style="padding:3px 10px;border-radius:var(--radius-full);font-size:11px;font-weight:500;background:var(--blue-tint);color:var(--blue-dark);">Mitglied</span>
<div style="font-size:12px;color:var(--color-text-muted);cursor:pointer;padding:4px 8px;">Details ▼</div>
</div>
<!-- Invite section -->
<div style="margin-top:24px;"><div style="font-size:11px;font-weight:500;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:12px;">Einladungen · 1</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);"><span style="font-family:var(--font-mono);font-size:13px;flex:1;">inv_x8K2j</span><span style="font-size:11px;color:var(--yellow-text);background:var(--yellow-tint);padding:2px 8px;border-radius:var(--radius-sm);">2 Tage</span><button style="font-size:12px;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--color-page);border:1px solid var(--color-border);cursor:pointer;">Kopieren</button></div>
</div>
</main>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile · 390px</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="width:390px;min-height:680px;background:var(--color-page);display:flex;flex-direction:column;">
<div style="padding:16px 20px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--color-border);background:var(--color-surface);"><div style="font-family:var(--font-display);font-size:20px;font-weight:500;">Mitglieder</div><button style="background:var(--green-dark);color:#fff;font-size:12px;font-weight:500;padding:7px 14px;border-radius:var(--radius-md);border:none;">+ Einladen</button></div>
<div style="flex:1;padding:0 20px;">
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--green-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--green-dark);flex-shrink:0;">MR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Marcel Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Planner</div></div>
<div style="font-size:16px;color:var(--color-text-muted);cursor:pointer;"></div>
</div>
<!-- Expanded -->
<div style="border-bottom:1px solid var(--color-subtle);">
<div style="display:flex;align-items:center;gap:12px;padding:12px 0 8px;background:var(--color-surface);margin:0 -4px;padding:12px 8px 8px;">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">SR</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Sarah Raddatz</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied</div></div>
<div style="font-size:16px;color:var(--color-text-muted);cursor:pointer;"></div>
</div>
<div style="background:var(--color-surface);padding:0 8px 12px;margin:0 -4px;"><div style="font-size:12px;color:var(--color-text-muted);margin-bottom:10px;">Seit 15. Januar 2026 · Lesen + Einkauf</div><button style="width:100%;font-size:13px;font-weight:500;color:var(--color-error);background:none;border:1px solid var(--color-error);padding:8px;border-radius:var(--radius-md);cursor:pointer;">Zugang entziehen</button></div>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--blue-tint);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:var(--blue-dark);flex-shrink:0;">TM</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:500;">Tom Meier</div><div style="font-size:11px;color:var(--color-text-muted);">Mitglied</div></div>
<div style="font-size:16px;color:var(--color-text-muted);cursor:pointer;"></div>
</div>
</div>
<nav style="background:var(--color-surface);border-top:1px solid var(--color-border);padding:8px 0 20px;display:flex;"><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Planer</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Rezepte</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--color-text-muted);text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--color-border);border-radius:3px;display:inline-block;"></span>Einkauf</a><a style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px;color:var(--green-dark);font-weight:500;text-decoration:none;padding:4px 0;"><span style="width:20px;height:20px;background:var(--green-light);border-radius:3px;display:inline-block;"></span>Einstellungen</a></nav>
</div>
</div>
</div>
</div>
</div>
<div class="var-notes">
<div class="var-notes-label">Design-Entscheidungen</div>
<ul>
<li>Der "Zugang entziehen"-Button ist erst nach dem Aufklappen sichtbar — eine natürliche Bestätigungsbarriere ohne expliziten Bestätigungsdialog für das erste Tap.</li>
<li>Aufgeklappte Zeile erhält leichten Surface-Hintergrund (--color-surface) zur visuellen Abgrenzung vom Rest der Liste.</li>
<li>Besser als V1 wenn die Entfernen-Aktion prominent sein soll, aber nicht permanent sichtbar. Schlechter als V1 wenn maximale Einfachheit das Ziel ist.</li>
<li>Auf Mobile: "▼" Chevron als Tap-Target — muss mindestens 44×44px sein. Den gesamten Zeilenbereich tapbar machen ist vorzuziehen.</li>
</ul>
</div>
</div>
</div><!-- /doc -->
<!-- ═══════════════════════════════════════════════════════
MACHINE-READABLE SPEC (LLM AGENT REGION)
════════════════════════════════════════════════════════ -->
<!--
spec:agent:start
document: E2 Members Page
version: 1.0
-->
<div class="agent-section">
<h2>Machine-readable spec — E2 Mitglieder</h2>
<p>Authoritative implementation reference for the /members page. Use before building any component for this route.</p>
<pre class="spec-comment">/* E2 Members page — implementation rules
* 1. Recommended variation: V1 (Roster list). Simplest, lowest overhead, matches household size 24.
* 2. Avatar colours: Planner = --green-tint bg + --green-dark text. Member = --blue-tint bg + --blue-dark text. Initials only.
* 3. Role badges: same colour pairing as avatars. border-radius: --radius-full. font-size: 11px. font-weight: 500.
* 4. The planner cannot remove themselves — no remove action on the Planner's row ever.
* 5. Pending invites: show expiry in --yellow-tint + --yellow-text when ≤ 3 days remaining, --color-subtle + --color-text-muted otherwise.
* 6. Remove action is destructive — requires a confirmation dialog with the member's name before execution.
* 7. Invite mechanism: generate a link/code. Copy to clipboard. No email delivery system in v1.
* 8. Expired invites: show "Abgelaufen" state. Provide one-tap regeneration — do not require re-opening an invite flow.
* 9. Household members can view this page in read-only mode (no invite button, no remove actions, no pending invites section).
* 10. Desktop sidebar: "Mitglieder" item is active, under "Haushalt" section. Mobile: "Einstellungen" tab is active.
* 11. WCAG 2.2 AA: avatar initials need 4.5:1 contrast. Role badge text needs 4.5:1 contrast. Confirm before implementing.
*/</pre>
<table class="agent-table">
<thead>
<tr><th>Element</th><th>Value / Rule</th><th>Notes</th></tr>
</thead>
<tbody>
<tr class="group-row"><td colspan="3">Avatar</td></tr>
<tr><td>Size</td><td>40px × 40px (desktop) · 36px × 36px (mobile)</td><td>border-radius: 50%</td></tr>
<tr><td>Planner colour</td><td>bg --green-tint · text --green-dark</td><td>Contrast OK: #2E6E39 on #E8F5EA ≈ 6.1:1</td></tr>
<tr><td>Member colour</td><td>bg --blue-tint · text --blue-dark</td><td>Contrast OK: #0C447C on #E6F1FB ≈ 7.4:1</td></tr>
<tr><td>Content</td><td>First letter of first + last name (uppercase)</td><td>Max 2 characters</td></tr>
<tr class="group-row"><td colspan="3">Role badge</td></tr>
<tr><td>Shape</td><td>border-radius: --radius-full · padding: 3px 10px</td><td>font-size: 11px · font-weight: 500</td></tr>
<tr><td>Planner</td><td>bg --green-tint · color --green-dark</td><td>Label: "Planner"</td></tr>
<tr><td>Member</td><td>bg --blue-tint · color --blue-dark</td><td>Label: "Mitglied"</td></tr>
<tr class="group-row"><td colspan="3">Invite / pending</td></tr>
<tr><td>Expiry badge — urgent (≤3d)</td><td>bg --yellow-tint · color --yellow-text</td><td>"Läuft ab in N Tagen"</td></tr>
<tr><td>Expiry badge — normal</td><td>bg --color-subtle · color --color-text-muted</td><td>"Läuft ab am DD. MMM"</td></tr>
<tr><td>Code font</td><td>font-family: --font-mono · font-size: 13px</td><td>Invite codes are monospace</td></tr>
<tr class="group-row"><td colspan="3">Interactions</td></tr>
<tr><td>Remove action</td><td>Confirmation dialog required</td><td>Dialog must show member name. Irreversible — member loses access immediately.</td></tr>
<tr><td>Copy invite link</td><td>navigator.clipboard.writeText()</td><td>Show transient "Kopiert!" feedback (checkmark, 2s)</td></tr>
<tr><td>Regenerate invite</td><td>POST /household/invite — returns new code</td><td>Old code is immediately invalidated</td></tr>
<tr class="group-row"><td colspan="3">Responsive</td></tr>
<tr><td>Desktop (≥1024px)</td><td>224px sidebar + full content area</td><td>Active sidebar item: Mitglieder (Haushalt section)</td></tr>
<tr><td>Mobile (&lt;768px)</td><td>No sidebar · bottom nav · "Einstellungen" tab active</td><td>V3 mobile uses tab bar within page, not app bottom nav tabs</td></tr>
</tbody>
</table>
</div>
<!--
spec:agent:end
-->
</body>
</html>

View File

@@ -0,0 +1,755 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planner C+E — Drei Zustände</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@300;400&family=DM+Sans:wght@400;500;600&family=DM+Mono&display=swap" rel="stylesheet">
<style>
:root {
--page: #fafaf7;
--surface: #f5f4ee;
--subtle: #edecea;
--border: #d8d7d0;
--text: #1c1c18;
--muted: #6b6a63;
--gt: #e8f5ea; --gl: #aedcb0; --g: #3d8c4a; --gd: #2e6e39;
--yt: #fdf6d8; --yl: #f9e08a; --y: #f2c12e; --yx: #8a6800;
--pt: #eeedfe; --p: #534ab7;
--ot: #fef0e6; --o: #e8862a; --od: #b46820;
--err: #dc4c3e;
--r-sm: 4px; --r-md: 6px; --r-lg: 10px; --r-xl: 16px; --r-full: 9999px;
--sh-card: 0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);
--sh-raised: 0 4px 12px rgba(28,28,24,.08),0 2px 4px rgba(28,28,24,.04);
--fd: 'Fraunces', Georgia, serif;
--fs: 'DM Sans', system-ui, sans-serif;
--fm: 'DM Mono', monospace;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--fs);background:#dddcd7;color:var(--text);padding:40px 24px 80px;line-height:1.4;}
/* ── Page chrome ─── */
.eyebrow{font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
.title{font-family:var(--fd);font-size:34px;font-weight:300;margin-bottom:6px;}
.sub{font-family:var(--fs);font-size:14px;color:var(--muted);max-width:700px;line-height:1.65;margin-bottom:20px;}
.flow-legend{display:flex;gap:20px;margin-bottom:44px;flex-wrap:wrap;}
.fl-item{display:flex;align-items:center;gap:8px;font-family:var(--fs);font-size:12px;color:var(--muted);}
.fl-dot{width:10px;height:10px;border-radius:50%;}
/* ── Frame chrome ─── */
.state-block{margin-bottom:60px;}
.state-label{display:flex;align-items:center;gap:10px;margin-bottom:12px;}
.state-num{font-family:var(--fm);font-size:11px;background:var(--subtle);color:var(--muted);padding:3px 8px;border-radius:var(--r-sm);}
.state-name{font-family:var(--fd);font-size:22px;font-weight:300;}
.state-when{font-family:var(--fs);font-size:12px;color:var(--muted);margin-left:auto;}
.note{font-family:var(--fs);font-size:12px;color:var(--muted);border-left:3px solid var(--border);padding:10px 14px;margin-top:14px;line-height:1.6;}
.note strong{color:var(--text);font-weight:500;}
/* ── Desktop frame ─── */
.frame{display:flex;flex-direction:column;background:var(--page);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--sh-raised);}
/* Topbar */
.tb{display:flex;align-items:center;gap:7px;padding:11px 18px;border-bottom:1px solid var(--border);background:var(--page);flex-shrink:0;}
.tb-h1{font-family:var(--fd);font-size:17px;font-weight:300;}
.tb-range{font-family:var(--fs);font-size:11px;color:var(--muted);}
.tb-arr{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--r-md);font-size:13px;color:var(--muted);}
.tb-btn{height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--r-md);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;color:var(--text);background:var(--page);}
.tb-ml{margin-left:auto;}
.tb-pri{background:var(--gd);color:#fff;border:none;}
/* 3-panel body */
.body{display:flex;flex:1;overflow:hidden;}
/* Sidebar */
.sb{width:184px;flex-shrink:0;border-right:1px solid var(--border);background:var(--surface);padding:13px;display:flex;flex-direction:column;gap:13px;overflow-y:auto;}
.sb-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:5px;}
.score-box{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;}
.sc-big{font-family:var(--fd);font-size:27px;font-weight:300;line-height:1;}
.sc-den{font-family:var(--fs);font-size:11px;color:var(--muted);}
.pbar{height:4px;border-radius:var(--r-full);background:var(--yl);overflow:hidden;margin-top:6px;}
.pb-fill{height:100%;border-radius:var(--r-full);background:var(--y);}
.pbg{background:var(--g);}
.pbt{background:var(--border);}
.sr{display:flex;align-items:center;gap:6px;margin-top:6px;}
.sr-l{font-family:var(--fs);font-size:10px;color:var(--muted);width:68px;flex-shrink:0;}
.sr-b{flex:1;height:3px;border-radius:var(--r-full);background:var(--border);overflow:hidden;}
.sr-f{height:100%;border-radius:var(--r-full);}
.sr-v{font-family:var(--fm);font-size:9px;color:var(--muted);width:18px;text-align:right;}
.w-item{font-family:var(--fs);font-size:10px;color:var(--yx);margin-top:4px;line-height:1.4;}
.dp{display:flex;gap:2px;margin-top:5px;}
.dp-s{flex:1;height:4px;border-radius:var(--r-full);}
/* Right panel */
.rp{width:228px;flex-shrink:0;border-left:1px solid var(--border);background:var(--page);padding:13px;display:flex;flex-direction:column;overflow-y:auto;}
.rp-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:7px;}
.rp-name{font-family:var(--fd);font-size:16px;font-weight:300;line-height:1.35;}
.rp-meta{font-family:var(--fs);font-size:11px;color:var(--muted);margin-top:3px;}
.rp-btn{display:block;width:100%;padding:7px;border-radius:var(--r-md);border:1px solid var(--border);background:var(--page);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;text-align:center;color:var(--text);margin-top:5px;}
.rp-pri{background:var(--gd);color:#fff;border:none;}
.rp-err{color:var(--err);border-color:var(--err);background:transparent;}
.hr{height:1px;background:var(--border);margin:10px 0;}
/* Main area */
.main{flex:1;overflow-y:auto;padding:13px;}
/* Calendar grid */
.grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;}
.d-abbr{font-family:var(--fs);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);text-align:center;margin-bottom:3px;}
.d-badge{width:20px;height:20px;border-radius:var(--r-full);display:flex;align-items:center;justify-content:center;font-family:var(--fs);font-size:10px;font-weight:500;margin:0 auto 5px;color:var(--text);}
.db-t{background:var(--y);color:#fff;}
.db-s{background:var(--gt);color:var(--gd);}
.tile{border-radius:var(--r-md);border:1px solid var(--border);background:var(--surface);padding:7px;cursor:pointer;box-shadow:var(--sh-card);}
.tile-t{border:2px solid var(--y);background:var(--yt);}
.tile-s{border:2px solid var(--g);background:var(--gt);}
.tile-e{border-style:dashed;background:transparent;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--muted);gap:2px;}
.tile-e-s{border:2px dashed var(--g);background:var(--gt);}
.tr{font-family:var(--fd);font-size:11px;font-weight:300;color:var(--text);line-height:1.3;}
.tm{font-family:var(--fs);font-size:9px;color:var(--muted);margin-top:2px;}
.tbdg{display:inline-flex;align-items:center;padding:1px 4px;border-radius:2px;font-family:var(--fs);font-size:8px;font-weight:500;margin-top:3px;}
.b-e{background:var(--gt);color:var(--gd);}
.b-m{background:var(--yt);color:var(--yx);}
.b-h{background:var(--ot);color:var(--od);}
/* Section header */
.sec-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px;padding-bottom:7px;border-bottom:1px solid var(--border);}
.sec-t{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);}
.sec-link{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);}
/* Agenda list */
.agenda{display:flex;flex-direction:column;gap:4px;}
.ag-item{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:var(--r-md);border:1px solid var(--border);background:var(--surface);cursor:pointer;}
.ag-item:hover{border-color:var(--gl);}
.ag-t{border-style:dashed;background:transparent;}
.ag-t .ag-day{color:var(--muted);}
.ag-day{font-family:var(--fs);font-size:10px;color:var(--muted);width:48px;flex-shrink:0;}
.ag-name{font-family:var(--fd);font-size:13px;font-weight:300;flex:1;}
.ag-meta{font-family:var(--fs);font-size:10px;color:var(--muted);flex-shrink:0;}
.ag-action{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--gd);flex-shrink:0;}
.ag-today-dot{width:6px;height:6px;border-radius:50%;background:var(--y);flex-shrink:0;}
.ag-item-today{border-color:var(--yl);background:var(--yt);}
/* Suggestions for empty days (in agenda / standalone) */
.sug-block{margin-top:14px;}
.sug-day-hd{display:flex;align-items:center;gap:6px;margin-bottom:6px;}
.sug-day-name{font-family:var(--fs);font-size:11px;font-weight:500;color:var(--text);}
.sug-empty-pill{font-family:var(--fs);font-size:9px;background:var(--subtle);color:var(--muted);padding:1px 6px;border-radius:var(--r-full);}
.sug-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;}
.sc-item{border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface);padding:9px;cursor:pointer;transition:border-color .1s,box-shadow .1s;}
.sc-item:hover{border-color:var(--gl);box-shadow:var(--sh-raised);}
.sc-name{font-family:var(--fd);font-size:12px;font-weight:300;line-height:1.35;color:var(--text);}
.sc-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;}
.sc-tag{font-family:var(--fs);font-size:9px;font-weight:500;padding:2px 6px;border-radius:2px;margin-top:6px;display:inline-block;background:var(--gt);color:var(--gd);}
.sc-tag-y{background:var(--yt);color:var(--yx);}
.sc-add{display:flex;align-items:center;justify-content:center;height:100%;min-height:72px;border:1px dashed var(--border);border-radius:var(--r-md);font-family:var(--fs);font-size:11px;color:var(--muted);cursor:pointer;}
.sc-add:hover{border-color:var(--gl);color:var(--gd);}
/* Expansion panel */
.expand{border:2px solid var(--g);border-radius:var(--r-lg);background:var(--gt);padding:14px;display:flex;gap:14px;position:relative;margin-top:10px;}
.expand-empty{border:2px solid var(--g);border-radius:var(--r-lg);background:var(--gt);padding:14px;position:relative;margin-top:10px;}
.exp-arrow-wrap{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;position:absolute;top:-8px;left:13px;right:13px;pointer-events:none;}
.exp-arrow{display:flex;justify-content:center;}
.exp-arr-shape{width:12px;height:12px;background:var(--gt);border-left:2px solid var(--g);border-top:2px solid var(--g);transform:rotate(45deg);}
.exp-arr-shape-e{background:var(--gt);}
.exp-left{flex:1;}
.exp-context{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--gd);margin-bottom:4px;}
.exp-name{font-family:var(--fd);font-size:22px;font-weight:300;line-height:1.25;}
.exp-meta{font-family:var(--fs);font-size:12px;color:var(--muted);margin-top:4px;}
.ing-wrap{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px;}
.ing{font-family:var(--fs);font-size:10px;background:#fff;border:1px solid var(--border);border-radius:var(--r-full);padding:2px 8px;color:var(--text);}
.ing-s{background:var(--subtle);border-color:var(--subtle);color:var(--muted);}
.exp-badges{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;}
.exp-right{display:flex;flex-direction:column;gap:5px;width:118px;flex-shrink:0;}
.exp-btn{padding:7px;border-radius:var(--r-md);border:1px solid var(--border);background:#fff;font-family:var(--fs);font-size:11px;font-weight:500;text-align:center;cursor:pointer;letter-spacing:.04em;}
.exp-pri{background:var(--gd);color:#fff;border:none;}
.exp-err{color:var(--err);border-color:var(--err);background:transparent;}
/* Compact remaining below expansion */
.remaining{margin-top:10px;}
.rem-hd{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
/* State 3: empty slot expansion */
.exp-sug-title{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--gd);margin-bottom:10px;}
.exp-sug-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;}
.exp-sc{border:1px solid var(--gl);border-radius:var(--r-md);background:#fff;padding:10px;cursor:pointer;}
.exp-sc:hover{box-shadow:var(--sh-raised);}
.exp-sc-name{font-family:var(--fd);font-size:13px;font-weight:300;line-height:1.3;color:var(--text);}
.exp-sc-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;}
.exp-sc-tag{font-family:var(--fs);font-size:9px;font-weight:500;padding:2px 6px;border-radius:2px;background:var(--gt);color:var(--gd);margin-top:6px;display:inline-block;}
.exp-all{font-family:var(--fs);font-size:11px;font-weight:500;color:var(--yx);margin-top:10px;display:block;}
/* Right panel: idle hint */
.rp-idle{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:6px;padding:16px;}
.rp-idle-t{font-family:var(--fs);font-size:12px;color:var(--text);}
.rp-idle-s{font-family:var(--fs);font-size:11px;color:var(--muted);max-width:150px;line-height:1.5;}
/* Right panel: today card */
.today-card{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;margin-bottom:10px;}
.tc-label{font-family:var(--fs);font-size:9px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--yx);margin-bottom:4px;}
.tc-name{font-family:var(--fd);font-size:14px;font-weight:300;line-height:1.3;}
.tc-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:2px;}
/* Right panel: picker mode */
.picker-search{display:flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:6px 9px;margin-bottom:9px;}
.ps-icon{font-size:11px;color:var(--muted);}
.ps-text{font-family:var(--fs);font-size:11px;color:var(--muted);}
.pick-item{display:flex;align-items:center;gap:7px;padding:7px 0;border-bottom:1px solid var(--subtle);cursor:pointer;}
.pick-item:last-child{border-bottom:none;}
.pick-name{font-family:var(--fd);font-size:12px;font-weight:300;flex:1;line-height:1.3;}
.pick-meta{font-family:var(--fs);font-size:10px;color:var(--muted);}
</style>
</head>
<body>
<p class="eyebrow">Mealplan · Planer · Konzept</p>
<h1 class="title">C + E Kombiniert — Drei Zustände</h1>
<p class="sub">
Der Hauptbereich unterhalb des Kalender-Grids zeigt immer nützlichen Inhalt — je nach Zustand.
Kein leerer Raum, kein Tab-Wechsel nötig. Das rechte Panel bleibt für den Rezept-Picker reserviert.
</p>
<div class="flow-legend">
<div class="fl-item"><div class="fl-dot" style="background:var(--muted);"></div>Zustand 1 — Kein Tag ausgewählt</div>
<div class="fl-item"><div class="fl-dot" style="background:var(--g);"></div>Zustand 2 — Tag mit Rezept angeklickt</div>
<div class="fl-item"><div class="fl-dot" style="background:var(--yx);"></div>Zustand 3 — Leerer Tag angeklickt</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 1: KEIN TAG AUSGEWÄHLT -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="state-block">
<div class="state-label">
<span class="state-num">Zustand 1</span>
<span class="state-name">Kein Tag ausgewählt</span>
<span class="state-when">Standard beim Laden der Seite</span>
</div>
<div class="frame" style="height:670px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<!-- Sidebar -->
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar"><div class="pb-fill" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a style="display:block;margin-top:8px;font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<!-- Main area -->
<div class="main">
<!-- Calendar grid: no selection -->
<div class="grid7">
<div><div class="d-abbr">Mo</div><div class="d-badge">7</div><div class="tile" style="height:86px;"><p class="tr">Hähnchen-Curry</p><p class="tm">35 Min</p><span class="tbdg b-m">mittel</span></div></div>
<div><div class="d-abbr">Di</div><div class="d-badge db-t">8</div><div class="tile tile-t" style="height:86px;"><p class="tr">Pasta Bolognese</p><p class="tm">45 Min</p><span class="tbdg b-m">mittel</span></div></div>
<div><div class="d-abbr">Mi</div><div class="d-badge">9</div><div class="tile" style="height:86px;"><p class="tr">Gemüse-Stir-fry</p><p class="tm">20 Min</p><span class="tbdg b-e">einfach</span></div></div>
<div><div class="d-abbr">Do</div><div class="d-badge">10</div><div class="tile" style="height:86px;"><p class="tr">Lachs mit Kartoffeln</p><p class="tm">30 Min</p><span class="tbdg b-e">einfach</span></div></div>
<div><div class="d-abbr">Fr</div><div class="d-badge">11</div><div class="tile" style="height:86px;"><p class="tr">Pizza Margherita</p><p class="tm">50 Min</p><span class="tbdg b-h">aufwändig</span></div></div>
<div><div class="d-abbr">Sa</div><div class="d-badge">12</div><div class="tile tile-e" style="height:86px;"><span style="font-size:16px;">+</span><span style="font-family:var(--fs);font-size:9px;">wählen</span></div></div>
<div><div class="d-abbr">So</div><div class="d-badge">13</div><div class="tile tile-e" style="height:86px;"><span style="font-size:16px;">+</span><span style="font-family:var(--fs);font-size:9px;">wählen</span></div></div>
</div>
<!-- ▼ BELOW GRID: Agenda (all 7 days) ▼ -->
<div style="margin-top:16px;">
<div class="sec-hd">
<span class="sec-t">Diese Woche</span>
</div>
<div class="agenda">
<div class="ag-item">
<div class="ag-day">Mo 7.4</div>
<div class="ag-name">Hähnchen-Curry</div>
<div class="ag-meta">35 Min · mittel</div>
</div>
<div class="ag-item ag-item-today">
<div class="ag-today-dot"></div>
<div class="ag-day" style="color:var(--yx);">Di 8.4</div>
<div class="ag-name">Pasta Bolognese</div>
<div class="ag-meta">45 Min · mittel</div>
</div>
<div class="ag-item">
<div class="ag-day">Mi 9.4</div>
<div class="ag-name">Gemüse-Stir-fry</div>
<div class="ag-meta">20 Min · einfach</div>
</div>
<div class="ag-item">
<div class="ag-day">Do 10.4</div>
<div class="ag-name">Lachs mit Kartoffeln</div>
<div class="ag-meta">30 Min · einfach</div>
</div>
<div class="ag-item">
<div class="ag-day">Fr 11.4</div>
<div class="ag-name">Pizza Margherita</div>
<div class="ag-meta">50 Min · aufwändig</div>
</div>
<div class="ag-item ag-t">
<div class="ag-day">Sa 12.4</div>
<div class="ag-name" style="font-family:var(--fs);font-size:12px;color:var(--muted);">Noch kein Gericht</div>
<div class="ag-action">+ Hinzufügen</div>
</div>
<div class="ag-item ag-t">
<div class="ag-day">So 13.4</div>
<div class="ag-name" style="font-family:var(--fs);font-size:12px;color:var(--muted);">Noch kein Gericht</div>
<div class="ag-action">+ Hinzufügen</div>
</div>
</div>
</div>
<!-- ▼ BELOW AGENDA: Suggestions for empty days ▼ -->
<div style="margin-top:18px;">
<div class="sec-hd">
<span class="sec-t">Vorschläge für ungeplante Tage</span>
<a class="sec-link">Alle Rezepte →</a>
</div>
<!-- Sa -->
<div class="sug-block">
<div class="sug-day-hd">
<span class="sug-day-name">Samstag, 12. Apr</span>
<span class="sug-empty-pill">kein Gericht</span>
</div>
<div class="sug-cards">
<div class="sc-item">
<div class="sc-name">Ramen mit Ei</div>
<div class="sc-meta">40 Min · mittel</div>
<span class="sc-tag">Neues Protein</span>
</div>
<div class="sc-item">
<div class="sc-name">Shakshuka</div>
<div class="sc-meta">25 Min · einfach</div>
<span class="sc-tag">Keine Überschneidung</span>
</div>
<div class="sc-item">
<div class="sc-name">Rindfleisch-Tacos</div>
<div class="sc-meta">30 Min · einfach</div>
<span class="sc-tag sc-tag-y">Aufwand: einfach</span>
</div>
</div>
</div>
<!-- So -->
<div class="sug-block" style="margin-top:14px;">
<div class="sug-day-hd">
<span class="sug-day-name">Sonntag, 13. Apr</span>
<span class="sug-empty-pill">kein Gericht</span>
</div>
<div class="sug-cards">
<div class="sc-item">
<div class="sc-name">Pho Bo</div>
<div class="sc-meta">60 Min · aufwändig</div>
<span class="sc-tag">Neues Protein</span>
</div>
<div class="sc-item">
<div class="sc-name">Lachs-Avocado-Bowl</div>
<div class="sc-meta">15 Min · einfach</div>
<span class="sc-tag">Keine Überschneidung</span>
</div>
<div class="sc-item">
<div class="sc-name">Kürbissuppe</div>
<div class="sc-meta">35 Min · einfach</div>
<span class="sc-tag">Keine Überschneidung</span>
</div>
</div>
</div>
</div>
</div>
<!-- Right panel: idle, shows today quick-start -->
<div class="rp">
<div class="today-card">
<div class="tc-label">Heute Abend</div>
<div class="tc-name">Pasta Bolognese</div>
<div class="tc-meta">Dienstag · 45 Min · mittel</div>
<button class="rp-btn rp-pri" style="margin-top:8px;padding:6px;">Koch-Modus starten</button>
</div>
<div class="hr"></div>
<div class="rp-idle" style="flex:1;">
<div class="rp-idle-t">Tag auswählen</div>
<div class="rp-idle-s">Klicke eine Kachel um Details zu sehen oder ein Gericht zu wählen</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Inhalt ohne Klick:</strong> Agenda aller 7 Tage (geplante + leere), darunter Vorschläge für die beiden
ungeplanten Tage — anstelle von Score-Delta zeigen die Karten kurze Begründungs-Tags
(Neues Protein / Keine Überschneidung / Aufwand-Hinweis). Das rechte Panel zeigt „Heute Abend" als
direkte Koch-Modus-Abkürzung. Kein leerer Raum, kein Klick nötig.
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 2: TAG MIT REZEPT AUSGEWÄHLT (Mi) -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="state-block">
<div class="state-label">
<span class="state-num">Zustand 2</span>
<span class="state-name">Tag mit Rezept angeklickt</span>
<span class="state-when">Klick auf Mi, 9. Apr → Gemüse-Stir-fry</span>
</div>
<div class="frame" style="height:680px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<!-- Sidebar (same) -->
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar"><div class="pb-fill" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a style="display:block;margin-top:8px;font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<!-- Main area -->
<div class="main">
<!-- Calendar grid: Mi selected -->
<div class="grid7">
<div><div class="d-abbr">Mo</div><div class="d-badge">7</div><div class="tile" style="height:86px;"><p class="tr">Hähnchen-Curry</p><p class="tm">35 Min</p><span class="tbdg b-m">mittel</span></div></div>
<div><div class="d-abbr">Di</div><div class="d-badge db-t">8</div><div class="tile tile-t" style="height:86px;"><p class="tr">Pasta Bolognese</p><p class="tm">45 Min</p><span class="tbdg b-m">mittel</span></div></div>
<!-- Mi SELECTED -->
<div style="position:relative;">
<div class="d-abbr" style="color:var(--gd);">Mi</div>
<div class="d-badge db-s">9</div>
<div class="tile tile-s" style="height:86px;position:relative;">
<p class="tr">Gemüse-Stir-fry</p>
<p class="tm">20 Min</p>
<span class="tbdg b-e">einfach</span>
<div style="position:absolute;bottom:5px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--gd);"></div>
</div>
</div>
<div><div class="d-abbr">Do</div><div class="d-badge">10</div><div class="tile" style="height:86px;opacity:.55;"><p class="tr">Lachs mit Kartoffeln</p><p class="tm">30 Min</p></div></div>
<div><div class="d-abbr">Fr</div><div class="d-badge">11</div><div class="tile" style="height:86px;opacity:.55;"><p class="tr">Pizza Margherita</p><p class="tm">50 Min</p></div></div>
<div><div class="d-abbr">Sa</div><div class="d-badge">12</div><div class="tile tile-e" style="height:86px;opacity:.55;"><span style="font-size:16px;">+</span></div></div>
<div><div class="d-abbr">So</div><div class="d-badge">13</div><div class="tile tile-e" style="height:86px;opacity:.55;"><span style="font-size:16px;">+</span></div></div>
</div>
<!-- ▼ EXPANSION: full-width, below grid, arrow to Mi (3rd column) ▼ -->
<div style="position:relative;margin-top:2px;">
<!-- Arrow pointing up to Mi tile (column 3 of 7) -->
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:6px;position:absolute;top:-8px;left:0;right:0;pointer-events:none;">
<div></div><div></div>
<div class="exp-arrow"><div class="exp-arr-shape"></div></div>
<div></div><div></div><div></div><div></div>
</div>
<div class="expand">
<div class="exp-left">
<div class="exp-context">Mittwoch, 9. Apr · Abendessen</div>
<div class="exp-name">Gemüse-Stir-fry</div>
<div class="exp-meta">20 Min · einfach · 4 Portionen</div>
<div class="ing-wrap">
<span class="ing">Tofu</span>
<span class="ing">Paprika</span>
<span class="ing">Brokkoli</span>
<span class="ing">Karotten</span>
<span class="ing">Ingwer</span>
<span class="ing">Zucchini</span>
<span class="ing-s">Sesamöl</span>
<span class="ing-s">Sojasauce</span>
<span class="ing-s">Knoblauch</span>
</div>
<div class="exp-badges">
<span class="tbdg b-e" style="font-size:10px;padding:3px 7px;">einfach</span>
<span style="font-family:var(--fs);font-size:10px;font-weight:500;padding:3px 7px;border-radius:2px;background:var(--pt);color:var(--p);">Protein: Tofu</span>
<span style="font-family:var(--fs);font-size:10px;font-weight:500;padding:3px 7px;border-radius:2px;background:var(--gt);color:var(--gd);">Score ▲ +0.4</span>
</div>
</div>
<div class="exp-right">
<button class="exp-btn exp-pri">Koch-Modus</button>
<button class="exp-btn">Rezept ansehen</button>
<button class="exp-btn">Gericht tauschen</button>
<button class="exp-btn exp-err">Entfernen</button>
</div>
</div>
</div>
<!-- ▼ REMAINING WEEK below expansion ▼ -->
<div class="remaining">
<div class="rem-hd">Restliche Woche</div>
<div class="agenda">
<div class="ag-item">
<div class="ag-day">Do 10.4</div>
<div class="ag-name">Lachs mit Kartoffeln</div>
<div class="ag-meta">30 Min · einfach</div>
</div>
<div class="ag-item">
<div class="ag-day">Fr 11.4</div>
<div class="ag-name">Pizza Margherita</div>
<div class="ag-meta">50 Min · aufwändig</div>
</div>
<div class="ag-item ag-t">
<div class="ag-day">Sa 12.4</div>
<div class="ag-name" style="font-family:var(--fs);font-size:12px;color:var(--muted);">Noch kein Gericht</div>
<div class="ag-action">+ Hinzufügen</div>
</div>
<div class="ag-item ag-t">
<div class="ag-day">So 13.4</div>
<div class="ag-name" style="font-family:var(--fs);font-size:12px;color:var(--muted);">Noch kein Gericht</div>
<div class="ag-action">+ Hinzufügen</div>
</div>
</div>
</div>
</div>
<!-- Right panel: score context for selected day -->
<div class="rp">
<div class="rp-lbl">Mittwoch, 9. Apr</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--muted);margin-bottom:10px;">Wie wirkt dieses Gericht?</div>
<div style="display:flex;align-items:baseline;gap:4px;margin-bottom:4px;">
<span style="font-family:var(--fd);font-size:22px;font-weight:300;">7.8</span>
<span style="font-family:var(--fs);font-size:11px;color:var(--muted);">/10</span>
<span style="font-family:var(--fs);font-size:11px;color:var(--gd);font-weight:500;margin-left:6px;">▲ +0.4</span>
</div>
<div class="pbar pbt" style="margin-bottom:12px;"><div class="pb-fill pbg" style="width:78%;"></div></div>
<div class="hr"></div>
<div style="font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);margin-bottom:8px;">Dieses Gericht</div>
<div style="display:flex;flex-direction:column;gap:5px;">
<div style="display:flex;align-items:center;gap:6px;font-family:var(--fs);font-size:11px;color:var(--gd);"><span></span><span>Kein Protein-Overlap</span></div>
<div style="display:flex;align-items:center;gap:6px;font-family:var(--fs);font-size:11px;color:var(--gd);"><span></span><span>Neue Zutaten (Tofu, Paprika)</span></div>
<div style="display:flex;align-items:center;gap:6px;font-family:var(--fs);font-size:11px;color:var(--yx);"><span>~</span><span>Tofu 2× diese Woche</span></div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Nach Klick auf Mittwoch:</strong> Expansion öffnet sich direkt unter dem Grid mit Pfeil-Indikator.
Enthält Rezeptname groß, Zutaten-Tags (normale vs. Grundzutaten <span style="opacity:.6;">gedimmt</span>),
Score-Impact-Badge und alle Aktionen. Unselektierte Kacheln werden leicht ausgeblendet (opacity 55%).
Darunter: kompakte Liste der restlichen Woche — geplante Tage + leere Tage mit „+ Hinzufügen".
Das rechte Panel wechselt auf einen Variety-Kontext: zeigt was dieses Gericht zur Woche beiträgt (positiv/neutral/negativ).
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 3: LEERER TAG AUSGEWÄHLT (Sa) -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="state-block">
<div class="state-label">
<span class="state-num">Zustand 3</span>
<span class="state-name">Leerer Tag angeklickt</span>
<span class="state-when">Klick auf Sa, 12. Apr → kein Gericht</span>
</div>
<div class="frame" style="height:680px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<!-- Sidebar -->
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar"><div class="pb-fill" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a style="display:block;margin-top:8px;font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<!-- Main area -->
<div class="main">
<!-- Calendar grid: Sa selected (empty) -->
<div class="grid7">
<div><div class="d-abbr">Mo</div><div class="d-badge">7</div><div class="tile" style="height:86px;opacity:.55;"><p class="tr">Hähnchen-Curry</p><p class="tm">35 Min</p></div></div>
<div><div class="d-abbr">Di</div><div class="d-badge db-t">8</div><div class="tile tile-t" style="height:86px;opacity:.55;"><p class="tr">Pasta Bolognese</p><p class="tm">45 Min</p></div></div>
<div><div class="d-abbr">Mi</div><div class="d-badge">9</div><div class="tile" style="height:86px;opacity:.55;"><p class="tr">Gemüse-Stir-fry</p><p class="tm">20 Min</p></div></div>
<div><div class="d-abbr">Do</div><div class="d-badge">10</div><div class="tile" style="height:86px;opacity:.55;"><p class="tr">Lachs mit Kartoffeln</p><p class="tm">30 Min</p></div></div>
<div><div class="d-abbr">Fr</div><div class="d-badge">11</div><div class="tile" style="height:86px;opacity:.55;"><p class="tr">Pizza Margherita</p><p class="tm">50 Min</p></div></div>
<!-- Sa: SELECTED EMPTY -->
<div style="position:relative;">
<div class="d-abbr" style="color:var(--gd);">Sa</div>
<div class="d-badge db-s">12</div>
<div class="tile tile-e tile-e-s" style="height:86px;">
<span style="font-size:20px;color:var(--gd);">+</span>
<span style="font-family:var(--fs);font-size:9px;color:var(--gd);">wählen</span>
<div style="position:absolute;bottom:5px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--gd);"></div>
</div>
</div>
<div><div class="d-abbr">So</div><div class="d-badge">13</div><div class="tile tile-e" style="height:86px;opacity:.55;"><span style="font-size:16px;">+</span></div></div>
</div>
<!-- ▼ EXPANSION: suggestion cards for Sa ▼ -->
<div style="position:relative;margin-top:2px;">
<!-- Arrow to Sa (column 6) -->
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:6px;position:absolute;top:-8px;left:0;right:0;pointer-events:none;">
<div></div><div></div><div></div><div></div><div></div>
<div class="exp-arrow"><div class="exp-arr-shape exp-arr-shape-e"></div></div>
<div></div>
</div>
<div class="expand-empty">
<div class="exp-sug-title">Vorschläge für Samstag, 12. Apr</div>
<div class="exp-sug-cards">
<div class="exp-sc">
<div class="exp-sc-name">Ramen mit Ei</div>
<div class="exp-sc-meta">40 Min · mittel</div>
<span class="exp-sc-tag">Neues Protein</span>
</div>
<div class="exp-sc">
<div class="exp-sc-name">Shakshuka</div>
<div class="exp-sc-meta">25 Min · einfach</div>
<span class="exp-sc-tag">Keine Überschneidung</span>
</div>
<div class="exp-sc">
<div class="exp-sc-name">Rindfleisch-Tacos</div>
<div class="exp-sc-meta">30 Min · einfach</div>
<span class="exp-sc-tag">Keine Überschneidung</span>
</div>
<div class="exp-sc">
<div class="exp-sc-name">Kürbissuppe</div>
<div class="exp-sc-meta">35 Min · einfach</div>
<span class="exp-sc-tag" style="background:var(--yt);color:var(--yx);">Aufwand: einfach</span>
</div>
<div class="exp-sc">
<div class="exp-sc-name">Tofu-Teriyaki</div>
<div class="exp-sc-meta">30 Min · einfach</div>
<span class="exp-sc-tag" style="background:var(--yt);color:var(--yx);">Gleiche Zutaten</span>
</div>
<!-- Alle Rezepte card -->
<div class="sc-add" style="min-height:auto;">Alle Rezepte →</div>
</div>
</div>
</div>
<!-- ▼ REMAINING WEEK below ▼ -->
<div class="remaining">
<div class="rem-hd">Noch diese Woche</div>
<div class="agenda">
<div class="ag-item">
<div class="ag-day">Mo 7.4</div>
<div class="ag-name">Hähnchen-Curry</div>
<div class="ag-meta">35 Min · mittel</div>
</div>
<div class="ag-item ag-item-today">
<div class="ag-today-dot"></div>
<div class="ag-day" style="color:var(--yx);">Di 8.4</div>
<div class="ag-name">Pasta Bolognese</div>
<div class="ag-meta">45 Min</div>
</div>
<div class="ag-item ag-t">
<div class="ag-day">So 13.4</div>
<div class="ag-name" style="font-family:var(--fs);font-size:12px;color:var(--muted);">Noch kein Gericht</div>
<div class="ag-action">+ Hinzufügen</div>
</div>
</div>
</div>
</div>
<!-- Right panel: full recipe picker (search + list) -->
<div class="rp">
<div class="rp-lbl">Samstag, 12. Apr</div>
<div class="picker-search">
<span class="ps-icon"></span>
<span class="ps-text">Rezept suchen…</span>
</div>
<div style="overflow-y:auto;flex:1;">
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Ramen mit Ei</div><div class="pick-meta">40 Min · mittel</div></div>
<span style="font-family:var(--fm);font-size:9px;background:var(--gt);color:var(--gd);padding:2px 5px;border-radius:2px;">Top</span>
</div>
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Shakshuka</div><div class="pick-meta">25 Min · einfach</div></div>
<span style="font-family:var(--fm);font-size:9px;background:var(--gt);color:var(--gd);padding:2px 5px;border-radius:2px;">Top</span>
</div>
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Kürbissuppe</div><div class="pick-meta">35 Min · einfach</div></div>
</div>
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Tofu-Teriyaki</div><div class="pick-meta">30 Min · einfach</div></div>
</div>
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Gemüse-Curry</div><div class="pick-meta">40 Min · mittel</div></div>
</div>
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Linseneintopf</div><div class="pick-meta">50 Min · einfach</div></div>
</div>
<div class="pick-item">
<div style="flex:1;"><div class="pick-name">Ofen-Lachs</div><div class="pick-meta">35 Min · einfach</div></div>
</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Nach Klick auf leeren Samstag:</strong> Expansion zeigt 5 Vorschläge (3×3 Grid + „Alle Rezepte" als letztes Feld)
mit Begründungs-Tags statt Score-Delta. Die Farbkodierung der Tags macht die Qualität der Empfehlung lesbar ohne Zahlenwert:
Grün = klar besser, Gelb = neutral/tradeoff. Das rechte Panel öffnet gleichzeitig den vollständigen Rezept-Picker mit Suche —
für Nutzer die keinen der Vorschläge wollen. Klick auf eine Karte (main) oder Picker-Item (rechts) führt dasselbe aus: Rezept eintragen.
<br><br>
<strong>Tofu-Teriyaki</strong> erscheint mit „Gleiche Zutaten" (gelb) — das ist die sinnvolle Alternative zu einem Score-Delta von 0.2:
nicht versteckt, aber klar als Abwägung markiert.
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,773 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planner — Full-Bleed Tiles</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@300;400&family=DM+Sans:wght@400;500;600&family=DM+Mono&display=swap" rel="stylesheet">
<style>
:root {
--page: #fafaf7;
--surface: #f5f4ee;
--subtle: #edecea;
--border: #d8d7d0;
--text: #1c1c18;
--muted: #6b6a63;
--gt: #e8f5ea; --gl: #aedcb0; --g: #3d8c4a; --gd: #2e6e39;
--yt: #fdf6d8; --yl: #f9e08a; --y: #f2c12e; --yx: #8a6800;
--pt: #eeedfe; --p: #534ab7;
--ot: #fef0e6; --od: #b46820;
--err: #dc4c3e;
--r-sm: 4px; --r-md: 6px; --r-lg: 10px; --r-xl: 16px; --r-full: 9999px;
--sh-card: 0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);
--sh-raised: 0 6px 18px rgba(28,28,24,.14),0 2px 6px rgba(28,28,24,.08);
--fd: 'Fraunces', Georgia, serif;
--fs: 'DM Sans', system-ui, sans-serif;
--fm: 'DM Mono', monospace;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--fs);background:#dddcd7;color:var(--text);padding:40px 24px 80px;line-height:1.4;}
.eyebrow{font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
.title{font-family:var(--fd);font-size:34px;font-weight:300;margin-bottom:6px;}
.sub{font-family:var(--fs);font-size:14px;color:var(--muted);max-width:700px;line-height:1.65;margin-bottom:44px;}
.block{margin-bottom:56px;}
.block-label{display:flex;align-items:baseline;gap:10px;margin-bottom:12px;}
.bl-num{font-family:var(--fm);font-size:11px;background:var(--subtle);color:var(--muted);padding:3px 8px;border-radius:var(--r-sm);}
.bl-name{font-family:var(--fd);font-size:22px;font-weight:300;}
.bl-when{font-family:var(--fs);font-size:12px;color:var(--muted);margin-left:auto;}
.note{font-family:var(--fs);font-size:12px;color:var(--muted);border-left:3px solid var(--border);padding:10px 14px;margin-top:14px;line-height:1.6;}
.note strong{color:var(--text);font-weight:500;}
/* ── Desktop frame ─── */
.frame{display:flex;flex-direction:column;background:var(--page);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--sh-raised);}
.tb{display:flex;align-items:center;gap:7px;padding:11px 18px;border-bottom:1px solid var(--border);background:var(--page);flex-shrink:0;}
.tb-h1{font-family:var(--fd);font-size:17px;font-weight:300;}
.tb-range{font-family:var(--fs);font-size:11px;color:var(--muted);}
.tb-arr{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--r-md);font-size:13px;color:var(--muted);}
.tb-btn{height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--r-md);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;color:var(--text);background:var(--page);}
.tb-ml{margin-left:auto;}
.tb-pri{background:var(--gd);color:#fff;border:none;}
.body{display:flex;flex:1;overflow:hidden;}
/* Sidebar */
.sb{width:184px;flex-shrink:0;border-right:1px solid var(--border);background:var(--surface);padding:13px;display:flex;flex-direction:column;gap:13px;}
.sb-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:5px;}
.score-box{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;}
.sc-big{font-family:var(--fd);font-size:27px;font-weight:300;line-height:1;}
.sc-den{font-family:var(--fs);font-size:11px;color:var(--muted);}
.pbar{height:4px;border-radius:var(--r-full);overflow:hidden;margin-top:6px;}
.pb-y{background:var(--yl);} .pb-t{background:var(--border);}
.pb-fill{height:100%;border-radius:var(--r-full);}
.pb-fg-y{background:var(--y);} .pb-fg-g{background:var(--g);}
.sr{display:flex;align-items:center;gap:6px;margin-top:6px;}
.sr-l{font-family:var(--fs);font-size:10px;color:var(--muted);width:68px;flex-shrink:0;}
.sr-b{flex:1;height:3px;border-radius:var(--r-full);background:var(--border);overflow:hidden;}
.sr-f{height:100%;border-radius:var(--r-full);}
.sr-v{font-family:var(--fm);font-size:9px;color:var(--muted);width:18px;text-align:right;}
.w-item{font-family:var(--fs);font-size:10px;color:var(--yx);margin-top:4px;line-height:1.4;}
.dp{display:flex;gap:2px;margin-top:5px;}
.dp-s{flex:1;height:4px;border-radius:var(--r-full);}
.sb-link{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);display:block;margin-top:8px;}
/* Right panel */
.rp{width:228px;flex-shrink:0;border-left:1px solid var(--border);background:var(--page);padding:13px;display:flex;flex-direction:column;overflow-y:auto;}
.rp-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:7px;}
.rp-btn{display:block;width:100%;padding:7px;border-radius:var(--r-md);border:1px solid var(--border);background:var(--page);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;text-align:center;color:var(--text);margin-top:5px;}
.rp-pri{background:var(--gd);color:#fff;border:none;}
.rp-err{color:var(--err);border-color:var(--err);background:transparent;}
.hr{height:1px;background:var(--border);margin:10px 0;}
.picker-search{display:flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:6px 9px;margin-bottom:9px;}
.pick-item{display:flex;align-items:center;gap:7px;padding:7px 0;border-bottom:1px solid var(--subtle);cursor:pointer;}
.pick-item:last-child{border-bottom:none;}
.pick-name{font-family:var(--fd);font-size:12px;font-weight:300;flex:1;line-height:1.3;}
.pick-meta{font-family:var(--fs);font-size:10px;color:var(--muted);}
.pick-top{font-family:var(--fm);font-size:9px;background:var(--gt);color:var(--gd);padding:2px 5px;border-radius:2px;}
/* Main */
.main{flex:1;overflow-y:auto;padding:12px;}
/* ════════════════════════════════════════════
FULL-BLEED TILE SYSTEM
════════════════════════════════════════════ */
.grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:7px;}
/* The tile is a positioned container — image is the background */
.tile{
position:relative;
border-radius:var(--r-lg);
overflow:hidden;
cursor:pointer;
box-shadow:var(--sh-card);
transition:box-shadow .15s, transform .1s;
/* Image fills entire tile */
background-size:cover;
background-position:center;
}
.tile:hover{box-shadow:var(--sh-raised);transform:translateY(-1px);}
/* State borders — use box-shadow to avoid layout shift with border:2px */
.tile-default{}
.tile-today{box-shadow:0 0 0 2px var(--y), var(--sh-card);}
.tile-today:hover{box-shadow:0 0 0 2px var(--y), var(--sh-raised);}
.tile-sel{box-shadow:0 0 0 2px var(--g), var(--sh-raised);}
.tile-faded{opacity:.42;pointer-events:none;}
/* Full-height gradient overlay: dark at bottom for text, subtle at top for day label */
.tile-overlay{
position:absolute;inset:0;
background:
linear-gradient(to bottom,
rgba(0,0,0,.32) 0%,
rgba(0,0,0,0) 30%,
rgba(0,0,0,0) 45%,
rgba(0,0,0,.55) 100%
);
border-radius:inherit;
}
/* Day header — top of tile, over image */
.tile-head{
position:absolute;top:0;left:0;right:0;
display:flex;align-items:center;justify-content:space-between;
padding:7px 8px;
z-index:2;
}
.tile-day-abbr{
font-family:var(--fs);font-size:9px;text-transform:uppercase;
letter-spacing:.06em;color:rgba(255,255,255,.85);font-weight:500;
}
.tile-day-num{
width:20px;height:20px;border-radius:var(--r-full);
display:flex;align-items:center;justify-content:center;
font-family:var(--fs);font-size:10px;font-weight:500;
color:rgba(255,255,255,.9);
background:rgba(255,255,255,.22);
}
.dn-today{background:var(--y) !important;color:#fff !important;}
.dn-sel{background:var(--g) !important;color:#fff !important;}
/* Recipe info — bottom of tile, over image */
.tile-info{
position:absolute;bottom:0;left:0;right:0;
padding:8px 9px 9px;
z-index:2;
}
.tile-name{
font-family:var(--fd);font-size:13px;font-weight:300;
color:#fff;line-height:1.3;
text-shadow:0 1px 3px rgba(0,0,0,.4);
}
.tile-meta{
font-family:var(--fs);font-size:10px;
color:rgba(255,255,255,.75);
margin-top:2px;
}
.tile-tags{display:flex;gap:3px;flex-wrap:wrap;margin-top:5px;}
.tag{
font-family:var(--fs);font-size:8px;font-weight:500;
padding:2px 5px;border-radius:2px;
background:rgba(255,255,255,.2);
color:rgba(255,255,255,.92);
backdrop-filter:blur(2px);
}
/* State-specific tag tints */
.tag-today{background:rgba(242,193,46,.35);}
.tag-sel{background:rgba(61,140,74,.45);}
/* ── EMPTY TILE ────────────────────────────── */
/* Empty tile: no image, dashed border, suggestion list fills height */
.tile-empty{
border-radius:var(--r-lg);
border:1.5px dashed var(--border);
background:var(--surface);
cursor:pointer;
display:flex;flex-direction:column;
overflow:hidden;
box-shadow:var(--sh-card);
}
.tile-empty-sel{
border:2px dashed var(--g);
background:rgba(232,245,234,.5);
}
.tile-empty-faded{opacity:.3;pointer-events:none;}
.empty-head{
display:flex;align-items:center;justify-content:space-between;
padding:7px 8px 0;flex-shrink:0;
}
.empty-day-abbr{font-family:var(--fs);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);}
.empty-day-num{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--muted);}
.empty-cta{
display:flex;flex-direction:column;align-items:center;justify-content:center;
padding:8px 6px 6px;
gap:2px;flex-shrink:0;
border-bottom:1px solid var(--border);
}
.empty-plus{font-size:18px;color:var(--border);}
.empty-label{font-family:var(--fs);font-size:9px;color:var(--muted);}
.empty-sel-label{color:var(--gd);}
.empty-arr{font-size:9px;color:var(--gd);margin-top:1px;}
/* Inline suggestion list */
.sug-list{
display:flex;flex-direction:column;
padding:5px 7px 6px;flex:1;overflow:hidden;
}
.sug-hd{
font-family:var(--fs);font-size:8px;font-weight:500;
letter-spacing:.07em;text-transform:uppercase;
color:var(--muted);padding:3px 0 5px;
border-bottom:1px solid var(--subtle);
margin-bottom:2px;
}
.sug-row{
display:flex;align-items:center;gap:4px;
padding:5px 0;border-bottom:1px solid var(--subtle);
cursor:pointer;
}
.sug-row:last-of-type{border-bottom:none;}
.sug-row:hover .sug-name{color:var(--gd);}
.sug-name{font-family:var(--fd);font-size:11px;font-weight:300;color:var(--text);flex:1;line-height:1.2;}
.sug-tag{
font-family:var(--fs);font-size:8px;font-weight:500;
padding:1px 4px;border-radius:2px;white-space:nowrap;flex-shrink:0;
}
.st-g{background:var(--gt);color:var(--gd);}
.st-y{background:var(--yt);color:var(--yx);}
.sug-more{
font-family:var(--fs);font-size:9px;font-weight:500;
color:var(--yx);text-align:center;padding-top:5px;margin-top:auto;
}
/* ── EXPANSION ─────────────────────────────── */
.expand-wrap{position:relative;margin-top:3px;}
.exp-arrows{
display:grid;grid-template-columns:repeat(7,1fr);gap:7px;
position:absolute;top:-8px;left:0;right:0;pointer-events:none;
}
.exp-arr{display:flex;justify-content:center;}
.arr{width:12px;height:12px;border-left:2px solid var(--g);border-top:2px solid var(--g);transform:rotate(45deg);}
.arr-fill{background:var(--gt);}
.arr-empty{background:rgba(232,245,234,.5);}
.expand{
border:2px solid var(--g);border-radius:var(--r-lg);
background:var(--gt);padding:14px;display:flex;gap:14px;
}
.exp-left{flex:1;}
.exp-ctx{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--gd);margin-bottom:4px;}
.exp-name{font-family:var(--fd);font-size:22px;font-weight:300;line-height:1.25;}
.exp-meta{font-family:var(--fs);font-size:12px;color:var(--muted);margin-top:4px;}
.ing-wrap{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px;}
.ing{font-family:var(--fs);font-size:10px;background:#fff;border:1px solid var(--border);border-radius:var(--r-full);padding:2px 8px;color:var(--text);}
.ing-s{background:var(--subtle);border-color:var(--subtle);color:var(--muted);}
.exp-bdgs{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px;}
.ebdg{font-family:var(--fs);font-size:10px;font-weight:500;padding:3px 7px;border-radius:2px;}
.ebdg-e{background:var(--gt);color:var(--gd);}
.ebdg-p{background:var(--pt);color:var(--p);}
.ebdg-sc{background:var(--gt);color:var(--gd);}
.exp-right{display:flex;flex-direction:column;gap:5px;width:116px;flex-shrink:0;}
.exp-btn{padding:7px;border-radius:var(--r-md);border:1px solid var(--border);background:#fff;font-family:var(--fs);font-size:11px;font-weight:500;text-align:center;cursor:pointer;letter-spacing:.04em;}
.exp-pri{background:var(--gd);color:#fff;border:none;}
.exp-err{color:var(--err);border-color:var(--err);background:transparent;}
/* Right panel: idle today card */
.today-card{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;margin-bottom:10px;}
.tc-lbl{font-family:var(--fs);font-size:9px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--yx);margin-bottom:4px;}
.tc-name{font-family:var(--fd);font-size:14px;font-weight:300;line-height:1.3;}
.tc-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:2px;}
/* ── CLOSE-UP SPECIMENS ─────────────────────── */
.specimen-row{display:flex;gap:14px;margin-bottom:32px;flex-wrap:wrap;}
.specimen{flex-shrink:0;}
.specimen-label{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;text-align:center;}
/* Image colour palettes for each recipe (placeholder for heroImageUrl) */
.img-curry{background:linear-gradient(160deg,#d4923a 0%,#a85e1a 40%,#7a3d0c 100%);}
.img-pasta{background:linear-gradient(160deg,#c04545 0%,#8b2020 40%,#5a1010 100%);}
.img-stirfry{background:linear-gradient(160deg,#5fa85e 0%,#2e7031 40%,#1a4a1e 100%);}
.img-lachs{background:linear-gradient(160deg,#5b9fd4 0%,#2868a0 40%,#10406e 100%);}
.img-pizza{background:linear-gradient(160deg,#d4a832 0%,#a07010 40%,#6e4a00 100%);}
</style>
</head>
<body>
<p class="eyebrow">Mealplan · Planer · Full-Bleed Tiles</p>
<h1 class="title">Vollflächige Kacheln</h1>
<p class="sub">
Das Bild füllt die gesamte Kachelhöhe. Text und Tags werden über einen Gradienten am unteren Rand eingeblendet.
Kein leerer Bereich mehr — jeder Pixel der Kachel ist Bild.
Leere Kacheln behalten die Vorschlagsliste von innen.
</p>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- CLOSE-UP: Tile variants -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="block-label">
<span class="bl-num">Nahaufnahme</span>
<span class="bl-name">Kachel-Varianten</span>
</div>
<div class="specimen-row">
<!-- Filled: default -->
<div class="specimen">
<div class="specimen-label">Gefüllt — Standard</div>
<div class="tile img-curry" style="width:140px;height:220px;">
<div class="tile-overlay"></div>
<div class="tile-head">
<span class="tile-day-abbr">Mo</span>
<span class="tile-day-num">7</span>
</div>
<div class="tile-info">
<div class="tile-name">Hähnchen-Curry</div>
<div class="tile-meta">35 Min · mittel</div>
<div class="tile-tags">
<span class="tag">Hähnchen</span>
<span class="tag">4 Port.</span>
</div>
</div>
</div>
</div>
<!-- Filled: today -->
<div class="specimen">
<div class="specimen-label">Gefüllt — Heute</div>
<div class="tile tile-today img-pasta" style="width:140px;height:220px;">
<div class="tile-overlay"></div>
<div class="tile-head">
<span class="tile-day-abbr">Di</span>
<span class="tile-day-num dn-today">8</span>
</div>
<div class="tile-info">
<div class="tile-name">Pasta Bolognese</div>
<div class="tile-meta">45 Min · mittel</div>
<div class="tile-tags">
<span class="tag tag-today">Rind</span>
<span class="tag tag-today">4 Port.</span>
</div>
</div>
</div>
</div>
<!-- Filled: selected -->
<div class="specimen">
<div class="specimen-label">Gefüllt — Ausgewählt</div>
<div class="tile tile-sel img-stirfry" style="width:140px;height:220px;">
<div class="tile-overlay"></div>
<div class="tile-head">
<span class="tile-day-abbr">Mi</span>
<span class="tile-day-num dn-sel">9</span>
</div>
<div class="tile-info">
<div class="tile-name">Gemüse-Stir-fry</div>
<div class="tile-meta">20 Min · einfach</div>
<div class="tile-tags">
<span class="tag tag-sel">Tofu</span>
<span class="tag tag-sel">2 Port.</span>
</div>
<div style="text-align:center;font-size:9px;color:rgba(255,255,255,.7);margin-top:4px;"></div>
</div>
</div>
</div>
<!-- Filled: faded -->
<div class="specimen">
<div class="specimen-label">Gefüllt — Gedimmt</div>
<div class="tile tile-faded img-lachs" style="width:140px;height:220px;pointer-events:auto;">
<div class="tile-overlay"></div>
<div class="tile-head">
<span class="tile-day-abbr">Do</span>
<span class="tile-day-num">10</span>
</div>
<div class="tile-info">
<div class="tile-name">Lachs mit Kartoffeln</div>
<div class="tile-meta">30 Min · einfach</div>
<div class="tile-tags"><span class="tag">Fisch</span></div>
</div>
</div>
</div>
<!-- Empty with suggestions -->
<div class="specimen">
<div class="specimen-label">Leer — Vorschläge</div>
<div class="tile-empty" style="width:140px;height:220px;">
<div class="empty-head">
<span class="empty-day-abbr">Sa</span>
<span class="empty-day-num">12</span>
</div>
<div class="empty-cta">
<div class="empty-plus">+</div>
<div class="empty-label">Gericht wählen</div>
</div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row">
<span class="sug-name">Ramen mit Ei</span>
<span class="sug-tag st-g">Neu</span>
</div>
<div class="sug-row">
<span class="sug-name">Shakshuka</span>
<span class="sug-tag st-g">Kein Overlap</span>
</div>
<div class="sug-row">
<span class="sug-name">Tacos</span>
<span class="sug-tag st-y">Leicht</span>
</div>
<div class="sug-more">Alle →</div>
</div>
</div>
</div>
<!-- Empty selected -->
<div class="specimen">
<div class="specimen-label">Leer — Ausgewählt</div>
<div class="tile-empty tile-empty-sel" style="width:140px;height:220px;">
<div class="empty-head">
<span class="empty-day-abbr" style="color:var(--gd);">Sa</span>
<span class="empty-day-num" style="color:var(--gd);">12</span>
</div>
<div class="empty-cta" style="border-bottom-color:var(--gl);">
<div class="empty-plus" style="color:var(--gl);">+</div>
<div class="empty-label empty-sel-label">Gericht wählen</div>
<div class="empty-arr"></div>
</div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row">
<span class="sug-name">Ramen mit Ei</span>
<span class="sug-tag st-g">Neu</span>
</div>
<div class="sug-row">
<span class="sug-name">Shakshuka</span>
<span class="sug-tag st-g">Kein Overlap</span>
</div>
<div class="sug-row">
<span class="sug-name">Tacos</span>
<span class="sug-tag st-y">Leicht</span>
</div>
<div class="sug-more">Alle →</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Gradient-Overlay:</strong> Dunkel oben (30% → 0%) für den Tages-Header, dunkel unten (0% → 55%) für den Text.
Der mittlere Bereich ist transparent — das Bild ist klar zu sehen.
Text-Shadow auf dem Rezeptnamen sichert Lesbarkeit auch bei hellen Bildern.
Tags nutzen <code>rgba(255,255,255,.2)</code> als Glasmorphismus-Hintergrund.
Zustands-Borders werden per <code>box-shadow</code> umgesetzt (kein Layout-Shift durch border:2px).
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 1 — VOLLSTÄNDIGE SEITENANSICHT -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="block-label">
<span class="bl-num">01</span>
<span class="bl-name">Kein Tag ausgewählt — volle Seite</span>
<span class="bl-when">Kacheln füllen die Viewport-Höhe komplett</span>
</div>
<div class="frame" style="height:560px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar pb-y"><div class="pb-fill pb-fg-y" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a class="sb-link">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<div class="main" style="padding:12px;">
<!-- Grid fills full height of main -->
<div class="grid7" style="height:100%;">
<div class="tile img-curry" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Mo</span><span class="tile-day-num">7</span></div>
<div class="tile-info">
<div class="tile-name">Hähnchen-Curry</div>
<div class="tile-meta">35 Min · mittel</div>
<div class="tile-tags"><span class="tag">Hähnchen</span><span class="tag">4 Port.</span></div>
</div>
</div>
<div class="tile tile-today img-pasta" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Di</span><span class="tile-day-num dn-today">8</span></div>
<div class="tile-info">
<div class="tile-name">Pasta Bolognese</div>
<div class="tile-meta">45 Min · mittel</div>
<div class="tile-tags"><span class="tag tag-today">Rind</span><span class="tag tag-today">Heute</span></div>
</div>
</div>
<div class="tile img-stirfry" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Mi</span><span class="tile-day-num">9</span></div>
<div class="tile-info">
<div class="tile-name">Gemüse-Stir-fry</div>
<div class="tile-meta">20 Min · einfach</div>
<div class="tile-tags"><span class="tag">Tofu</span><span class="tag">2 Port.</span></div>
</div>
</div>
<div class="tile img-lachs" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Do</span><span class="tile-day-num">10</span></div>
<div class="tile-info">
<div class="tile-name">Lachs mit Kartoffeln</div>
<div class="tile-meta">30 Min · einfach</div>
<div class="tile-tags"><span class="tag">Fisch</span><span class="tag">2 Port.</span></div>
</div>
</div>
<div class="tile img-pizza" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Fr</span><span class="tile-day-num">11</span></div>
<div class="tile-info">
<div class="tile-name">Pizza Margherita</div>
<div class="tile-meta">50 Min · aufwändig</div>
<div class="tile-tags"><span class="tag">vegetarisch</span><span class="tag">4 Port.</span></div>
</div>
</div>
<div class="tile-empty" style="height:100%;">
<div class="empty-head"><span class="empty-day-abbr">Sa</span><span class="empty-day-num">12</span></div>
<div class="empty-cta">
<div class="empty-plus">+</div>
<div class="empty-label">Gericht wählen</div>
</div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row"><span class="sug-name">Ramen mit Ei</span><span class="sug-tag st-g">Neues Protein</span></div>
<div class="sug-row"><span class="sug-name">Shakshuka</span><span class="sug-tag st-g">Kein Overlap</span></div>
<div class="sug-row"><span class="sug-name">Tacos</span><span class="sug-tag st-y">Aufwand: leicht</span></div>
<div class="sug-more">Alle Rezepte →</div>
</div>
</div>
<div class="tile-empty" style="height:100%;">
<div class="empty-head"><span class="empty-day-abbr">So</span><span class="empty-day-num">13</span></div>
<div class="empty-cta">
<div class="empty-plus">+</div>
<div class="empty-label">Gericht wählen</div>
</div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row"><span class="sug-name">Pho Bo</span><span class="sug-tag st-g">Neues Protein</span></div>
<div class="sug-row"><span class="sug-name">Avocado-Bowl</span><span class="sug-tag st-g">Kein Overlap</span></div>
<div class="sug-row"><span class="sug-name">Kürbissuppe</span><span class="sug-tag st-g">Kein Overlap</span></div>
<div class="sug-more">Alle Rezepte →</div>
</div>
</div>
</div>
</div>
<div class="rp">
<div class="today-card">
<div class="tc-lbl">Heute Abend</div>
<div class="tc-name">Pasta Bolognese</div>
<div class="tc-meta">Dienstag · 45 Min · mittel</div>
<button class="rp-btn rp-pri" style="margin-top:8px;padding:6px;font-size:11px;">Koch-Modus starten</button>
</div>
<div class="hr"></div>
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:6px;padding:16px;">
<div style="font-family:var(--fs);font-size:12px;color:var(--text);">Tag auswählen</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--muted);max-width:152px;line-height:1.5;">Klicke eine Kachel um Details zu sehen oder ein Gericht zu planen</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Kein leerer Bereich.</strong> Das Grid nimmt die volle Höhe ein (<code>height:100%</code> auf Kacheln und Grid).
Gefüllte Kacheln: Bild von oben bis unten, Text per Overlay am unteren Rand.
Leere Kacheln: dieselbe Höhe, oben "+ Gericht wählen", darunter die Vorschlagsliste.
Da MoFr alle Bilder haben, entsteht ein visuell abwechslungsreicher Kalender ohne Blank Space.
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 2 — REZEPT AUSGEWÄHLT (Mi), EXPANSION UNTEN -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="block-label">
<span class="bl-num">02</span>
<span class="bl-name">Tag mit Rezept angeklickt</span>
<span class="bl-when">Mi — Expansion öffnet sich unter dem Grid</span>
</div>
<div class="frame" style="height:680px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar pb-y"><div class="pb-fill pb-fg-y" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a class="sb-link">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
</div>
</div>
<div class="main">
<!-- Tiles: shorter when expansion open -->
<div class="grid7" style="height:290px;">
<div class="tile tile-faded img-curry" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Mo</span><span class="tile-day-num">7</span></div>
<div class="tile-info"><div class="tile-name">Hähnchen-Curry</div><div class="tile-meta">35 Min</div></div>
</div>
<div class="tile tile-today tile-faded img-pasta" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Di</span><span class="tile-day-num dn-today">8</span></div>
<div class="tile-info"><div class="tile-name">Pasta Bolognese</div><div class="tile-meta">45 Min</div></div>
</div>
<!-- Mi selected -->
<div class="tile tile-sel img-stirfry" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr" style="color:rgba(255,255,255,.9);">Mi</span><span class="tile-day-num dn-sel">9</span></div>
<div class="tile-info">
<div class="tile-name">Gemüse-Stir-fry</div>
<div class="tile-meta">20 Min · einfach</div>
<div class="tile-tags"><span class="tag tag-sel">Tofu</span></div>
<div style="text-align:center;font-size:9px;color:rgba(255,255,255,.7);margin-top:4px;"></div>
</div>
</div>
<div class="tile tile-faded img-lachs" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Do</span><span class="tile-day-num">10</span></div>
<div class="tile-info"><div class="tile-name">Lachs mit Kartoffeln</div><div class="tile-meta">30 Min</div></div>
</div>
<div class="tile tile-faded img-pizza" style="height:100%;">
<div class="tile-overlay"></div>
<div class="tile-head"><span class="tile-day-abbr">Fr</span><span class="tile-day-num">11</span></div>
<div class="tile-info"><div class="tile-name">Pizza Margherita</div><div class="tile-meta">50 Min</div></div>
</div>
<div class="tile-empty tile-empty-faded" style="height:100%;">
<div class="empty-head"><span class="empty-day-abbr">Sa</span><span class="empty-day-num">12</span></div>
<div class="empty-cta"><div class="empty-plus">+</div></div>
</div>
<div class="tile-empty tile-empty-faded" style="height:100%;">
<div class="empty-head"><span class="empty-day-abbr">So</span><span class="empty-day-num">13</span></div>
<div class="empty-cta"><div class="empty-plus">+</div></div>
</div>
</div>
<!-- Expansion -->
<div class="expand-wrap">
<div class="exp-arrows">
<div></div><div></div>
<div class="exp-arr"><div class="arr arr-fill"></div></div>
<div></div><div></div><div></div><div></div>
</div>
<div class="expand">
<div class="exp-left">
<div class="exp-ctx">Mittwoch, 9. Apr · Abendessen</div>
<div class="exp-name">Gemüse-Stir-fry</div>
<div class="exp-meta">20 Min · einfach · 2 Portionen</div>
<div class="ing-wrap">
<span class="ing">Tofu</span><span class="ing">Paprika</span><span class="ing">Brokkoli</span>
<span class="ing">Karotten</span><span class="ing">Zucchini</span><span class="ing">Ingwer</span>
<span class="ing-s">Sesamöl</span><span class="ing-s">Sojasauce</span><span class="ing-s">Knoblauch</span>
</div>
<div class="exp-bdgs">
<span class="ebdg ebdg-e">einfach</span>
<span class="ebdg ebdg-p">Protein: Tofu</span>
<span class="ebdg ebdg-sc">Score ▲ +0.4</span>
</div>
</div>
<div class="exp-right">
<button class="exp-btn exp-pri">Koch-Modus</button>
<button class="exp-btn">Rezept ansehen</button>
<button class="exp-btn">Gericht tauschen</button>
<button class="exp-btn exp-err">Entfernen</button>
</div>
</div>
</div>
</div>
<div class="rp">
<div class="rp-lbl">Mittwoch, 9. Apr</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--muted);margin-bottom:10px;">Wie wirkt dieses Gericht?</div>
<div style="display:flex;align-items:baseline;gap:4px;margin-bottom:4px;">
<span style="font-family:var(--fd);font-size:22px;font-weight:300;">7.8</span>
<span style="font-family:var(--fs);font-size:11px;color:var(--muted);">/10</span>
<span style="font-family:var(--fs);font-size:11px;color:var(--gd);font-weight:500;margin-left:6px;">▲ +0.4</span>
</div>
<div class="pbar pb-t" style="margin-bottom:12px;"><div class="pb-fill pb-fg-g" style="width:78%;"></div></div>
<div class="hr"></div>
<div style="display:flex;flex-direction:column;gap:6px;">
<div style="font-family:var(--fs);font-size:11px;color:var(--gd);">✓ Kein Protein-Overlap</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--gd);">✓ Neue Zutaten</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--yx);">~ Tofu zum 2. Mal</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Beim Klick auf eine gefüllte Kachel</strong> werden alle anderen auf 42% Deckkraft gedimmt
(gefüllte <em>und</em> leere). Die Tiles bleiben in ihrer Höhe, die Expansion wächst darunter.
Da das Bild jetzt die volle Kachelhöhe einnimmt, wirkt das Dimmen als echter Fokus-Effekt —
wie eine Lupe auf das ausgewählte Gericht.
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,847 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planner — Tall Tiles</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@300;400&family=DM+Sans:wght@400;500;600&family=DM+Mono&display=swap" rel="stylesheet">
<style>
:root {
--page: #fafaf7;
--surface: #f5f4ee;
--subtle: #edecea;
--border: #d8d7d0;
--text: #1c1c18;
--muted: #6b6a63;
--gt: #e8f5ea; --gl: #aedcb0; --g: #3d8c4a; --gd: #2e6e39;
--yt: #fdf6d8; --yl: #f9e08a; --y: #f2c12e; --yx: #8a6800;
--pt: #eeedfe; --p: #534ab7;
--ot: #fef0e6; --o: #e8862a; --od: #b46820;
--err: #dc4c3e;
--r-sm: 4px; --r-md: 6px; --r-lg: 10px; --r-xl: 16px; --r-full: 9999px;
--sh-card: 0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);
--sh-raised: 0 4px 12px rgba(28,28,24,.10),0 2px 4px rgba(28,28,24,.06);
--fd: 'Fraunces', Georgia, serif;
--fs: 'DM Sans', system-ui, sans-serif;
--fm: 'DM Mono', monospace;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--fs);background:#dddcd7;color:var(--text);padding:40px 24px 80px;line-height:1.4;}
.eyebrow{font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
.title{font-family:var(--fd);font-size:34px;font-weight:300;margin-bottom:6px;}
.sub{font-family:var(--fs);font-size:14px;color:var(--muted);max-width:700px;line-height:1.65;margin-bottom:44px;}
.block{margin-bottom:60px;}
.block-label{display:flex;align-items:baseline;gap:10px;margin-bottom:12px;}
.bl-num{font-family:var(--fm);font-size:11px;background:var(--subtle);color:var(--muted);padding:3px 8px;border-radius:var(--r-sm);}
.bl-name{font-family:var(--fd);font-size:22px;font-weight:300;}
.bl-when{font-family:var(--fs);font-size:12px;color:var(--muted);margin-left:auto;}
.note{font-family:var(--fs);font-size:12px;color:var(--muted);border-left:3px solid var(--border);padding:10px 14px;margin-top:14px;line-height:1.6;}
.note strong{color:var(--text);font-weight:500;}
/* Frame */
.frame{display:flex;flex-direction:column;background:var(--page);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--sh-raised);}
.tb{display:flex;align-items:center;gap:7px;padding:11px 18px;border-bottom:1px solid var(--border);background:var(--page);flex-shrink:0;}
.tb-h1{font-family:var(--fd);font-size:17px;font-weight:300;}
.tb-range{font-family:var(--fs);font-size:11px;color:var(--muted);}
.tb-arr{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--r-md);font-size:13px;color:var(--muted);}
.tb-btn{height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--r-md);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;color:var(--text);background:var(--page);}
.tb-ml{margin-left:auto;}
.tb-pri{background:var(--gd);color:#fff;border:none;}
.body{display:flex;flex:1;overflow:hidden;}
/* Sidebar */
.sb{width:184px;flex-shrink:0;border-right:1px solid var(--border);background:var(--surface);padding:13px;display:flex;flex-direction:column;gap:13px;}
.sb-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:5px;}
.score-box{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;}
.sc-big{font-family:var(--fd);font-size:27px;font-weight:300;line-height:1;}
.sc-den{font-family:var(--fs);font-size:11px;color:var(--muted);}
.pbar{height:4px;border-radius:var(--r-full);background:var(--yl);overflow:hidden;margin-top:6px;}
.pb-fill{height:100%;border-radius:var(--r-full);background:var(--y);}
.pbg{background:var(--g);}
.sr{display:flex;align-items:center;gap:6px;margin-top:6px;}
.sr-l{font-family:var(--fs);font-size:10px;color:var(--muted);width:68px;flex-shrink:0;}
.sr-b{flex:1;height:3px;border-radius:var(--r-full);background:var(--border);overflow:hidden;}
.sr-f{height:100%;border-radius:var(--r-full);}
.sr-v{font-family:var(--fm);font-size:9px;color:var(--muted);width:18px;text-align:right;}
.w-item{font-family:var(--fs);font-size:10px;color:var(--yx);margin-top:4px;line-height:1.4;}
.dp{display:flex;gap:2px;margin-top:5px;}
.dp-s{flex:1;height:4px;border-radius:var(--r-full);}
.sb-link{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);display:block;margin-top:8px;}
/* Right panel */
.rp{width:228px;flex-shrink:0;border-left:1px solid var(--border);background:var(--page);padding:13px;display:flex;flex-direction:column;overflow-y:auto;}
.rp-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:7px;}
.rp-name{font-family:var(--fd);font-size:16px;font-weight:300;line-height:1.35;}
.rp-meta{font-family:var(--fs);font-size:11px;color:var(--muted);margin-top:3px;}
.rp-btn{display:block;width:100%;padding:7px;border-radius:var(--r-md);border:1px solid var(--border);background:var(--page);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;text-align:center;color:var(--text);margin-top:5px;}
.rp-pri{background:var(--gd);color:#fff;border:none;}
.rp-err{color:var(--err);border-color:var(--err);background:transparent;}
.hr{height:1px;background:var(--border);margin:10px 0;}
.picker-search{display:flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:6px 9px;margin-bottom:9px;}
.pick-item{display:flex;align-items:center;gap:7px;padding:7px 0;border-bottom:1px solid var(--subtle);cursor:pointer;}
.pick-item:last-child{border-bottom:none;}
.pick-name{font-family:var(--fd);font-size:12px;font-weight:300;flex:1;line-height:1.3;}
.pick-meta{font-family:var(--fs);font-size:10px;color:var(--muted);}
.pick-top{font-family:var(--fm);font-size:9px;background:var(--gt);color:var(--gd);padding:2px 5px;border-radius:2px;}
/* Main */
.main{flex:1;overflow-y:auto;padding:13px;}
/* ── TALL TILE SYSTEM ────────────────────────── */
.grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:7px;}
/* Filled tile */
.tile{
border-radius:var(--r-lg);
border:1px solid var(--border);
background:var(--surface);
cursor:pointer;
overflow:hidden;
box-shadow:var(--sh-card);
display:flex;
flex-direction:column;
transition:box-shadow .12s, border-color .12s;
}
.tile:hover{box-shadow:var(--sh-raised);}
.tile-today{border:2px solid var(--y);}
.tile-sel{border:2px solid var(--g);}
.tile-faded{opacity:.45;}
/* Image area (top of tile) */
.tile-img{
width:100%;
flex-shrink:0;
border-radius:0;
position:relative;
overflow:hidden;
}
/* Overlay gradient so text on image is readable if needed */
.tile-img::after{
content:'';
position:absolute;
inset:0;
background:linear-gradient(to bottom, transparent 40%, rgba(28,28,24,.18) 100%);
}
/* Day header (inside tile, above image or overlaid) */
.tile-head{
display:flex;
align-items:center;
justify-content:space-between;
padding:7px 8px 0;
flex-shrink:0;
}
.tile-day-abbr{font-family:var(--fs);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);}
.tile-day-num{
width:20px;height:20px;border-radius:var(--r-full);
display:flex;align-items:center;justify-content:center;
font-family:var(--fs);font-size:10px;font-weight:500;
color:var(--text);
}
.dn-today{background:var(--y);color:#fff;}
.dn-sel{background:var(--gd);color:#fff;}
/* Tile body text */
.tile-body{padding:7px 8px 8px;display:flex;flex-direction:column;flex:1;}
.tile-name{font-family:var(--fd);font-size:13px;font-weight:300;line-height:1.35;color:var(--text);}
.tile-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;}
.tile-tags{display:flex;gap:3px;flex-wrap:wrap;margin-top:6px;}
.tag{font-family:var(--fs);font-size:8px;font-weight:500;padding:2px 5px;border-radius:2px;}
.tag-e{background:var(--gt);color:var(--gd);}
.tag-m{background:var(--yt);color:var(--yx);}
.tag-h{background:var(--ot);color:var(--od);}
.tag-p{background:var(--pt);color:var(--p);}
.tile-serves{font-family:var(--fs);font-size:9px;color:var(--muted);margin-top:auto;padding-top:5px;}
/* Empty tile */
.tile-empty{
border-radius:var(--r-lg);
border:1px dashed var(--border);
background:transparent;
cursor:pointer;
display:flex;
flex-direction:column;
overflow:hidden;
}
.tile-empty-sel{border:2px dashed var(--g);background:rgba(232,245,234,.35);}
.tile-empty-head{
display:flex;align-items:center;justify-content:space-between;
padding:7px 8px 0;flex-shrink:0;
}
.tile-empty-body{
display:flex;flex-direction:column;
align-items:center;justify-content:center;
padding:8px 6px 4px;
gap:3px;
flex-shrink:0;
border-bottom:1px solid var(--border);
margin-bottom:0;
}
.tile-empty-plus{font-size:20px;color:var(--border);}
.tile-empty-label{font-family:var(--fs);font-size:9px;color:var(--muted);}
/* Inline suggestions inside empty tile */
.tile-sug-list{
display:flex;flex-direction:column;
padding:6px 7px;
gap:0;
flex:1;
overflow:hidden;
}
.tile-sug-hd{
font-family:var(--fs);font-size:8px;font-weight:500;
letter-spacing:.07em;text-transform:uppercase;
color:var(--muted);
padding:3px 0 5px;
border-bottom:1px solid var(--subtle);
margin-bottom:3px;
}
.tile-sug-item{
display:flex;align-items:center;gap:5px;
padding:5px 0;
border-bottom:1px solid var(--subtle);
cursor:pointer;
}
.tile-sug-item:last-child{border-bottom:none;}
.tile-sug-item:hover .tile-sug-name{color:var(--gd);}
.tile-sug-name{font-family:var(--fd);font-size:11px;font-weight:300;color:var(--text);flex:1;line-height:1.2;}
.tile-sug-tag{font-family:var(--fs);font-size:8px;font-weight:500;padding:1px 4px;border-radius:2px;white-space:nowrap;flex-shrink:0;}
.ts-green{background:var(--gt);color:var(--gd);}
.ts-yellow{background:var(--yt);color:var(--yx);}
.tile-sug-more{
font-family:var(--fs);font-size:9px;font-weight:500;
color:var(--yx);text-align:center;
padding:5px 0 2px;
margin-top:auto;
}
/* Expansion below grid */
.expand-wrap{position:relative;margin-top:3px;}
.exp-arrows{
display:grid;grid-template-columns:repeat(7,1fr);gap:7px;
position:absolute;top:-8px;left:0;right:0;pointer-events:none;
}
.exp-arr{display:flex;justify-content:center;}
.arr-shape{width:12px;height:12px;border-left:2px solid var(--g);border-top:2px solid var(--g);transform:rotate(45deg);}
.arr-bg-filled{background:var(--gt);}
.arr-bg-empty{background:rgba(232,245,234,.35);}
.expand{
border:2px solid var(--g);border-radius:var(--r-lg);
background:var(--gt);padding:14px;
display:flex;gap:14px;
}
.exp-left{flex:1;}
.exp-ctx{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--gd);margin-bottom:4px;}
.exp-name{font-family:var(--fd);font-size:22px;font-weight:300;line-height:1.25;}
.exp-meta{font-family:var(--fs);font-size:12px;color:var(--muted);margin-top:4px;}
.ing-wrap{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px;}
.ing{font-family:var(--fs);font-size:10px;background:#fff;border:1px solid var(--border);border-radius:var(--r-full);padding:2px 8px;color:var(--text);}
.ing-s{background:var(--subtle);border-color:var(--subtle);color:var(--muted);}
.exp-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px;}
.exp-right{display:flex;flex-direction:column;gap:5px;width:116px;flex-shrink:0;}
.exp-btn{padding:7px;border-radius:var(--r-md);border:1px solid var(--border);background:#fff;font-family:var(--fs);font-size:11px;font-weight:500;text-align:center;cursor:pointer;letter-spacing:.04em;}
.exp-pri{background:var(--gd);color:#fff;border:none;}
.exp-err{color:var(--err);border-color:var(--err);background:transparent;}
/* Right panel idle */
.rp-today{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;margin-bottom:10px;}
.rtc-lbl{font-family:var(--fs);font-size:9px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--yx);margin-bottom:4px;}
.rtc-name{font-family:var(--fd);font-size:14px;font-weight:300;line-height:1.3;}
.rtc-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:2px;}
</style>
</head>
<body>
<p class="eyebrow">Mealplan · Planer · Tall Tiles</p>
<h1 class="title">Hohe Kacheln — drei Zustände</h1>
<p class="sub">
Kein separater Agenda-Bereich. Die Kacheln selbst sind die Informationsschicht:
Bild-Placeholder oben, Rezeptname, Metadaten, Tags. Leere Kacheln nutzen die Höhe
für Vorschläge direkt inline. Unter dem Grid erscheint nur noch die Expansion beim Klick.
</p>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 1 — KEIN TAG AUSGEWÄHLT -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="block-label">
<span class="bl-num">01</span>
<span class="bl-name">Kein Tag ausgewählt</span>
<span class="bl-when">Standard beim Laden der Seite</span>
</div>
<div class="frame" style="height:580px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<!-- Sidebar -->
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar"><div class="pb-fill" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a class="sb-link">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<!-- MAIN -->
<div class="main" style="padding:13px;">
<div class="grid7" style="height:calc(100% - 0px);">
<!-- Mo: Hähnchen-Curry — warm amber image -->
<div class="tile" style="height:100%;">
<div class="tile-head">
<span class="tile-day-abbr">Mo</span>
<span class="tile-day-num">7</span>
</div>
<div class="tile-img" style="height:90px;background:linear-gradient(135deg,#e8c88a 0%,#c9a05a 100%);flex-shrink:0;">
<!-- hero image placeholder — warm/spice tones for curry -->
<div style="position:absolute;inset:0;display:flex;align-items:flex-end;padding:5px 7px;">
</div>
</div>
<div class="tile-body">
<div class="tile-name">Hähnchen-Curry</div>
<div class="tile-meta">35 Min</div>
<div class="tile-tags">
<span class="tag tag-m">mittel</span>
<span class="tag tag-p">Hähnchen</span>
</div>
<div class="tile-serves">4 Portionen</div>
</div>
</div>
<!-- Di: Pasta Bolognese — rich red (TODAY) -->
<div class="tile tile-today" style="height:100%;">
<div class="tile-head">
<span class="tile-day-abbr" style="color:var(--yx);">Di</span>
<span class="tile-day-num dn-today">8</span>
</div>
<div class="tile-img" style="height:90px;background:linear-gradient(135deg,#c96060 0%,#8b2828 100%);flex-shrink:0;">
</div>
<div class="tile-body" style="background:var(--yt);">
<div class="tile-name">Pasta Bolognese</div>
<div class="tile-meta">45 Min</div>
<div class="tile-tags">
<span class="tag tag-m">mittel</span>
<span class="tag tag-p">Rind</span>
</div>
<div class="tile-serves">4 Portionen</div>
</div>
</div>
<!-- Mi: Gemüse-Stir-fry — fresh green -->
<div class="tile" style="height:100%;">
<div class="tile-head">
<span class="tile-day-abbr">Mi</span>
<span class="tile-day-num">9</span>
</div>
<div class="tile-img" style="height:90px;background:linear-gradient(135deg,#7bbf7e 0%,#3d7a42 100%);flex-shrink:0;">
</div>
<div class="tile-body">
<div class="tile-name">Gemüse-Stir-fry</div>
<div class="tile-meta">20 Min</div>
<div class="tile-tags">
<span class="tag tag-e">einfach</span>
<span class="tag tag-p">Tofu</span>
</div>
<div class="tile-serves">2 Portionen</div>
</div>
</div>
<!-- Do: Lachs — blue-teal -->
<div class="tile" style="height:100%;">
<div class="tile-head">
<span class="tile-day-abbr">Do</span>
<span class="tile-day-num">10</span>
</div>
<div class="tile-img" style="height:90px;background:linear-gradient(135deg,#6baed6 0%,#2171b5 100%);flex-shrink:0;">
</div>
<div class="tile-body">
<div class="tile-name">Lachs mit Kartoffeln</div>
<div class="tile-meta">30 Min</div>
<div class="tile-tags">
<span class="tag tag-e">einfach</span>
<span class="tag tag-p">Fisch</span>
</div>
<div class="tile-serves">2 Portionen</div>
</div>
</div>
<!-- Fr: Pizza — warm cream/yellow -->
<div class="tile" style="height:100%;">
<div class="tile-head">
<span class="tile-day-abbr">Fr</span>
<span class="tile-day-num">11</span>
</div>
<div class="tile-img" style="height:90px;background:linear-gradient(135deg,#f0d080 0%,#c8960a 100%);flex-shrink:0;">
</div>
<div class="tile-body">
<div class="tile-name">Pizza Margherita</div>
<div class="tile-meta">50 Min</div>
<div class="tile-tags">
<span class="tag tag-h">aufwändig</span>
<span class="tag tag-p">vegetarisch</span>
</div>
<div class="tile-serves">4 Portionen</div>
</div>
</div>
<!-- Sa: EMPTY — inline suggestions -->
<div class="tile-empty" style="height:100%;">
<div class="tile-empty-head">
<span class="tile-day-abbr">Sa</span>
<span class="tile-day-num" style="color:var(--muted);">12</span>
</div>
<div class="tile-empty-body">
<div class="tile-empty-plus">+</div>
<div class="tile-empty-label">Gericht wählen</div>
</div>
<div class="tile-sug-list">
<div class="tile-sug-hd">Vorschläge</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Ramen mit Ei</span>
<span class="tile-sug-tag ts-green">Neues Protein</span>
</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Shakshuka</span>
<span class="tile-sug-tag ts-green">Kein Overlap</span>
</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Tacos</span>
<span class="tile-sug-tag ts-yellow">Aufwand: leicht</span>
</div>
<div class="tile-sug-more">Alle Rezepte →</div>
</div>
</div>
<!-- So: EMPTY — inline suggestions -->
<div class="tile-empty" style="height:100%;">
<div class="tile-empty-head">
<span class="tile-day-abbr">So</span>
<span class="tile-day-num" style="color:var(--muted);">13</span>
</div>
<div class="tile-empty-body">
<div class="tile-empty-plus">+</div>
<div class="tile-empty-label">Gericht wählen</div>
</div>
<div class="tile-sug-list">
<div class="tile-sug-hd">Vorschläge</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Pho Bo</span>
<span class="tile-sug-tag ts-green">Neues Protein</span>
</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Avocado-Bowl</span>
<span class="tile-sug-tag ts-green">Kein Overlap</span>
</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Kürbissuppe</span>
<span class="tile-sug-tag ts-green">Kein Overlap</span>
</div>
<div class="tile-sug-more">Alle Rezepte →</div>
</div>
</div>
</div><!-- /grid7 -->
</div><!-- /main -->
<!-- Right panel: idle + heute -->
<div class="rp">
<div class="rp-today">
<div class="rtc-lbl">Heute Abend</div>
<div class="rtc-name">Pasta Bolognese</div>
<div class="rtc-meta">Dienstag · 45 Min · mittel</div>
<button class="rp-btn rp-pri" style="margin-top:8px;padding:6px;font-size:11px;">Koch-Modus starten</button>
</div>
<div class="hr"></div>
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:6px;padding:16px;">
<div style="font-family:var(--fs);font-size:12px;color:var(--text);">Tag auswählen</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--muted);max-width:152px;line-height:1.5;">Klicke eine Kachel um Details zu sehen oder ein Gericht zu planen</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Kein Agenda-Bereich.</strong> Die Kacheln füllen die volle Höhe des Hauptbereichs.
Geplante Tage zeigen: farbiges Bild-Placeholder (wird durch <code>heroImageUrl</code> ersetzt),
Rezeptname, Kochzeit, Effort-Badge, Protein-Tag, Portionenanzahl.
Leere Kacheln (Sa, So) zeigen direkt 3 Vorschläge mit Begründungs-Tags — kein Scrollen nötig.
Klick auf einen Vorschlag-Eintrag → Rezept wird direkt eingetragen.
Klick auf die Kachel selbst → öffnet Expansion unten (Zustand 3).
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 2 — TAG MIT REZEPT ANGEKLICKT (Mi) -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="block-label">
<span class="bl-num">02</span>
<span class="bl-name">Tag mit Rezept angeklickt</span>
<span class="bl-when">Klick auf Mi — Gemüse-Stir-fry</span>
</div>
<div class="frame" style="height:700px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar"><div class="pb-fill" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a class="sb-link">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<div class="main">
<!-- Tiles: Mi selected, others faded -->
<div class="grid7" style="height:320px;">
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Mo</span><span class="tile-day-num">7</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#e8c88a,#c9a05a);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Hähnchen-Curry</div><div class="tile-meta">35 Min</div></div>
</div>
<div class="tile tile-today tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr" style="color:var(--yx);">Di</span><span class="tile-day-num dn-today">8</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#c96060,#8b2828);flex-shrink:0;"></div>
<div class="tile-body" style="background:var(--yt);"><div class="tile-name">Pasta Bolognese</div><div class="tile-meta">45 Min</div></div>
</div>
<!-- Mi: SELECTED — full opacity, green border -->
<div class="tile tile-sel" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr" style="color:var(--gd);">Mi</span><span class="tile-day-num dn-sel">9</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#7bbf7e,#3d7a42);flex-shrink:0;"></div>
<div class="tile-body" style="background:var(--gt);">
<div class="tile-name">Gemüse-Stir-fry</div>
<div class="tile-meta">20 Min</div>
<div class="tile-tags"><span class="tag tag-e">einfach</span><span class="tag tag-p">Tofu</span></div>
<div style="text-align:center;font-size:9px;color:var(--gd);margin-top:auto;padding-top:4px;"></div>
</div>
</div>
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Do</span><span class="tile-day-num">10</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#6baed6,#2171b5);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Lachs mit Kartoffeln</div><div class="tile-meta">30 Min</div></div>
</div>
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Fr</span><span class="tile-day-num">11</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#f0d080,#c8960a);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Pizza Margherita</div><div class="tile-meta">50 Min</div></div>
</div>
<div class="tile-empty tile-faded" style="height:100%;opacity:.3;">
<div class="tile-empty-head"><span class="tile-day-abbr">Sa</span><span class="tile-day-num">12</span></div>
<div class="tile-empty-body"><div class="tile-empty-plus">+</div></div>
</div>
<div class="tile-empty tile-faded" style="height:100%;opacity:.3;">
<div class="tile-empty-head"><span class="tile-day-abbr">So</span><span class="tile-day-num">13</span></div>
<div class="tile-empty-body"><div class="tile-empty-plus">+</div></div>
</div>
</div>
<!-- EXPANSION below grid -->
<div class="expand-wrap">
<div class="exp-arrows">
<div></div><div></div>
<div class="exp-arr"><div class="arr-shape arr-bg-filled"></div></div>
<div></div><div></div><div></div><div></div>
</div>
<div class="expand">
<div class="exp-left">
<div class="exp-ctx">Mittwoch, 9. Apr · Abendessen</div>
<div class="exp-name">Gemüse-Stir-fry</div>
<div class="exp-meta">20 Min · einfach · 2 Portionen</div>
<div class="ing-wrap">
<span class="ing">Tofu</span>
<span class="ing">Paprika</span>
<span class="ing">Brokkoli</span>
<span class="ing">Karotten</span>
<span class="ing">Zucchini</span>
<span class="ing">Ingwer</span>
<span class="ing-s">Sesamöl</span>
<span class="ing-s">Sojasauce</span>
<span class="ing-s">Knoblauch</span>
<span class="ing-s">Salz, Pfeffer</span>
</div>
<div class="exp-badges">
<span class="tag tag-e" style="font-size:10px;padding:3px 7px;">einfach</span>
<span style="font-family:var(--fs);font-size:10px;font-weight:500;padding:3px 7px;border-radius:2px;background:var(--pt);color:var(--p);">Protein: Tofu</span>
<span style="font-family:var(--fs);font-size:10px;font-weight:500;padding:3px 7px;border-radius:2px;background:var(--gt);color:var(--gd);">Score ▲ +0.4</span>
</div>
</div>
<div class="exp-right">
<button class="exp-btn exp-pri">Koch-Modus</button>
<button class="exp-btn">Rezept ansehen</button>
<button class="exp-btn">Gericht tauschen</button>
<button class="exp-btn exp-err">Entfernen</button>
</div>
</div>
</div>
</div>
<!-- Right: score context -->
<div class="rp">
<div class="rp-lbl">Mittwoch, 9. Apr</div>
<div style="font-family:var(--fs);font-size:11px;color:var(--muted);margin-bottom:10px;">Wie wirkt dieses Gericht auf die Woche?</div>
<div style="display:flex;align-items:baseline;gap:4px;margin-bottom:4px;">
<span style="font-family:var(--fd);font-size:22px;font-weight:300;">7.8</span>
<span style="font-family:var(--fs);font-size:11px;color:var(--muted);">/10</span>
<span style="font-family:var(--fs);font-size:11px;color:var(--gd);font-weight:500;margin-left:6px;">▲ +0.4</span>
</div>
<div class="pbar" style="background:var(--border);margin-bottom:12px;"><div class="pb-fill pbg" style="width:78%;"></div></div>
<div class="hr"></div>
<div style="font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);margin-bottom:8px;">Bewertung</div>
<div style="display:flex;flex-direction:column;gap:6px;">
<div style="display:flex;align-items:center;gap:6px;font-family:var(--fs);font-size:11px;color:var(--gd);">✓ Kein Protein-Overlap</div>
<div style="display:flex;align-items:center;gap:6px;font-family:var(--fs);font-size:11px;color:var(--gd);">✓ Neue Zutaten</div>
<div style="display:flex;align-items:center;gap:6px;font-family:var(--fs);font-size:11px;color:var(--yx);">~ Tofu zum 2. Mal</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Nach Klick auf Mittwoch:</strong> Nicht-ausgewählte Kacheln werden auf 45% Deckkraft gedimmt.
Die Expansion erscheint direkt unter dem Grid (Pfeil zeigt zu Mi).
Die Kacheln bleiben auf ihrer Höhe — der Expansion-Bereich wächst <em>zusätzlich</em> darunter.
Zutaten zeigen normale Zutaten als Pills; Grundzutaten (Sesamöl, Sojasauce…) gedimmt.
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ZUSTAND 3 — LEERER TAG ANGEKLICKT (Sa) -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="block-label">
<span class="bl-num">03</span>
<span class="bl-name">Leerer Tag angeklickt</span>
<span class="bl-when">Klick auf Sa — kein Gericht geplant</span>
</div>
<div class="frame" style="height:700px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn">Heute</button>
<button class="tb-btn tb-ml tb-pri">+ Gericht hinzufügen</button>
</div>
<div class="body">
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar"><div class="pb-fill" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a class="sb-link">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
</div>
<div class="main">
<div class="grid7" style="height:320px;">
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Mo</span><span class="tile-day-num">7</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#e8c88a,#c9a05a);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Hähnchen-Curry</div><div class="tile-meta">35 Min</div></div>
</div>
<div class="tile tile-today tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr" style="color:var(--yx);">Di</span><span class="tile-day-num dn-today">8</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#c96060,#8b2828);flex-shrink:0;"></div>
<div class="tile-body" style="background:var(--yt);"><div class="tile-name">Pasta Bolognese</div><div class="tile-meta">45 Min</div></div>
</div>
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Mi</span><span class="tile-day-num">9</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#7bbf7e,#3d7a42);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Gemüse-Stir-fry</div><div class="tile-meta">20 Min</div></div>
</div>
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Do</span><span class="tile-day-num">10</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#6baed6,#2171b5);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Lachs mit Kartoffeln</div><div class="tile-meta">30 Min</div></div>
</div>
<div class="tile tile-faded" style="height:100%;">
<div class="tile-head"><span class="tile-day-abbr">Fr</span><span class="tile-day-num">11</span></div>
<div class="tile-img" style="height:80px;background:linear-gradient(135deg,#f0d080,#c8960a);flex-shrink:0;"></div>
<div class="tile-body"><div class="tile-name">Pizza Margherita</div><div class="tile-meta">50 Min</div></div>
</div>
<!-- Sa: SELECTED EMPTY -->
<div class="tile-empty tile-empty-sel" style="height:100%;">
<div class="tile-empty-head">
<span class="tile-day-abbr" style="color:var(--gd);">Sa</span>
<span class="tile-day-num dn-sel">12</span>
</div>
<div class="tile-empty-body" style="border-bottom-color:var(--gl);">
<div class="tile-empty-plus" style="color:var(--gl);">+</div>
<div class="tile-empty-label" style="color:var(--gd);">Gericht wählen</div>
<div style="text-align:center;font-size:9px;color:var(--gd);margin-top:2px;"></div>
</div>
<div class="tile-sug-list">
<div class="tile-sug-hd">Vorschläge</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Ramen mit Ei</span>
<span class="tile-sug-tag ts-green">Neues Protein</span>
</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Shakshuka</span>
<span class="tile-sug-tag ts-green">Kein Overlap</span>
</div>
<div class="tile-sug-item">
<span class="tile-sug-name">Tacos</span>
<span class="tile-sug-tag ts-yellow">Aufwand: leicht</span>
</div>
<div class="tile-sug-more">Alle Rezepte →</div>
</div>
</div>
<div class="tile-empty tile-faded" style="height:100%;opacity:.3;">
<div class="tile-empty-head"><span class="tile-day-abbr">So</span><span class="tile-day-num">13</span></div>
<div class="tile-empty-body"><div class="tile-empty-plus">+</div></div>
</div>
</div>
<!-- EXPANSION: suggestions full width -->
<div class="expand-wrap">
<div class="exp-arrows">
<div></div><div></div><div></div><div></div><div></div>
<div class="exp-arr"><div class="arr-shape arr-bg-empty"></div></div>
<div></div>
</div>
<div class="expand" style="flex-direction:column;gap:10px;">
<div style="font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--gd);">Samstag, 12. Apr — Alle Vorschläge</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:7px;">
<div style="border:1px solid var(--gl);border-radius:var(--r-md);background:#fff;padding:10px;cursor:pointer;">
<div style="font-family:var(--fd);font-size:13px;font-weight:300;line-height:1.3;">Ramen mit Ei</div>
<div style="font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;">40 Min · mittel</div>
<span style="font-family:var(--fs);font-size:9px;font-weight:500;padding:2px 6px;border-radius:2px;background:var(--gt);color:var(--gd);margin-top:6px;display:inline-block;">Neues Protein</span>
</div>
<div style="border:1px solid var(--gl);border-radius:var(--r-md);background:#fff;padding:10px;cursor:pointer;">
<div style="font-family:var(--fd);font-size:13px;font-weight:300;line-height:1.3;">Shakshuka</div>
<div style="font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;">25 Min · einfach</div>
<span style="font-family:var(--fs);font-size:9px;font-weight:500;padding:2px 6px;border-radius:2px;background:var(--gt);color:var(--gd);margin-top:6px;display:inline-block;">Kein Overlap</span>
</div>
<div style="border:1px solid var(--gl);border-radius:var(--r-md);background:#fff;padding:10px;cursor:pointer;">
<div style="font-family:var(--fd);font-size:13px;font-weight:300;line-height:1.3;">Rindfleisch-Tacos</div>
<div style="font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;">30 Min · einfach</div>
<span style="font-family:var(--fs);font-size:9px;font-weight:500;padding:2px 6px;border-radius:2px;background:var(--yt);color:var(--yx);margin-top:6px;display:inline-block;">Gleiche Zutaten</span>
</div>
<div style="border:1px solid var(--gl);border-radius:var(--r-md);background:#fff;padding:10px;cursor:pointer;">
<div style="font-family:var(--fd);font-size:13px;font-weight:300;line-height:1.3;">Kürbissuppe</div>
<div style="font-family:var(--fs);font-size:10px;color:var(--muted);margin-top:3px;">35 Min · einfach</div>
<span style="font-family:var(--fs);font-size:9px;font-weight:500;padding:2px 6px;border-radius:2px;background:var(--gt);color:var(--gd);margin-top:6px;display:inline-block;">Kein Overlap</span>
</div>
</div>
</div>
</div>
</div>
<!-- Right: full recipe picker -->
<div class="rp">
<div class="rp-lbl">Samstag, 12. Apr</div>
<div class="picker-search">
<span style="font-size:12px;color:var(--muted);"></span>
<span style="font-family:var(--fs);font-size:11px;color:var(--muted);">Rezept suchen…</span>
</div>
<div style="overflow-y:auto;flex:1;">
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Ramen mit Ei</div><div class="pick-meta">40 Min · mittel</div></div><span class="pick-top">Top</span></div>
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Shakshuka</div><div class="pick-meta">25 Min · einfach</div></div><span class="pick-top">Top</span></div>
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Kürbissuppe</div><div class="pick-meta">35 Min · einfach</div></div></div>
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Tofu-Teriyaki</div><div class="pick-meta">30 Min · einfach</div></div></div>
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Gemüse-Curry</div><div class="pick-meta">40 Min · mittel</div></div></div>
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Linseneintopf</div><div class="pick-meta">50 Min · einfach</div></div></div>
<div class="pick-item"><div style="flex:1;"><div class="pick-name">Ofen-Lachs</div><div class="pick-meta">35 Min · einfach</div></div></div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Nach Klick auf leeren Samstag:</strong> Die Kachel selbst zeigt schon die 3 Inline-Vorschläge (sichtbar seit Zustand 1).
Der Pfeil-Indikator erscheint, die Expansion zeigt alle 4 Vorschläge nebeneinander als klickbare Karten.
Das rechte Panel öffnet gleichzeitig den vollständigen Rezept-Picker mit Suche — für alle anderen Optionen.
Klick auf eine Karte (main) oder Picker-Eintrag (rechts) trägt das Rezept ein und schließt die Expansion.
</div>
</div>
</body>
</html>