{
  "openapi": "3.1.1",
  "info": {
    "title": "Hearth API",
    "version": "1.0.0",
    "summary": "Deterministic tools for people and AI agents.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://hearth.usefulweb.site"
    }
  ],
  "paths": {
    "/api/v1/tools": {
      "get": {
        "operationId": "listTools",
        "summary": "Catalog of all tools",
        "responses": {
          "200": {
            "description": "Tool list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/{id}": {
      "get": {
        "operationId": "getTool",
        "summary": "Definition of one tool",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "weekly-meal-planner",
                "leftover-chef",
                "food-storage-guide",
                "household-chore-rotator",
                "chore-roulette",
                "household-maintenance-calendar",
                "disaster-prep-builder",
                "moving-box-planner",
                "will-it-fit",
                "room-layout-optimizer",
                "jet-lag-planner",
                "travel-connection-checker",
                "currency-converter",
                "travel-exchange-fee-calculator",
                "tip-bill-split-calculator",
                "movie-night-consensus-picker",
                "runtime-marathon-planner"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/weekly-meal-planner/calculate": {
      "post": {
        "operationId": "calculateWeeklyMealPlanner",
        "summary": "Turn your pantry and its expiry dates into a 7-day dinner plan and a shopping list.",
        "description": "List what is in your fridge and cupboards (with quantities and best-before dates) and your preferences (vegetarian, vegan, pescatarian, maximum cooking time, ingredients to avoid). The planner picks one dinner per day from about 60 simple home-cooking recipes, using food that expires soonest first, and adds up a shopping list of everything still missing.\n\nMethodology:\n- Recipes come from a bundled editorial dataset of about 60 simple home-cooking dinners with ingredient quantities, cooking time and diet. Quantities are scaled linearly to the number of people.\n- Recipes are filtered by diet (vegan ⊂ vegetarian ⊂ pescatarian ⊂ everything), maximum cooking time and the ingredients to avoid.\n- Pantry names are matched to recipe ingredients after normalising plurals and common synonyms (e.g. eggs → egg, courgette → zucchini, spaghetti → pasta). kg and l are converted to g and ml.\n- Day by day, every remaining recipe gets a score: for each ingredient, the share covered by the pantry × an urgency weight (expires within 1 day: 6, 3 days: 4, 7 days: 2, later: 1, no date: 0.5) × 10, plus the share of that pantry item used up × urgency × 5, minus 3 for every ingredient that must be bought. The highest score wins (ties: dataset order); the pantry is then reduced by what the recipe uses.\n- Items past their best-before date on the cooking day are not used. A recipe is repeated only when every eligible recipe has already been planned.\n- The shopping list adds up all missing amounts per ingredient (pieces rounded up to whole pieces). Salt, pepper, cooking oil and water are assumed to be at home.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "weekly_meal_planner",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WeeklyMealPlannerInput"
              },
              "examples": {
                "example1": {
                  "summary": "Use up spinach, eggs and chicken this week",
                  "value": {
                    "startDate": "2026-10-19",
                    "days": 7,
                    "servings": 2,
                    "diet": "any",
                    "maxMinutes": 45,
                    "pantry": [
                      {
                        "item": "Spinach",
                        "quantity": 250,
                        "unit": "g",
                        "expires": "2026-10-20"
                      },
                      {
                        "item": "Eggs",
                        "quantity": 6,
                        "unit": "pcs",
                        "expires": "2026-10-28"
                      },
                      {
                        "item": "Chicken breast",
                        "quantity": 300,
                        "unit": "g",
                        "expires": "2026-10-21"
                      },
                      {
                        "item": "Rice",
                        "quantity": 1,
                        "unit": "kg"
                      },
                      {
                        "item": "Onions",
                        "quantity": 4,
                        "unit": "pcs"
                      },
                      {
                        "item": "Canned tomatoes",
                        "quantity": 800,
                        "unit": "g"
                      },
                      {
                        "item": "Feta",
                        "quantity": 200,
                        "unit": "g",
                        "expires": "2026-10-23"
                      },
                      {
                        "item": "Pasta",
                        "quantity": 500,
                        "unit": "g"
                      }
                    ]
                  }
                },
                "example2": {
                  "summary": "Vegan, 5 days, max 30 minutes",
                  "value": {
                    "startDate": "2026-11-02",
                    "days": 5,
                    "servings": 3,
                    "diet": "vegan",
                    "maxMinutes": 30,
                    "pantry": [
                      {
                        "item": "Chickpeas",
                        "quantity": 800,
                        "unit": "g"
                      },
                      {
                        "item": "Broccoli",
                        "quantity": 400,
                        "unit": "g",
                        "expires": "2026-11-04"
                      },
                      {
                        "item": "Tofu",
                        "quantity": 400,
                        "unit": "g",
                        "expires": "2026-11-05"
                      },
                      {
                        "item": "Rice",
                        "quantity": 500,
                        "unit": "g"
                      }
                    ],
                    "exclude": [
                      "mushrooms"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeeklyMealPlannerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/leftover-chef/calculate": {
      "post": {
        "operationId": "calculateLeftoverChef",
        "summary": "Turn leftovers and fridge odds and ends into 5 recipe ideas, using what expires first.",
        "description": "List what is in your fridge and cupboard, mark what must be used soon, and set diet, equipment and time. Leftover Chef matches your ingredients against 37 flexible recipe templates (frittata, fried rice, soup, curry…) and ranks the five that use the most of what you have with the fewest missing items.\n\nMethodology:\n- Each ingredient line is normalised: lower-cased, quantities/units and words like \"leftover\", \"fresh\", \"chopped\" removed, plurals singularised, synonyms mapped (courgette → zucchini, garbanzo → chickpea…), then classified (vegetable, protein, grain, dairy…) with diet flags. \"Vegan …\", \"dairy-free …\", \"gluten-free …\" prefixes clear the matching flags.\n- Templates are editorial: each has core requirements (one of several alternatives), flexible slots (e.g. 1–4 vegetables, optional cheese), equipment alternatives and a total time. Salt, pepper, cooking oil, water and dried spices are assumed available.\n- A template is eligible if its time ≤ your maximum, you have one of its equipment options, every unmet core requirement still has a diet-compatible alternative, at least one of your ingredients is used and at most 3 items are missing.\n- Core requirements are filled first, then slots; use-soon items are always tried first and each ingredient is used once per recipe.\n- Score = 50 × share of core requirements you have + 30 × share of your use-soon items used + 3 × core requirements you have + 4 × number of your ingredients used (max 8) − 12 × missing items. Ties: fewer missing, shorter time, then template id.\n\nGeneric cooking templates, not tested recipes. Check that leftovers are still safe to eat and cook meat, fish and eggs thoroughly.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "leftover_chef",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeftoverChefInput"
              },
              "examples": {
                "example1": {
                  "summary": "Eggs, rice and wilting veg",
                  "value": {
                    "ingredients": [
                      "4 eggs",
                      "leftover rice",
                      "half a courgette",
                      "spinach",
                      "cheddar",
                      "spring onions",
                      "soy sauce"
                    ],
                    "useSoon": [
                      "spinach",
                      "courgette"
                    ],
                    "equipment": [
                      "stove",
                      "oven"
                    ],
                    "maxMinutes": 30
                  }
                },
                "example2": {
                  "summary": "Vegan pantry dinner",
                  "value": {
                    "ingredients": [
                      "chickpeas",
                      "tinned tomatoes",
                      "onion",
                      "garlic",
                      "coconut milk",
                      "curry paste",
                      "rice",
                      "sweet potato",
                      "kale"
                    ],
                    "useSoon": [
                      "kale"
                    ],
                    "diet": [
                      "vegan"
                    ],
                    "equipment": [
                      "stove",
                      "oven"
                    ],
                    "maxMinutes": 45
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeftoverChefResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/food-storage-guide/calculate": {
      "post": {
        "operationId": "calculateFoodStorageGuide",
        "summary": "How long is food safe in the fridge or freezer? Dates from the official FoodSafety.gov chart.",
        "description": "Choose a food and the date you stored or opened it. The guide shows the refrigerator and freezer limits exactly as published in the FoodSafety.gov Cold Food Storage Chart, the resulting dates, and whether the food is fine, should be used soon, or should be discarded.\n\nMethodology:\n- Storage limits are copied verbatim from the FoodSafety.gov Cold Food Storage Chart (refrigerator at 40°F (4°C) or below, freezer at 0°F (-18°C) or below).\n- Text periods are converted to days with 1 week = 7 days, 1 month = 30 days, 1 year = 365 days; \"use immediately after thawing\" = 0 days.\n- Dates = date stored + lower limit (use-by earliest) and + upper limit (use-by latest).\n- Status (refrigerator/pantry): fine before the lower limit; use-soon from the lower limit up to the upper limit (for single values: on the last day); discard after the upper limit.\n- Freezer: the chart says freezer times are for quality only and food kept continuously at 0°F (-18°C) or below can be kept indefinitely, so the status after the upper limit is \"quality-past\", not \"discard\".\n- Items the chart marks \"Do not freeze\", \"Does not freeze well\" or \"Not recommended\" get status not-recommended.\n\nWhen in doubt, throw it out. Times assume correct storage temperatures throughout.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "food_storage_guide",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FoodStorageGuideInput"
              },
              "examples": {
                "example1": {
                  "summary": "Leftover chicken cooked 3 days ago",
                  "value": {
                    "food": "leftover-meat-poultry",
                    "storage": "fridge",
                    "dateStored": "2026-10-09",
                    "today": "2026-10-12"
                  }
                },
                "example2": {
                  "summary": "Ground beef in the freezer since summer",
                  "value": {
                    "food": "ground-meat",
                    "storage": "freezer",
                    "dateStored": "2026-06-01",
                    "today": "2026-10-12"
                  }
                },
                "example3": {
                  "summary": "Opened deli meat after a week",
                  "value": {
                    "food": "luncheon-meat-opened",
                    "storage": "fridge",
                    "dateStored": "2026-10-05",
                    "today": "2026-10-12"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FoodStorageGuideResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/household-chore-rotator/calculate": {
      "post": {
        "operationId": "calculateHouseholdChoreRotator",
        "summary": "Fair chore rotation by effort and availability, with per-person totals and a calendar.",
        "description": "List your recurring chores (daily, weekly, every two weeks or monthly) with how long they take, and who lives with you and on which days they are available. The rotator builds a calendar for up to 12 weeks that balances everyone's minutes (optionally weighted, e.g. half for a child), rotates who does what, shows per-person totals and exports an .ics calendar file.\n\nMethodology:\n- Occurrences: daily chores happen every day; weekly ones once a week on their day; every-2-weeks ones in weeks 1, 3, 5 …; monthly ones every 4 weeks (13 times a year) so they stay on the same weekday. Weeks are counted from the start date.\n- Weekly-type chores without a fixed day are placed on the weekday with the least planned effort so far (that someone is available on), and keep that day.\n- Occurrences are assigned longest chore first (then by date); each goes to an available person with the lowest score = (minutes already assigned + this chore) ÷ capacity + a rotation term of ¼ × chore minutes × (how often this person already did this chore − the least anyone available did it). Ties go to the person listed first. This keeps everyone's effort proportional to their capacity and rotates the chores.\n- Target share = capacity ÷ sum of capacities. Availability limits can make a perfect balance impossible; the totals table shows the actual shares.\n- The .ics file has one all-day event per chore and day (RFC 5545: CRLF line endings, folded lines, stable UIDs derived from the inputs, DTSTAMP = time of calculation).",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "household_chore_rotator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HouseholdChoreRotatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "Flat share of three, 4 weeks",
                  "value": {
                    "startDate": "2026-10-19",
                    "weeks": 4,
                    "chores": [
                      {
                        "name": "Dishes",
                        "frequency": "daily",
                        "minutes": 15,
                        "day": "any"
                      },
                      {
                        "name": "Vacuum living room",
                        "frequency": "weekly",
                        "minutes": 30,
                        "day": "sat"
                      },
                      {
                        "name": "Clean bathroom",
                        "frequency": "weekly",
                        "minutes": 45,
                        "day": "any"
                      },
                      {
                        "name": "Take out recycling",
                        "frequency": "biweekly",
                        "minutes": 10,
                        "day": "tue"
                      },
                      {
                        "name": "Defrost freezer",
                        "frequency": "monthly",
                        "minutes": 60,
                        "day": "sun"
                      }
                    ],
                    "people": [
                      {
                        "name": "Ana",
                        "days": ""
                      },
                      {
                        "name": "Ben",
                        "days": "Mon-Fri"
                      },
                      {
                        "name": "Chen",
                        "days": "Wed-Sun"
                      }
                    ]
                  }
                },
                "example2": {
                  "summary": "Family with a child at half capacity, 2 weeks",
                  "value": {
                    "startDate": "2026-11-02",
                    "weeks": 2,
                    "chores": [
                      {
                        "name": "Set the table",
                        "frequency": "daily",
                        "minutes": 5,
                        "day": "any"
                      },
                      {
                        "name": "Laundry",
                        "frequency": "weekly",
                        "minutes": 60,
                        "day": "sat"
                      },
                      {
                        "name": "Water plants",
                        "frequency": "weekly",
                        "minutes": 10,
                        "day": "wed"
                      }
                    ],
                    "people": [
                      {
                        "name": "Parent A",
                        "capacity": 1
                      },
                      {
                        "name": "Parent B",
                        "capacity": 1
                      },
                      {
                        "name": "Kid",
                        "capacity": 0.5,
                        "days": "Sat,Sun"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdChoreRotatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/chore-roulette/calculate": {
      "post": {
        "operationId": "calculateChoreRoulette",
        "summary": "Spin a fair chore rota: random but balanced by effort, with a “not again” rule.",
        "description": "Enter the people (with an optional capacity, e.g. 0.5 for a child) and the chores with effort points. Chore Roulette draws a reproducible random assignment that balances everyone’s total effort, can avoid giving anyone the same chore as last week, and shows each person’s load against their fair share.\n\nMethodology:\n- Each chore occurrence is a task with effort points. A person’s fair share is total effort × their capacity ÷ the sum of capacities.\n- The seeded random generator (sfc32 seeded with SHA-256 of the seed) shuffles the tasks and the people; tasks are then handed out largest first to whoever would end up with the lowest load relative to their capacity (ties go to the seeded order).\n- A local search then moves single tasks and swaps pairs whenever that lowers the sum of squared differences between each person’s effort and their fair share, until nothing improves. Moves never give someone a chore they did last week when the rule is on.\n- Fairness = 100 × (1 − Σ|effort − fair share| ÷ (2 × total effort)): the share of the work that sits with the right person. Spread = highest minus lowest effort per full share of capacity.\n- If everyone did a chore last week, a repeat is unavoidable and is reported. Without a seed, the seed is a hash of the inputs, so the same rota appears on the page, in the API and in MCP; change the seed to re-spin.",
        "tags": [
          "fun"
        ],
        "x-mcp-tool": "chore_roulette",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChoreRouletteInput"
              },
              "examples": {
                "example1": {
                  "summary": "Flat share of three",
                  "value": {
                    "people": [
                      {
                        "name": "Alex"
                      },
                      {
                        "name": "Bea"
                      },
                      {
                        "name": "Chris"
                      }
                    ],
                    "chores": [
                      {
                        "chore": "Bathroom",
                        "effort": 4
                      },
                      {
                        "chore": "Kitchen",
                        "effort": 3
                      },
                      {
                        "chore": "Vacuum",
                        "effort": 3
                      },
                      {
                        "chore": "Bins",
                        "effort": 1
                      },
                      {
                        "chore": "Groceries",
                        "effort": 2
                      },
                      {
                        "chore": "Dishes",
                        "effort": 1,
                        "count": 4
                      }
                    ],
                    "lastWeek": [
                      {
                        "chore": "Bathroom",
                        "person": "Alex"
                      },
                      {
                        "chore": "Kitchen",
                        "person": "Bea"
                      }
                    ],
                    "avoidRepeats": true,
                    "seed": "week-42"
                  }
                },
                "example2": {
                  "summary": "Family with a younger child",
                  "value": {
                    "people": [
                      {
                        "name": "Mum"
                      },
                      {
                        "name": "Dad"
                      },
                      {
                        "name": "Mia",
                        "capacity": 1
                      },
                      {
                        "name": "Leo",
                        "capacity": 0.5
                      }
                    ],
                    "chores": [
                      {
                        "chore": "Cook dinner",
                        "effort": 3,
                        "count": 3
                      },
                      {
                        "chore": "Laundry",
                        "effort": 4
                      },
                      {
                        "chore": "Feed the cat",
                        "effort": 0.5,
                        "count": 7
                      },
                      {
                        "chore": "Tidy living room",
                        "effort": 2
                      },
                      {
                        "chore": "Water plants",
                        "effort": 1
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChoreRouletteResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/household-maintenance-calendar/calculate": {
      "post": {
        "operationId": "calculateHouseholdMaintenanceCalendar",
        "summary": "A dated home maintenance schedule for your appliances and features, as a calendar file.",
        "description": "Tick the features of your home — heating, alarms, appliances, gutters, garden, pool, car — and get a dated maintenance plan for up to 24 months, grouped by month, with safety intervals backed by official guidance and a recurring-event calendar file to import.\n\nMethodology:\n- Tasks come from an editorial catalog of generic maintenance best practice; a task is included if it applies to every home, to your home type, or to a feature you ticked.\n- Interval tasks (every N months): the first date is the start date for monthly tasks, and start date + (catalog position × 3 mod 28) days for longer intervals so that work is spread out; then every N months. A first date on the 29th–31st moves to the 28th of the following month so the calendar recurrence is valid in every month.\n- Day-interval tasks (e.g. weekly pool checks) repeat every N days from the start date.\n- Seasonal tasks fall on the 1st of their months (northern hemisphere months; +6 months in the southern hemisphere).\n- Only dates from the start date to the day before start + N months are listed. The .ics file has one all-day recurring VEVENT per task (RRULE FREQ=MONTHLY/DAILY/YEARLY with UNTIL), CRLF line endings, folded lines and UIDs derived from the inputs.\n- Safety intervals (smoke and CO alarm tests and batteries, chimney inspection, septic inspection, tyre pressure) follow the cited official guidance; all other intervals are editorial — your manufacturer’s manual takes precedence.\n\nGeneric reminders, not a substitute for your appliance manuals, local regulations or professional inspections. Do not climb on roofs; hire professionals for gas, electrical and roof work.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "household_maintenance_calendar",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HouseholdMaintenanceCalendarInput"
              },
              "examples": {
                "example1": {
                  "summary": "Apartment, 12 months",
                  "value": {
                    "homeType": "apartment",
                    "features": [
                      "smoke-detectors",
                      "co-detectors",
                      "gas-heating",
                      "dishwasher",
                      "washing-machine",
                      "fridge"
                    ],
                    "hemisphere": "north",
                    "startDate": "2026-11-01",
                    "months": 12
                  }
                },
                "example2": {
                  "summary": "House with garden, pool and car, 24 months",
                  "value": {
                    "homeType": "house",
                    "features": [
                      "smoke-detectors",
                      "heat-pump",
                      "water-heater",
                      "dryer",
                      "gutters",
                      "chimney",
                      "garden",
                      "pool",
                      "septic",
                      "solar",
                      "car"
                    ],
                    "hemisphere": "north",
                    "startDate": "2026-10-15",
                    "months": 24
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdMaintenanceCalendarResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/disaster-prep-builder/calculate": {
      "post": {
        "operationId": "calculateDisasterPrepBuilder",
        "summary": "Build an emergency supply list for your household from official Ready.gov or BBK guidance.",
        "description": "Enter your household (adults, children, infants, pets), the number of days and the hazards you face. The tool calculates water and food quantities from the official guideline you choose (Ready.gov in the US or the German BBK), builds a grouped checklist with hazard-specific additions quoted from the sources, and estimates cost only if you enter your own prices.\n\nMethodology:\n- Ready.gov: water = 1 US gallon (3.785 L) per person per day \"for several days, for drinking and sanitation\". Ready.gov names no fixed number of days; this tool defaults to 3 days unless you enter another value.\n- BBK: water = 2 liters per person per day (0.5 L of it for cooking); default 10 days (\"möglichst 10 Tage\"), with 3 days as a useful first step.\n- Water total = (adults + children + infants) × liters per person per day × days. Heat selected: an upper figure of 2 × that amount is shown (Ready.gov: \"In very hot temperatures, water needs can double\").\n- Food energy = (adults × kcal per adult + children × kcal per child) × days. Defaults: 2,200 kcal per adult (basis of the German BLE stock calculator) and 1,500 kcal per child (tool assumption).\n- Counted items: flashlights = number of people (Ready.gov: \"flashlights for every household member\"); sleeping bag or warm blanket for each person (Ready.gov); spare battery sets = flashlights + 1 radio (tool assumption).\n- Checklist groups and hazard additions are quoted from the chosen guideline and the Ready.gov hazard pages; items without a quantity in the source are listed without one.\n- Cost = quantity × your unit price, only for items you price; no prices are assumed.\n\nFollow instructions from local authorities. Medical and infant needs vary; ask your doctor or pharmacist.",
        "tags": [
          "high-value"
        ],
        "x-mcp-tool": "disaster_prep_builder",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisasterPrepBuilderInput"
              },
              "examples": {
                "example1": {
                  "summary": "US family of four, power outage + winter storm",
                  "value": {
                    "guideline": "readygov",
                    "adults": 2,
                    "children": 2,
                    "dogs": 1,
                    "hazards": [
                      "power-outage",
                      "winter-storm"
                    ],
                    "currency": "USD"
                  }
                },
                "example2": {
                  "summary": "German couple with infant, 10 days, with prices",
                  "value": {
                    "guideline": "bbk",
                    "adults": 2,
                    "infants": 1,
                    "hazards": [
                      "power-outage",
                      "flood",
                      "heat"
                    ],
                    "medicalNeeds": true,
                    "currency": "EUR",
                    "budget": 150,
                    "unitPrices": [
                      {
                        "item": "water-liter",
                        "price": 0.3
                      },
                      {
                        "item": "food-1000kcal",
                        "price": 1.2
                      },
                      {
                        "item": "flashlight",
                        "price": 8
                      },
                      {
                        "item": "radio",
                        "price": 25
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisasterPrepBuilderResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/moving-box-planner/calculate": {
      "post": {
        "operationId": "calculateMovingBoxPlanner",
        "summary": "How many moving boxes, how much tape, which truck and when to start packing.",
        "description": "List your rooms and how much stuff you own. The planner estimates boxes by size, packing supplies, load volume, a suitable truck class, crew size and hours, optional cost, and a packing schedule counting back from moving day. All counts are transparent rules of thumb you can check.\n\nMethodology:\n- Boxes per room = editorial base count for a medium room at average density × room size factor (small 0.7, medium 1, large 1.4) × stuff factor (minimalist 0.7, average 1, collector 1.4), rounded up per box type. One extra wardrobe box per person for hanging clothes.\n- Box sizes: small 40×30×30 cm (0.036 m³, books/heavy), medium 50×40×40 cm (0.08 m³), large 60×45×45 cm (0.1215 m³, light/bulky), wardrobe 50×50×120 cm (0.3 m³).\n- Furniture allowance per room (medium): bedroom 4, living 6, kitchen 2, bathroom 0.3, office 2.5, kids 3, garage 3, storage 1.5 m³, scaled by room size and stuff factor. Load volume = (boxes + furniture) × 1.15 for imperfect stacking.\n- Truck: smallest vehicle class whose nominal volume ≥ load (panel van 12, Luton 20, 7.5 t 35, 12 t 50, 18 t 70 m³); larger loads need multiple trips of the 18 t class.\n- Crew: 2 people up to 20 m³, 3 up to 40 m³, 4 above. Loading and unloading each at 2.5 m³ per person-hour, +10% per floor without elevator; driving at 60 km/h average one way.\n- Supplies: 3 m of tape per box on 50 m rolls (+1 spare roll), packing paper and bubble wrap per box by room type (kitchen and living highest), 1 marker per 2 people (min 2).\n- Packing pace: 10 boxes per day; rooms are packed in the order storage → garage → office → kids → living → bedroom → bathroom → kitchen, finishing the day before the move.\n\nRules of thumb, not a quote. Professional movers survey your home for a binding estimate.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "moving_box_planner",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MovingBoxPlannerInput"
              },
              "examples": {
                "example1": {
                  "summary": "2-bedroom flat, couple",
                  "value": {
                    "rooms": [
                      {
                        "type": "bedroom",
                        "size": "medium"
                      },
                      {
                        "type": "bedroom",
                        "size": "small"
                      },
                      {
                        "type": "living",
                        "size": "medium"
                      },
                      {
                        "type": "kitchen",
                        "size": "medium"
                      },
                      {
                        "type": "bathroom",
                        "size": "small"
                      }
                    ],
                    "people": 2,
                    "density": "average",
                    "distanceKm": 15,
                    "floorsWithoutElevator": 3,
                    "moveDate": "2026-11-28",
                    "currency": "EUR"
                  }
                },
                "example2": {
                  "summary": "Family house with movers",
                  "value": {
                    "rooms": [
                      {
                        "type": "bedroom",
                        "size": "large"
                      },
                      {
                        "type": "kids",
                        "size": "medium"
                      },
                      {
                        "type": "kids",
                        "size": "medium"
                      },
                      {
                        "type": "living",
                        "size": "large"
                      },
                      {
                        "type": "kitchen",
                        "size": "large"
                      },
                      {
                        "type": "office",
                        "size": "medium"
                      },
                      {
                        "type": "bathroom",
                        "size": "medium"
                      },
                      {
                        "type": "garage",
                        "size": "medium"
                      }
                    ],
                    "people": 4,
                    "density": "collector",
                    "distanceKm": 250,
                    "floorsWithoutElevator": 1,
                    "moveDate": "2027-03-15",
                    "moverHourlyRate": 45,
                    "truckHourlyRate": 30,
                    "currency": "EUR"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MovingBoxPlannerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/will-it-fit/calculate": {
      "post": {
        "operationId": "calculateWillItFit",
        "summary": "Check if a sofa, wardrobe or box fits through doors, corners, stairs and lifts.",
        "description": "Enter the item’s length, width and height and the measurements along the delivery path. Will It Fit? tries every orientation, in-plane diagonal tilts and the classic moving-sofa corner calculation, then reports pass/fail per checkpoint with spare centimetres and how to carry the item.\n\nMethodology:\n- The item is treated as a rigid rectangular box; the safety clearance is subtracted from every opening dimension.\n- Doors, stairs and elevator doors: for each of the 3 choices of travel axis, the remaining two sides form the cross-section; it is rotated in the opening plane from 0° to 90° in 0.25° steps (bounding box p·cosθ+q·sinθ by p·sinθ+q·cosθ) and the best margin is kept — this covers the \"diagonal\" tilt through a door.\n- Corner: the classic moving-sofa formula for a right-angle turn between corridors of widths a and b gives the longest flat length L_max(w) = min over θ of a/sinθ + b/cosθ − w/(sinθ·cosθ), evaluated numerically in 0.05° steps.\n- Corner, 3D: every side may stand vertical if it fits under the ceiling; the item may also be tilted upward by φ (constant during the turn) so its plan length becomes L·cosφ + h·sinφ while L·sinφ + h·cosφ stays under the ceiling. Fixed-tilt is a conservative approximation of real manoeuvring.\n- Elevator cabin and room: one side stays horizontal while the other two stand in a vertical plane tilted by φ = 0…90° (1° steps): height p·sinφ + q·cosφ must fit under the ceiling and the footprint (p·cosφ + q·sinφ) × side is rotated on the floor (0.25° steps) to fit the plan — this covers upright placement, standing diagonally in the cabin and leaning.\n- Stairs are modelled as an opening of stair width × headroom; landings and turns of the stairwell are not modelled.\n\nGeometric estimate for rigid boxes. Real items have legs, curves and soft parts; real people need grip room. Measure twice.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "will_it_fit",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WillItFitInput"
              },
              "examples": {
                "example1": {
                  "summary": "3-seat sofa through a flat door and corridor",
                  "value": {
                    "itemLength": 210,
                    "itemWidth": 90,
                    "itemHeight": 85,
                    "clearance": 2,
                    "doorWidth": 80,
                    "doorHeight": 200,
                    "hallwayWidthBefore": 100,
                    "hallwayWidthAfter": 100,
                    "ceilingHeight": 250,
                    "roomWidth": 400,
                    "roomDepth": 350,
                    "roomHeight": 250
                  }
                },
                "example2": {
                  "summary": "Wardrobe box via elevator and stairs",
                  "value": {
                    "itemLength": 200,
                    "itemWidth": 60,
                    "itemHeight": 50,
                    "clearance": 3,
                    "doorWidth": 76,
                    "doorHeight": 198,
                    "ceilingHeight": 250,
                    "stairsWidth": 90,
                    "stairsHeadroom": 200,
                    "elevatorDoorWidth": 80,
                    "elevatorDoorHeight": 200,
                    "elevatorCabinWidth": 110,
                    "elevatorCabinDepth": 140,
                    "elevatorCabinHeight": 210
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WillItFitResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/room-layout-optimizer/calculate": {
      "post": {
        "operationId": "calculateRoomLayoutOptimizer",
        "summary": "Auto-arrange furniture in a room around doors, windows and a clear walkway.",
        "description": "Give the room size, doors, windows and your furniture with the clearance each piece needs. A deterministic grid search places everything inside the room without overlaps, keeps door swings and a walkway clear, prefers walls and windows where requested and returns coordinates plus a text floor plan.\n\nMethodology:\n- The room is rasterised into grid cells (default 10 cm); furniture sizes, clearances and door zones are rounded up to whole cells, the room size down.\n- Each door gets a keep-clear square (door width × door width) inside the room in front of it, approximating the quarter-circle swing.\n- Each item can face any of the 4 directions; its body must lie inside the room and not overlap other bodies, other clearance zones or door squares; its front clearance zone must lie inside the room and not overlap bodies. \"Back against wall\" items must touch the wall behind them.\n- Walkway: the free floor is eroded by a square of the walkway width; squares connected to a door square form the reachable walkway. Every item’s clearance zone (or the cell strip in front of it) must touch that walkway.\n- Search: items are placed largest-area first with a beam search (4 partial layouts kept). For each layout, every valid grid position is pre-scored (wall hugging, corner use, distance to the nearest window for \"prefer near window\" items); the 25 best are checked for walkway access and scored.\n- Score = 1000 × items placed + 10 × reachable walkway m² + 2 per wall item in a corner − 3 × distance (m) of free-standing items from the nearest wall − 5 × distance (m) from window-loving items to the nearest window. Ties break by input order, then north-to-south, west-to-east position.\n\nRectangles only; radiators, sockets, sloped ceilings and personal taste are not considered. Treat the result as a starting point.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "room_layout_optimizer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoomLayoutOptimizerInput"
              },
              "examples": {
                "example1": {
                  "summary": "Bedroom with desk by the window",
                  "value": {
                    "roomWidth": 400,
                    "roomDepth": 350,
                    "gridCm": 10,
                    "walkwayCm": 60,
                    "openings": [
                      {
                        "kind": "door",
                        "wall": "south",
                        "offset": 20,
                        "width": 80
                      },
                      {
                        "kind": "window",
                        "wall": "north",
                        "offset": 150,
                        "width": 120
                      }
                    ],
                    "furniture": [
                      {
                        "name": "Double bed",
                        "width": 160,
                        "depth": 200,
                        "needsWall": true,
                        "clearanceFront": 60,
                        "nearWindow": false
                      },
                      {
                        "name": "Wardrobe",
                        "width": 150,
                        "depth": 60,
                        "needsWall": true,
                        "clearanceFront": 70,
                        "nearWindow": false
                      },
                      {
                        "name": "Desk",
                        "width": 120,
                        "depth": 60,
                        "needsWall": true,
                        "clearanceFront": 70,
                        "nearWindow": true
                      }
                    ]
                  }
                },
                "example2": {
                  "summary": "Living room",
                  "value": {
                    "roomWidth": 500,
                    "roomDepth": 420,
                    "gridCm": 10,
                    "walkwayCm": 70,
                    "openings": [
                      {
                        "kind": "door",
                        "wall": "west",
                        "offset": 30,
                        "width": 90
                      },
                      {
                        "kind": "window",
                        "wall": "east",
                        "offset": 120,
                        "width": 180
                      }
                    ],
                    "furniture": [
                      {
                        "name": "Sofa",
                        "width": 220,
                        "depth": 95,
                        "needsWall": true,
                        "clearanceFront": 80,
                        "nearWindow": false
                      },
                      {
                        "name": "TV unit",
                        "width": 180,
                        "depth": 45,
                        "needsWall": true,
                        "clearanceFront": 50,
                        "nearWindow": false
                      },
                      {
                        "name": "Armchair",
                        "width": 85,
                        "depth": 85,
                        "needsWall": false,
                        "clearanceFront": 50,
                        "nearWindow": true
                      },
                      {
                        "name": "Bookshelf",
                        "width": 100,
                        "depth": 35,
                        "needsWall": true,
                        "clearanceFront": 50,
                        "nearWindow": false
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomLayoutOptimizerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/jet-lag-planner/calculate": {
      "post": {
        "operationId": "calculateJetLagPlanner",
        "summary": "A day-by-day sleep, light and caffeine plan to beat jet lag, with calendar export.",
        "description": "Pick your home and destination time zones, travel date and usual sleep times. The planner computes the real time difference for that date (including daylight saving), shifts your sleep before departure, and lists when to seek or avoid bright light and when to stop caffeine until you are adapted. Download it as a calendar file.\n\nMethodology:\n- Time difference = destination UTC offset (at noon UTC on the arrival date) − home UTC offset (at noon UTC on the departure date), via the Intl time-zone database, so daylight saving is included. It is normalised to −12…+12 h; positive = eastward (clock must advance), negative = westward (delay).\n- Pre-flight: sleep and wake move 1 hour per day toward destination time (earlier for east, later for west), as suggested in the CDC Yellow Book jet-lag chapter for the 2–3 days before travel; days beyond what is needed are skipped.\n- After arrival you sleep on local time; the body clock is assumed to catch up 1 h/day after eastward and 1.5 h/day after westward travel (common planning rule of thumb, not an individual prediction).\n- Core body temperature minimum (CBTmin) is estimated at 2.5 h before the body’s habitual wake time (2 h for early birds, 3 h for night owls).\n- Light rule (phase-response curve): to advance (east) seek bright light in the 3 h after CBTmin and avoid it in the 3 h before; to delay (west) seek light in the 3 h before CBTmin and avoid it in the 3 h after. If a window falls while you sleep, use only its waking part.\n- Caffeine cut-off = 6 h before planned bedtime (CDC Yellow Book example).\n- The .ics file contains bedtime, light and caffeine events in UTC with UIDs derived from a hash of your inputs, CRLF line endings and folded lines.\n\nGeneral planning guidance, not medical advice. Ask a clinician before using melatonin or sleep medication.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "jet_lag_planner",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JetLagPlannerInput"
              },
              "examples": {
                "example1": {
                  "summary": "Berlin → New York (westward, 6 h)",
                  "value": {
                    "homeZone": "Europe/Berlin",
                    "destinationZone": "America/New_York",
                    "departureDate": "2026-11-10",
                    "bedtime": "23:00",
                    "wakeTime": "07:00",
                    "daysBefore": 3,
                    "chronotype": "intermediate"
                  }
                },
                "example2": {
                  "summary": "London → Tokyo (eastward, 9 h)",
                  "value": {
                    "homeZone": "Europe/London",
                    "destinationZone": "Asia/Tokyo",
                    "departureDate": "2027-04-02",
                    "arrivalDate": "2027-04-03",
                    "bedtime": "23:30",
                    "wakeTime": "07:30",
                    "daysBefore": 3,
                    "chronotype": "late"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JetLagPlannerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/travel-connection-checker/calculate": {
      "post": {
        "operationId": "calculateTravelConnectionChecker",
        "summary": "Estimate your odds of making a flight connection, step by step.",
        "description": "Enter your arrival and onward departure times and what you have to do in between (terminal change, passport control, security, baggage). The checker adds up transparent time ranges for each step, compares them with your connection time and estimates the chance of making it under a simple inbound-delay model. It is an estimate, never a guarantee.\n\nMethodology:\n- Available time = scheduled departure − scheduled arrival − gate-closure minutes.\n- Required time = deplaning + walking (+ terminal change) + optional immigration, security and baggage re-check, each an editorial [best, typical, worst] range by airport size; walking and terminal transfer are multiplied by 1.4 (slow) / 1.0 / 0.8 (fast).\n- Buffer = available − required, for each of best/typical/worst.\n- Inbound delay model: on time with probability p, otherwise delay ~ Exponential(mean delay). P(miss) = (1 − p)·e^(−buffer/mean) if buffer ≥ 0, else 100%. P(make) = 1 − P(miss). Early arrivals and onward-flight delays are ignored (both would help).\n- Overall chance: total processing time is treated as a triangular distribution between the best, typical and worst totals, and P(make) is averaged over it numerically (400 slices). The best/worst columns show the chance if processing is at its fastest/slowest.\n- Verdict from the overall chance: ≥ 95% comfortable, ≥ 85% ok, ≥ 70% tight, ≥ 50% risky, below that very risky.\n\nA planning estimate from simplified assumptions — not a guarantee. Airline, airport and border procedures and queues vary widely.",
        "tags": [
          "home-travel"
        ],
        "x-mcp-tool": "travel_connection_checker",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TravelConnectionCheckerInput"
              },
              "examples": {
                "example1": {
                  "summary": "Schengen → non-Schengen at a large hub, 70 min",
                  "value": {
                    "arrival": "2026-11-03T09:40",
                    "departure": "2026-11-03T10:50",
                    "terminalChange": true,
                    "immigration": true,
                    "securityRecheck": false,
                    "baggageRecheck": false,
                    "airportSize": "large",
                    "walkingSpeed": "average",
                    "gateClosesMinutes": 15,
                    "onTimeProbability": 75,
                    "meanDelayMinutes": 35,
                    "officialMct": 50
                  }
                },
                "example2": {
                  "summary": "US arrival with baggage recheck, 2 h 30",
                  "value": {
                    "arrival": "2026-12-18T14:05",
                    "departure": "2026-12-18T16:35",
                    "terminalChange": true,
                    "immigration": true,
                    "securityRecheck": true,
                    "baggageRecheck": true,
                    "airportSize": "mega",
                    "walkingSpeed": "slow",
                    "gateClosesMinutes": 15,
                    "onTimeProbability": 75,
                    "meanDelayMinutes": 35
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TravelConnectionCheckerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/currency-converter/calculate": {
      "post": {
        "operationId": "calculateCurrencyConverter",
        "summary": "Convert an amount into several currencies at ECB euro reference rates, with rate date and source.",
        "description": "Converts an amount between the 30 currencies of the European Central Bank’s daily euro foreign exchange reference rates – into several target currencies at once, at the latest rates or those of a past date. Cross rates are calculated via the euro; every result shows the rate, the inverse rate, the reference date and the source.\n\nMethodology:\n- Rates come from the ECB’s daily reference-rate file (eurofxref-daily.xml) or, for past dates, from the ECB Data Portal (dataset EXR, daily series D.<currency>.EUR.SP00.A). They are cached per day; the daily file is re-checked at most hourly until the day’s rates (around 16:00 CET) are available.\n- All ECB rates are quoted as units of currency per 1 euro, so a cross rate is rate(to) ÷ rate(from); converting X from A to B = X × rate(B) ÷ rate(A). Rates are shown with 6 significant digits; amounts are rounded half away from zero to the currency’s minor units (0 for JPY, KRW and ISK).\n- For a date without a publication (weekends, TARGET holidays) the last earlier publication is used and named.\n- Reference rates are published for information only. Card schemes, banks and exchange offices use their own rates and add fees, so the amount you actually pay or receive will differ.\n\nECB reference rates are not transaction rates. For a real payment, check your provider’s rate and fees.",
        "tags": [
          "money"
        ],
        "x-mcp-tool": "currency_converter",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CurrencyConverterInput"
              },
              "examples": {
                "example1": {
                  "summary": "100 EUR into USD, GBP, CHF and JPY (latest)",
                  "value": {
                    "amount": 100,
                    "from": "EUR",
                    "to": "USD, GBP, CHF, JPY"
                  }
                },
                "example2": {
                  "summary": "250 USD into EUR and GBP on 2024-01-02",
                  "value": {
                    "amount": 250,
                    "from": "USD",
                    "to": "EUR, GBP",
                    "date": "2024-01-02"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CurrencyConverterResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/travel-exchange-fee-calculator/calculate": {
      "post": {
        "operationId": "calculateTravelExchangeFeeCalculator",
        "summary": "Compare what cards, ATMs, DCC and exchange bureaus really cost abroad vs the ECB reference rate.",
        "description": "Enter what you spend abroad and the terms of your payment options – foreign transaction fee, exchange-rate mark-up, ATM fees, “pay in your currency” (dynamic currency conversion) and exchange-bureau spreads. The calculator converts at the ECB reference rate (or your own rate) and shows the effective rate, total cost and extra cost of each option, cheapest first.\n\nMethodology:\n- Reference cost = amount ÷ reference rate (the ECB euro reference rate, cross-calculated via EUR for non-euro pairs, or your manual rate).\n- Each option: cost at its own rate = reference cost × (1 + mark-up %); plus the percentage fee on that amount; plus fixed fees (ATM fixed fees × number of withdrawals). Effective rate = amount ÷ total cost; extra cost = total − reference cost.\n- Dynamic currency conversion (DCC, “pay in your currency”) means the shop’s or ATM operator’s provider converts instead of your card issuer; its mark-up is entered as a rate mark-up. Under Regulation (EU) 2019/518 DCC providers and card issuers in the EU must express their conversion charges as a mark-up over the latest ECB reference rate before you pay, which is the comparison this tool makes.\n- The default rows are illustrative example values, not market data. Replace them with your card’s published terms and the mark-up shown on the terminal or at the exchange counter.\n\nAn estimate: the rate your card network applies on the booking day and any fees not entered here change the final amount.",
        "tags": [
          "money"
        ],
        "x-mcp-tool": "travel_exchange_fee_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TravelExchangeFeeCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "500 USD with the example options (manual rate 1 EUR = 1.10 USD)",
                  "value": {
                    "amount": 500,
                    "spendCurrency": "USD",
                    "homeCurrency": "EUR",
                    "rateSource": "manual",
                    "manualRate": 1.1
                  }
                },
                "example2": {
                  "summary": "20,000 JPY, 2 ATM withdrawals (manual rate 1 GBP = 190 JPY)",
                  "value": {
                    "amount": 20000,
                    "spendCurrency": "JPY",
                    "homeCurrency": "GBP",
                    "rateSource": "manual",
                    "manualRate": 190,
                    "atmWithdrawals": 2,
                    "options": [
                      {
                        "name": "Travel card",
                        "method": "card",
                        "feePercent": 0,
                        "markupPercent": 0.4,
                        "fixedFee": 0
                      },
                      {
                        "name": "High-street bank ATM",
                        "method": "atm",
                        "feePercent": 2.99,
                        "markupPercent": 0,
                        "fixedFee": 1.5
                      },
                      {
                        "name": "DCC offer at the ATM",
                        "method": "dcc",
                        "feePercent": 0,
                        "markupPercent": 8,
                        "fixedFee": 1.5
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TravelExchangeFeeCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/tip-bill-split-calculator/calculate": {
      "post": {
        "operationId": "calculateTipBillSplitCalculator",
        "summary": "Tip, tax and who pays what: split a bill equally, by shares or by items, rounded up.",
        "description": "Enter the bill, the tax rule (included as in most of Europe, or added at the till as in the US) and a tip as a percentage or an amount, before or after tax. Split equally, by shares or by what each person ordered, round each share up to 0.50 or 1, and get an exact who-pays-what table that adds up to the cent.\n\nMethodology:\n- Tax included (VAT): subtotal before tax = bill ÷ (1 + rate); tax = bill − subtotal. Tax added (sales tax): tax = bill × rate, rounded to the cent; bill total = bill + tax.\n- Tip = tip % × tip base, where the base is the subtotal before tax or the total with tax (your choice), rounded to the cent; or a fixed tip amount.\n- Weights: equal = 1 per person; shares = the share you enter; items = each person's ordered amount plus an equal part of anything not assigned to anyone.\n- Bill, tax and tip are each split by weight with the largest-remainder method in whole cents, so the shares add up exactly to the totals.\n- Rounding is applied per person to their exact total; the difference between what is collected and the exact total goes to the tip and is shown separately.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "tip_bill_split_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TipBillSplitCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "Dinner for 4 in Germany, 84.50 incl. VAT, 10% tip, round up to 1",
                  "value": {
                    "symbol": "€",
                    "billAmount": 84.5,
                    "taxMode": "included",
                    "taxPercent": 19,
                    "tipMode": "percent",
                    "tipPercent": 10,
                    "tipBase": "post-tax",
                    "splitMode": "equal",
                    "people": 4,
                    "rounding": "up-1"
                  }
                },
                "example2": {
                  "summary": "US restaurant, 120 + 8.875% sales tax, 20% tip on pre-tax, by items",
                  "value": {
                    "symbol": "$",
                    "billAmount": 120,
                    "taxMode": "added",
                    "taxPercent": 8.875,
                    "tipMode": "percent",
                    "tipPercent": 20,
                    "tipBase": "pre-tax",
                    "splitMode": "items",
                    "persons": [
                      {
                        "name": "Ana",
                        "items": 34
                      },
                      {
                        "name": "Ben",
                        "items": 46
                      },
                      {
                        "name": "Chloe",
                        "items": 28
                      }
                    ],
                    "rounding": "none"
                  }
                },
                "example3": {
                  "summary": "Couple + 2 singles by shares, fixed tip 15",
                  "value": {
                    "symbol": "£",
                    "billAmount": 150,
                    "taxMode": "none",
                    "taxPercent": 0,
                    "tipMode": "amount",
                    "tipAmount": 15,
                    "splitMode": "shares",
                    "persons": [
                      {
                        "name": "Sam & Alex",
                        "shares": 2
                      },
                      {
                        "name": "Jo",
                        "shares": 1
                      },
                      {
                        "name": "Kim",
                        "shares": 1
                      }
                    ],
                    "rounding": "up-0.5"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TipBillSplitCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/movie-night-consensus-picker/calculate": {
      "post": {
        "operationId": "calculateMovieNightConsensusPicker",
        "summary": "Everyone ranks the films, vetoes count: Borda, approval, runoff and Condorcet decide.",
        "description": "List the candidate films (or restaurants, games, holiday spots), then let each person rank them and veto the ones they cannot stand. The picker runs four voting methods — Borda count, approval, instant-runoff and Condorcet/Schulze — shows each winner and why, and breaks ties fairly with a seed.\n\nMethodology:\n- Each ballot is a ranking, best first; films a person leaves out count as tied below all films they ranked. Names are matched case-insensitively; unknown names are reported and skipped.\n- Vetoes: in “remove” mode every film vetoed by at least one person is struck before any counting (if that would remove all films, vetoes are ignored and a warning is shown).\n- Borda: with n films, 1st place earns n−1 points, 2nd n−2, … ; unranked films share the remaining points equally. Approval: each voter approves their top N films; most approvals wins. Instant-runoff (IRV): count first preferences, eliminate the film with the fewest, transfer those ballots to their next choice, repeat until one film has a majority of the ballots still in play.\n- Condorcet/Schulze: for every pair, count how many voters prefer each. A Condorcet winner beats every other film head-to-head. The Schulze method (strongest paths) always finds a winner and agrees with the Condorcet winner when one exists.\n- Ties are broken with a random order of the films drawn from the seed (sfc32 seeded with SHA-256), shown in the result, so the outcome is reproducible and nobody can claim favouritism. The recommendation is the Schulze winner.",
        "tags": [
          "fun"
        ],
        "x-mcp-tool": "movie_night_consensus_picker",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MovieNightConsensusPickerInput"
              },
              "examples": {
                "example1": {
                  "summary": "Five friends, four films, one veto",
                  "value": {
                    "candidates": [
                      "Dune: Part Two",
                      "Paddington 2",
                      "Alien",
                      "Amélie"
                    ],
                    "ballots": [
                      {
                        "voter": "Ana",
                        "ranking": "Dune: Part Two > Alien > Amélie > Paddington 2"
                      },
                      {
                        "voter": "Ben",
                        "ranking": "Paddington 2 > Amélie > Dune: Part Two",
                        "veto": "Alien"
                      },
                      {
                        "voter": "Cleo",
                        "ranking": "Amélie > Paddington 2 > Dune: Part Two > Alien"
                      },
                      {
                        "voter": "Dev",
                        "ranking": "Dune: Part Two > Paddington 2 > Amélie > Alien"
                      },
                      {
                        "voter": "Eli",
                        "ranking": "Paddington 2 > Dune: Part Two > Amélie > Alien"
                      }
                    ],
                    "vetoMode": "remove"
                  }
                },
                "example2": {
                  "summary": "Methods disagree (classic three-way split)",
                  "value": {
                    "candidates": [
                      "Action",
                      "Comedy",
                      "Drama"
                    ],
                    "ballots": [
                      {
                        "voter": "A1",
                        "ranking": "Action > Drama > Comedy"
                      },
                      {
                        "voter": "A2",
                        "ranking": "Action > Drama > Comedy"
                      },
                      {
                        "voter": "A3",
                        "ranking": "Action > Drama > Comedy"
                      },
                      {
                        "voter": "A4",
                        "ranking": "Action > Drama > Comedy"
                      },
                      {
                        "voter": "B1",
                        "ranking": "Comedy > Drama > Action"
                      },
                      {
                        "voter": "B2",
                        "ranking": "Comedy > Drama > Action"
                      },
                      {
                        "voter": "B3",
                        "ranking": "Comedy > Drama > Action"
                      },
                      {
                        "voter": "C1",
                        "ranking": "Drama > Comedy > Action"
                      },
                      {
                        "voter": "C2",
                        "ranking": "Drama > Comedy > Action"
                      }
                    ],
                    "vetoMode": "ignore",
                    "approvalCount": 1,
                    "seed": "popcorn"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MovieNightConsensusPickerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/runtime-marathon-planner/calculate": {
      "post": {
        "operationId": "calculateRuntimeMarathonPlanner",
        "summary": "Plan a binge: when each movie or episode starts and ends, total days, and a calendar.",
        "description": "Add movies (runtime) and series (episodes × minutes × seasons), your breaks, when you start and how many hours a day you want to watch. The planner schedules every movie and episode, tells you how many days the marathon takes and whether you finish by a date, and exports the viewing sessions as an .ics calendar.\n\nMethodology:\n- Each movie is one item; each series becomes episodes × seasons items of the given length.\n- Items are watched in the order entered. Within a day, a break follows each item (the episode break inside a title, the title break between titles); breaks do not count towards the daily maximum.\n- If the next item would push the day over the maximum viewing time, it moves to the next day at the daily start time. With pausing allowed, an item is split instead when at least 15 minutes fit into the day.\n- An item longer than the daily maximum is scheduled in one piece (with a warning) unless pausing is allowed.\n- A viewing session belongs to the day it starts, even if it runs past midnight. Times are local wall-clock times without a time zone; the .ics file uses floating times (RFC 5545), so calendars show them at the same clock time wherever you are.\n- Finish-by check: the last item must end before midnight at the end of that date. Required hours per day = total watching time ÷ number of days from the start date to the finish-by date — a lower bound, because only whole movies/episodes are scheduled (unless pausing is allowed). If you miss the date, the planner re-plans in 0.1 h steps to find the daily maximum that really finishes in time, and checks whether pausing at your current limit would.",
        "tags": [
          "fun"
        ],
        "x-mcp-tool": "runtime_marathon_planner",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RuntimeMarathonPlannerInput"
              },
              "examples": {
                "example1": {
                  "summary": "Lord-of-the-Rings-length trilogy over a weekend",
                  "value": {
                    "titles": [
                      {
                        "title": "Part 1",
                        "minutes": 178
                      },
                      {
                        "title": "Part 2",
                        "minutes": 179
                      },
                      {
                        "title": "Part 3",
                        "minutes": 201
                      }
                    ],
                    "start": "2026-10-23T19:00",
                    "dailyStart": "14:00",
                    "maxHoursPerDay": 6,
                    "breakMinutes": 10,
                    "titleBreakMinutes": 30,
                    "allowSplit": false,
                    "finishBy": "2026-10-25"
                  }
                },
                "example2": {
                  "summary": "A 5-season drama at 2 hours a night",
                  "value": {
                    "titles": [
                      {
                        "title": "Drama",
                        "minutes": 47,
                        "episodes": 10,
                        "seasons": 5
                      }
                    ],
                    "start": "2026-11-01T20:00",
                    "dailyStart": "20:00",
                    "maxHoursPerDay": 2,
                    "breakMinutes": 5,
                    "titleBreakMinutes": 20,
                    "allowSplit": false,
                    "finishBy": "2026-11-20"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeMarathonPlannerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workflows": {
      "get": {
        "operationId": "listWorkflows",
        "summary": "Multi-tool workflows",
        "responses": {
          "200": {
            "description": "Workflow list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "WeeklyMealPlannerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "startDate": {
            "title": "First day",
            "description": "Default: today.",
            "type": "string",
            "format": "date"
          },
          "days": {
            "title": "Number of days",
            "type": "integer",
            "minimum": 1,
            "maximum": 14,
            "default": 7
          },
          "servings": {
            "title": "People per meal",
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "default": 2
          },
          "diet": {
            "title": "Diet",
            "type": "string",
            "enum": [
              "any",
              "vegetarian",
              "vegan",
              "pescatarian"
            ],
            "default": "any"
          },
          "maxMinutes": {
            "title": "Maximum cooking time",
            "type": "integer",
            "minimum": 10,
            "maximum": 240,
            "default": 45,
            "x-unit": "min"
          },
          "pantry": {
            "title": "Pantry and fridge",
            "description": "Leave the quantity empty if you are not sure (counted as enough). Best-before dates make the planner use those items first.",
            "type": "array",
            "maxItems": 40,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "item": {
                  "title": "Item",
                  "type": "string",
                  "maxLength": 60
                },
                "quantity": {
                  "title": "Quantity",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 100000
                },
                "unit": {
                  "title": "Unit",
                  "type": "string",
                  "enum": [
                    "g",
                    "kg",
                    "ml",
                    "l",
                    "pcs"
                  ],
                  "default": "pcs"
                },
                "expires": {
                  "title": "Best before",
                  "type": "string",
                  "format": "date"
                }
              },
              "required": [
                "item"
              ]
            }
          },
          "exclude": {
            "title": "Avoid these ingredients",
            "description": "One per line, e.g. mushrooms or feta. Recipes containing them are skipped.",
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 30
          }
        },
        "required": []
      },
      "WeeklyMealPlannerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "startDate",
              "days",
              "servings",
              "diet",
              "plan",
              "shoppingList",
              "pantryUse",
              "stats",
              "datasetVersion"
            ],
            "properties": {
              "startDate": {
                "type": "string",
                "format": "date"
              },
              "days": {
                "type": "integer"
              },
              "servings": {
                "type": "integer"
              },
              "diet": {
                "type": "string"
              },
              "plan": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "day",
                    "date",
                    "weekday",
                    "recipeId",
                    "recipe",
                    "minutes",
                    "diet",
                    "usesFromPantry",
                    "toBuy",
                    "score"
                  ],
                  "properties": {
                    "day": {
                      "type": "integer"
                    },
                    "date": {
                      "type": "string"
                    },
                    "weekday": {
                      "type": "string"
                    },
                    "recipeId": {
                      "type": "string"
                    },
                    "recipe": {
                      "type": "string"
                    },
                    "minutes": {
                      "type": "integer"
                    },
                    "diet": {
                      "type": "string"
                    },
                    "usesFromPantry": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "item",
                          "amount",
                          "unit",
                          "daysToExpiry"
                        ],
                        "properties": {
                          "item": {
                            "type": "string"
                          },
                          "amount": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "unit": {
                            "type": "string"
                          },
                          "daysToExpiry": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "toBuy": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "item",
                          "amount",
                          "unit"
                        ],
                        "properties": {
                          "item": {
                            "type": "string"
                          },
                          "amount": {
                            "type": "number"
                          },
                          "unit": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "score": {
                      "type": "number"
                    }
                  }
                }
              },
              "shoppingList": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "item",
                    "amount",
                    "unit",
                    "forDays"
                  ],
                  "properties": {
                    "item": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "number"
                    },
                    "unit": {
                      "type": "string"
                    },
                    "forDays": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "pantryUse": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "item",
                    "unit",
                    "start",
                    "used",
                    "left",
                    "expires",
                    "status"
                  ],
                  "properties": {
                    "item": {
                      "type": "string"
                    },
                    "unit": {
                      "type": "string"
                    },
                    "start": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "used": {
                      "type": "number"
                    },
                    "left": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "expires": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "used up",
                        "partly used",
                        "not used",
                        "expires unused",
                        "expired before start",
                        "no recipe uses it"
                      ]
                    }
                  }
                }
              },
              "stats": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "candidateRecipes",
                  "expiringItems",
                  "expiringItemsUsed",
                  "shoppingItems",
                  "repeats"
                ],
                "properties": {
                  "candidateRecipes": {
                    "type": "integer"
                  },
                  "expiringItems": {
                    "type": "integer"
                  },
                  "expiringItemsUsed": {
                    "type": "integer"
                  },
                  "shoppingItems": {
                    "type": "integer"
                  },
                  "repeats": {
                    "type": "integer"
                  }
                }
              },
              "datasetVersion": {
                "type": "string"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "LeftoverChefInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "ingredients": {
            "title": "Ingredients you have",
            "description": "One per line, e.g. \"2 eggs\", \"leftover rice\", \"half a courgette\".",
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 60
          },
          "useSoon": {
            "title": "Must use soon",
            "description": "Items close to expiry get priority (added to the ingredients automatically).",
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 20
          },
          "diet": {
            "title": "Diet",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "vegetarian",
                "vegan",
                "gluten-free",
                "dairy-free"
              ]
            }
          },
          "equipment": {
            "title": "Equipment available",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "stove",
                "oven",
                "microwave",
                "blender"
              ]
            },
            "default": [
              "stove",
              "oven",
              "microwave"
            ]
          },
          "maxMinutes": {
            "title": "Maximum time",
            "type": "integer",
            "minimum": 5,
            "maximum": 240,
            "default": 45,
            "x-unit": "min"
          }
        },
        "required": [
          "ingredients"
        ]
      },
      "LeftoverChefResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "recognized",
              "recipes",
              "templatesConsidered",
              "templatesEligible"
            ],
            "properties": {
              "recognized": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "input",
                    "normalized",
                    "category",
                    "known"
                  ],
                  "properties": {
                    "input": {
                      "type": "string"
                    },
                    "normalized": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "known": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "recipes": {
                "type": "array",
                "maxItems": 5,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "rank",
                    "id",
                    "name",
                    "score",
                    "timeMinutes",
                    "uses",
                    "useSoonUsed",
                    "missing",
                    "optionalExtras",
                    "steps"
                  ],
                  "properties": {
                    "rank": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "score": {
                      "type": "number"
                    },
                    "timeMinutes": {
                      "type": "integer"
                    },
                    "uses": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "useSoonUsed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "optionalExtras": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "steps": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "templatesConsidered": {
                "type": "integer"
              },
              "templatesEligible": {
                "type": "integer"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "FoodStorageGuideInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "food": {
            "title": "Food",
            "type": "string",
            "enum": [
              "salads",
              "hot-dogs-opened",
              "hot-dogs-unopened",
              "luncheon-meat-opened",
              "luncheon-meat-unopened",
              "bacon",
              "sausage-raw",
              "sausage-cooked",
              "sausage-purchased-frozen",
              "ground-meat",
              "steaks",
              "chops",
              "roasts",
              "ham-fresh-uncooked",
              "ham-fresh-cooked",
              "ham-cured-uncooked",
              "ham-vacuum-sealed",
              "ham-cooked-whole",
              "ham-cooked-sliced",
              "country-ham-cooked",
              "canned-ham-keep-refrigerated",
              "canned-ham-shelf-stable-opened",
              "canned-ham-shelf-stable-unopened",
              "prosciutto",
              "poultry-whole",
              "poultry-pieces",
              "fatty-fish",
              "lean-fish-cod",
              "lean-fish-pollock",
              "crab-meat",
              "lobster",
              "live-crab-lobster",
              "live-clams",
              "shrimp",
              "shucked-clams",
              "squid",
              "eggs-in-shell",
              "egg-whites-yolks",
              "egg-frozen-in-shell",
              "hard-cooked-eggs",
              "egg-substitute-liquid-unopened",
              "egg-substitute-liquid-opened",
              "egg-substitute-frozen-unopened",
              "egg-substitute-frozen-opened",
              "egg-casseroles",
              "eggnog-commercial",
              "eggnog-homemade",
              "pie-pumpkin-pecan",
              "pie-custard-chiffon",
              "quiche",
              "soups-stews",
              "leftover-meat-poultry",
              "chicken-nuggets",
              "pizza"
            ],
            "default": "leftover-meat-poultry"
          },
          "storage": {
            "title": "Where is it stored?",
            "type": "string",
            "enum": [
              "fridge",
              "freezer",
              "pantry"
            ],
            "default": "fridge"
          },
          "dateStored": {
            "title": "Date stored / opened / cooked",
            "description": "Leave empty to use today.",
            "type": "string",
            "format": "date"
          },
          "today": {
            "title": "Today (optional)",
            "description": "Defaults to the current date.",
            "type": "string",
            "format": "date"
          }
        },
        "required": []
      },
      "FoodStorageGuideResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "food",
              "type",
              "storage",
              "dateStored",
              "today",
              "daysElapsed",
              "status",
              "methods"
            ],
            "properties": {
              "food": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "storage": {
                "type": "string",
                "enum": [
                  "fridge",
                  "freezer",
                  "pantry"
                ]
              },
              "dateStored": {
                "type": "string",
                "format": "date"
              },
              "today": {
                "type": "string",
                "format": "date"
              },
              "daysElapsed": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "fine",
                  "use-soon",
                  "discard",
                  "quality-past",
                  "not-recommended",
                  "see-note",
                  "no-data"
                ]
              },
              "methods": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "method",
                    "published",
                    "minDays",
                    "maxDays",
                    "useByEarliest",
                    "useByLatest",
                    "condition",
                    "orUseByDate",
                    "status",
                    "qualityOnly"
                  ],
                  "properties": {
                    "method": {
                      "type": "string",
                      "enum": [
                        "fridge",
                        "freezer",
                        "pantry"
                      ]
                    },
                    "published": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Verbatim chart text."
                    },
                    "minDays": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "maxDays": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "useByEarliest": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date",
                      "description": "Stored date + lower limit."
                    },
                    "useByLatest": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date",
                      "description": "Stored date + upper limit."
                    },
                    "condition": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "e.g. \"After baking\" – the period starts after that event."
                    },
                    "orUseByDate": {
                      "type": "boolean",
                      "description": "Chart says to follow the package \"use by\" date as alternative."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "fine",
                        "use-soon",
                        "discard",
                        "quality-past",
                        "not-recommended",
                        "see-note",
                        "no-data"
                      ]
                    },
                    "qualityOnly": {
                      "type": "boolean",
                      "description": "True for freezer times: the chart states they are for quality only."
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "HouseholdChoreRotatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "startDate": {
            "title": "Start date",
            "description": "Default: today. Weeks are counted from this day.",
            "type": "string",
            "format": "date"
          },
          "weeks": {
            "title": "Weeks to plan",
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "default": 4
          },
          "chores": {
            "title": "Chores",
            "type": "array",
            "minItems": 1,
            "maxItems": 30,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Chore",
                  "type": "string",
                  "maxLength": 60
                },
                "frequency": {
                  "title": "How often",
                  "type": "string",
                  "enum": [
                    "daily",
                    "weekly",
                    "biweekly",
                    "monthly"
                  ],
                  "default": "weekly"
                },
                "minutes": {
                  "title": "Effort",
                  "type": "number",
                  "minimum": 1,
                  "maximum": 600,
                  "default": 15,
                  "x-unit": "min"
                },
                "day": {
                  "title": "Day",
                  "type": "string",
                  "enum": [
                    "any",
                    "mon",
                    "tue",
                    "wed",
                    "thu",
                    "fri",
                    "sat",
                    "sun"
                  ],
                  "default": "any"
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "people": {
            "title": "People",
            "description": "Available days like Mon-Fri or Sat,Sun (empty = every day). Capacity: 1 = full share, 0.5 = half share (e.g. a child).",
            "type": "array",
            "minItems": 1,
            "maxItems": 12,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 40
                },
                "days": {
                  "title": "Available days",
                  "type": "string",
                  "maxLength": 60
                },
                "capacity": {
                  "title": "Capacity",
                  "type": "number",
                  "minimum": 0.1,
                  "maximum": 3,
                  "default": 1
                }
              },
              "required": [
                "name"
              ]
            }
          }
        },
        "required": [
          "chores",
          "people"
        ]
      },
      "HouseholdChoreRotatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "startDate",
              "endDate",
              "weeks",
              "assignments",
              "totals",
              "unassigned",
              "ics"
            ],
            "properties": {
              "startDate": {
                "type": "string",
                "format": "date"
              },
              "endDate": {
                "type": "string",
                "format": "date"
              },
              "weeks": {
                "type": "integer"
              },
              "assignments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "date",
                    "weekday",
                    "week",
                    "chore",
                    "person",
                    "minutes"
                  ],
                  "properties": {
                    "date": {
                      "type": "string"
                    },
                    "weekday": {
                      "type": "string"
                    },
                    "week": {
                      "type": "integer"
                    },
                    "chore": {
                      "type": "string"
                    },
                    "person": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "minutes": {
                      "type": "number"
                    }
                  }
                }
              },
              "totals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "person",
                    "capacity",
                    "tasks",
                    "minutes",
                    "sharePercent",
                    "targetSharePercent",
                    "minutesPerWeek"
                  ],
                  "properties": {
                    "person": {
                      "type": "string"
                    },
                    "capacity": {
                      "type": "number"
                    },
                    "tasks": {
                      "type": "integer"
                    },
                    "minutes": {
                      "type": "number"
                    },
                    "sharePercent": {
                      "type": "number"
                    },
                    "targetSharePercent": {
                      "type": "number"
                    },
                    "minutesPerWeek": {
                      "type": "array",
                      "items": {
                        "type": "number"
                      }
                    }
                  }
                }
              },
              "unassigned": {
                "type": "integer",
                "description": "Occurrences on days when nobody is available."
              },
              "ics": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "filename",
                  "events"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "events": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "ChoreRouletteInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "people": {
            "title": "People",
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 40
                },
                "capacity": {
                  "title": "Capacity",
                  "description": "1 = a full share; 0.5 = half a share (e.g. a younger child).",
                  "type": "number",
                  "minimum": 0.1,
                  "maximum": 10,
                  "default": 1
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "chores": {
            "title": "Chores",
            "type": "array",
            "minItems": 1,
            "maxItems": 60,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "chore": {
                  "title": "Chore",
                  "type": "string",
                  "maxLength": 60
                },
                "effort": {
                  "title": "Effort points",
                  "description": "E.g. minutes per week, or 1–5 for how unpleasant it is.",
                  "type": "number",
                  "minimum": 0.1,
                  "maximum": 100,
                  "default": 1
                },
                "count": {
                  "title": "Times per week",
                  "description": "Each occurrence is assigned separately (e.g. dishes ×7 shares out the days).",
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 14,
                  "default": 1
                }
              },
              "required": [
                "chore"
              ]
            }
          },
          "lastWeek": {
            "title": "Last week (optional)",
            "description": "Who did which chore last week, for the “not again” rule.",
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "chore": {
                  "title": "Chore",
                  "type": "string",
                  "maxLength": 60
                },
                "person": {
                  "title": "Person",
                  "type": "string",
                  "maxLength": 40
                }
              },
              "required": [
                "chore",
                "person"
              ]
            }
          },
          "avoidRepeats": {
            "title": "Avoid giving anyone the same chore as last week",
            "type": "boolean",
            "default": true
          },
          "seed": {
            "title": "Seed (optional)",
            "description": "Same seed + same inputs = same draw. Leave empty to derive it from the inputs; change it (e.g. to the week number) to re-spin.",
            "type": "string",
            "maxLength": 60
          }
        },
        "required": [
          "people",
          "chores"
        ]
      },
      "ChoreRouletteResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "seed",
              "seedDerived",
              "assignments",
              "loads",
              "totalEffort",
              "spread",
              "fairnessPercent",
              "repeats",
              "unavoidableRepeats"
            ],
            "properties": {
              "seed": {
                "type": "string"
              },
              "seedDerived": {
                "type": "boolean"
              },
              "assignments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "chore",
                    "effort",
                    "person",
                    "repeatOfLastWeek"
                  ],
                  "properties": {
                    "chore": {
                      "type": "string"
                    },
                    "effort": {
                      "type": "number"
                    },
                    "person": {
                      "type": "string"
                    },
                    "repeatOfLastWeek": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "loads": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "person",
                    "capacity",
                    "effort",
                    "target",
                    "share",
                    "chores",
                    "deviation"
                  ],
                  "properties": {
                    "person": {
                      "type": "string"
                    },
                    "capacity": {
                      "type": "number"
                    },
                    "effort": {
                      "type": "number"
                    },
                    "target": {
                      "type": "number"
                    },
                    "share": {
                      "type": "number"
                    },
                    "chores": {
                      "type": "integer"
                    },
                    "deviation": {
                      "type": "number"
                    }
                  }
                }
              },
              "totalEffort": {
                "type": "number"
              },
              "spread": {
                "type": "number",
                "description": "Max − min of effort ÷ capacity, i.e. points per full share (0 = perfectly even)."
              },
              "fairnessPercent": {
                "type": "number",
                "description": "100 × (1 − Σ|effort − fair share| ÷ (2 × total effort)); 100 = everyone exactly at their fair share."
              },
              "repeats": {
                "type": "integer"
              },
              "unavoidableRepeats": {
                "type": "integer"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "HouseholdMaintenanceCalendarInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "homeType": {
            "title": "Home type",
            "type": "string",
            "enum": [
              "apartment",
              "house",
              "townhouse"
            ],
            "default": "apartment"
          },
          "features": {
            "title": "Your home has",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "gas-heating",
                "heat-pump",
                "ac",
                "smoke-detectors",
                "co-detectors",
                "water-heater",
                "dishwasher",
                "washing-machine",
                "dryer",
                "fridge",
                "gutters",
                "chimney",
                "garden",
                "pool",
                "septic",
                "solar",
                "car"
              ]
            },
            "default": [
              "smoke-detectors",
              "dishwasher",
              "washing-machine",
              "fridge"
            ]
          },
          "hemisphere": {
            "title": "Hemisphere (for seasons)",
            "type": "string",
            "enum": [
              "north",
              "south"
            ],
            "default": "north"
          },
          "startDate": {
            "title": "Start date",
            "type": "string",
            "format": "date"
          },
          "months": {
            "title": "Months to plan",
            "type": "integer",
            "minimum": 1,
            "maximum": 24,
            "default": 12
          }
        },
        "required": [
          "startDate"
        ]
      },
      "HouseholdMaintenanceCalendarResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "startDate",
              "endDate",
              "taskCount",
              "occurrenceCount",
              "tasks",
              "occurrences"
            ],
            "properties": {
              "startDate": {
                "type": "string",
                "format": "date"
              },
              "endDate": {
                "type": "string",
                "format": "date"
              },
              "taskCount": {
                "type": "integer",
                "minimum": 0
              },
              "occurrenceCount": {
                "type": "integer",
                "minimum": 0
              },
              "tasks": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "title",
                    "category",
                    "interval",
                    "rrule"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "interval": {
                      "type": "string"
                    },
                    "rrule": {
                      "type": "string"
                    },
                    "source": {
                      "type": "string",
                      "format": "uri"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              },
              "occurrences": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "date",
                    "taskId",
                    "title",
                    "category"
                  ],
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "taskId": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "DisasterPrepBuilderInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "guideline": {
            "title": "Guideline",
            "type": "string",
            "enum": [
              "readygov",
              "bbk"
            ],
            "default": "readygov"
          },
          "adults": {
            "title": "Adults (and teenagers)",
            "type": "integer",
            "minimum": 0,
            "maximum": 50,
            "default": 2
          },
          "children": {
            "title": "Children",
            "type": "integer",
            "minimum": 0,
            "maximum": 50,
            "default": 0
          },
          "infants": {
            "title": "Infants",
            "type": "integer",
            "minimum": 0,
            "maximum": 20,
            "default": 0
          },
          "dogs": {
            "title": "Dogs",
            "type": "integer",
            "minimum": 0,
            "maximum": 20,
            "default": 0
          },
          "cats": {
            "title": "Cats",
            "type": "integer",
            "minimum": 0,
            "maximum": 20,
            "default": 0
          },
          "days": {
            "title": "Days of supply",
            "description": "Empty = guideline default (BBK: 10 days; Ready.gov says \"several days\", this tool uses 3).",
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "x-unit": "days"
          },
          "hazards": {
            "title": "Hazards to prepare for",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "power-outage",
                "flood",
                "heat",
                "winter-storm",
                "earthquake",
                "wildfire",
                "pandemic"
              ]
            },
            "default": [
              "power-outage"
            ]
          },
          "medicalNeeds": {
            "title": "Someone needs regular medication or medical devices",
            "type": "boolean",
            "default": false
          },
          "kcalPerAdultDay": {
            "title": "Food energy per adult per day",
            "description": "2,200 kcal is the basis of the German BLE stock calculator; Ready.gov gives no calorie figure.",
            "type": "number",
            "minimum": 500,
            "maximum": 6000,
            "default": 2200,
            "x-unit": "kcal"
          },
          "kcalPerChildDay": {
            "title": "Food energy per child per day",
            "description": "Tool assumption; adjust to your children's ages.",
            "type": "number",
            "minimum": 300,
            "maximum": 4000,
            "default": 1500,
            "x-unit": "kcal"
          },
          "dogWaterLitersPerDay": {
            "title": "Water per dog per day (optional)",
            "description": "The sources give no amount; ask your vet. Leave empty to list pet water without a quantity.",
            "type": "number",
            "minimum": 0,
            "maximum": 20,
            "x-unit": "L"
          },
          "catWaterLitersPerDay": {
            "title": "Water per cat per day (optional)",
            "type": "number",
            "minimum": 0,
            "maximum": 5,
            "x-unit": "L"
          },
          "currency": {
            "title": "Currency",
            "type": "string",
            "maxLength": 8,
            "default": "EUR"
          },
          "budget": {
            "title": "Budget (optional)",
            "type": "number",
            "minimum": 0
          },
          "unitPrices": {
            "title": "Your unit prices (optional)",
            "description": "Costs are only estimated for items you price here.",
            "type": "array",
            "maxItems": 7,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "item": {
                  "title": "Item",
                  "type": "string",
                  "enum": [
                    "water-liter",
                    "food-1000kcal",
                    "flashlight",
                    "battery-set",
                    "radio",
                    "first-aid-kit",
                    "sleeping-bag-blanket"
                  ]
                },
                "price": {
                  "title": "Price per unit",
                  "type": "number",
                  "minimum": 0
                }
              },
              "required": [
                "item",
                "price"
              ]
            }
          }
        },
        "required": [
          "adults"
        ]
      },
      "DisasterPrepBuilderResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "guideline",
              "days",
              "people",
              "waterLiters",
              "waterLitersPerPersonPerDay",
              "waterLitersHotWeather",
              "petWaterLiters",
              "foodKcal",
              "quantities",
              "checklist",
              "hazardAdditions",
              "cost"
            ],
            "properties": {
              "guideline": {
                "type": "string",
                "enum": [
                  "readygov",
                  "bbk"
                ]
              },
              "days": {
                "type": "integer"
              },
              "people": {
                "type": "integer"
              },
              "waterLitersPerPersonPerDay": {
                "type": "number"
              },
              "waterLiters": {
                "type": "number",
                "description": "people × liters per person per day × days."
              },
              "waterLitersHotWeather": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Double the water figure (Ready.gov: in very hot temperatures water needs can double); only when heat is selected."
              },
              "petWaterLiters": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Only if pet water per day was entered."
              },
              "foodKcal": {
                "type": "number",
                "description": "(adults × kcal/adult + children × kcal/child) × days; infants excluded (formula/baby food listed separately)."
              },
              "quantities": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "item",
                    "quantity",
                    "unit",
                    "basis"
                  ],
                  "properties": {
                    "item": {
                      "type": "string"
                    },
                    "quantity": {
                      "type": "number"
                    },
                    "unit": {
                      "type": "string"
                    },
                    "basis": {
                      "type": "string"
                    }
                  }
                }
              },
              "checklist": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "group",
                    "items",
                    "source"
                  ],
                  "properties": {
                    "group": {
                      "type": "string"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "source": {
                      "type": "string"
                    }
                  }
                }
              },
              "hazardAdditions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "hazard",
                    "items",
                    "source"
                  ],
                  "properties": {
                    "hazard": {
                      "type": "string"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "source": {
                      "type": "string"
                    }
                  }
                }
              },
              "cost": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "required": [
                  "currency",
                  "lines",
                  "total",
                  "budget",
                  "withinBudget"
                ],
                "properties": {
                  "currency": {
                    "type": "string"
                  },
                  "total": {
                    "type": "number"
                  },
                  "budget": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "withinBudget": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "lines": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "item",
                        "quantity",
                        "unitPrice",
                        "cost"
                      ],
                      "properties": {
                        "item": {
                          "type": "string"
                        },
                        "quantity": {
                          "type": "number"
                        },
                        "unitPrice": {
                          "type": "number"
                        },
                        "cost": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "MovingBoxPlannerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "rooms": {
            "title": "Rooms to pack",
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "type": {
                  "title": "Room",
                  "type": "string",
                  "enum": [
                    "bedroom",
                    "living",
                    "kitchen",
                    "bathroom",
                    "office",
                    "kids",
                    "garage",
                    "storage"
                  ]
                },
                "size": {
                  "title": "Size / fullness",
                  "type": "string",
                  "enum": [
                    "small",
                    "medium",
                    "large"
                  ],
                  "default": "medium"
                }
              },
              "required": [
                "type"
              ]
            }
          },
          "people": {
            "title": "People in household",
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "default": 2
          },
          "density": {
            "title": "How much stuff?",
            "type": "string",
            "enum": [
              "minimalist",
              "average",
              "collector"
            ],
            "default": "average"
          },
          "distanceKm": {
            "title": "Driving distance",
            "type": "number",
            "minimum": 0,
            "maximum": 5000,
            "default": 20,
            "x-unit": "km"
          },
          "floorsWithoutElevator": {
            "title": "Floors without elevator (old + new home)",
            "type": "integer",
            "minimum": 0,
            "maximum": 20,
            "default": 0
          },
          "moveDate": {
            "title": "Moving day",
            "type": "string",
            "format": "date"
          },
          "moverHourlyRate": {
            "title": "Mover rate per person-hour",
            "description": "Optional; cost is only calculated if given.",
            "type": "number",
            "minimum": 0,
            "maximum": 1000
          },
          "truckHourlyRate": {
            "title": "Truck rate per hour",
            "description": "Optional.",
            "type": "number",
            "minimum": 0,
            "maximum": 1000
          },
          "currency": {
            "title": "Currency",
            "type": "string",
            "maxLength": 8,
            "default": "EUR"
          }
        },
        "required": [
          "rooms",
          "moveDate"
        ]
      },
      "MovingBoxPlannerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "boxes",
              "totalBoxes",
              "supplies",
              "volume",
              "truck",
              "crew",
              "cost",
              "timeline"
            ],
            "properties": {
              "boxes": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "small",
                  "medium",
                  "large",
                  "wardrobe"
                ],
                "properties": {
                  "small": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "medium": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "large": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "wardrobe": {
                    "type": "integer",
                    "minimum": 0
                  }
                }
              },
              "totalBoxes": {
                "type": "integer",
                "minimum": 0
              },
              "perRoom": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "room",
                    "size",
                    "small",
                    "medium",
                    "large",
                    "wardrobe",
                    "furnitureM3"
                  ],
                  "properties": {
                    "room": {
                      "type": "string"
                    },
                    "size": {
                      "type": "string"
                    },
                    "small": {
                      "type": "integer"
                    },
                    "medium": {
                      "type": "integer"
                    },
                    "large": {
                      "type": "integer"
                    },
                    "wardrobe": {
                      "type": "integer"
                    },
                    "furnitureM3": {
                      "type": "number"
                    }
                  }
                }
              },
              "supplies": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "tapeRolls",
                  "packingPaperKg",
                  "bubbleWrapM",
                  "markers"
                ],
                "properties": {
                  "tapeRolls": {
                    "type": "integer"
                  },
                  "packingPaperKg": {
                    "type": "number"
                  },
                  "bubbleWrapM": {
                    "type": "number"
                  },
                  "markers": {
                    "type": "integer"
                  }
                }
              },
              "volume": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "boxesM3",
                  "furnitureM3",
                  "loadM3"
                ],
                "properties": {
                  "boxesM3": {
                    "type": "number"
                  },
                  "furnitureM3": {
                    "type": "number"
                  },
                  "loadM3": {
                    "type": "number",
                    "description": "(boxes + furniture) × 1.15 stowage factor."
                  }
                }
              },
              "truck": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "vehicle",
                  "vehicleM3",
                  "trips"
                ],
                "properties": {
                  "vehicle": {
                    "type": "string"
                  },
                  "vehicleM3": {
                    "type": "number"
                  },
                  "trips": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              },
              "crew": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "people",
                  "loadingHours",
                  "drivingHours",
                  "totalHours"
                ],
                "properties": {
                  "people": {
                    "type": "integer"
                  },
                  "loadingHours": {
                    "type": "number"
                  },
                  "drivingHours": {
                    "type": "number"
                  },
                  "totalHours": {
                    "type": "number"
                  }
                }
              },
              "cost": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "calculated",
                  "labour",
                  "truck",
                  "total",
                  "currency"
                ],
                "properties": {
                  "calculated": {
                    "type": "boolean"
                  },
                  "labour": {
                    "type": "number"
                  },
                  "truck": {
                    "type": "number"
                  },
                  "total": {
                    "type": "number"
                  },
                  "currency": {
                    "type": "string"
                  }
                }
              },
              "timeline": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "date",
                    "task"
                  ],
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "task": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "WillItFitInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "itemLength": {
            "title": "Item length",
            "type": "number",
            "minimum": 1,
            "maximum": 2000,
            "x-unit": "cm"
          },
          "itemWidth": {
            "title": "Item width/depth",
            "type": "number",
            "minimum": 1,
            "maximum": 2000,
            "x-unit": "cm"
          },
          "itemHeight": {
            "title": "Item height",
            "type": "number",
            "minimum": 1,
            "maximum": 2000,
            "x-unit": "cm"
          },
          "clearance": {
            "title": "Safety clearance",
            "description": "Subtracted from every opening dimension (hands, padding, wobble).",
            "type": "number",
            "minimum": 0,
            "maximum": 30,
            "default": 2,
            "x-unit": "cm"
          },
          "doorWidth": {
            "title": "Door clear width",
            "description": "Measure between door stops with the door fully open.",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "doorHeight": {
            "title": "Door clear height",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "hallwayWidthBefore": {
            "title": "Hallway width before corner",
            "description": "Leave empty to skip the corner check.",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "hallwayWidthAfter": {
            "title": "Hallway width after corner",
            "description": "Defaults to the width before the corner.",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "ceilingHeight": {
            "title": "Hallway ceiling height",
            "description": "Used to tilt the item upward in the corner.",
            "type": "number",
            "minimum": 50,
            "maximum": 1000,
            "default": 250,
            "x-unit": "cm"
          },
          "stairsWidth": {
            "title": "Stair width",
            "description": "Leave empty to skip stairs.",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "stairsHeadroom": {
            "title": "Stair headroom (perpendicular to pitch)",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "elevatorDoorWidth": {
            "title": "Elevator door width",
            "description": "Leave elevator fields empty to skip.",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "elevatorDoorHeight": {
            "title": "Elevator door height",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "elevatorCabinWidth": {
            "title": "Elevator cabin width",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "elevatorCabinDepth": {
            "title": "Elevator cabin depth",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "elevatorCabinHeight": {
            "title": "Elevator cabin height",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          },
          "roomWidth": {
            "title": "Room width",
            "description": "Leave empty to skip the room check.",
            "type": "number",
            "minimum": 10,
            "maximum": 5000,
            "x-unit": "cm"
          },
          "roomDepth": {
            "title": "Room depth",
            "type": "number",
            "minimum": 10,
            "maximum": 5000,
            "x-unit": "cm"
          },
          "roomHeight": {
            "title": "Room height",
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "x-unit": "cm"
          }
        },
        "required": [
          "itemLength",
          "itemWidth",
          "itemHeight",
          "doorWidth",
          "doorHeight"
        ]
      },
      "WillItFitResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "item",
              "clearanceCm",
              "overallFits",
              "bottleneck",
              "checkpoints"
            ],
            "properties": {
              "item": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "lengthCm",
                  "widthCm",
                  "heightCm",
                  "diagonalCm"
                ],
                "properties": {
                  "lengthCm": {
                    "type": "number"
                  },
                  "widthCm": {
                    "type": "number"
                  },
                  "heightCm": {
                    "type": "number"
                  },
                  "diagonalCm": {
                    "type": "number"
                  }
                }
              },
              "clearanceCm": {
                "type": "number",
                "minimum": 0
              },
              "overallFits": {
                "type": "boolean"
              },
              "bottleneck": {
                "type": "string",
                "description": "Checkpoint id with the smallest margin."
              },
              "checkpoints": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "name",
                    "fits",
                    "marginCm",
                    "orientation",
                    "detail"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "enum": [
                        "door",
                        "corner",
                        "stairs",
                        "elevator-door",
                        "elevator-cabin",
                        "room"
                      ]
                    },
                    "name": {
                      "type": "string"
                    },
                    "fits": {
                      "type": "boolean"
                    },
                    "marginCm": {
                      "type": "number",
                      "description": "Spare centimetres after clearance; negative = too big by that much."
                    },
                    "orientation": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "RoomLayoutOptimizerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "roomWidth": {
            "title": "Room width (west → east)",
            "type": "number",
            "minimum": 100,
            "maximum": 2000,
            "x-unit": "cm"
          },
          "roomDepth": {
            "title": "Room depth (north → south)",
            "type": "number",
            "minimum": 100,
            "maximum": 2000,
            "x-unit": "cm"
          },
          "gridCm": {
            "title": "Grid size",
            "description": "Positions snap to this grid; furniture sizes are rounded up to it.",
            "type": "integer",
            "minimum": 5,
            "maximum": 50,
            "default": 10,
            "x-unit": "cm"
          },
          "walkwayCm": {
            "title": "Minimum walkway width",
            "type": "integer",
            "minimum": 30,
            "maximum": 150,
            "default": 60,
            "x-unit": "cm"
          },
          "openings": {
            "title": "Doors and windows",
            "description": "Offset is measured along the wall from its west end (north/south walls) or north end (east/west walls).",
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "kind": {
                  "title": "Type",
                  "type": "string",
                  "enum": [
                    "door",
                    "window"
                  ]
                },
                "wall": {
                  "title": "Wall",
                  "type": "string",
                  "enum": [
                    "north",
                    "east",
                    "south",
                    "west"
                  ]
                },
                "offset": {
                  "title": "Offset",
                  "type": "number",
                  "minimum": 0,
                  "x-unit": "cm"
                },
                "width": {
                  "title": "Width",
                  "type": "number",
                  "minimum": 30,
                  "maximum": 500,
                  "x-unit": "cm"
                }
              },
              "required": [
                "kind",
                "wall",
                "offset",
                "width"
              ]
            }
          },
          "furniture": {
            "title": "Furniture",
            "description": "Width = side along its back; depth = front to back. Clearance = free space needed in front (to open drawers, get into bed, sit).",
            "type": "array",
            "minItems": 1,
            "maxItems": 15,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 40
                },
                "width": {
                  "title": "Width",
                  "type": "number",
                  "minimum": 10,
                  "maximum": 1000,
                  "x-unit": "cm"
                },
                "depth": {
                  "title": "Depth",
                  "type": "number",
                  "minimum": 10,
                  "maximum": 1000,
                  "x-unit": "cm"
                },
                "needsWall": {
                  "title": "Back against wall",
                  "type": "boolean",
                  "default": true
                },
                "clearanceFront": {
                  "title": "Clearance in front",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 300,
                  "default": 60,
                  "x-unit": "cm"
                },
                "nearWindow": {
                  "title": "Prefer near window",
                  "type": "boolean",
                  "default": false
                }
              },
              "required": [
                "name",
                "width",
                "depth"
              ]
            }
          }
        },
        "required": [
          "roomWidth",
          "roomDepth",
          "furniture"
        ]
      },
      "RoomLayoutOptimizerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "roomWidthCm",
              "roomDepthCm",
              "gridCm",
              "placed",
              "unplaced",
              "walkwayAreaM2",
              "freeFloorAreaM2",
              "score"
            ],
            "properties": {
              "roomWidthCm": {
                "type": "number"
              },
              "roomDepthCm": {
                "type": "number"
              },
              "gridCm": {
                "type": "integer"
              },
              "placed": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "label",
                    "name",
                    "xCm",
                    "yCm",
                    "sizeXCm",
                    "sizeYCm",
                    "facing",
                    "againstWall",
                    "accessible"
                  ],
                  "properties": {
                    "label": {
                      "type": "string",
                      "description": "Letter used in the text plan."
                    },
                    "name": {
                      "type": "string"
                    },
                    "xCm": {
                      "type": "number",
                      "description": "Distance of the item’s west edge from the west wall."
                    },
                    "yCm": {
                      "type": "number",
                      "description": "Distance of the item’s north edge from the north wall."
                    },
                    "sizeXCm": {
                      "type": "number"
                    },
                    "sizeYCm": {
                      "type": "number"
                    },
                    "facing": {
                      "type": "string",
                      "enum": [
                        "north",
                        "east",
                        "south",
                        "west"
                      ],
                      "description": "Direction the front of the item faces."
                    },
                    "againstWall": {
                      "type": "string",
                      "enum": [
                        "north",
                        "east",
                        "south",
                        "west",
                        "none"
                      ]
                    },
                    "accessible": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "unplaced": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "reason"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              },
              "walkwayAreaM2": {
                "type": "number",
                "minimum": 0
              },
              "freeFloorAreaM2": {
                "type": "number",
                "minimum": 0
              },
              "score": {
                "type": "number"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "JetLagPlannerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "homeZone": {
            "title": "Home time zone",
            "type": "string",
            "enum": [
              "Pacific/Honolulu",
              "America/Anchorage",
              "America/Los_Angeles",
              "America/Denver",
              "America/Phoenix",
              "America/Chicago",
              "America/Mexico_City",
              "America/New_York",
              "America/Toronto",
              "America/Bogota",
              "America/Lima",
              "America/Santiago",
              "America/Sao_Paulo",
              "America/Argentina/Buenos_Aires",
              "Atlantic/Reykjavik",
              "Europe/London",
              "Europe/Lisbon",
              "Europe/Dublin",
              "Europe/Paris",
              "Europe/Berlin",
              "Europe/Madrid",
              "Europe/Rome",
              "Europe/Zurich",
              "Europe/Vienna",
              "Europe/Amsterdam",
              "Europe/Stockholm",
              "Europe/Warsaw",
              "Europe/Athens",
              "Europe/Istanbul",
              "Europe/Moscow",
              "Africa/Cairo",
              "Africa/Johannesburg",
              "Africa/Lagos",
              "Africa/Nairobi",
              "Asia/Dubai",
              "Asia/Karachi",
              "Asia/Kolkata",
              "Asia/Bangkok",
              "Asia/Singapore",
              "Asia/Shanghai",
              "Asia/Hong_Kong",
              "Asia/Seoul",
              "Asia/Tokyo",
              "Australia/Perth",
              "Australia/Sydney",
              "Pacific/Auckland"
            ],
            "default": "Europe/Berlin"
          },
          "destinationZone": {
            "title": "Destination time zone",
            "type": "string",
            "enum": [
              "Pacific/Honolulu",
              "America/Anchorage",
              "America/Los_Angeles",
              "America/Denver",
              "America/Phoenix",
              "America/Chicago",
              "America/Mexico_City",
              "America/New_York",
              "America/Toronto",
              "America/Bogota",
              "America/Lima",
              "America/Santiago",
              "America/Sao_Paulo",
              "America/Argentina/Buenos_Aires",
              "Atlantic/Reykjavik",
              "Europe/London",
              "Europe/Lisbon",
              "Europe/Dublin",
              "Europe/Paris",
              "Europe/Berlin",
              "Europe/Madrid",
              "Europe/Rome",
              "Europe/Zurich",
              "Europe/Vienna",
              "Europe/Amsterdam",
              "Europe/Stockholm",
              "Europe/Warsaw",
              "Europe/Athens",
              "Europe/Istanbul",
              "Europe/Moscow",
              "Africa/Cairo",
              "Africa/Johannesburg",
              "Africa/Lagos",
              "Africa/Nairobi",
              "Asia/Dubai",
              "Asia/Karachi",
              "Asia/Kolkata",
              "Asia/Bangkok",
              "Asia/Singapore",
              "Asia/Shanghai",
              "Asia/Hong_Kong",
              "Asia/Seoul",
              "Asia/Tokyo",
              "Australia/Perth",
              "Australia/Sydney",
              "Pacific/Auckland"
            ],
            "default": "America/New_York"
          },
          "homeZoneCustom": {
            "title": "Other home zone (IANA name)",
            "description": "Overrides the list if filled in.",
            "type": "string",
            "maxLength": 64
          },
          "destinationZoneCustom": {
            "title": "Other destination zone (IANA name)",
            "description": "Overrides the list if filled in.",
            "type": "string",
            "maxLength": 64
          },
          "departureDate": {
            "title": "Departure date",
            "type": "string",
            "format": "date"
          },
          "arrivalDate": {
            "title": "Arrival date (local)",
            "description": "Defaults to the departure date.",
            "type": "string",
            "format": "date"
          },
          "bedtime": {
            "title": "Usual bedtime",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
            "default": "23:00"
          },
          "wakeTime": {
            "title": "Usual wake time",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
            "default": "07:00"
          },
          "daysBefore": {
            "title": "Days to pre-adjust before departure",
            "type": "integer",
            "minimum": 0,
            "maximum": 7,
            "default": 3
          },
          "chronotype": {
            "title": "Chronotype",
            "type": "string",
            "enum": [
              "early",
              "intermediate",
              "late"
            ],
            "default": "intermediate"
          }
        },
        "required": [
          "departureDate"
        ]
      },
      "JetLagPlannerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "homeZone",
              "destinationZone",
              "offsetMinutes",
              "direction",
              "preShiftMinutes",
              "daysToAdapt",
              "schedule"
            ],
            "properties": {
              "homeZone": {
                "type": "string"
              },
              "destinationZone": {
                "type": "string"
              },
              "offsetMinutes": {
                "type": "integer",
                "description": "Destination UTC offset − home UTC offset on the travel dates, normalised to (−720, 720]."
              },
              "direction": {
                "type": "string",
                "enum": [
                  "east",
                  "west",
                  "none"
                ]
              },
              "preShiftMinutes": {
                "type": "integer",
                "minimum": 0
              },
              "daysToAdapt": {
                "type": "integer",
                "minimum": 0,
                "description": "Days after arrival until the body clock is estimated to match local time."
              },
              "schedule": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "date",
                    "phase",
                    "clock",
                    "bedtime",
                    "wakeTime",
                    "bodyTempMinimum",
                    "seekLight",
                    "avoidLight",
                    "caffeineCutoff",
                    "remainingShiftHours"
                  ],
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "phase": {
                      "type": "string",
                      "enum": [
                        "pre-flight",
                        "travel-day",
                        "post-arrival"
                      ]
                    },
                    "clock": {
                      "type": "string",
                      "enum": [
                        "home",
                        "destination"
                      ],
                      "description": "Which local time the times are expressed in."
                    },
                    "bedtime": {
                      "type": "string",
                      "pattern": "^\\d{2}:\\d{2}$"
                    },
                    "wakeTime": {
                      "type": "string",
                      "pattern": "^\\d{2}:\\d{2}$"
                    },
                    "bodyTempMinimum": {
                      "type": "string",
                      "pattern": "^\\d{2}:\\d{2}$"
                    },
                    "seekLight": {
                      "type": "string"
                    },
                    "avoidLight": {
                      "type": "string"
                    },
                    "caffeineCutoff": {
                      "type": "string",
                      "pattern": "^\\d{2}:\\d{2}$"
                    },
                    "remainingShiftHours": {
                      "type": "number",
                      "minimum": 0
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "TravelConnectionCheckerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "arrival": {
            "title": "Scheduled arrival (local)",
            "type": "string",
            "description": "Local date-time YYYY-MM-DDTHH:mm"
          },
          "departure": {
            "title": "Scheduled onward departure (local, same airport)",
            "type": "string",
            "description": "Local date-time YYYY-MM-DDTHH:mm"
          },
          "terminalChange": {
            "title": "Terminal change",
            "type": "boolean",
            "default": false
          },
          "immigration": {
            "title": "Passport / immigration check",
            "type": "boolean",
            "default": false
          },
          "securityRecheck": {
            "title": "Security screening again",
            "type": "boolean",
            "default": false
          },
          "baggageRecheck": {
            "title": "Collect and re-check baggage",
            "type": "boolean",
            "default": false
          },
          "airportSize": {
            "title": "Airport size",
            "type": "string",
            "enum": [
              "small",
              "medium",
              "large",
              "mega"
            ],
            "default": "large"
          },
          "walkingSpeed": {
            "title": "Walking speed",
            "type": "string",
            "enum": [
              "slow",
              "average",
              "fast"
            ],
            "default": "average"
          },
          "gateClosesMinutes": {
            "title": "Gate closes before departure",
            "description": "Check your boarding pass; often 10–20 minutes.",
            "type": "integer",
            "minimum": 0,
            "maximum": 60,
            "default": 15,
            "x-unit": "min"
          },
          "onTimeProbability": {
            "title": "Inbound flight on-time probability",
            "description": "Planning default; replace with the route’s on-time record if you know it.",
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 75,
            "x-unit": "%"
          },
          "meanDelayMinutes": {
            "title": "Average delay when late",
            "type": "number",
            "minimum": 1,
            "maximum": 600,
            "default": 35,
            "x-unit": "min"
          },
          "officialMct": {
            "title": "Official minimum connection time (if known)",
            "description": "From the airline/airport for your connection type.",
            "type": "integer",
            "minimum": 0,
            "maximum": 600,
            "x-unit": "min"
          }
        },
        "required": [
          "arrival",
          "departure"
        ]
      },
      "TravelConnectionCheckerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "connectionMinutes",
              "availableMinutes",
              "requiredMinutes",
              "bufferMinutes",
              "overallProbabilityPercent",
              "probabilityPercent",
              "verdict",
              "belowOfficialMct",
              "steps"
            ],
            "properties": {
              "connectionMinutes": {
                "type": "number",
                "description": "Scheduled departure − scheduled arrival."
              },
              "availableMinutes": {
                "type": "number",
                "description": "Connection minus gate-closure time."
              },
              "requiredMinutes": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "best",
                  "typical",
                  "worst"
                ],
                "properties": {
                  "best": {
                    "type": "number"
                  },
                  "typical": {
                    "type": "number"
                  },
                  "worst": {
                    "type": "number"
                  }
                }
              },
              "bufferMinutes": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "best",
                  "typical",
                  "worst"
                ],
                "properties": {
                  "best": {
                    "type": "number"
                  },
                  "typical": {
                    "type": "number"
                  },
                  "worst": {
                    "type": "number"
                  }
                }
              },
              "overallProbabilityPercent": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "Chance averaged over a triangular distribution of total processing time."
              },
              "probabilityPercent": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "best",
                  "typical",
                  "worst"
                ],
                "description": "Model chance of reaching the gate before it closes, for best/typical/worst processing times.",
                "properties": {
                  "best": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "typical": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "worst": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  }
                }
              },
              "verdict": {
                "type": "string",
                "enum": [
                  "comfortable",
                  "ok",
                  "tight",
                  "risky",
                  "very-risky"
                ]
              },
              "belowOfficialMct": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "step",
                    "best",
                    "typical",
                    "worst"
                  ],
                  "properties": {
                    "step": {
                      "type": "string"
                    },
                    "best": {
                      "type": "number"
                    },
                    "typical": {
                      "type": "number"
                    },
                    "worst": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "CurrencyConverterInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "amount": {
            "title": "Amount",
            "type": "number",
            "minimum": 0,
            "maximum": 1000000000000000,
            "default": 100
          },
          "from": {
            "title": "From currency",
            "description": "ISO code, e.g. EUR, USD, GBP, CHF, JPY.",
            "type": "string",
            "maxLength": 3,
            "default": "EUR"
          },
          "to": {
            "title": "To currencies",
            "description": "One or more ISO codes, comma-separated, or \"all\".",
            "type": "string",
            "maxLength": 200,
            "default": "USD, GBP, CHF, JPY"
          },
          "date": {
            "title": "Rate date",
            "description": "Optional: use the reference rates of this date (the last publication on or before it). Empty: latest rates.",
            "type": "string",
            "format": "date"
          }
        },
        "required": [
          "amount",
          "from",
          "to"
        ]
      },
      "CurrencyConverterResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "amount",
              "from",
              "rates",
              "conversions"
            ],
            "properties": {
              "amount": {
                "type": "number"
              },
              "from": {
                "type": "string"
              },
              "rates": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "date",
                  "source",
                  "url",
                  "fetchedAt",
                  "stale"
                ],
                "properties": {
                  "date": {
                    "type": "string",
                    "description": "Reference date of the rates."
                  },
                  "requestedDate": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "ecb-daily",
                      "ecb-sdmx",
                      "manual"
                    ]
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "fetchedAt": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "stale": {
                    "type": "boolean"
                  }
                }
              },
              "conversions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "currency",
                    "name",
                    "rate",
                    "inverseRate",
                    "amount",
                    "rounded"
                  ],
                  "properties": {
                    "currency": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "rate": {
                      "type": "number",
                      "description": "Units of the target currency per 1 unit of the source currency."
                    },
                    "inverseRate": {
                      "type": "number"
                    },
                    "amount": {
                      "type": "number"
                    },
                    "rounded": {
                      "type": "number",
                      "description": "Rounded to the currency’s minor units."
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "TravelExchangeFeeCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "amount": {
            "title": "Amount spent abroad",
            "description": "In the local (spending) currency.",
            "type": "number",
            "minimum": 0.01,
            "maximum": 1000000000,
            "default": 500
          },
          "spendCurrency": {
            "title": "Spending currency",
            "type": "string",
            "maxLength": 3,
            "default": "USD"
          },
          "homeCurrency": {
            "title": "Home (account) currency",
            "type": "string",
            "maxLength": 3,
            "default": "EUR"
          },
          "rateSource": {
            "title": "Reference rate",
            "type": "string",
            "enum": [
              "ecb",
              "manual"
            ],
            "default": "ecb"
          },
          "manualRate": {
            "title": "Manual rate: 1 home-currency unit =",
            "description": "…units of the spending currency (e.g. 1 EUR = 1.10 USD → 1.10). Used with \"Enter a rate manually\", or as fallback when the ECB is unreachable.",
            "type": "number",
            "minimum": 0.000001,
            "maximum": 1000000000
          },
          "options": {
            "title": "Payment options",
            "description": "Example values – replace them with your own card’s and provider’s terms. EU card issuers must state their conversion mark-up over the ECB rate.",
            "type": "array",
            "maxItems": 12,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Option",
                  "type": "string",
                  "maxLength": 80
                },
                "method": {
                  "title": "Type",
                  "type": "string",
                  "enum": [
                    "card",
                    "atm",
                    "dcc",
                    "exchange"
                  ],
                  "default": "card"
                },
                "feePercent": {
                  "title": "Fee %",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 50,
                  "x-unit": "%"
                },
                "markupPercent": {
                  "title": "Rate mark-up %",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 50,
                  "x-unit": "%"
                },
                "fixedFee": {
                  "title": "Fixed fee (home currency)",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 10000
                }
              },
              "required": []
            },
            "default": [
              {
                "name": "Card without foreign transaction fee",
                "method": "card",
                "feePercent": 0,
                "markupPercent": 0,
                "fixedFee": 0
              },
              {
                "name": "Card with foreign transaction fee",
                "method": "card",
                "feePercent": 1.75,
                "markupPercent": 0,
                "fixedFee": 0
              },
              {
                "name": "ATM cash withdrawal",
                "method": "atm",
                "feePercent": 1,
                "markupPercent": 0,
                "fixedFee": 4
              },
              {
                "name": "\"Pay in your currency\" (DCC) at the terminal",
                "method": "dcc",
                "feePercent": 0,
                "markupPercent": 5,
                "fixedFee": 0
              },
              {
                "name": "Exchange bureau cash",
                "method": "exchange",
                "feePercent": 0,
                "markupPercent": 7,
                "fixedFee": 0
              }
            ]
          },
          "atmWithdrawals": {
            "title": "Number of ATM withdrawals",
            "description": "The fixed fee of ATM options is charged per withdrawal.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 1
          }
        },
        "required": [
          "amount",
          "spendCurrency",
          "homeCurrency"
        ]
      },
      "TravelExchangeFeeCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "amount",
              "spendCurrency",
              "homeCurrency",
              "referenceRate",
              "referenceCost",
              "rates",
              "options",
              "cheapest",
              "mostExpensive"
            ],
            "properties": {
              "amount": {
                "type": "number"
              },
              "spendCurrency": {
                "type": "string"
              },
              "homeCurrency": {
                "type": "string"
              },
              "referenceRate": {
                "type": "number",
                "description": "Spending-currency units per 1 home-currency unit."
              },
              "referenceCost": {
                "type": "number"
              },
              "rates": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "date",
                  "source",
                  "url",
                  "fetchedAt",
                  "stale"
                ],
                "properties": {
                  "date": {
                    "type": "string",
                    "description": "Reference date of the rates."
                  },
                  "requestedDate": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "ecb-daily",
                      "ecb-sdmx",
                      "manual"
                    ]
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "fetchedAt": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "stale": {
                    "type": "boolean"
                  }
                }
              },
              "options": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "method",
                    "homeCost",
                    "extraCost",
                    "extraPercent",
                    "effectiveRate",
                    "breakdown"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "method": {
                      "type": "string"
                    },
                    "homeCost": {
                      "type": "number"
                    },
                    "extraCost": {
                      "type": "number"
                    },
                    "extraPercent": {
                      "type": "number"
                    },
                    "effectiveRate": {
                      "type": "number"
                    },
                    "breakdown": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "atReference",
                        "markup",
                        "percentFee",
                        "fixedFees"
                      ],
                      "properties": {
                        "atReference": {
                          "type": "number"
                        },
                        "markup": {
                          "type": "number"
                        },
                        "percentFee": {
                          "type": "number"
                        },
                        "fixedFees": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              },
              "cheapest": {
                "type": "string"
              },
              "mostExpensive": {
                "type": "string"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "TipBillSplitCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "symbol": {
            "title": "Currency symbol",
            "description": "Display only, e.g. €, $, £, CHF.",
            "type": "string",
            "maxLength": 4,
            "default": "€"
          },
          "billAmount": {
            "title": "Bill amount",
            "description": "The amount printed on the bill (before tip).",
            "type": "number",
            "minimum": 0.01,
            "maximum": 10000000,
            "default": 84.5
          },
          "taxMode": {
            "title": "Tax",
            "type": "string",
            "enum": [
              "included",
              "added",
              "none"
            ],
            "default": "included"
          },
          "taxPercent": {
            "title": "Tax rate",
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 19,
            "x-unit": "%"
          },
          "tipMode": {
            "title": "Tip given as",
            "type": "string",
            "enum": [
              "percent",
              "amount"
            ],
            "default": "percent"
          },
          "tipPercent": {
            "title": "Tip percentage",
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 10,
            "x-unit": "%"
          },
          "tipAmount": {
            "title": "Tip amount",
            "type": "number",
            "minimum": 0,
            "default": 0
          },
          "tipBase": {
            "title": "Tip calculated on",
            "type": "string",
            "enum": [
              "pre-tax",
              "post-tax"
            ],
            "default": "post-tax"
          },
          "splitMode": {
            "title": "Split",
            "type": "string",
            "enum": [
              "equal",
              "shares",
              "items"
            ],
            "default": "equal"
          },
          "people": {
            "title": "Number of people (equal split)",
            "description": "Used when the list of people below is empty.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 4
          },
          "persons": {
            "title": "People",
            "description": "Optional for an equal split. Shares: weight per person (default 1). Items: the bill amount of what each person ordered, in the same terms as the bill (with tax if tax is included); anything not assigned is shared equally.",
            "type": "array",
            "maxItems": 40,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 40
                },
                "shares": {
                  "title": "Shares",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 100
                },
                "items": {
                  "title": "Ordered items",
                  "type": "number",
                  "minimum": 0
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "rounding": {
            "title": "Rounding per person",
            "description": "Any rounding difference is added to (or taken from) the tip.",
            "type": "string",
            "enum": [
              "none",
              "up-0.1",
              "up-0.5",
              "up-1",
              "up-5",
              "nearest-0.05"
            ],
            "default": "none"
          }
        },
        "required": [
          "billAmount"
        ]
      },
      "TipBillSplitCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "symbol",
              "subtotalBeforeTax",
              "tax",
              "billTotal",
              "tipBase",
              "tip",
              "tipPercentOfBill",
              "grandTotal",
              "collected",
              "roundingAdded",
              "effectiveTip",
              "effectiveTipPercent",
              "people"
            ],
            "properties": {
              "symbol": {
                "type": "string"
              },
              "subtotalBeforeTax": {
                "type": "number"
              },
              "tax": {
                "type": "number"
              },
              "billTotal": {
                "type": "number",
                "description": "Bill including tax, before tip."
              },
              "tipBase": {
                "type": "number",
                "description": "Amount the tip percentage was applied to."
              },
              "tip": {
                "type": "number",
                "description": "Tip before per-person rounding."
              },
              "tipPercentOfBill": {
                "type": "number",
                "description": "Tip as % of the bill including tax."
              },
              "grandTotal": {
                "type": "number",
                "description": "Bill including tax + tip, before rounding."
              },
              "collected": {
                "type": "number",
                "description": "Sum of what everyone pays after rounding."
              },
              "roundingAdded": {
                "type": "number",
                "description": "collected − grandTotal (goes to the tip)."
              },
              "effectiveTip": {
                "type": "number"
              },
              "effectiveTipPercent": {
                "type": "number",
                "description": "Effective tip as % of the tip base."
              },
              "people": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "weight",
                    "billShare",
                    "taxShare",
                    "tipShare",
                    "exact",
                    "pays"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "weight": {
                      "type": "number",
                      "description": "Relative split weight: 1 per person (equal), the entered share (shares), or the ordered amount plus an equal part of anything unassigned, in cents (items)."
                    },
                    "sharePercent": {
                      "type": "number",
                      "description": "This person's share of the bill, tax and tip in percent (weight ÷ sum of weights × 100)."
                    },
                    "billShare": {
                      "type": "number"
                    },
                    "taxShare": {
                      "type": "number"
                    },
                    "tipShare": {
                      "type": "number"
                    },
                    "exact": {
                      "type": "number"
                    },
                    "pays": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "MovieNightConsensusPickerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "candidates": {
            "title": "Candidates (one per line)",
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 20
          },
          "ballots": {
            "title": "Ballots",
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "voter": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 40
                },
                "ranking": {
                  "title": "Ranking, best first",
                  "description": "Separate with “>” or commas. Unlisted films count as tied last.",
                  "type": "string",
                  "maxLength": 1000
                },
                "veto": {
                  "title": "Veto (optional)",
                  "description": "Films this person refuses to watch, separated by commas.",
                  "type": "string",
                  "maxLength": 400
                }
              },
              "required": [
                "ranking"
              ]
            }
          },
          "vetoMode": {
            "title": "Vetoes",
            "type": "string",
            "enum": [
              "remove",
              "ignore"
            ],
            "default": "remove"
          },
          "approvalCount": {
            "title": "Approval: top N of each ranking count as approved",
            "description": "Empty = half of the candidates, rounded up.",
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "seed": {
            "title": "Tie-break seed (optional)",
            "description": "Ties are broken by a seeded random order of the candidates. Empty = derived from the inputs.",
            "type": "string",
            "maxLength": 60
          }
        },
        "required": [
          "candidates",
          "ballots"
        ]
      },
      "MovieNightConsensusPickerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "seed",
              "candidates",
              "vetoed",
              "eligible",
              "voters",
              "methods",
              "condorcetWinner",
              "consensus",
              "recommendation",
              "borda",
              "approval",
              "irvRounds",
              "pairwise",
              "tieBreakOrder"
            ],
            "properties": {
              "seed": {
                "type": "string"
              },
              "candidates": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "vetoed": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "candidate",
                    "by"
                  ],
                  "properties": {
                    "candidate": {
                      "type": "string"
                    },
                    "by": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "eligible": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "voters": {
                "type": "integer"
              },
              "methods": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "method",
                    "winner",
                    "tieBroken",
                    "explanation"
                  ],
                  "properties": {
                    "method": {
                      "type": "string"
                    },
                    "winner": {
                      "type": "string"
                    },
                    "tieBroken": {
                      "type": "boolean"
                    },
                    "explanation": {
                      "type": "string"
                    }
                  }
                }
              },
              "condorcetWinner": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "consensus": {
                "type": "boolean",
                "description": "All four methods pick the same film."
              },
              "recommendation": {
                "type": "string"
              },
              "borda": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "candidate",
                    "points"
                  ],
                  "properties": {
                    "candidate": {
                      "type": "string"
                    },
                    "points": {
                      "type": "number"
                    }
                  }
                }
              },
              "approval": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "candidate",
                    "approvals"
                  ],
                  "properties": {
                    "candidate": {
                      "type": "string"
                    },
                    "approvals": {
                      "type": "integer"
                    }
                  }
                }
              },
              "irvRounds": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "round",
                    "counts",
                    "exhausted",
                    "eliminated",
                    "tieBreak"
                  ],
                  "properties": {
                    "round": {
                      "type": "integer"
                    },
                    "counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "exhausted": {
                      "type": "integer"
                    },
                    "eliminated": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tieBreak": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "pairwise": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "a",
                    "b",
                    "aOverB",
                    "bOverA"
                  ],
                  "properties": {
                    "a": {
                      "type": "string"
                    },
                    "b": {
                      "type": "string"
                    },
                    "aOverB": {
                      "type": "integer"
                    },
                    "bOverA": {
                      "type": "integer"
                    }
                  }
                }
              },
              "tieBreakOrder": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "RuntimeMarathonPlannerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "titles": {
            "title": "Movies and series",
            "description": "Movie: runtime in minutes. Series: minutes per episode, episodes per season and seasons.",
            "type": "array",
            "minItems": 1,
            "maxItems": 30,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "title": {
                  "title": "Title",
                  "type": "string",
                  "maxLength": 80
                },
                "minutes": {
                  "title": "Minutes (per episode)",
                  "type": "number",
                  "minimum": 1,
                  "maximum": 600
                },
                "episodes": {
                  "title": "Episodes per season",
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 500,
                  "default": 1
                },
                "seasons": {
                  "title": "Seasons",
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 60,
                  "default": 1
                }
              },
              "required": [
                "title",
                "minutes"
              ]
            }
          },
          "start": {
            "title": "Start",
            "description": "When the marathon begins (local time). Local date-time YYYY-MM-DDTHH:mm",
            "type": "string"
          },
          "dailyStart": {
            "title": "On the following days, start at",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
            "default": "18:00"
          },
          "maxHoursPerDay": {
            "title": "Maximum viewing per day",
            "description": "Watching time only; breaks come on top.",
            "type": "number",
            "minimum": 0.5,
            "maximum": 20,
            "default": 4,
            "x-unit": "h"
          },
          "breakMinutes": {
            "title": "Break between episodes",
            "type": "number",
            "minimum": 0,
            "maximum": 240,
            "default": 10,
            "x-unit": "min"
          },
          "titleBreakMinutes": {
            "title": "Break between titles",
            "type": "number",
            "minimum": 0,
            "maximum": 480,
            "default": 20,
            "x-unit": "min"
          },
          "allowSplit": {
            "title": "Allow pausing a movie or episode and continuing the next day",
            "type": "boolean",
            "default": false
          },
          "finishBy": {
            "title": "Finish by",
            "description": "Optional: checks whether you make it by the end of this day.",
            "type": "string",
            "format": "date"
          }
        },
        "required": [
          "titles",
          "start"
        ]
      },
      "RuntimeMarathonPlannerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "totalWatchMinutes",
              "totalBreakMinutes",
              "items",
              "days",
              "firstStart",
              "finish",
              "finishBy",
              "onTime",
              "requiredHoursPerDay",
              "titles",
              "perDay",
              "schedule",
              "sessions"
            ],
            "properties": {
              "totalWatchMinutes": {
                "type": "number"
              },
              "totalBreakMinutes": {
                "type": "number"
              },
              "items": {
                "type": "integer",
                "description": "Movies + episodes."
              },
              "days": {
                "type": "integer",
                "description": "Viewing days, counted from the start date to the last session."
              },
              "firstStart": {
                "type": "string"
              },
              "finish": {
                "type": "string",
                "description": "Local date-time the last item ends."
              },
              "finishBy": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "onTime": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "requiredHoursPerDay": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Viewing hours per day needed to finish by the date (watching time only)."
              },
              "requiredMaxHoursPerDay": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Not on time only: smallest daily maximum (0.1 h steps) that finishes by the date with whole movies/episodes (or with pausing, if allowed); null if on time or unreachable at 20 h."
              },
              "finishWithRequiredMax": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the last item would end with requiredMaxHoursPerDay."
              },
              "finishWithPausing": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Not on time and pausing off: when the last item would end at the current limit with pausing allowed."
              },
              "titles": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "title",
                    "items",
                    "minutes",
                    "start",
                    "end"
                  ],
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "items": {
                      "type": "integer"
                    },
                    "minutes": {
                      "type": "number"
                    },
                    "start": {
                      "type": "string"
                    },
                    "end": {
                      "type": "string"
                    }
                  }
                }
              },
              "perDay": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "day",
                    "date",
                    "watchMinutes",
                    "from",
                    "to"
                  ],
                  "properties": {
                    "day": {
                      "type": "integer"
                    },
                    "date": {
                      "type": "string"
                    },
                    "watchMinutes": {
                      "type": "number"
                    },
                    "from": {
                      "type": "string"
                    },
                    "to": {
                      "type": "string"
                    }
                  }
                }
              },
              "schedule": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "title",
                    "label",
                    "part",
                    "day",
                    "date",
                    "start",
                    "end",
                    "minutes"
                  ],
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "part": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "day": {
                      "type": "integer"
                    },
                    "date": {
                      "type": "string"
                    },
                    "start": {
                      "type": "string"
                    },
                    "end": {
                      "type": "string"
                    },
                    "minutes": {
                      "type": "number"
                    }
                  }
                }
              },
              "sessions": {
                "type": "integer"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}