# Decap CMS config for Hear Glue Ear.
#
# This file drives the editor UI Tamsin uses to add and edit content.
# It is Decap-compatible YAML; Decap reads it directly. Each
# `collection` becomes a folder under `content/` in the content repo,
# with one JSON file per entry. The Cloudflare Worker (in
# manifest-worker/src/sources/git.ts) reads these files via the GitHub
# API and produces the same signed manifest the Flutter app consumes.
#
# Why git-based: a hosted CMS means per-seat pricing, which is
# untenable for a free clinical kids' app. Decap is free, the content
# lives in our git repo (already where backups are mirrored), and
# there's no vendor pricing risk. See docs/handover/cms-deployment.md
# for the full architecture.

backend:
  name: github
  repo: Paul-PSDigital/HearGlueEar-content
  branch: main
  # OAuth proxy: use a Cloudflare Worker as the auth handler (avoids
  # depending on Netlify for the OAuth flow). See cms-setup/README.md
  # for the worker's `oauth.ts` setup.
  base_url: https://hge-cms-auth.ping-b4f.workers.dev

# Review gate: editor saves open a PR/branch instead of committing
# straight to `main`. Tamsin's saves land as editorial-workflow entries
# for review before they hit the published content branch.
publish_mode: editorial_workflow

# Editor URL displayed to logged-in users (top-right of Decap).
site_url: https://hge-cms.pages.dev
display_url: https://hge-cms.pages.dev
logo_url: /branding/icon-1024.png

# Media: uploads go to Cloudflare R2 via the `r2` media library plugin
# registered in admin/index.html (which POSTs to the hge-asset-upload
# worker and stores the returned hge-assets URL in the field). We do NOT
# commit binaries to git. media_folder/public_folder are only the
# fallback path for Decap's default picker, which the r2 plugin replaces.
media_folder: assets
public_folder: assets
media_library:
  name: r2

# Live preview: opens https://hge-cms.pages.dev/preview/<slug> in a
# side-pane iframe and pushes editor changes via postMessage. The
# ?cms=1 query flag tells preview.html to listen for editor messages.
editor:
  preview: true
preview_path: /preview/{{slug}}?cms=1

# Translation: enable Decap i18n globally so any collection that opts
# in (i18n: true) gets a per-locale column for every field flagged
# i18n: true. Fields flagged i18n: duplicate share the same value
# across locales (keys, slugs, structural pickers). Fields with no
# i18n marker live in the default locale only. Storage uses
# structure: single_file for files-collections; folder-collections
# can override to multiple_files if we want one file per language.
i18n:
  locales: [en-GB, cy-GB, ne-NP, ny-MW, ar]
  default_locale: en-GB
  structure: single_file

# ---- Collections ---------------------------------------------------------
# Each root content type maps to a Decap collection here. Order roughly
# matches the editor sidebar order Tamsin is used to.

