# cms/config.yml – Decap-CMS-Konfiguration (Arbeitspaket B1, Issue #8)
#
# Spiegelt das Datenschema v1.6 (docs/sonntagskind_datenschema.md) und die
# JSON-Schemas in schemas/ – die einzige Feldschema-Quelle. Der bidirektionale
# Konsistenz-Check (E17) prüft diese Datei ab B3 gegen schemas/.
#
# Arbeitsteilung CMS ↔ Validierung: Das CMS erzwingt nur [S]-Pflichten und
# Wertemengen. Publish-Pflichten [P], Cross-Regeln (E33, XOR-Ort, Diff-Checks)
# und Warnungen prüft die validate-Action (B3) auf dem PR – das CMS zeigt sie
# als Feldhilfe an, damit R sie beim Erfassen schon kennt.
#
# base_url/auth_endpoint (OAuth über Netlify-Function) kommen mit B2.

backend:
  name: github
  repo: sonntagskind-bayern/sonntagskind
  branch: main
  squash_merges: true
  commit_messages:
    create: 'Content: {{collection}} „{{slug}}" angelegt'
    update: 'Content: {{collection}} „{{slug}}" aktualisiert'
    delete: 'Content: {{collection}} „{{slug}}" entfernt'
    uploadMedia: "Content: Medium {{path}} hochgeladen"
    deleteMedia: "Content: Medium {{path}} entfernt"

# Editorial Workflow verbindlich (E21): Publish = PR-Merge nach grüner
# Validierung; „Veröffentlichen" im CMS erzeugt den Merge, nie einen Direkt-Commit.
publish_mode: editorial_workflow

site_url: https://sonntagskind.bayern
media_folder: web/public/uploads
public_folder: /uploads

slug:
  encoding: ascii
  clean_accents: true

