{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://roadbookformat.org/roadbook.schema.json",
  "title": "Roadbook",
  "description": "Roadbook Format 1.2 — a complete travel itinerary in a single JSON document. Players MUST ignore unknown fields (forward compatibility within a major version).",
  "type": "object",
  "required": ["id", "name", "start", "end", "members", "days"],
  "properties": {
    "formatVersion": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+$",
      "description": "Roadbook format version (MAJOR.MINOR). Absent means \"1.0\"."
    },
    "currency": { "$ref": "#/$defs/currencyCode", "description": "Default currency for all prices in the trip. Absent means \"EUR\". Since 1.1." },
    "timezone": { "$ref": "#/$defs/timezone", "description": "Default IANA timezone of the trip. Since 1.2." },
    "id": { "type": "string", "minLength": 1, "description": "Unique trip identifier." },
    "name": { "type": "string", "minLength": 1, "description": "Trip name." },
    "subtitle": { "type": "string", "description": "Tagline." },
    "start": { "$ref": "#/$defs/dateISO", "description": "First day of the trip." },
    "end": { "$ref": "#/$defs/dateISO", "description": "Last day of the trip." },
    "cover": { "$ref": "#/$defs/imageRef", "description": "Cover image." },
    "members": { "type": "array", "items": { "$ref": "#/$defs/member" } },
    "days": { "type": "array", "items": { "$ref": "#/$defs/day" } },
    "bookings": { "type": "array", "items": { "$ref": "#/$defs/booking" } },
    "checklists": { "type": "array", "items": { "$ref": "#/$defs/checklist" } },
    "contacts": { "type": "array", "items": { "$ref": "#/$defs/contact" } },
    "logistics": { "type": "array", "items": { "$ref": "#/$defs/logisticsNote" } },
    "apps": { "type": "array", "items": { "$ref": "#/$defs/appLink" } },
    "people": { "type": "array", "items": { "$ref": "#/$defs/person" }, "description": "On-site people to meet: guides, drivers, hosts. Since 1.2." },
    "vehicles": { "type": "array", "items": { "$ref": "#/$defs/vehicle" }, "description": "Vehicles used during the trip. Since 1.2." }
  },
  "$defs": {
    "dateISO": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "ISO date YYYY-MM-DD." },
    "timeHM": { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "description": "24-hour time \"HH:MM\" (ISO 8601 style), always local wall-clock at the item’s location. Players display it localized to the reader’s locale and edit it with native time components." },
    "timezone": { "type": "string", "description": "IANA timezone identifier (\"Europe/Paris\"). Times stay local wall-clock; timezones are metadata enabling cross-zone computations. Since 1.2." },
    "imageRef": { "type": "string", "description": "Image reference: absolute URL, player-relative path, or data-URL." },
    "currencyCode": { "type": "string", "pattern": "^[A-Z]{3}$", "description": "ISO 4217 currency code (\"EUR\", \"USD\"…)." },
    "coords": {
      "type": "object",
      "required": ["lat", "lon"],
      "properties": { "lat": { "type": "number" }, "lon": { "type": "number" } },
      "description": "Geographic position (offline maps, proximity). Since 1.2."
    },
    "member": {
      "type": "object",
      "required": ["id", "name", "role", "kind"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "role": { "enum": ["owner", "editor", "viewer"], "description": "Rights: owner = administers, editor = can modify, viewer = read-only." },
        "kind": { "enum": ["adult", "teen", "child"], "description": "Adult / teen / child." },
        "age": { "type": "number" },
        "surprise": { "type": "boolean", "description": "Deprecated (1.0) — kept for compatibility, prefer surpriseMode." },
        "surpriseMode": { "enum": ["day", "activity"], "description": "Spoiler protection: 'day' = future days hidden; 'activity' = only the next item is revealed." },
        "phone": { "type": "string" },
        "email": { "type": "string" },
        "loyalty": {
          "type": "array",
          "items": { "type": "object", "required": ["program", "number"], "properties": { "program": { "type": "string" }, "number": { "type": "string" } } },
          "description": "Loyalty programs (frequent flyer, hotel status…). Since 1.2."
        },
        "dietary": { "type": "array", "items": { "type": "string" }, "description": "Dietary restrictions (free tokens: vegetarian, halal, gluten_free…). Since 1.2." },
        "accessibility": { "type": "array", "items": { "type": "string" }, "description": "Accessibility needs (free tokens: wheelchair, low_walking…). Since 1.2." }
      }
    },
    "person": {
      "type": "object",
      "required": ["id", "name"],
      "description": "A person to meet on-site: guide, driver, host… Referenced by personIds on items and bookings. Since 1.2.",
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "role": { "enum": ["guide", "driver", "host", "contact", "other"] },
        "photo": { "$ref": "#/$defs/imageRef", "description": "Photo — to recognize the person at the meeting point." },
        "phone": { "type": "string" },
        "email": { "type": "string" },
        "messaging": {
          "type": "array",
          "items": { "type": "object", "required": ["app", "handle"], "properties": { "app": { "type": "string", "description": "Free token: whatsapp, wechat, telegram…" }, "handle": { "type": "string" } } }
        },
        "languages": { "type": "array", "items": { "type": "string" }, "description": "Spoken languages (ISO 639-1: \"fr\", \"en\"…)." },
        "org": { "type": "string", "description": "Organization (tour operator, agency…)." },
        "sign": { "type": "string", "description": "Recognition sign (\"name board GODON\", \"red umbrella\")." },
        "note": { "type": "string" }
      }
    },
    "vehicle": {
      "type": "object",
      "required": ["id", "name"],
      "description": "A vehicle used during the trip. Referenced by route.vehicleId / transport.vehicleId. Since 1.2.",
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "kind": { "enum": ["car", "van", "motorcycle", "bike", "camper", "other"] },
        "energy": { "enum": ["electric", "gasoline", "diesel", "hybrid", "plugin_hybrid", "other"] },
        "plate": { "type": "string", "description": "License plate (parking bookings often require it)." },
        "rental": {
          "type": "object",
          "properties": { "company": { "type": "string" }, "ref": { "type": "string" }, "pickup": { "type": "string" }, "dropoff": { "type": "string" } }
        },
        "note": { "type": "string" }
      }
    },
    "transportPoint": {
      "type": "object",
      "description": "One end of a journey: airport, station, pickup point… Since 1.2.",
      "properties": {
        "name": { "type": "string", "description": "\"Paris CDG\", \"Lille Flandres\"…" },
        "address": { "type": "string" },
        "coords": { "$ref": "#/$defs/coords" },
        "terminal": { "type": "string" },
        "gate": { "type": "string" },
        "platform": { "type": "string" },
        "timezone": { "$ref": "#/$defs/timezone" }
      }
    },
    "transportDetails": {
      "type": "object",
      "description": "Structured journey details: flight, train, transfer, drive… Since 1.2.",
      "properties": {
        "carrier": { "type": "string", "description": "Airline / operator (\"Air France\", \"NS\", \"G7\")." },
        "number": { "type": "string", "description": "Flight / train number." },
        "ref": { "type": "string", "description": "PNR / booking reference." },
        "from": { "$ref": "#/$defs/transportPoint" },
        "to": { "$ref": "#/$defs/transportPoint" },
        "vehicleId": { "type": "string", "description": "Vehicle used (Vehicle.id)." }
      }
    },
    "payment": {
      "type": "object",
      "description": "Payment state of a booking. Since 1.2.",
      "properties": {
        "status": { "enum": ["prepaid", "deposit", "onsite", "invoice"] },
        "deposit": { "type": "number", "description": "Deposit already paid (in the booking's currency)." },
        "payer": { "enum": ["personal", "company"], "description": "Who pays — business travel." },
        "note": { "type": "string" }
      }
    },
    "room": {
      "type": "object",
      "required": ["label"],
      "description": "One room of a multi-room stay. Since 1.2.",
      "properties": {
        "label": { "type": "string" },
        "memberIds": { "type": "array", "items": { "type": "string" }, "description": "Occupants (Member.id)." }
      }
    },
    "day": {
      "type": "object",
      "required": ["id", "index", "date", "title", "items"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "index": { "type": "integer", "minimum": 1, "description": "Day number (1 = first day), consistent with date order." },
        "date": { "$ref": "#/$defs/dateISO" },
        "title": { "type": "string", "minLength": 1 },
        "intro": { "type": "string", "description": "Narrative introduction for the day." },
        "timezone": { "$ref": "#/$defs/timezone", "description": "Dominant timezone of the day. Since 1.2." },
        "stayBookingId": { "type": "string", "description": "Overnight accommodation (id of a booking)." },
        "route": {
          "type": "object",
          "properties": {
            "km": { "type": "number" },
            "drive": { "type": "string", "description": "Human-readable driving time (\"2h30\")." },
            "charge": { "type": "string", "description": "EV charging plan." },
            "vehicleId": { "type": "string", "description": "Vehicle of the day (Vehicle.id). Since 1.2." }
          }
        },
        "coords": {
          "type": "object",
          "required": ["lat", "lon", "label"],
          "properties": { "lat": { "type": "number" }, "lon": { "type": "number" }, "label": { "type": "string" } },
          "description": "The day's position (weather, map)."
        },
        "items": { "type": "array", "items": { "$ref": "#/$defs/dayItem" } },
        "tip": { "type": "string", "description": "Tip of the day." },
        "planB": { "type": "string", "description": "Day-level fallback plan (free text)." }
      }
    },
    "dayItem": {
      "type": "object",
      "required": ["id", "title", "type", "status"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "time": { "$ref": "#/$defs/timeHM" },
        "endTime": { "$ref": "#/$defs/timeHM" },
        "endDate": { "$ref": "#/$defs/dateISO", "description": "End date when different from the day (overnight flight arriving D+1). Since 1.2." },
        "timezone": { "$ref": "#/$defs/timezone" },
        "endTimezone": { "$ref": "#/$defs/timezone" },
        "title": { "type": "string", "minLength": 1 },
        "desc": { "type": "string" },
        "type": {
          "enum": ["activity", "nature", "heritage", "hike", "sport", "shopping", "event", "nightlife", "wellness", "viewpoint", "tasting", "drive", "flight", "train", "boat", "ferry", "transit", "transfer", "bike", "walk", "meal", "charging", "fuel", "break", "laundry", "admin", "medical", "work", "meeting", "conference", "checkin", "checkout", "free", "night"],
          "description": "Item kind. Extended in 1.2 (transit, bike, ferry, fuel, event, meeting…)."
        },
        "stars": { "enum": [0, 1, 2, 3], "description": "Must-see level (0-3)." },
        "place": { "type": "string", "description": "Human-readable address or place." },
        "mapsQuery": { "type": "string", "description": "Query for a mapping service." },
        "coords": { "$ref": "#/$defs/coords" },
        "price": { "type": "number", "description": "Total price of the item, all travelers included (budgeting). Since 1.1." },
        "currency": { "$ref": "#/$defs/currencyCode", "description": "Currency of price; absent = trip currency. Since 1.1." },
        "priceEstimated": { "type": "boolean", "description": "true = estimated price (forecast), absent/false = actual amount. Since 1.2." },
        "menuUrl": { "type": "string", "description": "Link to the menu (restaurants). Since 1.1." },
        "outfit": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Recommended outfit / gear for this item. Known tokens: casual, sport, dressy, warm, windproof, waterproof, beach, swimsuit — other strings tolerated. Players may aggregate a day's values into a 'what to wear today' hint. Since 1.1."
        },
        "memberIds": { "type": "array", "items": { "type": "string" }, "description": "Travelers concerned (Member.id); absent = everyone. Since 1.2." },
        "personIds": { "type": "array", "items": { "type": "string" }, "description": "On-site people to meet (Person.id). Since 1.2." },
        "transport": { "$ref": "#/$defs/transportDetails" },
        "attachments": { "type": "array", "items": { "$ref": "#/$defs/attachment" }, "description": "Item-level documents. Since 1.2." },
        "bookingId": { "type": "string", "description": "Linked reservation (id of a booking)." },
        "critical": { "type": "string", "description": "Critical warning (strict time slot, etc.)." },
        "status": { "enum": ["planned", "done", "skipped", "replaced"], "description": "Lived state: planned / done / skipped / replaced." },
        "alternatives": { "type": "array", "items": { "$ref": "#/$defs/alternative" } },
        "options": { "type": "array", "items": { "$ref": "#/$defs/itemOption" } },
        "chosenOptionId": { "type": "string", "description": "Selected option (id of an options entry)." },
        "image": { "$ref": "#/$defs/imageRef" }
      }
    },
    "alternative": {
      "type": "object",
      "required": ["id", "trigger", "title"],
      "description": "Conditional fallback for an item (triggered by an event).",
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "trigger": { "type": "string", "description": "Trigger. Known values: rain, wind, cancellation, closure, fatigue, mood." },
        "title": { "type": "string", "minLength": 1 },
        "desc": { "type": "string" },
        "windThreshold": { "type": "number", "description": "Wind threshold (km/h) for the 'wind' trigger." }
      }
    },
    "itemOption": {
      "type": "object",
      "required": ["id", "title"],
      "description": "Interchangeable choice for an item's slot (e.g. several candidate restaurants) — not a fallback.",
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "title": { "type": "string", "minLength": 1 },
        "desc": { "type": "string" },
        "place": { "type": "string" },
        "mapsQuery": { "type": "string" },
        "coords": { "$ref": "#/$defs/coords" },
        "menuUrl": { "type": "string", "description": "Link to the menu (restaurants). Since 1.1." },
        "image": { "$ref": "#/$defs/imageRef", "description": "Replaces the item's image when this option is chosen." }
      }
    },
    "booking": {
      "type": "object",
      "required": ["id", "type", "status", "title"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "type": { "enum": ["stay", "activity", "parking", "transport", "restaurant", "other"], "description": "Stay / activity / parking / transport / restaurant / other." },
        "status": { "enum": ["confirmed", "to_book", "optional", "cancelled"], "description": "Confirmed / to book / optional / cancelled." },
        "title": { "type": "string", "minLength": 1 },
        "hotelStars": { "type": "number", "description": "Hotel star rating." },
        "dates": { "type": "string", "description": "Human-readable dates (\"Aug 2-3\")." },
        "checkin": { "$ref": "#/$defs/timeHM", "description": "Structured check-in time. Since 1.2." },
        "checkout": { "$ref": "#/$defs/timeHM", "description": "Structured check-out time. Since 1.2." },
        "breakfastIncluded": { "type": "boolean", "description": "Breakfast included (stays). Since 1.2." },
        "rooms": { "type": "array", "items": { "$ref": "#/$defs/room" } },
        "address": { "type": "string" },
        "gpsAddress": { "type": "string", "description": "GPS-optimized address when different (parking entrance…)." },
        "phone": { "type": "string" },
        "email": { "type": "string" },
        "website": { "type": "string" },
        "cancellation": { "type": "string", "description": "Cancellation policy." },
        "instructions": { "type": "string" },
        "memberIds": { "type": "array", "items": { "type": "string" }, "description": "Travelers concerned; absent = everyone. Since 1.2." },
        "personIds": { "type": "array", "items": { "type": "string" }, "description": "On-site people (host, driver…). Since 1.2." },
        "transport": { "$ref": "#/$defs/transportDetails" },
        "payment": { "$ref": "#/$defs/payment" },
        "price": { "type": "number", "description": "Total price of the booking (budgeting). Since 1.1." },
        "currency": { "$ref": "#/$defs/currencyCode", "description": "Currency of price; absent = trip currency. Since 1.1." },
        "priceEstimated": { "type": "boolean", "description": "true = estimated price. Since 1.2." },
        "menuUrl": { "type": "string", "description": "Link to the menu (restaurants). Since 1.1." },
        "refs": { "type": "array", "items": { "$ref": "#/$defs/bookingRef" } },
        "tickets": { "type": "array", "items": { "$ref": "#/$defs/ticket" } },
        "attachments": { "type": "array", "items": { "$ref": "#/$defs/attachment" } },
        "reminder": {
          "type": "object",
          "required": ["date", "action"],
          "properties": {
            "date": { "$ref": "#/$defs/dateISO" },
            "action": { "type": "string" },
            "done": { "type": "boolean" }
          }
        }
      }
    },
    "bookingRef": {
      "type": "object",
      "required": ["label", "value"],
      "properties": {
        "label": { "type": "string" },
        "value": { "type": "string" },
        "sensitive": { "type": "boolean", "description": "Masked by default (access code, etc.)." }
      }
    },
    "ticket": {
      "type": "object",
      "required": ["id", "label", "code"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "label": { "type": "string" },
        "code": { "type": "string", "description": "Human-readable ticket number." },
        "holder": { "type": "string", "description": "Displayed holder name (free string — prefer memberId for a strong link)." },
        "memberId": { "type": "string", "description": "Holder as a Member.id reference — survives renames. Since 1.2." },
        "payload": { "type": "string", "description": "The content actually encoded in the barcode — may differ from the ticket number. This is what a player renders scannable; always store the real payload." },
        "format": { "enum": ["qr", "code128", "pdf417", "aztec", "ean13", "datamatrix"], "description": "pdf417 = IATA boarding passes, aztec = European trains. Extended in 1.2." },
        "date": { "$ref": "#/$defs/dateISO", "description": "Ticket-specific date (individual time slot). Since 1.2." },
        "time": { "$ref": "#/$defs/timeHM", "description": "Ticket-specific time. Since 1.2." },
        "seat": { "type": "string", "description": "Assigned seat. Since 1.2." },
        "validUntil": { "$ref": "#/$defs/dateISO", "description": "Use-by date (open vouchers). Since 1.2." },
        "voucher": { "$ref": "#/$defs/imageRef", "description": "Original voucher image." }
      }
    },
    "attachment": {
      "type": "object",
      "required": ["id", "label", "url"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "label": { "type": "string" },
        "url": { "type": "string" }
      }
    },
    "checklist": {
      "type": "object",
      "required": ["id", "title", "items"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "title": { "type": "string", "minLength": 1 },
        "emoji": { "type": "string" },
        "items": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id", "label", "done"],
            "properties": {
              "id": { "type": "string", "minLength": 1 },
              "label": { "type": "string" },
              "done": { "type": "boolean" }
            }
          }
        }
      }
    },
    "contact": {
      "type": "object",
      "required": ["id", "name"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "phone": { "type": "string" },
        "note": { "type": "string" }
      }
    },
    "logisticsNote": {
      "type": "object",
      "required": ["id", "title", "content"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "emoji": { "type": "string" },
        "title": { "type": "string", "minLength": 1 },
        "content": { "type": "string" }
      }
    },
    "appLink": {
      "type": "object",
      "required": ["id", "name"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "emoji": { "type": "string" },
        "desc": { "type": "string" },
        "url": { "type": "string" },
        "icon": { "$ref": "#/$defs/imageRef" },
        "ios": { "type": "string", "description": "App Store link." },
        "android": { "type": "string", "description": "Google Play link." }
      }
    }
  }
}
