{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"$id": "https://docs.eighty-six.tech/spec/order/0.1/schema.json",
	"title": "Open Restaurant Data Exchange Reference 0.1",
	"type": "object",
	"required": ["order_version", "modules", "generator", "restaurant"],
	"patternProperties": {
		"^x_[a-z0-9_]+$": true
	},
	"properties": {
		"order_version": {
			"type": "string",
			"pattern": "^0\\.\\d+$",
			"description": "Major.minor. Minor versions are additive; a 0.1 reader must tolerate a 0.2 document."
		},
		"modules": {
			"type": "array",
			"minItems": 1,
			"uniqueItems": true,
			"items": {
				"type": "string",
				"pattern": "^[a-z][a-z0-9_]*$",
				"description": "Deliberately open, not an enum. A 1.0 reader must be able to consume the core half of a 1.1 document that declares a module it does not know; the validator warns (unknown_module) rather than rejecting."
			}
		},
		"generator": { "$ref": "#/$defs/generator" },
		"restaurant": { "$ref": "#/$defs/restaurant" },
		"rooms": {
			"type": "array",
			"items": { "$ref": "#/$defs/room" }
		},
		"tables": {
			"type": "array",
			"items": { "$ref": "#/$defs/table" }
		},
		"table_groups": {
			"type": "array",
			"items": { "$ref": "#/$defs/table_group" }
		},
		"service_periods": {
			"type": "array",
			"items": { "$ref": "#/$defs/service_period" }
		},
		"closures": {
			"type": "array",
			"items": { "$ref": "#/$defs/closure" }
		},
		"menus": {
			"type": "array",
			"items": { "$ref": "#/$defs/menu" }
		},
		"labels": {
			"type": "array",
			"items": { "$ref": "#/$defs/label" }
		},
		"reservations": {
			"type": "array",
			"items": { "$ref": "#/$defs/reservation" },
			"description": "Requires the \"reservations\" module. Contains personal data."
		}
	},
	"$defs": {
		"ref": {
			"type": "string",
			"minLength": 1,
			"maxLength": 128,
			"description": "Document-scoped identifier, UNIQUE across the entire document. All kinds share one namespace: no two entities (restaurant, room, table, table_group, service_period, turn, closure, menu, section, item, note, label, reservation) may carry the same ref. A duplicate makes the entity graph ambiguous and the reference validator rejects it as an error. Not globally meaningful — the same string in another document may name a different entity."
		},
		"generator": {
			"type": "object",
			"required": ["name", "exported_at"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"name": { "type": "string", "minLength": 1 },
				"version": { "type": "string" },
				"exported_at": { "type": "string", "format": "date-time" }
			}
		},
		"restaurant": {
			"type": "object",
			"required": [
				"ref",
				"name",
				"timezone",
				"currency",
				"default_language",
				"supported_languages"
			],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"name": { "type": "string", "minLength": 1 },
				"timezone": {
					"type": "string",
					"minLength": 1,
					"description": "IANA tz database identifier. NOT pattern-constrained: a regex checks shape, never existence. The validator checks existence via the platform Intl timezone database, accepting both canonical and alias/link names."
				},
				"currency": {
					"type": "string",
					"pattern": "^[A-Z]{3}$",
					"description": "ISO 4217."
				},
				"description": { "type": "string" },
				"default_language": { "type": "string", "minLength": 2 },
				"supported_languages": {
					"type": "array",
					"minItems": 1,
					"items": { "type": "string", "minLength": 2 }
				},
				"contact": { "$ref": "#/$defs/contact" },
				"address": { "$ref": "#/$defs/address" },
				"branding": { "$ref": "#/$defs/branding" },
				"public_hours": {
					"type": "array",
					"items": { "$ref": "#/$defs/hours_spec" },
					"description": "Display/marketing hours. MAY differ from service_periods; the validator warns when bookable time falls outside these."
				}
			}
		},
		"contact": {
			"type": "object",
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"phone": { "type": "string" },
				"email": { "type": "string", "format": "email" },
				"website": { "type": "string", "format": "uri" }
			}
		},
		"coordinates": {
			"type": "object",
			"required": ["lat", "lng"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"lat": { "type": "number", "minimum": -90, "maximum": 90 },
				"lng": { "type": "number", "minimum": -180, "maximum": 180 }
			}
		},
		"address": {
			"type": "object",
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"street": { "type": "string" },
				"city": { "type": "string" },
				"region": { "type": "string" },
				"postal_code": { "type": "string" },
				"country": {
					"type": "string",
					"pattern": "^[A-Z]{2}$",
					"description": "ISO 3166-1 alpha-2."
				},
				"coordinates": { "$ref": "#/$defs/coordinates" }
			}
		},
		"branding": {
			"type": "object",
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"description": "Portable brand identity. Presentation choices specific to one platform (template ids, custom CSS) belong in an x_ extension, not here.",
			"properties": {
				"logo": { "$ref": "#/$defs/media" },
				"colors": {
					"type": "object",
					"patternProperties": { "^x_[a-z0-9_]+$": true },
					"properties": {
						"primary": { "$ref": "#/$defs/color" },
						"secondary": { "$ref": "#/$defs/color" },
						"text": { "$ref": "#/$defs/color" }
					}
				},
				"font_family": { "type": "string" },
				"social_links": {
					"type": "object",
					"propertyNames": { "pattern": "^[a-z][a-z0-9_]*$" },
					"additionalProperties": { "type": "string", "format": "uri" }
				}
			}
		},
		"point": {
			"type": "object",
			"required": ["x", "y"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"x": { "type": "number" },
				"y": { "type": "number" }
			}
		},
		"coordinate_system": {
			"type": "object",
			"required": ["units", "origin", "x_axis", "y_axis"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"description": "Explicit and self-describing. A producer that cannot supply pixels_per_meter MUST still declare units honestly; the validator warns rather than rejecting.",
			"properties": {
				"units": { "enum": ["meters", "pixels"] },
				"origin": { "const": "top_left" },
				"x_axis": { "const": "right" },
				"y_axis": { "const": "down" },
				"pixels_per_meter": {
					"type": "number",
					"exclusiveMinimum": 0,
					"description": "RECOMMENDED when units is \"pixels\". Absent means the plan is unscaled; readers MUST NOT infer real-world distance."
				}
			}
		},
		"shape_requirements": {
			"$comment": "Ties required fields to `type`. Without this, {\"type\":\"circle\"} with no radius validates, which is geometrically meaningless. Kept as an inline allOf rather than a shared $ref base: additionalProperties:false only sees `properties` in its OWN schema object, so composing via $ref would need unevaluatedProperties — and that produces an `unevaluatedProperties` error keyword, which validate.ts's partition() does not reclassify, silently breaking x_ vendor extensions and forward compatibility on these types.",
			"allOf": [
				{
					"if": {
						"properties": { "type": { "const": "polygon" } },
						"required": ["type"]
					},
					"then": { "required": ["points"] }
				},
				{
					"if": {
						"properties": { "type": { "const": "rectangle" } },
						"required": ["type"]
					},
					"then": { "required": ["x", "y", "width", "height"] }
				},
				{
					"if": {
						"properties": { "type": { "const": "circle" } },
						"required": ["type"]
					},
					"then": { "required": ["cx", "cy", "radius"] }
				}
			]
		},
		"outline_segment": {
			"type": "object",
			"required": ["type"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"description": "One segment of a room's boundary. Points are ABSOLUTE coordinates in the room's coordinate system (contrast with a table's shape, whose points are relative to the table centre).",
			"properties": {
				"type": { "enum": ["polygon", "rectangle", "circle"] },
				"points": {
					"type": "array",
					"minItems": 3,
					"items": { "$ref": "#/$defs/point" }
				},
				"x": { "type": "number" },
				"y": { "type": "number" },
				"width": { "type": "number", "exclusiveMinimum": 0 },
				"height": { "type": "number", "exclusiveMinimum": 0 },
				"cx": { "type": "number" },
				"cy": { "type": "number" },
				"radius": { "type": "number", "exclusiveMinimum": 0 }
			},
			"$ref": "#/$defs/shape_requirements"
		},
		"unusable_area": {
			"type": "object",
			"required": ["ref", "type"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"description": "A region of the room that cannot seat guests (bar, kitchen pass, pillar). Points are ABSOLUTE coordinates in the room's coordinate system, as for outline_segment.",
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"type": { "enum": ["polygon", "rectangle", "circle"] },
				"label": { "type": "string" },
				"points": {
					"type": "array",
					"minItems": 3,
					"items": { "$ref": "#/$defs/point" }
				},
				"x": { "type": "number" },
				"y": { "type": "number" },
				"width": { "type": "number", "exclusiveMinimum": 0 },
				"height": { "type": "number", "exclusiveMinimum": 0 },
				"cx": { "type": "number" },
				"cy": { "type": "number" },
				"radius": { "type": "number", "exclusiveMinimum": 0 }
			},
			"$ref": "#/$defs/shape_requirements"
		},
		"floor_plan": {
			"type": "object",
			"required": ["coordinate_system"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"coordinate_system": { "$ref": "#/$defs/coordinate_system" },
				"area_sqm": { "type": "number", "exclusiveMinimum": 0 },
				"background_image_url": { "type": "string", "format": "uri" },
				"outline": {
					"type": "array",
					"items": { "$ref": "#/$defs/outline_segment" }
				},
				"unusable_areas": {
					"type": "array",
					"items": { "$ref": "#/$defs/unusable_area" }
				}
			}
		},
		"room": {
			"type": "object",
			"required": ["ref", "name"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"name": { "type": "string", "minLength": 1 },
				"floor_plan": { "$ref": "#/$defs/floor_plan" }
			}
		},
		"seats": {
			"type": "object",
			"required": ["min", "max"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"min": { "type": "integer", "minimum": 1 },
				"max": { "type": "integer", "minimum": 1 }
			},
			"$comment": "min <= max is a cross-field constraint that JSON Schema 2020-12 cannot express. It is enforced in validate.ts — see checkSeatRanges."
		},
		"shape": {
			"type": "object",
			"required": ["type"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"description": "A table's footprint, centred on its geometry.position. Polygon points are RELATIVE to that centre (0,0) — unlike outline_segment, whose points are absolute.",
			"properties": {
				"type": { "enum": ["rectangle", "circle", "polygon"] },
				"width": { "type": "number", "exclusiveMinimum": 0 },
				"height": { "type": "number", "exclusiveMinimum": 0 },
				"radius": { "type": "number", "exclusiveMinimum": 0 },
				"points": {
					"type": "array",
					"minItems": 3,
					"items": { "$ref": "#/$defs/point" }
				}
			},
			"$comment": "A table shape carries NO x/y — the centre is geometry.position — so it cannot reuse #/$defs/shape_requirements, whose rectangle branch requires x/y. Hence its own inline allOf. Without it, {\"type\":\"circle\"} with no radius would validate.",
			"allOf": [
				{
					"if": {
						"properties": { "type": { "const": "polygon" } },
						"required": ["type"]
					},
					"then": { "required": ["points"] }
				},
				{
					"if": {
						"properties": { "type": { "const": "rectangle" } },
						"required": ["type"]
					},
					"then": { "required": ["width", "height"] }
				},
				{
					"if": {
						"properties": { "type": { "const": "circle" } },
						"required": ["type"]
					},
					"then": { "required": ["radius"] }
				}
			]
		},
		"table_geometry": {
			"type": "object",
			"required": ["position", "shape"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"position": {
					"$ref": "#/$defs/point",
					"description": "The CENTRE of the table, not a corner."
				},
				"rotation_deg": {
					"type": "number",
					"minimum": 0,
					"exclusiveMaximum": 360,
					"description": "Degrees CLOCKWISE."
				},
				"shape": { "$ref": "#/$defs/shape" }
			}
		},
		"table": {
			"type": "object",
			"required": ["ref", "number", "seats", "status"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"source_id": { "type": "string" },
				"number": { "type": "string", "minLength": 1 },
				"display_name": { "type": "string" },
				"room_ref": { "$ref": "#/$defs/ref" },
				"seats": { "$ref": "#/$defs/seats" },
				"status": { "enum": ["active", "inactive", "maintenance"] },
				"geometry": { "$ref": "#/$defs/table_geometry" }
			}
		},
		"table_group": {
			"type": "object",
			"required": ["ref", "table_refs", "seats"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"table_refs": {
					"type": "array",
					"minItems": 2,
					"uniqueItems": true,
					"items": { "$ref": "#/$defs/ref" }
				},
				"seats": { "$ref": "#/$defs/seats" }
			}
		},
		"day": {
			"enum": [
				"monday",
				"tuesday",
				"wednesday",
				"thursday",
				"friday",
				"saturday",
				"sunday"
			]
		},
		"local_time": {
			"type": "string",
			"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
			"description": "Wall-clock HH:MM in the restaurant's timezone. Rules are wall-clock; instances carry offsets."
		},
		"local_date": {
			"type": "string",
			"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
			"format": "date",
			"$comment": "The pattern checks shape; `format: date` checks the calendar. Without it, 2026-02-30 and 2026-13-45 validate. ajv-formats is already registered in validate.ts, so this costs nothing. Same principle as checkTimezone: shape is not existence."
		},
		"hours_spec": {
			"type": "object",
			"required": ["days", "opens", "closes"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"days": {
					"type": "array",
					"minItems": 1,
					"uniqueItems": true,
					"items": { "$ref": "#/$defs/day" }
				},
				"opens": { "$ref": "#/$defs/local_time" },
				"closes": { "$ref": "#/$defs/local_time" },
				"ends_next_day": {
					"type": "boolean",
					"default": false,
					"description": "Explicit cross-midnight flag. When true, closes falls on the day after opens (e.g. opens 23:30, closes 01:00). A consumer reading a finished document MUST NOT infer it from closes < opens — it cannot know the producer's storage encoding, so it must not guess. A producer translating its own source, where end < start is a deliberate encoding, is not guessing and MAY set it from that source."
				}
			}
		},
		"party_size": {
			"type": "object",
			"required": ["min", "max"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"min": { "type": "integer", "minimum": 1 },
				"max": { "type": "integer", "minimum": 1 }
			},
			"$comment": "min <= max is a cross-field constraint that JSON Schema 2020-12 cannot express. It is enforced in validate.ts — see checkPartySizeRanges."
		},
		"turn": {
			"type": "object",
			"required": ["ref", "opens", "closes", "duration_minutes"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"opens": { "$ref": "#/$defs/local_time" },
				"closes": { "$ref": "#/$defs/local_time" },
				"ends_next_day": {
					"type": "boolean",
					"default": false,
					"description": "Explicit cross-midnight flag. When true, closes falls on the day after opens (e.g. opens 23:30, closes 01:00) — the only conforming way to express a turn that spans midnight. A consumer reading a finished document MUST NOT infer it from closes < opens — it cannot know the producer's storage encoding, so it must not guess. A producer translating its own source, where end < start is a deliberate encoding, is not guessing and MAY set it from that source."
				},
				"duration_minutes": {
					"type": "integer",
					"minimum": 1,
					"maximum": 480
				},
				"party_size": { "$ref": "#/$defs/party_size" }
			}
		},
		"service_period": {
			"type": "object",
			"required": ["ref", "name", "days", "opens", "closes"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"name": { "type": "string", "minLength": 1 },
				"description": { "type": "string" },
				"days": {
					"type": "array",
					"minItems": 1,
					"uniqueItems": true,
					"items": { "$ref": "#/$defs/day" }
				},
				"opens": { "$ref": "#/$defs/local_time" },
				"closes": { "$ref": "#/$defs/local_time" },
				"ends_next_day": {
					"type": "boolean",
					"default": false,
					"description": "Explicit cross-midnight flag. When true, closes falls on the day after opens (e.g. opens 23:30, closes 01:00). A consumer reading a finished document MUST NOT infer it from closes < opens — it cannot know the producer's storage encoding, so it must not guess. A producer translating its own source, where end < start is a deliberate encoding, is not guessing and MAY set it from that source."
				},
				"max_covers": { "type": ["integer", "null"], "minimum": 1 },
				"turns": { "type": "array", "items": { "$ref": "#/$defs/turn" } }
			}
		},
		"closure": {
			"type": "object",
			"required": ["ref", "date", "reason"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"date": { "$ref": "#/$defs/local_date" },
				"table_refs": {
					"type": "array",
					"items": { "$ref": "#/$defs/ref" },
					"description": "Empty OR absent means the closure applies to the whole restaurant — absent and empty are equivalent. A ref here that names no table is dangling: a consumer treats it as absent (dangling_ref warning)."
				},
				"turn_ref": {
					"oneOf": [{ "$ref": "#/$defs/ref" }, { "type": "null" }],
					"description": "null means a whole-day closure."
				},
				"reason": { "type": "string", "minLength": 1 },
				"notes": { "type": "string" }
			}
		},
		"money": {
			"type": "string",
			"pattern": "^-?\\d+(\\.\\d{1,4})?$",
			"description": "Decimal string, never a float. The minor-unit count is governed by restaurant.currency, not fixed at two: \"1200\" (JPY — no minor unit), \"6.00\" (EUR — two), and \"0.500\" (KWD — three) are all well-formed.",
			"$comment": "A binary float cannot represent decimal currency exactly, hence a string. The pattern is deliberately currency-agnostic (0-4 fraction digits, optional leading '-') rather than cross-validated against restaurant.currency's actual minor-unit count — that would be a cross-field constraint JSON Schema cannot express, the same class as seats.min<=max (see the $comment on #/$defs/seats). Loosening this pattern in a future 1.x would break the \"1.1 is a superset of 1.0\" guarantee in specification.mdx, so the widest correct shape ships now: a leading '-' (credit/discount line) and up to four fraction digits (KWD/BHD/OMR-class currencies use three) are accepted even though no known currency needs more than three today."
		},
		"color": {
			"type": "string",
			"pattern": "^#[0-9A-Fa-f]{6}$"
		},
		"translations": {
			"type": "object",
			"description": "Keys are BCP-47 tags. The source language lives in the base fields and MUST NOT appear here.",
			"propertyNames": {
				"pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})*$"
			},
			"additionalProperties": {
				"type": "object",
				"additionalProperties": { "type": "string" }
			}
		},
		"media": {
			"type": "object",
			"required": ["url"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"url": { "type": "string", "format": "uri" },
				"path": {
					"type": "string",
					"description": "Relative path within an ORDER Bundle. Absent in a bare ORDER Document."
				}
			}
		},
		"allergen": {
			"enum": [
				"gluten",
				"crustaceans",
				"eggs",
				"fish",
				"peanuts",
				"soybeans",
				"milk",
				"nuts",
				"celery",
				"mustard",
				"sesame",
				"sulphites",
				"lupin",
				"molluscs"
			],
			"description": "EU FIC Regulation 1169/2011 Annex II, in Annex order (1-14)."
		},
		"label": {
			"type": "object",
			"required": ["ref", "name"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"name": { "type": "string", "minLength": 1 },
				"color": { "$ref": "#/$defs/color" },
				"icon": { "type": "string" },
				"translations": { "$ref": "#/$defs/translations" }
			}
		},
		"price": {
			"type": "object",
			"required": ["amount"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"label": { "type": ["string", "null"] },
				"amount": { "$ref": "#/$defs/money" },
				"translations": { "$ref": "#/$defs/translations" }
			}
		},
		"pairing": {
			"type": "object",
			"required": ["item_ref"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"item_ref": { "$ref": "#/$defs/ref" },
				"note": { "type": "string" }
			}
		},
		"menu_item": {
			"type": "object",
			"required": ["ref", "name", "prices"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"source_id": { "type": "string" },
				"name": { "type": "string", "minLength": 1 },
				"description": { "type": "string" },
				"prices": {
					"type": "array",
					"minItems": 1,
					"items": { "$ref": "#/$defs/price" },
					"description": "Always an array. A single-price item has exactly one entry."
				},
				"allergens": {
					"type": "array",
					"uniqueItems": true,
					"items": { "$ref": "#/$defs/allergen" }
				},
				"label_refs": {
					"type": "array",
					"uniqueItems": true,
					"items": { "$ref": "#/$defs/ref" }
				},
				"available": { "type": "boolean", "default": true },
				"frozen": { "type": "boolean", "default": false },
				"pairs_with": {
					"type": "array",
					"items": { "$ref": "#/$defs/pairing" }
				},
				"image": { "$ref": "#/$defs/media" },
				"translations": { "$ref": "#/$defs/translations" }
			}
		},
		"menu_section": {
			"type": "object",
			"required": ["ref", "name"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"name": { "type": "string", "minLength": 1 },
				"description": { "type": "string" },
				"image": { "$ref": "#/$defs/media" },
				"sections": {
					"type": "array",
					"items": { "$ref": "#/$defs/menu_section" }
				},
				"items": {
					"type": "array",
					"items": { "$ref": "#/$defs/menu_item" }
				},
				"translations": { "$ref": "#/$defs/translations" }
			}
		},
		"menu_note": {
			"type": "object",
			"required": ["ref", "anchor", "position", "kind", "body"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"anchor": {
					"type": "object",
					"required": ["type", "ref"],
					"patternProperties": { "^x_[a-z0-9_]+$": true },
					"properties": {
						"type": { "enum": ["menu", "section", "item"] },
						"ref": { "$ref": "#/$defs/ref" }
					}
				},
				"position": { "enum": ["header", "footer"] },
				"kind": { "enum": ["info", "warning", "tip"] },
				"title": { "type": "string", "maxLength": 100 },
				"body": { "type": "string", "minLength": 1, "maxLength": 1000 },
				"color": { "$ref": "#/$defs/color" },
				"show_icon": { "type": "boolean" },
				"translations": { "$ref": "#/$defs/translations" }
			}
		},
		"menu": {
			"type": "object",
			"required": ["ref", "name"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"source_id": { "type": "string" },
				"name": { "type": "string", "minLength": 1 },
				"description": { "type": "string" },
				"image": { "$ref": "#/$defs/media" },
				"sections": {
					"type": "array",
					"items": { "$ref": "#/$defs/menu_section" }
				},
				"notes": {
					"type": "array",
					"items": { "$ref": "#/$defs/menu_note" }
				},
				"translations": { "$ref": "#/$defs/translations" }
			}
		},
		"offset_date_time": {
			"type": "string",
			"format": "date-time",
			"description": "RFC 3339 with a mandatory offset. Instances are absolute; rules are wall-clock (see local_time).",
			"$comment": "format:date-time, NOT a hand-rolled pattern. RFC 3339 requires an offset, so ajv-formats already rejects a naive \"2026-07-20T19:30:00\" — while also accepting fractional seconds and lowercase \"z\" (both valid, both rejected by the obvious regex) and rejecting 2026-02-30T19:30:00Z (which the obvious regex accepts). Verified against ajv-formats 3.0.1."
		},
		"guest": {
			"type": "object",
			"required": ["name"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"description": "Personal data. Present only in the reservations module.",
			"properties": {
				"name": { "type": "string", "minLength": 1 },
				"email": { "type": "string", "format": "email" },
				"phone": { "type": "string" }
			}
		},
		"reservation": {
			"type": "object",
			"required": ["ref", "date", "starts_at", "party_size", "status"],
			"patternProperties": { "^x_[a-z0-9_]+$": true },
			"properties": {
				"ref": { "$ref": "#/$defs/ref" },
				"source_id": { "type": "string" },
				"date": { "$ref": "#/$defs/local_date" },
				"starts_at": { "$ref": "#/$defs/offset_date_time" },
				"ends_at": { "$ref": "#/$defs/offset_date_time" },
				"party_size": { "type": "integer", "minimum": 1 },
				"table_refs": {
					"type": "array",
					"items": { "$ref": "#/$defs/ref" },
					"description": "Empty OR absent means unassigned — absent and empty are equivalent. A ref here that names no table is dangling: a consumer treats it as absent (dangling_ref warning)."
				},
				"turn_ref": {
					"oneOf": [{ "$ref": "#/$defs/ref" }, { "type": "null" }]
				},
				"status": {
					"enum": [
						"pending",
						"confirmed",
						"seated",
						"completed",
						"cancelled",
						"no_show"
					]
				},
				"source": {
					"enum": ["landing_page", "crm", "phone", "walk_in", "api"]
				},
				"guest": { "$ref": "#/$defs/guest" },
				"special_requests": { "type": "string" },
				"notes": { "type": "string" },
				"cancellation_reason": { "type": "string" },
				"locale": { "type": "string" }
			}
		}
	}
}