collections:
  # ===== Audiobooks ======================================================
  - name: audiobooks
    label: Audiobooks
    label_singular: Audiobook
    folder: content/audiobooks
    create: true
    delete: true
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{title}} ({{ageBand}})'
    fields:
      - {label: Title, name: title, widget: string}
      - {label: Slug, name: slug, widget: string, pattern: ['^[a-z0-9-]+$', 'Lowercase letters, numbers and hyphens only']}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}
      - {label: Narrator, name: narrator, widget: string, required: false, hint: 'Shows as a "Read by X" pill above the audio player. Migrated entries ship empty (the 2017 backup did not preserve narrator); fill in to credit.'}
      - label: Age band
        name: ageBand
        widget: string
        default: '4-6'
        pattern: ['^[0-9a-z+\-]+$', 'Like 2-4, 4-6, 6-8, 8-10, or any.']
        hint: 'Audience age band. Common: 2-4, 4-6, 6-8. Add new bands freely (e.g. 0-2, 8-10).'
      - {label: Cover image, name: cover, widget: image, required: false, hint: 'Click to upload — file goes to R2, URL stored here.'}
      - {label: Audio, name: audio, widget: file, required: false, hint: 'Click to upload — file goes to R2, URL stored here.'}
      - {label: Duration (seconds), name: durationSec, widget: number, value_type: int}
      # ---- HGE-SPECIFIC FIELD --------------------------------------------
      # `phonemes` is part of the Hear Glue Ear audiology overlay. Glue ear
      # primarily impairs perception of high-frequency consonants (s, sh,
      # ch, th, f, v, t, k); tagging audiobooks that emphasise those sounds
      # unlocks the Play screen's "filter by sound" feature and the
      # per-phoneme recommender. If forking this config for a non-clinical
      # app, drop this whole field (and the matching one on audio_quizzes).
      # Stored as a plain string array so the manifest worker stays agnostic.
      - label: Phonemes practised (HGE-specific)
        name: phonemes
        widget: select
        multiple: true
        required: false
        default: []
        hint: 'HGE audiology tag. Mark the consonants this audiobook emphasises. Used by the Play screen to suggest sound-specific practice.'
        options:
          # High-frequency consonants — most affected by glue ear.
          - s
          - z
          - sh
          - zh
          - ch
          - j
          - th
          - dh
          - f
          - v
          - t
          - d
          - k
          - g
          # Lower-frequency consonants.
          - p
          - b
          - m
          - n
          - ng
          - l
          - r
          - w
          - y
          - h
      - label: Pages (page-by-page tap-to-play)
        name: pages
        widget: list
        required: false
        # Collapsed-row label so the page list shows meaningful content
        # instead of just "1", "2", "3". Truncated to keep rows compact.
        summary: 'Page {{pageNumber}} — {{audioStart}}s → {{audioEnd}}s — {{text | truncate(60)}}'
        fields:
          - {label: Page number, name: pageNumber, widget: number, value_type: int}
          - {label: Image, name: image, widget: image, required: false}
          - {label: Audio start (seconds), name: audioStart, widget: number, value_type: float}
          - {label: Audio end (seconds), name: audioEnd, widget: number, value_type: float}
          # Field name was "transcript" but the migration scripts (and
          # mockups/preview.html's renderer) read it as "text". Renamed
          # so existing JSON files surface correctly in the editor.
          - {label: Text on this page, name: text, widget: text, required: false}
      - label: Comprehension questions (end-of-audiobook)
        name: comprehension
        widget: list
        required: false
        fields:
          - {label: Prompt text, name: promptText, widget: string}
          - {label: Prompt audio URL, name: promptAudio, widget: string, required: false}
          - label: Options
            name: options
            widget: list
            fields:
              - {label: Label, name: label, widget: string}
              - {label: Emoji, name: emoji, widget: string, required: false}
              - {label: Image URL, name: image, widget: string, required: false}
              - {label: Is correct answer, name: isAnswer, widget: boolean, default: false}

      # --- Fallback / advanced fields ----------------------------------
      # These render ONLY when no cover image is uploaded — a coloured
      # background tile with the emoji. Once cover image is set, both
      # fields are ignored. Pushed to the bottom so editors don't waste
      # attention on them in the steady state.
      - {label: Cover emoji (fallback only), name: coverEmoji, widget: string, required: false, hint: 'Used only when Cover image URL is empty. Picks an emoji shown on a coloured tile.'}
      - {label: Cover tone (fallback only), name: coverTone, widget: select, options: [blue, green, pink, yellow, orange, purple, teal, mustard], default: blue, hint: 'Background colour for the emoji fallback tile. Ignored when a cover image is set.'}

  # ===== Songs (music videos) ============================================
  - name: songs
    label: Songs
    label_singular: Song
    folder: content/songs
    create: true
    delete: true
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{title}} ({{ageBand}})'
    fields:
      - {label: Title, name: title, widget: string}
      - {label: Slug, name: slug, widget: string, pattern: ['^[a-z0-9-]+$', 'Lowercase letters, numbers and hyphens only']}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}
      - label: Age band
        name: ageBand
        widget: string
        default: '4-6'
        pattern: ['^[0-9a-z+\-]+$', 'Like 2-4, 4-6, 6-8, 8-10, or any.']
        hint: 'Audience age band. Common: 2-4, 4-6, 6-8. Add new bands freely (e.g. 0-2, 8-10).'
      - {label: Cover image, name: cover, widget: image, required: false, hint: 'Click to upload — file goes to R2, URL stored here.'}
      - {label: Audio / video, name: audio, widget: file, required: false, hint: 'MP4 music video preferred; MP3 audio-only accepted. Click to upload to R2.'}
      - {label: Duration (seconds), name: durationSec, widget: number, value_type: int}
      - {label: Description, name: description, widget: text, required: false, hint: 'One or two lines about the song. Shown under the player on the song page.'}
      - {label: Lyrics, name: lyrics, widget: text, required: false, hint: 'Optional. One line per line; rendered scrollable under the player so parents can sing along.'}
      # HGE-specific. See note on the audiobooks collection.
      - {label: Phonemes practised (HGE-specific), name: phonemes, widget: list, default: [], required: false, hint: 'HGE audiology tag. When populated, lets the Play screen filter songs by phoneme.'}
      # --- Fallback / advanced fields ----------------------------------
      - {label: Cover emoji (fallback only), name: coverEmoji, widget: string, required: false, hint: 'Used only when Cover image URL is empty.'}
      - {label: Cover tone (fallback only), name: coverTone, widget: select, options: [blue, green, pink, yellow, orange, purple, teal, mustard], default: blue, hint: 'Background colour for the emoji fallback. Ignored when a cover image is set.'}
      - {label: Loopable, name: loopable, widget: boolean, default: false}

  # ===== Audio quizzes (pick-the-right-option mini-games) ================
  # Generic audio-prompt + multiple-choice template. Used by HGE for
  # listening practice, but the shape is portable to any "play a clip,
  # pick the matching image" mini-game.
  - name: audio_quizzes
    label: Audio quizzes
    label_singular: Audio quiz
    folder: content/audio-quizzes
    create: true
    delete: true
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{title}} ({{template}})'
    fields:
      - {label: Title, name: title, widget: string}
      - {label: Slug, name: slug, widget: string, pattern: ['^[a-z0-9-]+$', '']}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}
      - {label: Template, name: template, widget: select, options: [pickOne, getDressed], hint: 'Which renderer in the app handles this quiz. Adding a new template requires an app release.'}
      - {label: Teaser, name: teaser, widget: string, required: false}
      - label: Age band
        name: ageBand
        widget: string
        default: '4-6'
        pattern: ['^[0-9a-z+\-]+$', 'Like 2-4, 4-6, 6-8, 8-10, or any.']
        hint: 'Audience age band. Common: 2-4, 4-6, 6-8. Add new bands freely (e.g. 0-2, 8-10).'
      - {label: Cover emoji, name: coverEmoji, widget: string, required: false}
      - {label: Cover tone, name: coverTone, widget: select, options: [blue, green, pink, yellow, orange, purple, teal, mustard], default: blue}
      - label: Prompts
        name: prompts
        widget: list
        fields:
          - {label: Transcript, name: transcript, widget: string}
          - {label: Audio, name: audio, widget: file, required: false, hint: 'Click to upload — file goes to R2, URL stored here.'}
          - label: Options
            name: options
            widget: list
            fields:
              - {label: Label, name: label, widget: string}
              - {label: Emoji, name: emoji, widget: string, required: false}
              - {label: Image, name: image, widget: image, required: false}
              - {label: Is correct answer, name: isAnswer, widget: boolean, default: false}

  # ===== Articles (FAQs, care plan, SLT videos, external links) ==========
  - name: articles
    label: Articles
    label_singular: Article
    folder: content/articles
    create: true
    delete: true
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{title}} ({{kind}})'
    fields:
      - {label: Title, name: title, widget: string}
      - {label: Slug, name: slug, widget: string, pattern: ['^[a-z0-9-]+$', '']}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}
      - label: Kind
        name: kind
        widget: string
        default: general
        pattern: ['^[a-z][a-zA-Z0-9]*$', 'camelCase letters and numbers, starting with a lowercase letter.']
        hint: |
          What sort of article this is. The Flutter card shows a small kind
          label based on this value. Known values that get nice labels: faq
          (FAQ), carePlan (Care plan), external (External), slt (SLT).
          Anything else (e.g. general, passport, videoGuide) displays as
          "Article". Adding a new kind is free.
      - {label: Body (markdown), name: body, widget: markdown}
      - {label: Category, name: category, widget: string, required: false}
      - label: Section
        name: section
        widget: select
        options: [reference, journey, care_plan, wellbeing]
        default: reference
        hint: |
          Where this article appears on the Hearing tab.
          - journey: the numbered "freshly-diagnosed parent course" (use Order to sequence).
          - care_plan: Care Plan / Passport artefacts (big tiles, not list).
          - wellbeing: caregiver self-care content.
          - reference: NHS/NDCS signposting + everything else (default).
      - label: Order
        name: order
        widget: number
        value_type: int
        default: 0
        required: false
        hint: 'Only used inside the journey section. 1, 2, 3, ... lower = earlier. Ignored elsewhere.'
      - {label: Issued by (NHS / NDCS / SLT name), name: issuedBy, widget: string, required: false}
      - {label: Status, name: status, widget: select, options: [draft, review, published], default: draft}
      - label: Age band
        name: ageBand
        widget: string
        default: any
        pattern: ['^[0-9a-z+\-]+$', 'Like 2-4, 4-6, 6-8, 8-10, any.']
        hint: 'Audience age band. Use "any" if the article works across ages.'
      - {label: External link, name: externalLink, widget: string, required: false}
      - {label: Video, name: video, widget: file, required: false, hint: 'Click to upload — file goes to R2, URL stored here.'}
      # Downloadable / printable attachment, e.g. the Glue Ear Passport
      # template. When set, the article screen shows a prominent
      # "Download / print" button as the primary action (used by care-plan
      # articles whose fillable document belongs in a PDF, not inline body).
      - {label: PDF attachment, name: pdf, widget: file, required: false, hint: 'Click to upload (file goes to R2, URL stored here). Shown as a Download / print button.'}
      - {label: Brand colour (hex), name: brandColor, widget: string, required: false}

  # ===== UI strings ======================================================
  # Folder collection (one file per key). Decap i18n only supports
  # folder collections, not files-collections, so each key is its own
  # entry. Inside each entry, `key` is i18n: duplicate (shared across
  # all locales) and `value` is i18n: true (Decap renders one column
  # per locale). Storage is one file per key with all 5 locales nested
  # at the top level. List view shows every key with the default-locale
  # value as a summary, so Tamsin can scan and spot missing translations.
  - name: ui_strings
    label: UI strings
    label_singular: UI string
    folder: content/ui-strings/keys
    create: true
    delete: true
    slug: '{{key}}'
    extension: json
    format: json
    editor: { preview: false }
    summary: '{{key}} → {{value | truncate(60)}}'
    i18n: true
    fields:
      - {label: Key, name: key, widget: string, i18n: duplicate, hint: 'e.g. home.greeting. Same across every locale.'}
      - {label: Value, name: value, widget: text, i18n: true}

  # ===== Feature flags ===================================================
  # Single-file collection mirroring the ui_strings UX: one form, one list
  # of {key, alpha, beta, production, note} rows. Means Tamsin can scan
  # every flag and its per-ring state without jumping in and out of 13
  # separate files.
  - name: feature_flags
    label: Feature flags
    editor: { preview: false }
    files:
      - file: content/feature-flags/flags.json
        label: All feature flags
        name: flags
        fields:
          - label: Flags
            name: flags
            widget: list
            # collapsed: false renders every row expanded with its boolean
            # toggles inline. Otherwise Decap shows a clickable accordion
            # header per row and you have to expand each one to flip a flag.
            collapsed: false
            summary: '{{key}}  •  α {{alpha}}  β {{beta}}  prod {{production}}'
            fields:
              - {label: Key, name: key, widget: string, hint: 'e.g. feature.songs, feature.hearing_screen'}
              - {label: Alpha (internal builds), name: alpha, widget: boolean, default: true}
              - {label: Beta (closed beta), name: beta, widget: boolean, default: false}
              - {label: Production (App Store / Play), name: production, widget: boolean, default: false}
              - {label: Note (why these settings), name: note, widget: text, required: false}

  # ===== Shop items ======================================================
  - name: shop_items
    label: Shop tiles
    label_singular: Shop tile
    folder: content/shop-items
    create: true
    delete: true
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{title}} ({{vendor}})'
    fields:
      - {label: Title, name: title, widget: string}
      - {label: Slug, name: slug, widget: string, pattern: ['^[a-z0-9-]+$', '']}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}
      - {label: Blurb, name: blurb, widget: text}
      - {label: Price, name: price, widget: string}
      - label: Category
        name: category
        widget: string
        pattern: ['^[a-z][a-z0-9-]*$', 'Lowercase letters, numbers and hyphens. Must start with a letter.']
        default: kits
        hint: |
          Free-text grouping for the shop. Used by shopSection's category
          filter (exact match). Common values today: kits, microphones,
          premium, books, accessories. Add new ones (e.g. earplugs,
          earmoulds) freely; they'll appear automatically wherever a
          category filter or label is rendered.
      - {label: Vendor, name: vendor, widget: string}
      - {label: External URL, name: externalUrl, widget: string}
      - {label: Product image, name: image, widget: image, required: false, hint: 'Click to upload — file goes to R2. When set, the tile shows this photo instead of the emoji fallback.'}
      - {label: Cover emoji (fallback only), name: coverEmoji, widget: string, required: false, hint: 'Shown when no Product image URL is set.'}
      - {label: Cover tone (fallback only), name: coverTone, widget: select, options: [blue, green, pink, yellow, orange, purple, teal, mustard], default: blue, hint: 'Background colour behind the fallback emoji. Ignored when an image is set.'}

  # ===== Onboarding steps ================================================
  - name: onboarding_steps
    label: Onboarding steps
    label_singular: Onboarding step
    folder: content/onboarding-steps
    create: true
    delete: true
    slug: '{{stepNumber}}-{{slug}}'
    extension: json
    format: json
    summary: 'Step {{stepNumber}}: {{heading}} ({{kind}})'
    fields:
      - {label: Step number, name: stepNumber, widget: number, value_type: int, hint: 'Drives the order of steps. The app sorts ascending. Use 10, 20, 30 (gaps) so you can insert a step between two existing ones without renumbering everything.'}
      - {label: Kind, name: kind, widget: select, options: [welcome, permissions, profile, pairing, done], hint: 'Tells the app what to do at the end of this step. Each kind has bespoke behaviour (profile shows the name+DOB form; pairing opens the headset assistant; done finishes onboarding). Editable copy lives below; behaviour is fixed in code.'}
      - {label: Heading, name: heading, widget: string}
      - {label: Body text, name: bodyText, widget: text}
      - {label: CTA label, name: ctaLabel, widget: string, required: false}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}

  # ===== Pairing steps ===================================================
  - name: pairing_steps
    label: Pairing assistant steps
    label_singular: Pairing step
    folder: content/pairing-steps
    create: true
    delete: true
    slug: '{{stepNumber}}-{{slug}}'
    extension: json
    format: json
    summary: 'Step {{stepNumber}}: {{heading}}'
    fields:
      - {label: Step number, name: stepNumber, widget: number, value_type: int}
      - {label: Heading, name: heading, widget: string}
      - {label: Body text, name: bodyText, widget: text}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}

  # ===== Pairing troubleshooting =========================================
  - name: pairing_troubleshoot
    label: Pairing troubleshooting
    label_singular: Troubleshooting card
    folder: content/pairing-troubleshoot
    create: true
    delete: true
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{question}}'
    fields:
      - {label: Order, name: order, widget: string}
      - {label: Question, name: question, widget: string}
      - {label: Answer (markdown), name: answer, widget: markdown}
      - {label: Locale, name: locale, widget: select, options: [en-GB, cy-GB, ne-NP, ny-MW, ar], default: en-GB}

  # ===== App pages (root layouts: home / listen / play / hearing) ========
  # These are the page-template entries that drive tab layouts. Tamsin
  # can reorder, rename, or hide tabs from this collection; adding a
  # genuinely new tab requires extending the `tabKey` enum below AND a
  # code release for any bespoke screen behaviour. Pages with a tabKey
  # that has no bespoke screen fall back to the generic ManifestPageScreen
  # via the /page/:tabKey route.
  - name: app_pages
    label: App pages (layout templates)
    label_singular: App page
    folder: content/app-pages
    create: false
    delete: false
    slug: '{{slug}}'
    extension: json
    format: json
    summary: '{{title}} (tab: {{tabKey}}, order: {{tabOrder}})'
    fields:
      - {label: Title, name: title, widget: string}
      - {label: Slug, name: slug, widget: string}
      - label: Tab key
        name: tabKey
        widget: string
        pattern: ['^[a-z][a-z0-9-]*$', 'Lowercase letters, numbers and hyphens. Must start with a letter.']
        hint: |
          Unique kebab-case identifier for this page. Also used as the route segment.
          Bespoke screens exist for: home, listen, play, hearing (custom behaviour
          like the daily hero, phoneme recommender, etc). Any other value renders
          via the generic /page/<tabKey> route, which shows the page body bloks
          but no custom widgets. Pick a fresh value for new pages.

      # ---- Tab bar metadata ----------------------------------------------
      # Drives the bottom-nav rendering in apps/mobile/lib/shell.dart.
      # Changing any of these only requires a CMS publish; no code release.
      - {label: Show in tab bar, name: showInTabBar, widget: boolean, default: true, hint: 'Uncheck to hide this page from the bottom navigation. The page is still reachable by direct route if linked from elsewhere.'}
      - {label: Tab bar order, name: tabOrder, widget: number, value_type: int, default: 100, hint: 'Lower numbers appear first. Use 0,10,20,30 etc so you can insert later without renumbering.'}
      - {label: Tab bar label key, name: tabLabelKey, widget: string, required: false, hint: 'A uiString key, e.g. "tab.home". Leave blank to default to "tab.<tabKey>".'}
      - {label: Tab bar icon, name: tabIcon, widget: select, options: [home, book, game, hearing, star, heart, settings, info, palette, mic, search, calendar, lightbulb, video, headphones], default: home, hint: 'Icon shown in the bottom navigation. The full set lives in apps/mobile/lib/shell.dart; ask Paul to add new ones.'}

      # ---- Scheduling ----------------------------------------------------
      # Optional. If set to a future timestamp, the manifest worker hides
      # the page from the published manifest until that moment. Editors
      # can stage a page change today and have it appear tomorrow morning
      # without staying up.
      #
      # IMPORTANT: the manifest is republished only when (a) the cron
      # fires (currently 03:00 UTC daily) or (b) someone manually
      # triggers a publish. A page going live at 14:00 won't appear in
      # users' apps until the next publish. For tight timing, trigger a
      # republish at the go-live moment.
      - label: Go live at
        name: goLiveAt
        widget: datetime
        required: false
        hint: |
          Optional. The page is published immediately when blank. If set
          to a future moment, the manifest worker hides this page until
          the time has passed. Note that families only see the change at
          the next manifest publish (daily cron at 03:00 UTC, or on demand).

      # Body composer. Tamsin clicks "Add" and picks one of the typed
      # bloks below; each blok gets its own structured form.
      #
      # Components match what mockups/preview.html renderBlok() handles.
      # When adding a new blok type to the renderer, add it here too.
      #
      # typeKey=component: Decap writes the discriminator field as
      # `component` (not the default `type`), matching the shape the
      # renderer and manifest worker already expect.
      - label: Body
        name: body
        widget: list
        required: false
        typeKey: component
        types_default: textBlock
        summary: '{{component}} — {{heading}}'
        types:
          - name: textBlock
            label: Text block
            widget: object
            summary: 'Text — {{heading}}'
            fields:
              - {label: Heading, name: heading, widget: string, required: false}
              - {label: Body, name: body, widget: text, required: false}
          - name: mediaSection
            label: Content list (audiobooks / songs / audio quizzes / articles)
            widget: object
            summary: 'List — {{heading}} ({{layout}})'
            fields:
              - {label: Heading, name: heading, widget: string, required: false}
              - {label: Caption, name: caption, widget: string, required: false}
              - {label: Layout, name: layout, widget: select, options: [row, stack], default: stack, hint: 'row = horizontal tile strip; stack = vertical cards'}
              # Source of items. If "manual", the editor picks specific
              # entries below. If set to a collection name (audiobooks / songs /
              # audio_quizzes / articles), an EMPTY items list auto-fills with every
              # published entry of that kind at publish time. Setting both
              # a kind AND specific items means the picked items act as a
              # featured-first ordering, then everything else by recency.
              - label: Source
                name: source
                widget: select
                options: [manual, audiobooks, songs, audio_quizzes, articles, shop_items]
                default: manual
                hint: '"manual" = exactly the items below. Any other value = auto-fill all of that kind when items is empty.'
              # Items are typed so each row opens a searchable picker against
              # the right collection — no slug typing. Decap's relation
              # widget auto-completes against titles + slugs and stores the
              # slug back into `target`. The renderer's resolveRefs() then
              # resolves the slug to the full content blob at manifest time.
              - label: Items
                name: items
                widget: list
                typeKey: kind
                types_default: audiobook
                summary: '{{kind}} → {{target}}'
                types:
                  - name: audiobook
                    label: Audiobook
                    widget: object
                    summary: 'Audiobook to {{target}}'
                    fields:
                      - {label: Component, name: component, widget: hidden, default: contentRef}
                      - {label: Kind, name: kind, widget: hidden, default: audiobook}
                      - label: Audiobook
                        name: target
                        widget: relation
                        collection: audiobooks
                        search_fields: [title, slug]
                        value_field: slug
                        display_fields: [title]
                  - name: song
                    label: Song
                    widget: object
                    summary: 'Song → {{target}}'
                    fields:
                      - {label: Component, name: component, widget: hidden, default: contentRef}
                      - {label: Kind, name: kind, widget: hidden, default: song}
                      - label: Song
                        name: target
                        widget: relation
                        collection: songs
                        search_fields: [title, slug]
                        value_field: slug
                        display_fields: [title]
                  - name: audioQuiz
                    label: Audio quiz
                    widget: object
                    summary: 'Audio quiz to {{target}}'
                    fields:
                      - {label: Component, name: component, widget: hidden, default: contentRef}
                      - {label: Kind, name: kind, widget: hidden, default: audioQuiz}
                      - label: Audio quiz
                        name: target
                        widget: relation
                        collection: audio_quizzes
                        search_fields: [title, slug]
                        value_field: slug
                        display_fields: [title]
                  - name: article
                    label: Article (FAQ / SLT / care plan)
                    widget: object
                    summary: 'Article → {{target}}'
                    fields:
                      - {label: Component, name: component, widget: hidden, default: contentRef}
                      - {label: Kind, name: kind, widget: hidden, default: article}
                      - label: Article
                        name: target
                        widget: relation
                        collection: articles
                        search_fields: [title, slug]
                        value_field: slug
                        display_fields: [title]
                  - name: shop
                    label: Shop tile
                    widget: object
                    summary: 'Shop → {{target}}'
                    fields:
                      - {label: Component, name: component, widget: hidden, default: contentRef}
                      - {label: Kind, name: kind, widget: hidden, default: shopItem}
                      - label: Shop item
                        name: target
                        widget: relation
                        collection: shop_items
                        search_fields: [title, slug, vendor]
                        value_field: slug
                        display_fields: ['{{title}} ({{vendor}})']
          - name: dailyHero
            label: Daily hero (home-page feature)
            widget: object
            summary: 'Hero, {{heading}}'
            fields:
              # Greeting line above the hero. `{name}` is substituted with
              # the signed-in profile's display name at runtime.
              - {label: Greeting, name: greeting, widget: string, required: false, hint: 'Use {name} as a placeholder for the active profile name. Example: "Hi {name}".'}
              - {label: Time of day label, name: timeOfDayLabel, widget: string, required: false, hint: 'Shown after the weekday. Examples: "today", "this morning".'}
              - {label: Heading, name: heading, widget: string, required: false, hint: 'Main hero line. Examples: "What shall we do today?", "Ready for a quick session?".'}
              - {label: Body text, name: bodyText, widget: string, required: false, hint: 'Supporting copy under the heading.'}
              - {label: Character emoji, name: character, widget: string, required: false, hint: 'Single emoji shown on the right of the hero card.'}
          - name: tileNav
            label: Big navigation tile (Listen / Play / Hearing rows)
            widget: object
            summary: 'Tile — {{label}} → {{target}}'
            fields:
              - {label: Label, name: label, widget: string}
              - {label: Sublabel, name: sublabel, widget: string, required: false}
              - {label: Icon (emoji), name: icon, widget: string, default: '•', hint: 'Single emoji shown on the tile. Used when iconKey is blank.'}
              - {label: Icon key (overrides emoji), name: iconKey, widget: select, required: false, default: '', options: ['', home, book, game, hearing, star, heart, settings, info, palette, mic, search, calendar, lightbulb, video, headphones], hint: 'Pick a Material icon. When set, this replaces the emoji. Leave blank to use the emoji above (or, if no icon is set anywhere, the renderer picks one from the target route).'}
              - {label: Hidden, name: hidden, widget: boolean, default: false, hint: 'Tick to hide this tile without deleting it. Useful for parking a tile during a content review.'}
              - {label: Tone, name: tone, widget: select, options: [blue, orange, green, neutral, purple, teal, mustard], default: blue}
              # Target is a short string the app router knows. Common values:
              # listen, play, hearing, hearing-test.
              - label: Target route
                name: target
                widget: string
                pattern: ['^[a-z][a-z0-9-]*$', 'Lowercase letters, numbers and hyphens. Must start with a letter.']
                default: listen
                hint: |
                  Where this tile takes the family. Use the tabKey of any
                  app_page (e.g. listen, play, hearing, or a new page Tamsin
                  has published) and the tile will navigate there. Special
                  routes the app knows about: hearing-test (the audiogram
                  screen, currently gated). Bespoke screens exist for the
                  four original tabs; other tabKeys route to /page/<target>
                  and render the page body bloks.
          - name: sectionHeading
            label: Section heading
            widget: object
            summary: 'Heading — {{heading}}'
            fields:
              - {label: Heading, name: heading, widget: string}
              - {label: Caption, name: caption, widget: string, required: false}
          - name: shopSection
            label: Shop section
            widget: object
            summary: 'Shop — {{heading}}'
            fields:
              - {label: Heading, name: heading, widget: string, required: false}
              - {label: Caption, name: caption, widget: string, required: false}
              - label: Category filter
                name: categoryFilter
                widget: string
                required: false
                default: ''
                pattern: ['^([a-z][a-z0-9-]*)?$', 'Leave blank for everything, or a single category slug.']
                hint: |
                  Leave blank to show every shop item. Otherwise enter one
                  category slug (e.g. kits, microphones, premium). Overridden
                  when specific items are picked below. Categories themselves
                  live on each shop item; add new ones there first.
              # Single multi-select relation widget. Decap renders this as
              # a searchable picker that already shows the chosen tiles as
              # chips, no need for a wrapping list-of-objects accordion.
              # Stored as an array of shop_item slugs; the preview adapter
              # + manifest resolver both expand the slugs at render time.
              - {label: Picked items (overrides filter), name: items, widget: relation, collection: shop_items, search_fields: [title, slug, vendor], value_field: slug, display_fields: ['{{title}} ({{vendor}})'], multiple: true, required: false, default: []}
          - name: searchBar
            label: Search bar
            widget: object
            summary: 'Search bar'
            fields:
              - {label: Placeholder key, name: placeholderKey, widget: string, default: 'search.placeholder', hint: 'A uiString key. Default search.placeholder works.'}