collections:
  # ── Aktivitäten ──────────────────────────────────────────────────────────
  - name: activities
    label: Aktivitäten
    label_singular: Aktivität
    folder: content/activities
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: name
    slug: "{{fields.slug}}"
    summary: "{{name}} · {{status}}"
    editor:
      preview: false # Raw-/Markdown-Modus, keine Vorschau (Konzeptpapier 4.4)
    description: >-
      Zuschnitt-Konventionen (Datenschema Kap. 2): Mehrstandort-Anbieter = eine
      Aktivität PRO Standort (eigene Location, gemeinsamer Provider).
      Binnenstaffelung (Parcours, Altersbereiche) = härteste Untergrenze in den
      harten Grenzen, Staffelung in den Body. Zeitgebundene/situative Auflagen
      (Sonderzeiten, Sockenpflicht, Kassenschluss) = nie in harte Grenzen,
      sondern Body/Kurzbeschreibung/Hinweis-Zeile. Öffentliche Orte ohne
      Anbieter = Träger als Sammel-Provider (z. B. prov-lhm).
    view_filters:
      - label: Entwürfe
        field: status
        pattern: draft
      - label: Aktiv
        field: status
        pattern: active
      - label: Pausiert
        field: status
        pattern: paused
      - label: Dauerhaft geschlossen
        field: status
        pattern: closed_permanently
    view_groups:
      - label: Status
        field: status
    sortable_fields: [name, status]
    fields:
      # Identität
      - name: id
        label: ID
        widget: string
        pattern:
          [
            "^[a-z0-9]+(-[a-z0-9]+)*$",
            "kebab-case, z. B. heldenverlies-kirchheim",
          ]
        hint: "Eindeutig, nach Aktivierung UNVERÄNDERLICH (Diff-Check)."
      - name: name
        label: Name
        widget: string
      - name: slug
        label: Slug (URL-Pfad)
        widget: string
        pattern:
          [
            "^[a-z0-9]+(-[a-z0-9]+)*$",
            "kebab-case, z. B. heldenverlies-kirchheim",
          ]
        hint: "Nach Aktivierung UNVERÄNDERLICH."
      - name: short_description
        label: Kurzbeschreibung
        widget: text
        required: false
        pattern: ['^[\s\S]{1,280}$', "max. 280 Zeichen"]
        hint: "[P] Sachlich, max. 280 Zeichen. Pflicht bei Status active."
      # Referenzen
      - name: location_id
        label: Location
        widget: relation
        collection: locations
        value_field: id
        search_fields: [name, id]
        display_fields: [name]
        hint: "Bei Aktivierung muss die Location active sein (E33)."
      - name: provider_id
        label: Provider
        widget: relation
        collection: providers
        value_field: id
        search_fields: [name, id]
        display_fields: [name]
        hint: "Bei öffentlichen Orten: Sammel-Provider des Trägers (z. B. prov-lhm)."
      # Klassifikation
      - name: categories
        label: Kategorien
        widget: select
        multiple: true
        min: 1
        max: 3
        options:
          [
            sport,
            kreativ,
            musisch,
            performativ,
            technisch,
            wissen,
            sozial,
            natur,
          ]
        hint: "1–3 aus der Achter-Taxonomie."
      - name: primary_category
        label: Primäre Kategorie
        widget: select
        options:
          [
            sport,
            kreativ,
            musisch,
            performativ,
            technisch,
            wissen,
            sozial,
            natur,
          ]
        hint: "Muss in den Kategorien enthalten sein."
      - name: tags
        label: Tags
        widget: list
        required: false
        field:
          name: tag
          label: Tag
          widget: string
          pattern: ["^[a-z0-9]+(-[a-z0-9]+)*$", "kebab-case"]
        hint: >-
          Kuratierte Liste: docs/tags.md. Unbekannter Tag gibt eine Warnung –
          neue Tags in der Sonntagskind-Stunde in die Registry aufnehmen.
      - name: seasons
        label: Saisons (kuratorische Passung)
        widget: select
        multiple: true
        required: false
        options: [fruehling, sommer, herbst, winter]
        hint: >-
          Saisonalitäts-Trias 1/3: WANN EMPFEHLEN WIR ES (Anzeige/Filter);
          leer = ganzjährig passend. Nie aus dem Betrieb ableiten – ein
          Rodelhang kann ganzjährig geöffnet sein und trotzdem nur [winter]
          tragen. Faktischer Betrieb → „Ganzjährig geöffnet", Erläuterung →
          „Saison-Hinweise".
      # Alter
      - name: age_bands
        label: Altersbänder (weiche Empfehlung)
        widget: select
        multiple: true
        min: 1
        options:
          - { label: "baby (0–2)", value: baby }
          - { label: "kita (3–5)", value: kita }
          - { label: "grundschule_jung (6–8)", value: grundschule_jung }
          - { label: "grundschule_alt (9–11)", value: grundschule_alt }
          - { label: "teen_jung (12–14)", value: teen_jung }
          - { label: "teen_alt (15–17)", value: teen_alt }
        hint: 'Kuratorische Empfehlung; harte Anbieter-Grenzen gehören in „Harte Grenzen".'
      - name: hard_constraints
        label: Harte Grenzen
        widget: object
        hint: >-
          NUR ZEITLOSE Anbieter-Grenzen (F49/F50): Staffelung nach Parcours →
          härteste Untergrenze hier, Rest in den Body. Situatives (Sonderzeiten,
          Sockenpflicht, Kassenschluss) nie hier.
        fields:
          - {
              name: min_age_years,
              label: "Mindestalter (Jahre)",
              widget: number,
              value_type: int,
              min: 0,
              max: 17,
              required: false,
            }
          - {
              name: max_age_years,
              label: "Höchstalter (Jahre)",
              widget: number,
              value_type: int,
              min: 0,
              max: 17,
              required: false,
            }
          - {
              name: min_height_cm,
              label: "Mindestgröße (cm)",
              widget: number,
              value_type: int,
              min: 0,
              required: false,
            }
          - {
              name: min_weight_kg,
              label: "Mindestgewicht (kg)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: max_weight_kg,
              label: "Höchstgewicht (kg)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - name: supervision
            label: Aufsicht
            widget: select
            options:
              [
                none,
                adult_required,
                coach_required,
                swimming_certificate_required,
                parent_must_book,
              ]
      # Situation
      - name: setting
        label: Setting
        widget: select
        options: [indoor, outdoor, mixed]
      - name: weather_dependency
        label: Wetterabhängigkeit
        widget: select
        options: [none, partial, full]
      - name: weather_notes
        label: Wetter-Hinweise
        widget: string
        required: false
      - name: duration_minutes_typical
        label: Dauer typisch (Minuten)
        widget: number
        value_type: int
        min: 1
        required: false
        hint: "[P] Pflicht bei Status active."
      - {
          name: duration_minutes_min,
          label: "Dauer min. (Minuten)",
          widget: number,
          value_type: int,
          min: 1,
          required: false,
        }
      - {
          name: duration_minutes_max,
          label: "Dauer max. (Minuten)",
          widget: number,
          value_type: int,
          min: 1,
          required: false,
        }
      - name: booking_mode
        label: Buchungsmodus
        widget: select
        options:
          [drop_in, slot_recommended, slot_required, private_booking_only]
      - name: advance_booking_lead_time_hours
        label: Vorlauf Buchung (Stunden)
        widget: number
        value_type: int
        min: 0
        required: false
      # Begleitung
      - name: parent_role
        label: Eltern-Rolle
        widget: select
        required: false
        options:
          [not_present, spectator, optional_participant, required_participant]
        hint: "[P] Pflicht bei Status active."
      - name: parent_pays_separately
        label: Eltern zahlen separat
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      # Preis
      - name: price
        label: Preis
        widget: object
        hint: "KEINE Annahme Kind ≤ Erwachsener – Indoorspielplätze drehen das um (F61)."
        fields:
          - name: model
            label: Modell
            widget: select
            options: [free, per_person, per_group, per_hour, tiered]
          - {
              name: adult_eur,
              label: "Erwachsene (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: child_eur,
              label: "Kind (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
              hint: "Altersgrenzen des Anbieters in die Notizen.",
            }
          - {
              name: from_eur,
              label: "Ab (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
              hint: "Günstigster regulärer Einzelpreis.",
            }
          - {
              name: to_eur,
              label: "Bis (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: family_ticket_eur,
              label: "Familienticket (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - { name: notes, label: Notizen, widget: string, required: false }
      # Charakter
      - name: bonding_score
        label: Bonding-Score
        widget: number
        value_type: int
        min: 1
        max: 5
        required: false
        hint: "[P] Anhang B: 5 = nur gemeinsam lösbar … 1 = passiv nebeneinander. Familien-Modi werden abgeleitet (Anhang D) – modus-blinde Einträge lösen eine Warnung aus."
      - name: energy_level
        label: Energie-Level
        widget: select
        required: false
        options: [low, medium, high]
        hint: "[P] Pflicht bei Status active."
      - name: noise_level
        label: Lautstärke
        widget: select
        required: false
        options: [quiet, medium, loud]
        hint: "[P] Pflicht bei Status active."
      - name: quiet_zone_available
        label: Ruhezone vorhanden
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      # Soziale Eignung
      - {
          name: group_size_min,
          label: "Gruppengröße min.",
          widget: number,
          value_type: int,
          min: 1,
          required: false,
        }
      - {
          name: group_size_max,
          label: "Gruppengröße max.",
          widget: number,
          value_type: int,
          min: 1,
          required: false,
        }
      - name: social_fit
        label: Soziale Eignung
        widget: select
        multiple: true
        required: false
        options: [birthday, class_trip, couple_with_kid, multi_family]
      # Hobby-Anschluss
      - name: trial_format_available
        label: Schnupperformat vorhanden
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: follow_up_paths
        label: Anschlusswege (Hobby)
        widget: list
        required: false
        field: { name: path, label: Anschlussweg, widget: string }
      # Saisonalität
      - name: open_year_round
        label: Ganzjährig geöffnet
        widget: boolean
        required: false
        hint: >-
          [P] Saisonalitäts-Trias 2/3: FAKTISCHE BETRIEBSAUSSAGE des Anbieters –
          unabhängig davon, wann wir empfehlen (Saisons). Pflicht bei active.
      - name: seasonal_notes
        label: Saison-Hinweise
        widget: text
        required: false
        hint: >-
          Saisonalitäts-Trias 3/3: Freitext-Erläuterung. Warnung, wenn
          „Ganzjährig geöffnet" = false ohne Hinweis hier.
      # Geburtstag (Ausbaustufe)
      - name: birthday_suitability
        label: Geburtstagseignung (Ausbaustufe)
        widget: object
        required: false
        collapsed: true
        fields:
          - { name: suitable, label: Geeignet, widget: boolean }
          - {
              name: age_range_min,
              label: "Alter von",
              widget: number,
              value_type: int,
              min: 0,
              max: 17,
              required: false,
            }
          - {
              name: age_range_max,
              label: "Alter bis",
              widget: number,
              value_type: int,
              min: 0,
              max: 17,
              required: false,
            }
          - {
              name: group_size_min,
              label: "Gruppe min.",
              widget: number,
              value_type: int,
              min: 1,
              required: false,
            }
          - {
              name: group_size_max,
              label: "Gruppe max.",
              widget: number,
              value_type: int,
              min: 1,
              required: false,
            }
          - {
              name: package_available,
              label: "Paket vorhanden",
              widget: boolean,
              required: false,
            }
          - {
              name: advance_booking_days,
              label: "Vorlauf (Tage)",
              widget: number,
              value_type: int,
              min: 0,
              required: false,
            }
          - {
              name: price_per_child_eur,
              label: "Preis je Kind (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: parents_included,
              label: "Eltern inklusive",
              widget: boolean,
              required: false,
            }
          - { name: notes, label: Notizen, widget: string, required: false }
      # Newsletter
      - name: newsletter_blurb
        label: Newsletter-Blurb
        widget: text
        required: false
      - name: last_featured_at
        label: Zuletzt featured
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "NIE MANUELL setzen – wird ausschließlich vom set-featured-Script gepflegt (Kap. 9)."
      # Pflege
      - name: status
        label: Status
        widget: select
        options: [draft, active, paused, closed_permanently]
        default: draft
        hint: "active → draft ist unzulässig; Delisting über paused/closed_permanently (E25). Aktivieren tut ausschließlich R."
      - name: last_verified_at
        label: Zuletzt verifiziert
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "[P] AUSSCHLIESSLICH bei tatsächlicher menschlicher Prüfung setzen (Konzeptpapier 5.2). KI-Drafts: nie."
      - name: data_source
        label: Datenquelle
        widget: select
        options:
          [
            provider_website,
            phone_call,
            on_site_visit,
            provider_confirmed,
            press,
          ]
        hint: >-
          Bei active nur provider_website | phone_call | on_site_visit |
          provider_confirmed. press = Draft-Marker (KI-Drafts stets press, E24).
          provider_confirmed setzt nur R nach schriftlicher Anbieter-Bestätigung (E32).
      - name: verification_priority
        label: Verifikations-Priorität
        widget: select
        options: [low, medium, high]
        default: medium
        hint: "Volatile Zeiten (Saison, Wetter, Terminbetrieb) → high."
      - name: notes_internal
        label: Interne Notizen
        widget: text
        required: false
        hint: "Erscheint NIRGENDS öffentlich (Leak-Test). Ziel für anonymisiertes Leserfeedback (E30)."
      - name: body
        label: Body (Markdown)
        widget: markdown
        required: false
        hint: >-
          Beschreibung „Warum für Familien", Altersstaffelung, situative
          Auflagen, Recherche-Notizen, offene Punkte. KI-Drafts führen hier den
          dreistufigen Zu-verifizieren-Block.

  # ── Events ───────────────────────────────────────────────────────────────
  - name: events
    label: Events
    label_singular: Event
    folder: content/events
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: name
    slug: "{{fields.slug}}"
    summary: "{{name}} · {{status}}"
    editor:
      preview: false
    description: >-
      Großevents mit Familienwinkel = EIN Event; Familienwinkel über
      Altersbänder, Kurzbeschreibung, Body und Termin-Notizen. Ort: ENTWEDER
      gepflegte Location ODER Inline-Ort – genau eines von beiden.
      Wiederkehrende Venues als gepflegte Location anlegen.
    view_filters:
      - label: Entwürfe
        field: status
        pattern: draft
      - label: Aktiv
        field: status
        pattern: active
      - label: Abgesagt
        field: status
        pattern: cancelled
    view_groups:
      - label: Status
        field: status
    sortable_fields: [name, status]
    fields:
      - name: id
        label: ID
        widget: string
        pattern:
          [
            "^[a-z0-9]+(-[a-z0-9]+)*$",
            "kebab-case, z. B. otterfinger-dorffest-2026",
          ]
        hint: "Nach Aktivierung UNVERÄNDERLICH (stabile Kalender-UIDs hängen daran)."
      - name: name
        label: Name
        widget: string
      - name: slug
        label: Slug (URL-Pfad)
        widget: string
        pattern: ["^[a-z0-9]+(-[a-z0-9]+)*$", "kebab-case"]
        hint: "Nach Aktivierung UNVERÄNDERLICH."
      - name: short_description
        label: Kurzbeschreibung
        widget: text
        required: false
        pattern: ['^[\s\S]{1,280}$', "max. 280 Zeichen"]
        hint: "[P] Sachlich, max. 280 Zeichen. Pflicht bei Status active."
      - name: event_series_id
        label: Event-Serie
        widget: relation
        collection: event_series
        value_field: id
        search_fields: [name, id]
        display_fields: [name]
        required: false
      - name: expected_to_recur
        label: Wiederholung erwartet
        widget: boolean
        required: false
        hint: "Nur relevant, wenn KEINE Serie gesetzt ist."
      - name: event_url
        label: Event-URL
        widget: string
        required: false
        pattern: ["^https?://", "http(s)-URL"]
        hint: '[P] Verifikationsquelle + „Mehr Infos"-Link. Pflicht bei Status active.'
      - name: provider_id
        label: Provider
        widget: relation
        collection: providers
        value_field: id
        search_fields: [name, id]
        display_fields: [name]
        required: false
        hint: >-
          Optional. Nur bei gesetztem Provider geht nach Aktivierung die
          Anbieter-Info-Mail raus (E32); Inline-Ort ohne Provider = dokumentierte Ausnahme.
      - name: occurrences
        label: Termine
        widget: list
        min: 1
        summary: "{{fields.date}} {{fields.start_time}} {{fields.note}}"
        hint: >-
          NIE umsortieren oder löschen, nur korrigieren/anfügen – die Position
          bestimmt die Kalender-UID. Korrektur oder Absage-Rücknahme NACH
          Veröffentlichung: Revision +1 (erstes Setzen = 1).
        fields:
          - name: date
            label: Datum
            widget: string
            pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
          - name: start_time
            label: Beginn
            widget: string
            required: false
            pattern: ["^([01][0-9]|2[0-3]):[0-5][0-9]$", "HH:MM"]
          - name: end_time
            label: Ende
            widget: string
            required: false
            pattern: ["^([01][0-9]|2[0-3]):[0-5][0-9]$", "HH:MM"]
          - {
              name: all_day,
              label: Ganztägig,
              widget: boolean,
              required: false,
            }
          - {
              name: note,
              label: Notiz,
              widget: string,
              required: false,
              hint: 'z. B. „Kinder- und Jugendnachmittag".',
            }
          - {
              name: cancelled,
              label: Abgesagt,
              widget: boolean,
              required: false,
              hint: "Nie löschen – wird im Kalender als CANCELLED geführt.",
            }
          - {
              name: revision,
              label: Revision,
              widget: number,
              value_type: int,
              min: 1,
              required: false,
              hint: "Entfällt bis zur ersten Korrektur nach Veröffentlichung; dann 1, danach +1 je Korrektur.",
            }
      - name: location_id
        label: "Ort: gepflegte Location"
        widget: relation
        collection: locations
        value_field: id
        search_fields: [name, id]
        display_fields: [name]
        required: false
        hint: "ENTWEDER dies ODER Inline-Ort – genau eines (Validierung prüft)."
      - name: location_inline
        label: "Ort: inline (einmalige Venues)"
        widget: object
        required: false
        collapsed: true
        hint: "Nur wenn keine Location gesetzt ist. Wiederkehrende Venues als Location pflegen."
        fields:
          - { name: name, label: Name, widget: string }
          - { name: address, label: Adresse, widget: string }
          - name: region
            label: Region
            widget: select
            options: [oberbayern]
            default: oberbayern
          - {
              name: lat,
              label: Breitengrad,
              widget: number,
              value_type: float,
              min: -90,
              max: 90,
              required: false,
              hint: "Pin-Kontrolle im PR-Review.",
            }
          - {
              name: lng,
              label: Längengrad,
              widget: number,
              value_type: float,
              min: -180,
              max: 180,
              required: false,
            }
      - name: categories
        label: Kategorien
        widget: select
        multiple: true
        min: 1
        max: 3
        options:
          [
            sport,
            kreativ,
            musisch,
            performativ,
            technisch,
            wissen,
            sozial,
            natur,
          ]
      - name: primary_category
        label: Primäre Kategorie
        widget: select
        options:
          [
            sport,
            kreativ,
            musisch,
            performativ,
            technisch,
            wissen,
            sozial,
            natur,
          ]
        hint: "Muss in den Kategorien enthalten sein."
      - name: tags
        label: Tags
        widget: list
        required: false
        field:
          name: tag
          label: Tag
          widget: string
          pattern: ["^[a-z0-9]+(-[a-z0-9]+)*$", "kebab-case"]
        hint: "Kuratierte Liste: docs/tags.md (unbekannter Tag = Warnung)."
      - name: seasons
        label: Saisons (kuratorische Passung)
        widget: select
        multiple: true
        required: false
        options: [fruehling, sommer, herbst, winter]
        hint: "Leer = ganzjährig passend."
      - name: age_bands
        label: Altersbänder
        widget: select
        multiple: true
        min: 1
        options:
          - { label: "baby (0–2)", value: baby }
          - { label: "kita (3–5)", value: kita }
          - { label: "grundschule_jung (6–8)", value: grundschule_jung }
          - { label: "grundschule_alt (9–11)", value: grundschule_alt }
          - { label: "teen_jung (12–14)", value: teen_jung }
          - { label: "teen_alt (15–17)", value: teen_alt }
      - name: hard_constraints
        label: Harte Grenzen
        widget: object
        hint: "Mind. Aufsicht; NUR zeitlose Grenzen – Situatives in Body/Notizen (F49)."
        fields:
          - {
              name: min_age_years,
              label: "Mindestalter (Jahre)",
              widget: number,
              value_type: int,
              min: 0,
              max: 17,
              required: false,
            }
          - {
              name: max_age_years,
              label: "Höchstalter (Jahre)",
              widget: number,
              value_type: int,
              min: 0,
              max: 17,
              required: false,
            }
          - {
              name: min_height_cm,
              label: "Mindestgröße (cm)",
              widget: number,
              value_type: int,
              min: 0,
              required: false,
            }
          - {
              name: min_weight_kg,
              label: "Mindestgewicht (kg)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: max_weight_kg,
              label: "Höchstgewicht (kg)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - name: supervision
            label: Aufsicht
            widget: select
            options:
              [
                none,
                adult_required,
                coach_required,
                swimming_certificate_required,
                parent_must_book,
              ]
      - name: setting
        label: Setting
        widget: select
        options: [indoor, outdoor, mixed]
      - name: weather_dependency
        label: Wetterabhängigkeit
        widget: select
        options: [none, partial, full]
      - {
          name: weather_notes,
          label: "Wetter-Hinweise",
          widget: string,
          required: false,
        }
      - name: bonding_score
        label: Bonding-Score
        widget: number
        value_type: int
        min: 1
        max: 5
        required: false
        hint: "[P] Anhang B. Pflicht bei Status active."
      - name: energy_level
        label: Energie-Level
        widget: select
        required: false
        options: [low, medium, high]
        hint: "[P] Pflicht bei Status active."
      - name: noise_level
        label: Lautstärke
        widget: select
        required: false
        options: [quiet, medium, loud]
        hint: "[P] Pflicht bei Status active."
      - name: quiet_zone_available
        label: Ruhezone vorhanden
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: price
        label: Preis
        widget: object
        fields:
          - name: model
            label: Modell
            widget: select
            options: [free, per_person, per_group, per_hour, tiered]
          - {
              name: adult_eur,
              label: "Erwachsene (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: child_eur,
              label: "Kind (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: from_eur,
              label: "Ab (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: to_eur,
              label: "Bis (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - {
              name: family_ticket_eur,
              label: "Familienticket (€)",
              widget: number,
              value_type: float,
              min: 0,
              required: false,
            }
          - { name: notes, label: Notizen, widget: string, required: false }
      - name: parent_role
        label: Eltern-Rolle
        widget: select
        required: false
        options:
          [not_present, spectator, optional_participant, required_participant]
        hint: "[P] Pflicht bei Status active."
      - name: parent_pays_separately
        label: Eltern zahlen separat
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - {
          name: newsletter_blurb,
          label: "Newsletter-Blurb",
          widget: text,
          required: false,
        }
      - name: last_featured_at
        label: Zuletzt featured
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "NIE MANUELL – nur via set-featured-Script."
      - name: status
        label: Status
        widget: select
        options: [draft, active, cancelled]
        default: draft
        hint: >-
          active bleibt auch nach Ablauf; cancelled nie löschen (Kalender führt
          CANCELLED bei stabiler UID). Absage der Absage: Revision +1 am Termin.
      - name: last_verified_at
        label: Zuletzt verifiziert
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "[P] Nur bei tatsächlicher menschlicher Prüfung. Re-Verify 14 Tage vor dem frühesten Termin, wenn älter als 30 Tage."
      - name: data_source
        label: Datenquelle
        widget: select
        options:
          [
            provider_website,
            phone_call,
            on_site_visit,
            provider_confirmed,
            press,
          ]
        hint: "Publish-Regel wie Aktivität: bei active kein press (E24/E32)."
      - name: notes_internal
        label: Interne Notizen
        widget: text
        required: false
        hint: "Erscheint NIRGENDS öffentlich (Leak-Test)."
      - name: body
        label: Body (Markdown)
        widget: markdown
        required: false
        hint: "Familienwinkel, Programm-Details, Recherche-Notizen, offene Punkte."

  # ── Event-Serien ─────────────────────────────────────────────────────────
  - name: event_series
    label: Event-Serien
    label_singular: Event-Serie
    folder: content/event-series
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: name
    slug: "{{fields.id}}"
    summary: "{{name}} · {{status}}"
    editor:
      preview: false
    view_filters:
      - label: Aktiv
        field: status
        pattern: active
      - label: Ruhend
        field: status
        pattern: dormant
      - label: Beendet
        field: status
        pattern: ended
    fields:
      - name: id
        label: ID
        widget: string
        pattern:
          ["^series-[a-z0-9]+(-[a-z0-9]+)*$", "Präfix series-, kebab-case"]
      - name: name
        label: Name
        widget: string
      - name: description
        label: Beschreibung
        widget: text
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: typical_timing
        label: Typischer Zeitraum
        widget: string
        required: false
        hint: '[P] z. B. „Sommer, meist erstes August-Wochenende (Fr–So)". Pflicht bei active.'
      - {
          name: typical_month,
          label: "Typischer Monat (1–12)",
          widget: number,
          value_type: int,
          min: 1,
          max: 12,
          required: false,
        }
      - name: recurrence_confidence
        label: Wiederholungs-Konfidenz
        widget: select
        options: [high, medium, low]
        hint: "high/medium: Freshness-Report schlägt Draft-Edition fürs Folgejahr vor."
      - name: status
        label: Status
        widget: select
        options: [active, dormant, ended]
        default: active
      - name: notes_internal
        label: Interne Notizen
        widget: text
        required: false
      - name: body
        label: Body (Markdown)
        widget: markdown
        required: false

  # ── Locations ────────────────────────────────────────────────────────────
  - name: locations
    label: Locations
    label_singular: Location
    folder: content/locations
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: name
    slug: "{{fields.id}}"
    summary: "{{name}} · {{status}}"
    editor:
      preview: false
    description: >-
      Lebenszyklus: paused = temporär zu; closed_permanently nie als
      Delisting-Ersatz (E25); Wiedereröffnung nur mit frischem
      Verifikationsdatum. Kein eigenes data_source – Locations werden im Zug
      der Aktivitäts-/Event-Prüfung mitgeprüft (v1.6).
    view_filters:
      - label: Entwürfe
        field: status
        pattern: draft
      - label: Aktiv
        field: status
        pattern: active
      - label: Pausiert
        field: status
        pattern: paused
      - label: Dauerhaft geschlossen
        field: status
        pattern: closed_permanently
    view_groups:
      - label: Status
        field: status
    sortable_fields: [name, status]
    fields:
      - name: id
        label: ID
        widget: string
        pattern: ["^loc-[a-z0-9]+(-[a-z0-9]+)*$", "Präfix loc-, kebab-case"]
        hint: "Nach Aktivierung UNVERÄNDERLICH."
      - name: name
        label: Name
        widget: string
      - name: address
        label: Adresse
        widget: string
        hint: "Straße Nr., PLZ Ort."
      - name: region
        label: Region
        widget: select
        options: [oberbayern]
        default: oberbayern
        hint: "Kuratierter Raum (E29); Enum wächst mit Ausbaustufen."
      - name: lat
        label: Breitengrad
        widget: number
        value_type: float
        min: -90
        max: 90
        required: false
        hint: "[P] Via Geocoding (B6) oder manuell; Draft ohne Koordinaten zulässig. Pin-Kontrolle im PR-Review."
      - name: lng
        label: Längengrad
        widget: number
        value_type: float
        min: -180
        max: 180
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: place_id_google
        label: Google Place-ID
        widget: string
        required: false
        hint: >-
          Einziges speicherbares Places-Datum (ToS). Speist nur den Deeplink
          „Route planen / Bewertungen bei Google" (E28) – NIE Ratings einblenden
          oder weitere Places-Daten speichern.
      - name: parking
        label: Parken
        widget: select
        required: false
        options: [none, street, paid_lot, free_lot]
        hint: "[P] Pflicht bei Status active."
      - name: transit_accessible
        label: ÖPNV erreichbar
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - {
          name: transit_notes,
          label: "ÖPNV-Hinweise",
          widget: string,
          required: false,
          hint: 'z. B. „S2 bis Heimstetten, dann Bus 263."',
        }
      - name: food_options
        label: Verpflegung
        widget: select
        multiple: true
        required: false
        options: [none, snacks, bistro, restaurant, byo_allowed]
        hint: "[P] Pflicht bei Status active (mind. ein Wert, ggf. none)."
      - name: stroller_friendly
        label: Kinderwagentauglich
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: wheelchair_accessible
        label: Rollstuhlgerecht
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: baby_changing
        label: Wickelmöglichkeit
        widget: boolean
        required: false
        hint: "[P] Pflicht bei Status active."
      - {
          name: lockers,
          label: Schließfächer,
          widget: boolean,
          required: false,
        }
      - {
          name: changing_room,
          label: Umkleide,
          widget: boolean,
          required: false,
        }
      - name: status
        label: Status
        widget: select
        options: [draft, active, paused, closed_permanently]
        default: draft
        hint: "active → draft unzulässig. paused-Location ⇒ Banner an aktiven Aktivitäten (E18/E33)."
      - name: last_verified_at
        label: Zuletzt verifiziert
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "[P] Nur bei tatsächlicher menschlicher Prüfung. Intervalle 6/12/18 Monate nach Priorität."
      - name: verification_priority
        label: Verifikations-Priorität
        widget: select
        options: [low, medium, high]
        default: medium
        hint: "Volatile Zeiten (Saison, Wetter, TERMINBETRIEB) → high."
      - name: notes_internal
        label: Interne Notizen
        widget: text
        required: false
        hint: "Erscheint NIRGENDS öffentlich."
      - name: body
        label: Body (Markdown, inkl. Öffnungszeiten)
        widget: markdown
        required: false
        hint: >-
          Öffnungszeiten unter „## Öffnungszeiten" (Pflicht, sobald eine
          Aktivität referenziert). Zeilenformen: „Mo: geschlossen" ·
          „Di-Fr: 14:00-22:00" · Mehrfachspannen kommagetrennt
          („Sa: 10:00-14:00, 15:00-19:00") · „So: frei zugänglich" ·
          „Hinweis: …" (wird angezeigt) · Terminbetrieb = Block NUR aus einer
          Hinweis-Zeile (dann Priorität high). Ausnahmen darunter als
          „Ausnahmen:" mit „YYYY-MM-DD: geschlossen". Gepflegt wird der
          aktuelle Saisonstand; Saisonwechsel ist Pflegeanlass.

  # ── Provider ─────────────────────────────────────────────────────────────
  - name: providers
    label: Provider
    label_singular: Provider
    folder: content/providers
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: name
    slug: "{{fields.id}}"
    summary: "{{name}}"
    editor:
      preview: false
    description: >-
      Anbieter und Träger. Öffentliche Orte ohne Anbieter führen ihren Träger
      als Sammel-Provider (z. B. prov-lhm – Landeshauptstadt München); einmal
      angelegt, vielfach referenziert. Bewusst ohne Lebenszyklus-Felder –
      Provider werden im Zug der Aktivitäts-/Event-Prüfung mitgeprüft.
    fields:
      - name: id
        label: ID
        widget: string
        pattern: ["^prov-[a-z0-9]+(-[a-z0-9]+)*$", "Präfix prov-, kebab-case"]
      - name: name
        label: Name
        widget: string
      - name: website
        label: Website
        widget: string
        pattern: ["^https?://", "http(s)-URL"]
      - { name: phone, label: Telefon, widget: string, required: false }
      - {
          name: email,
          label: "E-Mail",
          widget: string,
          required: false,
          hint: "Ziel der Anbieter-Info-Mail (E32), falls keine Impressumsadresse recherchiert wird.",
        }
      - name: booking_url
        label: Buchungs-URL
        widget: string
        required: false
        pattern: ["^https?://", "http(s)-URL"]
      - name: notes_internal
        label: Interne Notizen
        widget: text
        required: false
        hint: 'Erscheint NIRGENDS öffentlich. Auch Ort für E32-Opt-out („bitte keine Mails mehr").'
      - name: body
        label: Body (Markdown)
        widget: markdown
        required: false

  # ── Self-Make-Ideen ──────────────────────────────────────────────────────
  - name: self_make
    label: Self-Make-Ideen
    label_singular: Self-Make-Idee
    folder: content/self-make
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: name
    slug: "{{fields.id}}"
    summary: "{{name}} · {{status}}"
    editor:
      preview: false
    view_filters:
      - label: Entwürfe
        field: status
        pattern: draft
      - label: Aktiv
        field: status
        pattern: active
      - label: Ausgemustert
        field: status
        pattern: retired
    view_groups:
      - label: Status
        field: status
    sortable_fields: [name, status]
    fields:
      - name: id
        label: ID
        widget: string
        pattern: ["^sm-[a-z0-9]+(-[a-z0-9]+)*$", "Präfix sm-, kebab-case"]
        hint: "Nach Aktivierung UNVERÄNDERLICH."
      - name: name
        label: Name
        widget: string
      - name: short_description
        label: Kurzbeschreibung
        widget: text
        required: false
        pattern: ['^[\s\S]{1,280}$', "max. 280 Zeichen"]
        hint: "[P] Pflicht bei Status active."
      - name: categories
        label: Kategorien
        widget: select
        multiple: true
        min: 1
        max: 3
        options:
          [
            sport,
            kreativ,
            musisch,
            performativ,
            technisch,
            wissen,
            sozial,
            natur,
          ]
      - name: primary_category
        label: Primäre Kategorie
        widget: select
        options:
          [
            sport,
            kreativ,
            musisch,
            performativ,
            technisch,
            wissen,
            sozial,
            natur,
          ]
        hint: "Muss in den Kategorien enthalten sein."
      - name: tags
        label: Tags
        widget: list
        required: false
        field:
          name: tag
          label: Tag
          widget: string
          pattern: ["^[a-z0-9]+(-[a-z0-9]+)*$", "kebab-case"]
        hint: "Kuratierte Liste: docs/tags.md."
      - name: age_bands
        label: Altersbänder
        widget: select
        multiple: true
        min: 1
        options:
          - { label: "baby (0–2)", value: baby }
          - { label: "kita (3–5)", value: kita }
          - { label: "grundschule_jung (6–8)", value: grundschule_jung }
          - { label: "grundschule_alt (9–11)", value: grundschule_alt }
          - { label: "teen_jung (12–14)", value: teen_jung }
          - { label: "teen_alt (15–17)", value: teen_alt }
      - name: seasons
        label: Saisons (kuratorische Passung)
        widget: select
        multiple: true
        required: false
        options: [fruehling, sommer, herbst, winter]
        hint: "Leer = ganzjährig passend."
      - name: weather_fit
        label: Wetter-Passung
        widget: select
        multiple: true
        required: false
        options: [any, sunny, rainy, snow, cold, hot]
        hint: '„any" nur ALLEIN verwenden.'
      - name: setting
        label: Setting
        widget: select
        options: [home_indoor, home_outdoor, park, forest, water]
      - name: duration_minutes_typical
        label: Dauer typisch (Minuten)
        widget: number
        value_type: int
        min: 1
        required: false
        hint: "[P] Pflicht bei Status active."
      - name: prep_effort
        label: Vorbereitungsaufwand
        widget: select
        options: [none, shopping_list, multi_day]
      - name: shopping_list
        label: Einkaufsliste
        widget: list
        required: false
        field: { name: item, label: Posten, widget: string }
      - name: equipment_needed
        label: Benötigte Ausstattung
        widget: list
        required: false
        field: { name: item, label: Posten, widget: string }
      - name: bonding_score
        label: Bonding-Score
        widget: number
        value_type: int
        min: 1
        max: 5
        required: false
        hint: "[P] Anhang B. Pflicht bei Status active."
      - name: energy_level
        label: Energie-Level
        widget: select
        required: false
        options: [low, medium, high]
        hint: "[P] Pflicht bei Status active."
      - {
          name: newsletter_blurb,
          label: "Newsletter-Blurb",
          widget: text,
          required: false,
        }
      - name: last_featured_at
        label: Zuletzt featured
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "NIE MANUELL – nur via set-featured-Script."
      - name: status
        label: Status
        widget: select
        options: [draft, active, retired]
        default: draft
        hint: "active → draft unzulässig; Ausstieg über retired."
      - name: last_verified_at
        label: Zuletzt verifiziert
        widget: string
        required: false
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
        hint: "[P] Letzte redaktionelle Durchsicht; Freshness pauschal 18 Monate."
      - name: notes_internal
        label: Interne Notizen
        widget: text
        required: false
      - name: body
        label: Body (Markdown)
        widget: markdown
        required: false
        hint: "Vollständige Anleitung."

  # ── Newsletter-Ausgaben ──────────────────────────────────────────────────
  - name: newsletter
    label: Newsletter-Ausgaben
    label_singular: Newsletter-Ausgabe
    folder: content/newsletter
    create: true
    delete: false
    extension: md
    format: frontmatter
    identifier_field: id
    slug: "{{fields.date}}"
    summary: "Nr. {{issue_number}} · {{date}} · {{status}}"
    editor:
      preview: false
    view_filters:
      - label: Entwürfe
        field: status
        pattern: draft
      - label: Versendet
        field: status
        pattern: sent
    fields:
      - name: id
        label: ID
        widget: string
        pattern: ["^nl-[0-9]{4}-[0-9]{2}-[0-9]{2}$", "nl-YYYY-MM-DD"]
      - name: issue_number
        label: Ausgaben-Nummer
        widget: number
        value_type: int
        min: 0
        hint: "Fortlaufend, eindeutig."
      - name: date
        label: Versanddatum
        widget: string
        pattern: ["^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "YYYY-MM-DD"]
      - { name: subject, label: Betreff, widget: string, required: false }
      - name: featured_ids
        label: Featured-Einträge (IDs)
        widget: list
        required: false
        field:
          name: featured_id
          label: ID
          widget: string
          pattern: ["^[a-z0-9]+(-[a-z0-9]+)*$", "kebab-case-ID"]
        hint: >-
          IDs aller featured Einträge (Aktivität/Event/Self-Make). [P] Pflicht
          bei sent. Referenz-Existenz prüft die Validierung; last_featured_at
          setzt das set-featured-Script auf dem Archivierungs-PR – nie von Hand.
      - name: status
        label: Status
        widget: select
        options: [draft, sent]
        default: draft
      - name: body
        label: Body (Markdown)
        widget: markdown
        required: false
        hint: "Editorial/Intro der Ausgabe; Struktur folgt dem Newsletter-Template (D5)."
