<template>
  <div class="recipes-view">

    <!-- Tab bar: Find / Browse / Saved + Scan action button -->
    <div class="tab-bar-row flex gap-xs mb-md" style="align-items:center;">
      <div role="tablist" aria-label="Recipe sections" class="tab-bar-scroll">
        <button
          v-for="tab in tabs"
          :key="tab.id"
          :id="`tab-${tab.id}`"
          role="tab"
          :aria-selected="activeTab === tab.id"
          :tabindex="activeTab === tab.id ? 0 : -1"
          :class="['btn', 'tab-btn', activeTab === tab.id ? 'btn-primary' : 'btn-secondary']"
          @click="activateTab(tab.id)"
          @keydown="onTabKeydown"
        >
          <span v-if="tab.mobileLabel" class="desktop-only">{{ tab.label }}</span>
          <span v-if="tab.mobileLabel" class="mobile-only">{{ tab.mobileLabel }}</span>
          <template v-if="!tab.mobileLabel">{{ tab.label }}</template>
        </button>
      </div>
      <!-- Scan recipe button — opens modal to photograph a recipe card -->
      <button
        class="btn btn-secondary scan-btn"
        style="flex-shrink:0;"
        @click="scanModalOpen = true"
        title="Scan a recipe card or cookbook page"
        aria-label="Scan a recipe"
      >
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
          <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
          <circle cx="12" cy="13" r="4"/>
        </svg>
        <span class="desktop-only">Scan</span>
      </button>
    </div>

    <!-- Scan success toast -->
    <div
      v-if="lastScannedTitle"
      class="undo-toast"
      role="status"
      aria-live="polite"
    >
      Saved "{{ lastScannedTitle }}" to your recipes.
    </div>

    <!-- Scan modal -->
    <RecipeScanModal
      v-if="scanModalOpen"
      @close="scanModalOpen = false"
      @saved="onScanSaved"
    />

    <!-- Browse tab -->
    <RecipeBrowserPanel
      v-if="activeTab === 'browse'"
      role="tabpanel"
      aria-labelledby="tab-browse"
      tabindex="0"
      @open-recipe="openRecipeById"
    />

    <!-- Saved tab -->
    <SavedRecipesPanel
      v-else-if="activeTab === 'saved'"
      role="tabpanel"
      aria-labelledby="tab-saved"
      tabindex="0"
      @open-recipe="openRecipeById"
      @go-to-tab="(tab: string) => activateTab(tab as TabId)"
    />

    <!-- Community tab -->
    <CommunityFeedPanel
      v-else-if="activeTab === 'community'"
      role="tabpanel"
      aria-labelledby="tab-community"
      tabindex="0"
      @plan-forked="onPlanForked"
    />

    <!-- Build Your Own tab -->
    <BuildYourOwnTab
      v-else-if="activeTab === 'build'"
      role="tabpanel"
      aria-labelledby="tab-build"
      tabindex="0"
    />

    <!-- Ask tab — natural-language recipe search with optional LLM synthesis -->
    <div
      v-else-if="activeTab === 'ask'"
      ref="askPanelRef"
      role="tabpanel"
      aria-labelledby="tab-ask"
      tabindex="0"
      class="ask-panel"
    >
      <div class="card mb-md">
        <h2 class="section-title text-xl mb-xs">Ask about recipes</h2>
        <p class="text-sm text-secondary mb-md">Search by ingredient, dish type, or any cooking question.</p>

        <!-- Question input -->
        <div class="ask-input-row">
          <input
            type="text"
            v-model="askQuestion"
            class="form-input ask-input"
            placeholder="e.g. What can I make with chicken and pasta?"
            aria-label="Recipe question"
            @keydown="onAskKeydown"
            :disabled="askLoading"
          />
          <button
            class="btn btn-primary ask-submit"
            :disabled="askLoading || !askQuestion.trim()"
            @click="handleAsk()"
            aria-label="Search recipes"
          >
            <span v-if="askLoading" class="spinner spinner-sm inline-spinner"></span>
            <span v-else>Search</span>
          </button>
        </div>

        <!-- Example questions — shown before first search -->
        <div v-if="!askResult && !askLoading" class="ask-examples">
          <p class="text-xs text-secondary mb-xs">Try asking:</p>
          <div class="flex flex-wrap gap-xs">
            <button
              v-for="ex in ASK_EXAMPLES"
              :key="ex"
              class="btn btn-xs btn-secondary ask-example-btn"
              @click="handleAsk(ex)"
            >{{ ex }}</button>
          </div>
        </div>

        <!-- Error state -->
        <div v-if="askError" role="alert" class="status-badge status-error mt-sm">
          {{ askError }}
        </div>
      </div>

      <!-- Result: LLM answer + recipe list -->
      <div v-if="askResult">
        <!-- LLM-synthesized answer (Paid tier) -->
        <div v-if="askResult.answer" class="ask-answer card mb-md" role="status" aria-live="polite">
          <p class="text-sm">{{ askResult.answer }}</p>
        </div>

        <!-- Recipe results -->
        <div v-if="askResult.recipes.length > 0" class="mb-md">
          <p class="text-sm text-secondary mb-sm">
            {{ askResult.recipes.length }} recipe{{ askResult.recipes.length !== 1 ? 's' : '' }} found
          </p>
          <div class="grid-auto">
            <div
              v-for="hit in askResult.recipes"
              :key="hit.id"
              class="card recipe-card-compact ask-hit-card slide-up"
              role="article"
              tabindex="0"
              @click="openRecipeById(hit.id)"
              @keydown.enter="openRecipeById(hit.id)"
              :aria-label="'View recipe: ' + hit.title"
            >
              <h3 class="text-base font-bold mb-xs recipe-title">{{ hit.title }}</h3>
              <div class="flex flex-wrap gap-xs">
                <span v-if="hit.category" class="status-badge status-neutral text-xs">{{ hit.category }}</span>
                <span
                  v-if="hit.match_pct !== null"
                  :class="['status-badge', 'text-xs', hit.match_pct >= 0.7 ? 'status-success' : hit.match_pct >= 0.4 ? 'status-info' : 'status-neutral']"
                >{{ Math.round(hit.match_pct * 100) }}% pantry match</span>
              </div>
            </div>
          </div>
        </div>

        <!-- No results -->
        <div v-else class="card text-center text-muted">
          <p>No recipes found for this question. Try different keywords.</p>
        </div>
      </div>

      <!-- Session history (last 3 questions) -->
      <div v-if="askHistory.length > 1" class="ask-history mt-md">
        <div class="flex-between mb-xs">
          <p class="text-xs text-secondary">Recent questions</p>
          <button class="btn btn-xs btn-ghost" @click="askHistory = []" aria-label="Clear question history">Clear</button>
        </div>
        <div class="flex flex-wrap gap-xs">
          <button
            v-for="entry in askHistory.slice(1)"
            :key="entry.question"
            class="btn btn-xs btn-secondary ask-history-btn"
            @click="handleAsk(entry.question)"
          >{{ entry.question }}</button>
        </div>
      </div>
    </div>

    <!-- Find tab (existing search UI) -->
    <div v-else ref="findPanelRef" role="tabpanel" aria-labelledby="tab-find" tabindex="0">
    <!-- Controls Panel -->
    <div class="card mb-controls">
      <h2 class="section-title text-xl mb-md">Find Recipes</h2>

      <!-- Active-filter summary bar -->
      <div v-if="activeFilterCount > 0" class="active-filter-bar" role="status">
        <span class="text-sm">{{ activeFilterCount }} filter{{ activeFilterCount !== 1 ? 's' : '' }} active</span>
        <button class="btn btn-xs btn-secondary" @click="clearAllFindFilters" aria-label="Clear all active filters">
          Clear all
        </button>
      </div>

      <!-- Level Selector -->
      <div class="form-group">
        <label class="form-label">How far should we stretch?</label>
        <div class="flex flex-wrap gap-sm">
          <button
            v-for="lvl in levels"
            :key="lvl.value"
            :class="['btn', 'btn-secondary', { active: recipesStore.level === lvl.value }]"
            @click="recipesStore.level = lvl.value"
            :aria-pressed="recipesStore.level === lvl.value"
            :title="lvl.description"
          >
            {{ lvl.label }}
          </button>
        </div>
        <p v-if="activeLevel" class="level-description text-sm text-secondary mt-xs">
          {{ activeLevel.description }}
        </p>
        <!-- Orch budget pill — only visible when user has opted in (Settings toggle) -->
        <OrchUsagePill class="mt-xs" />
      </div>

      <!-- Surprise Me confirmation -->
      <div v-if="recipesStore.level === 4" class="status-badge status-info wildcard-warning">
        <span id="wildcard-warning-desc">Wildcard mode lets the AI get creative with whatever you have on hand. Results might surprise you.</span>
        <label class="flex-start gap-sm mt-xs">
          <input type="checkbox" v-model="recipesStore.wildcardConfirmed" aria-describedby="wildcard-warning-desc" />
          <span>I understand, go for it</span>
        </label>
      </div>

      <!-- Hard Day Mode — surfaced as a top-level toggle button -->
      <button
        :class="['btn', 'hard-day-btn', recipesStore.hardDayMode ? 'hard-day-active' : 'btn-secondary']"
        @click="recipesStore.hardDayMode = !recipesStore.hardDayMode"
        :aria-pressed="recipesStore.hardDayMode"
      >
        🌿 Hard Day Mode
        <span class="hard-day-sub">{{ recipesStore.hardDayMode ? 'on — quick &amp; simple only' : 'quick, simple recipes only' }}</span>
      </button>
      <p v-if="recipesStore.hardDayMode && recipesStore.result && recipesStore.result.suggestions.length > 0" class="text-muted text-sm mt-xs">
        Tap "Find recipes" again to apply.
      </p>

      <!-- Refine panel toggle — wraps time budget + dietary + nutrition filters -->
      <button
        class="refine-toggle btn btn-secondary btn-sm"
        @click="toggleFilters"
        :aria-expanded="filtersOpen"
        aria-controls="refine-panel"
      >
        {{ filtersOpen ? '▲' : '▼' }} Refine
        <span v-if="activeFilterCount > 0" class="refine-count">{{ activeFilterCount }}</span>
      </button>

      <div id="refine-panel" v-show="filtersOpen" class="refine-body">

      <!-- Time Budget selector — always visible; closes #131 -->
      <div class="form-group time-bucket-group">
        <!-- Hands-on / active time row -->
        <div class="time-row">
          <span class="time-row-label">Hands-on time</span>
          <div class="flex flex-wrap gap-xs">
            <button
              v-for="bucket in activeTimeBuckets"
              :key="'active-' + bucket.label"
              :class="['btn', 'btn-sm', 'time-bucket-btn',
                recipesStore.maxActiveMin === bucket.value ? 'time-bucket-active' : 'btn-secondary']"
              @click="recipesStore.maxActiveMin = recipesStore.maxActiveMin === bucket.value ? null : bucket.value"
              :aria-pressed="recipesStore.maxActiveMin === bucket.value"
              :title="'Max ' + bucket.label + ' of active cooking'"
            >{{ bucket.label }}</button>
            <button
              v-if="recipesStore.maxActiveMin !== null"
              class="btn btn-sm btn-secondary time-bucket-clear"
              @click="recipesStore.maxActiveMin = null"
              aria-label="Clear hands-on time limit"
            >No limit</button>
          </div>
        </div>

        <!-- Total time (including passive waits) row -->
        <div class="time-row">
          <span class="time-row-label">Total time</span>
          <div class="flex flex-wrap gap-xs">
            <button
              v-for="bucket in totalTimeBuckets"
              :key="'total-' + bucket.label"
              :class="['btn', 'btn-sm', 'time-bucket-btn',
                recipesStore.maxTotalMin === bucket.value ? 'time-bucket-active' : 'btn-secondary']"
              @click="recipesStore.maxTotalMin = recipesStore.maxTotalMin === bucket.value ? null : bucket.value"
              :aria-pressed="recipesStore.maxTotalMin === bucket.value"
              :title="'Max ' + bucket.label + ' start to finish'"
            >{{ bucket.label }}</button>
            <button
              v-if="recipesStore.maxTotalMin !== null"
              class="btn btn-sm btn-secondary time-bucket-clear"
              @click="recipesStore.maxTotalMin = null"
              aria-label="Clear total time limit"
            >No limit</button>
          </div>
        </div>

        <p class="form-hint">
          Both limits apply when set. Hands-on excludes wait time (marinating, baking, etc.).
        </p>
      </div>

      <!-- Dietary Preferences (collapsible) -->
      <details class="collapsible form-group" @toggle="(e: Event) => dietaryOpen = (e.target as HTMLDetailsElement).open">
        <summary class="collapsible-summary filter-summary" :aria-expanded="dietaryOpen">
          Dietary preferences
          <span v-if="dietaryActive" class="filter-active-dot" aria-label="filters active"></span>
        </summary>

        <div class="collapsible-body">
          <!-- Dietary Constraints — preset checkboxes + other -->
          <div class="form-group">
            <label class="form-label">I eat</label>
            <div class="preset-grid">
              <button
                v-for="opt in dietaryPresets"
                :key="opt.value"
                :class="['btn', 'btn-sm', 'preset-btn', recipesStore.constraints.includes(opt.value) ? 'preset-active' : '']"
                @click="toggleConstraint(opt.value)"
                :aria-pressed="recipesStore.constraints.includes(opt.value)"
              >{{ opt.label }}</button>
            </div>
            <input
              class="form-input mt-sm"
              v-model="constraintInput"
              placeholder="Other (e.g. low-sodium, raw, whole30)"
              aria-describedby="constraint-hint"
              @keydown="onConstraintKey"
              @blur="commitConstraintInput"
              autocomplete="off"
            />
            <span id="constraint-hint" class="form-hint">Press Enter or comma to add.</span>
          </div>

          <!-- Allergies — Big 9 quick-select + other -->
          <div class="form-group">
            <label class="form-label">Allergies <span class="text-muted text-xs">(hard exclusions)</span></label>
            <div class="preset-grid">
              <button
                v-for="allergen in allergenPresets"
                :key="allergen.value"
                :class="['btn', 'btn-sm', 'allergen-btn', recipesStore.allergies.includes(allergen.value) ? 'allergen-active' : '']"
                @click="toggleAllergy(allergen.value)"
                :aria-pressed="recipesStore.allergies.includes(allergen.value)"
              >{{ allergen.label }}</button>
            </div>
            <!-- Active custom allergy tags -->
            <div v-if="recipesStore.allergies.filter(a => !allergenPresets.map(p=>p.value).includes(a)).length > 0" class="tags-wrap flex flex-wrap gap-xs mt-xs">
              <span
                v-for="tag in recipesStore.allergies.filter(a => !allergenPresets.map(p=>p.value).includes(a))"
                :key="tag"
                class="tag-chip status-badge status-error"
              >
                {{ tag }}
                <button class="chip-remove" @click="removeAllergy(tag)" :aria-label="'Remove allergy: ' + tag">×</button>
              </span>
            </div>
            <input
              class="form-input mt-sm"
              v-model="allergyInput"
              placeholder="Other allergen…"
              aria-describedby="allergy-hint"
              @keydown="onAllergyKey"
              @blur="commitAllergyInput"
              autocomplete="off"
            />
            <span id="allergy-hint" class="form-hint">No recipes containing these ingredients will appear.</span>
          </div>

          <!-- Not Today — ingredient exclusions, persisted to localStorage -->
          <div class="form-group">
            <label class="form-label">Not today <span class="text-muted text-xs">(saved between visits)</span></label>
            <div v-if="recipesStore.excludeIngredients.length > 0" class="tags-wrap flex flex-wrap gap-xs mb-xs">
              <span
                v-for="tag in recipesStore.excludeIngredients"
                :key="tag"
                class="tag-chip status-badge status-warning"
              >
                {{ tag }}
                <button class="chip-remove" @click="removeExcludeIngredient(tag)" :aria-label="'Stop excluding: ' + tag">×</button>
              </span>
            </div>
            <input
              class="form-input"
              v-model="excludeIngredientInput"
              placeholder="e.g. eggs, chicken, broccoli"
              aria-describedby="exclude-hint"
              @keydown="onExcludeIngredientKey"
              @blur="commitExcludeIngredientInput"
              autocomplete="off"
            />
            <span id="exclude-hint" class="form-hint">Recipes containing these won't appear. Press Enter or comma to add.</span>
          </div>

          <!-- Can Make Now toggle -->
          <div class="form-group">
            <label class="flex-start gap-sm shopping-toggle">
              <input type="checkbox" v-model="recipesStore.pantryMatchOnly" :disabled="recipesStore.shoppingMode" />
              <span class="form-label" style="margin-bottom: 0;">Can make now (no missing ingredients)</span>
            </label>
            <p v-if="recipesStore.pantryMatchOnly && !recipesStore.shoppingMode" class="text-sm text-secondary mt-xs">
              Only recipes where every ingredient is in your pantry — no substitutions, no shopping.
            </p>
          </div>

          <!-- Shopping Mode (temporary home — moves to Shopping tab in #71) -->
          <div class="form-group">
            <label class="flex-start gap-sm shopping-toggle">
              <input type="checkbox" v-model="recipesStore.shoppingMode" />
              <span class="form-label" style="margin-bottom: 0;">Open to buying missing ingredients</span>
            </label>
            <p v-if="recipesStore.shoppingMode" class="text-sm text-secondary mt-xs">
              All recipes shown regardless of missing ingredients. Affiliate links appear for anything you'd need to buy.
            </p>
          </div>

          <!-- Max Missing — hidden in shopping mode or pantry-match-only mode -->
          <div v-if="!recipesStore.shoppingMode && !recipesStore.pantryMatchOnly" class="form-group">
            <label class="form-label" for="max-missing">Max Missing Ingredients <span class="text-muted text-xs">(optional)</span></label>
            <input
              id="max-missing"
              type="number"
              class="form-input"
              min="0"
              max="5"
              placeholder="Leave blank for no limit"
              :value="recipesStore.maxMissing ?? ''"
              @input="onMaxMissingInput"
            />
          </div>
        </div>
      </details>

      <!-- Advanced Filters (collapsible) -->
      <details class="collapsible form-group" @toggle="(e: Event) => advancedOpen = (e.target as HTMLDetailsElement).open">
        <summary class="collapsible-summary filter-summary" :aria-expanded="advancedOpen">
          Nutrition Filters{{ activeNutritionFilterCount > 0 ? ` (${activeNutritionFilterCount} active)` : '' }}
          <span v-if="advancedActive" class="filter-active-dot" aria-label="filters active"></span>
        </summary>

        <div class="collapsible-body">
          <!-- Nutrition Filters -->
          <div class="form-group">
            <label class="form-label">Nutrition limits <span class="text-muted text-xs">(per recipe, optional)</span></label>
            <div class="nutrition-filters-grid mt-xs">
              <div class="form-group">
                <label class="form-label" for="filter-max-cal">Max Calories</label>
                <input id="filter-max-cal" type="number" class="form-input" min="0" placeholder="e.g. 600"
                  :value="recipesStore.nutritionFilters.max_calories ?? ''"
                  @input="onNutritionInput('max_calories', $event)" />
              </div>
              <div class="form-group">
                <label class="form-label" for="filter-max-sugar">Max Sugar (g)</label>
                <input id="filter-max-sugar" type="number" class="form-input" min="0" placeholder="e.g. 10"
                  :value="recipesStore.nutritionFilters.max_sugar_g ?? ''"
                  @input="onNutritionInput('max_sugar_g', $event)" />
              </div>
              <div class="form-group">
                <label class="form-label" for="filter-max-carbs">Max Carbs (g)</label>
                <input id="filter-max-carbs" type="number" class="form-input" min="0" placeholder="e.g. 50"
                  :value="recipesStore.nutritionFilters.max_carbs_g ?? ''"
                  @input="onNutritionInput('max_carbs_g', $event)" />
              </div>
              <div class="form-group">
                <label class="form-label" for="filter-max-sodium">Max Sodium (mg)</label>
                <input id="filter-max-sodium" type="number" class="form-input" min="0" placeholder="e.g. 800"
                  :value="recipesStore.nutritionFilters.max_sodium_mg ?? ''"
                  @input="onNutritionInput('max_sodium_mg', $event)" />
              </div>
            </div>
            <p class="text-xs text-muted mt-xs">
              Recipes without nutrition data always appear.
            </p>
          </div>

          <!-- Cuisine Style (Level 3+ only) -->
          <div v-if="recipesStore.level >= 3" class="form-group">
            <label class="form-label">Cuisine Style</label>
            <div class="flex flex-wrap gap-xs">
              <button
                v-for="style in cuisineStyles"
                :key="style.id"
                :class="['btn', 'btn-secondary', 'btn-sm', { active: recipesStore.styleId === style.id }]"
                @click="recipesStore.styleId = recipesStore.styleId === style.id ? null : style.id"
                :aria-pressed="recipesStore.styleId === style.id"
              >{{ style.label }}</button>
            </div>
          </div>

          <!-- Category Filter (Level 1–2 only) -->
          <div v-if="recipesStore.level <= 2" class="form-group">
            <label class="form-label" for="adv-category">Category <span class="text-muted text-xs">(optional)</span></label>
            <input
              id="adv-category"
              class="form-input"
              v-model="categoryInput"
              placeholder="e.g. Breakfast, Asian, Chicken, &lt; 30 Mins"
              @blur="recipesStore.category = categoryInput.trim() || null"
              @keydown.enter="recipesStore.category = categoryInput.trim() || null"
            />
          </div>
        </div>
      </details>

      </div><!-- end #refine-panel -->

      <!-- Suggest Button -->
      <div class="suggest-row">
        <button
          class="btn btn-primary btn-lg flex-1"
          :disabled="recipesStore.loading || pantryItems.length === 0 || (recipesStore.level === 4 && !recipesStore.wildcardConfirmed)"
          @click="handleSuggest"
        >
          <span v-if="recipesStore.loading && !isLoadingMore">
            <span class="spinner spinner-sm inline-spinner"></span>
            <span v-if="recipesStore.jobStatus === 'queued'">Queued…</span>
            <span v-else-if="recipesStore.jobStatus === 'running'">Generating…</span>
            <span v-else>Finding recipes…</span>
          </span>
          <span v-else>Suggest Recipes</span>
        </button>
        <button
          v-if="recipesStore.level === 3 || recipesStore.level === 4"
          class="btn btn-secondary btn-sm"
          :disabled="isStreaming || recipesStore.loading || pantryItems.length === 0"
          @click="streamRecipe(recipesStore.level as 3 | 4, recipesStore.wildcardConfirmed)"
          title="Stream recipe generation token-by-token via cf-orch"
        >
          {{ isStreaming ? 'Streaming…' : 'Stream (L' + recipesStore.level + ')' }}
        </button>
        <button
          v-if="recipesStore.dismissedCount > 0"
          class="btn btn-ghost btn-sm"
          @click="recipesStore.clearDismissed()"
          title="Show all dismissed recipes again"
        >Clear dismissed ({{ recipesStore.dismissedCount }})</button>
      </div>

      <!-- Empty pantry nudge -->
      <p v-if="pantryItems.length === 0 && !recipesStore.loading" class="text-sm text-muted text-center mt-xs">
        Add items to your pantry first, then tap Suggest to find recipes.
      </p>
    </div>

    <!-- Error -->
    <div v-if="recipesStore.error" role="alert" class="status-badge status-error mb-md">
      {{ recipesStore.error }}
    </div>

    <!-- Streaming recipe generation panel -->
    <div v-if="isStreaming || streamChunks || streamError" class="stream-panel">
      <div v-if="isStreaming" class="stream-status" role="status">
        <span class="stream-dot" aria-hidden="true"></span>
        Generating recipe…
      </div>
      <div v-if="streamError" class="stream-error text-sm" role="alert">{{ streamError }}</div>

      <!-- Progressive reveal card: skeleton while streaming, full card on complete -->
      <div v-if="isStreaming || (streamChunks && !streamError)" class="stream-recipe-card card">
        <!-- Title -->
        <div class="stream-title mb-sm">
          <h3 v-if="parsedStream.title" class="text-lg font-semibold">{{ parsedStream.title }}</h3>
          <div v-else class="skeleton-line skeleton-title"></div>
        </div>

        <!-- Ingredients -->
        <div class="stream-section mb-sm">
          <strong class="stream-section-label text-sm">Ingredients</strong>
          <ul v-if="parsedStream.ingredients.length > 0" class="stream-ingredients mt-xs">
            <li v-for="(ing, i) in parsedStream.ingredients" :key="i" class="text-sm">{{ ing }}</li>
          </ul>
          <template v-else-if="isStreaming">
            <div class="skeleton-line skeleton-medium mt-xs"></div>
            <div class="skeleton-line skeleton-short"></div>
            <div class="skeleton-line skeleton-long"></div>
          </template>
        </div>

        <!-- Steps — only show once ingredients have arrived -->
        <div v-if="parsedStream.steps.length > 0 || (isStreaming && parsedStream.ingredients.length > 0)" class="stream-section mb-sm">
          <strong class="stream-section-label text-sm">Directions</strong>
          <ol v-if="parsedStream.steps.length > 0" class="stream-steps mt-xs">
            <li v-for="(step, i) in parsedStream.steps" :key="i" class="text-sm">{{ step }}</li>
          </ol>
          <template v-else-if="isStreaming">
            <div class="skeleton-line skeleton-long mt-xs"></div>
            <div class="skeleton-line skeleton-medium"></div>
          </template>
        </div>

        <!-- Notes — only after stream complete -->
        <p v-if="parsedStream.notes && !isStreaming" class="stream-notes text-sm text-muted">
          <strong>Notes:</strong> {{ parsedStream.notes }}
        </p>
      </div>
    </div>

    <!-- Screen reader announcement for loading + results -->
    <div aria-live="polite" aria-atomic="true" class="sr-only">
      <span v-if="recipesStore.loading && recipesStore.jobStatus === 'queued'">Recipe request queued, waiting for model…</span>
      <span v-else-if="recipesStore.loading && recipesStore.jobStatus === 'running'">Generating your recipe now…</span>
      <span v-else-if="recipesStore.loading">Finding recipes…</span>
      <span v-else-if="recipesStore.result">
        {{ filteredSuggestions.length }} recipe{{ filteredSuggestions.length !== 1 ? 's' : '' }} found
      </span>
    </div>

    <!-- Results -->
    <div v-if="recipesStore.result" class="results-section fade-in" :aria-busy="recipesStore.loading">
      <!-- Rate limit warning -->
      <div
        v-if="recipesStore.result.rate_limited"
        class="status-badge status-warning rate-limit-banner mb-md"
      >
        Today's free suggestions are used up. Your limit resets tomorrow, or
        <a href="#" class="link-inline" @click.prevent="$emit('upgrade')">upgrade for unlimited access</a>.
      </div>

      <!-- Element gaps -->
      <div v-if="recipesStore.result.element_gaps.length > 0" class="card card-secondary mb-md">
        <p class="text-sm font-semibold">These would expand your options:</p>
        <div class="flex flex-wrap gap-xs mt-xs">
          <span
            v-for="gap in recipesStore.result.element_gaps"
            :key="gap"
            class="status-badge status-info"
          >{{ gap }}</span>
        </div>
      </div>

      <!-- Filter bar -->
      <div v-if="recipesStore.result.suggestions.length > 0" class="filter-bar mb-md">
        <input
          class="form-input filter-search"
          v-model="filterText"
          placeholder="Search recipes or ingredients…"
          aria-label="Filter recipes"
          autocomplete="off"
        />
        <div class="filter-chips">
          <template v-if="availableLevels.length > 1">
            <button
              v-for="lvl in availableLevels"
              :key="lvl"
              :class="['filter-chip', { active: filterLevel === lvl }]"
              :aria-pressed="filterLevel === lvl"
              @click="filterLevel = filterLevel === lvl ? null : lvl"
            >{{ levelLabels[lvl] ?? `Level ${lvl}` }}</button>
          </template>
          <button
            :class="['filter-chip', { active: filterMissing === 0 }]"
            :aria-pressed="filterMissing === 0"
            @click="filterMissing = filterMissing === 0 ? null : 0"
          >Can make now</button>
          <button
            :class="['filter-chip', { active: filterMissing === 2 }]"
            :aria-pressed="filterMissing === 2"
            @click="filterMissing = filterMissing === 2 ? null : 2"
          >≤2 missing</button>
          <!-- Complexity filter chips (#55 / #58) -->
          <button
            v-for="cx in ['easy', 'moderate', 'involved']"
            :key="cx"
            :class="['filter-chip', { active: filterComplexity === cx }]"
            :aria-pressed="filterComplexity === cx"
            @click="filterComplexity = filterComplexity === cx ? null : cx"
          >{{ cx }}</button>
          <button
            v-if="hasActiveFilters"
            class="filter-chip filter-chip-clear"
            aria-label="Clear all recipe filters"
            @click="clearFilters"
          ><span aria-hidden="true">✕</span> Clear</button>
        </div>

        <!-- Zero-decision picks (#53 Surprise Me / #57 Just Pick One) -->
        <div v-if="filteredSuggestions.length > 0" class="flex gap-sm flex-wrap" style="margin-top: var(--spacing-sm)">
          <button class="btn btn-secondary btn-sm" @click="pickSurprise" :disabled="filteredSuggestions.length === 0">
            🎲 Surprise me
          </button>
          <button class="btn btn-secondary btn-sm" @click="pickBest" :disabled="filteredSuggestions.length === 0">
            ⚡ Just pick one
          </button>
        </div>
      </div>

      <!-- Spotlight (Surprise Me / Just Pick One result) -->
      <div v-if="spotlightRecipe" class="card spotlight-card slide-up mb-md">
        <div class="flex-between mb-sm">
          <h3 class="text-lg font-bold">{{ spotlightRecipe.title }}</h3>
          <div class="flex gap-xs" style="align-items:center">
            <span v-if="spotlightRecipe.complexity" :class="['status-badge', `complexity-${spotlightRecipe.complexity}`]">{{ spotlightRecipe.complexity }}</span>
            <span v-if="spotlightRecipe.estimated_time_min" class="status-badge status-neutral">~{{ spotlightRecipe.estimated_time_min }}m</span>
            <button class="btn-icon" @click="spotlightRecipe = null" aria-label="Dismiss">✕</button>
          </div>
        </div>
        <p class="text-sm text-secondary mb-xs">{{ spotlightRecipe.match_count }} ingredients matched from your pantry</p>
        <button class="btn btn-primary btn-sm" @click="selectedRecipe = spotlightRecipe; spotlightRecipe = null">
          Cook this
        </button>
        <button class="btn btn-ghost btn-sm ml-sm" @click="pickSurprise">Try another</button>
      </div>

      <!-- No suggestions -->
      <div
        v-if="filteredSuggestions.length === 0"
        class="card text-center text-muted"
      >
        <template v-if="hasActiveFilters">
          <p>No recipes match your filters.</p>
          <button class="btn btn-ghost btn-sm mt-xs" @click="clearFilters">Clear filters</button>
        </template>
        <p v-else>We didn't find matches at this level. Try <button class="btn btn-ghost btn-sm" @click="recipesStore.level = 1; handleSuggest()">Level 1 — Use What I Have</button> or adjust your filters.</p>
      </div>

      <!-- Recipe Cards — compact summary; full detail opens in RecipeDetailPanel -->
      <div class="grid-auto mb-md">
        <div
          v-for="recipe in filteredSuggestions"
          :key="recipe.id"
          class="card recipe-card-compact slide-up"
          role="article"
        >
          <!-- Title + actions -->
          <div class="flex-between mb-xs" style="gap: var(--spacing-xs)">
            <h3 class="text-base font-bold recipe-title" style="flex:1; min-width:0">{{ recipe.title }}</h3>
            <div class="flex gap-xs" style="align-items:center; flex-shrink:0">
              <span
                v-if="recipe.id"
                :class="['btn-icon', 'btn-bookmark', { active: savedStore.isSaved(recipe.id) }]"
                :aria-label="savedStore.isSaved(recipe.id) ? 'Saved: ' + recipe.title : 'Save ' + recipe.title"
                :title="savedStore.isSaved(recipe.id) ? 'Saved' : 'Save'"
              >{{ savedStore.isSaved(recipe.id) ? '★' : '☆' }}</span>
              <button
                v-if="recipe.id"
                class="btn-icon btn-dismiss"
                @click.stop="recipesStore.dismiss(recipe.id)"
                :aria-label="'Hide recipe: ' + recipe.title"
              >✕</button>
            </div>
          </div>

          <!-- Badges row -->
          <div class="flex flex-wrap gap-xs mb-sm">
            <span class="status-badge status-success">{{ recipe.match_count }} matched</span>
            <span v-if="recipe.complexity" :class="['status-badge', `complexity-${recipe.complexity}`]">{{ recipe.complexity }}</span>
            <span v-if="recipe.estimated_time_min" class="status-badge status-neutral">~{{ recipe.estimated_time_min }}m</span>
            <span class="status-badge status-info">L{{ recipe.level }}</span>
            <span v-if="recipe.is_wildcard" class="status-badge status-info">Wildcard</span>
          </div>

          <!-- Pantry ingredients preview (max 4 chips) -->
          <div v-if="recipe.matched_ingredients?.length > 0" class="flex flex-wrap gap-xs mb-xs">
            <span
              v-for="ing in recipe.matched_ingredients.slice(0, 4)"
              :key="ing"
              class="ingredient-chip ingredient-chip-have"
            >{{ ing }}</span>
            <span
              v-if="recipe.matched_ingredients.length > 4"
              class="ingredient-chip ingredient-chip-have chip-overflow"
            >+{{ recipe.matched_ingredients.length - 4 }} more</span>
          </div>

          <!-- Missing count + calorie hint -->
          <p class="text-sm text-secondary mb-sm recipe-card-meta">
            <span v-if="recipe.missing_ingredients.length > 0">+{{ recipe.missing_ingredients.length }} needed</span>
            <span v-if="recipe.missing_ingredients.length > 0 && recipe.nutrition?.calories != null"> · </span>
            <span v-if="recipe.nutrition?.calories != null">{{ Math.round(recipe.nutrition.calories) }} kcal</span>
          </p>

          <!-- Primary action -->
          <div class="card-actions">
            <button class="btn btn-primary btn-sm btn-make" @click="openRecipe(recipe)">
              Make this
            </button>
          </div>
        </div>
      </div>

      <!-- Load More -->
      <div v-if="recipesStore.result.suggestions.length > 0" class="load-more-row">
        <button
          class="btn btn-secondary"
          :disabled="recipesStore.loading"
          @click="handleLoadMore"
        >
          <span v-if="recipesStore.loading && isLoadingMore">
            <span class="spinner spinner-sm inline-spinner"></span> Loading…
          </span>
          <span v-else>Load more recipes</span>
        </button>
      </div>

      <!-- Soft Build Your Own nudge when corpus results are sparse -->
      <div
        v-if="!recipesStore.loading && filteredSuggestions.length <= 2 && recipesStore.result"
        class="byo-nudge text-sm text-secondary mt-md"
      >
        Not finding what you want?
        <button class="btn-link" @click="activeTab = 'build'">Try Build Your Own</button>
        to make something from scratch.
      </div>

    </div>

    <!-- Recipe detail panel — mounts as a full-screen overlay -->
    <RecipeDetailPanel
      v-if="selectedRecipe"
      :recipe="selectedRecipe"
      :grocery-links="selectedGroceryLinks"
      @close="selectedRecipe = null"
      @cooked="(recipe) => { onCooked(recipe); selectedRecipe = null }"
    />

    <!-- Empty state when no results yet and pantry has items -->
    <div
      v-if="!recipesStore.result && !recipesStore.loading && pantryItems.length > 0"
      class="card text-center text-muted"
    >
      <svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5" style="width:40px;height:40px;color:var(--color-text-muted);margin-bottom:var(--spacing-sm)">
        <path d="M12 8c0 0 4-4 12-4s12 4 12 4v8H12V8z"/>
        <path d="M10 16h28v4l-2 20H12L10 20v-4z"/>
        <line x1="20" y1="24" x2="28" y2="24"/>
      </svg>
      <p class="mt-xs">Tap "Suggest Recipes" to find recipes using your pantry items.</p>
    </div>

    </div><!-- end Find tab -->

    <!-- Recipe load error — announced to screen readers via aria-live -->
    <div v-if="browserRecipeError" role="alert" class="status-badge status-error mb-sm">
      {{ browserRecipeError }}
    </div>

    <!-- Detail panel for browser/saved recipe lookups -->
    <RecipeDetailPanel
      v-if="browserSelectedRecipe"
      :recipe="browserSelectedRecipe"
      :grocery-links="[]"
      @close="browserSelectedRecipe = null"
      @cooked="browserSelectedRecipe = null"
    />

    <!-- Undo toast for "I cooked this" dismiss -->
    <div
      v-if="lastCookedRecipe"
      class="undo-toast"
      role="status"
      aria-live="polite"
    >
      Dismissed from suggestions.
      <button class="btn-link" @click="undoCooked">Undo</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { useRecipesStore } from '../stores/recipes'
import { useInventoryStore } from '../stores/inventory'
import { useSavedRecipesStore } from '../stores/savedRecipes'
import RecipeDetailPanel from './RecipeDetailPanel.vue'
import RecipeBrowserPanel from './RecipeBrowserPanel.vue'
import SavedRecipesPanel from './SavedRecipesPanel.vue'
import CommunityFeedPanel from './CommunityFeedPanel.vue'
import BuildYourOwnTab from './BuildYourOwnTab.vue'
import OrchUsagePill from './OrchUsagePill.vue'
import RecipeScanModal from './RecipeScanModal.vue'
import type { ForkResult } from '../stores/community'
import type { RecipeSuggestion, GroceryLink, StreamTokenResponse, AskResponse } from '../services/api'
import { recipesAPI } from '../services/api'

// ── Scan modal ────────────────────────────────────────────────────────────────
const scanModalOpen = ref(false)
const lastScannedTitle = ref<string | null>(null)

function onScanSaved(recipe: { id: number; title: string }) {
  lastScannedTitle.value = recipe.title
  scanModalOpen.value = false
  // Dismiss the toast after 4 seconds
  setTimeout(() => { lastScannedTitle.value = null }, 4000)
}

// Streaming state
const isStreaming = ref(false)
const streamChunks = ref('')
const streamError = ref<string | null>(null)

interface ParsedStreamRecipe {
  title: string | null
  ingredients: string[]
  steps: string[]
  notes: string | null
}

const parsedStream = computed((): ParsedStreamRecipe => {
  const text = streamChunks.value
  if (!text) return { title: null, ingredients: [], steps: [], notes: null }

  const titleMatch = text.match(/^Title:\s*(.+)$/m)
  const title = titleMatch?.[1]?.trim() ?? null

  const ingMatch = text.match(/^Ingredients:\s*(.+)$/m)
  const ingredients = ingMatch?.[1]
    ? ingMatch[1]!.split(',').map((s) => s.trim()).filter(Boolean)
    : []

  const steps: string[] = []
  const dirIdx = text.indexOf('Directions:')
  const notesIdx = text.indexOf('\nNotes:')
  if (dirIdx !== -1) {
    const dirSection = text.slice(dirIdx + 'Directions:'.length, notesIdx !== -1 ? notesIdx : undefined)
    for (const m of dirSection.matchAll(/^\d+\.\s*(.+)$/mg)) {
      steps.push(m[1]?.trim() ?? '')
    }
  }

  const notesMatch = text.match(/^Notes:\s*([\s\S]+)$/m)
  const notes = notesMatch?.[1]?.trim() ?? null

  return { title, ingredients, steps, notes }
})

const recipesStore = useRecipesStore()
const inventoryStore = useInventoryStore()

// Tab state
type TabId = 'find' | 'browse' | 'saved' | 'community' | 'build' | 'ask'
const tabs: Array<{ id: TabId; label: string; mobileLabel?: string }> = [
  { id: 'saved',     label: 'Saved' },
  { id: 'build',     label: 'Build Your Own', mobileLabel: 'Build' },
  { id: 'community', label: 'Community' },
  { id: 'find',      label: 'Find' },
  { id: 'browse',    label: 'Browse' },
  { id: 'ask',       label: 'Ask' },
]
const activeTab = ref<TabId>('saved')
const savedStore = useSavedRecipesStore()
// Template refs for panels that need explicit focus management on tab switch
const findPanelRef = ref<HTMLElement | null>(null)
const askPanelRef = ref<HTMLElement | null>(null)

function onTabKeydown(e: KeyboardEvent) {
  const tabIds: TabId[] = ['saved', 'build', 'community', 'find', 'browse', 'ask']
  const current = tabIds.indexOf(activeTab.value)
  if (e.key === 'ArrowRight') {
    e.preventDefault()
    activateTab(tabIds[(current + 1) % tabIds.length]!)
  } else if (e.key === 'ArrowLeft') {
    e.preventDefault()
    activateTab(tabIds[(current - 1 + tabIds.length) % tabIds.length]!)
  }
}

async function activateTab(tab: TabId) {
  activeTab.value = tab
  await nextTick()
  // Move focus to the newly active panel so keyboard users don't have to Tab
  // through the entire tab bar again to reach the panel content.
  // findPanelRef handles the Find tab (a plain div); other tabs are child
  // components so we locate their panel via querySelector.
  if (tab === 'find' && findPanelRef.value) {
    findPanelRef.value.focus()
  } else if (tab === 'ask' && askPanelRef.value) {
    askPanelRef.value.focus()
  } else {
    const panel = document.querySelector('[role="tabpanel"]') as HTMLElement | null
    panel?.focus()
  }
}

// Community tab: navigate to Find tab after a plan fork (full plan view deferred to Task 9)
function onPlanForked(_payload: ForkResult) {
  activeTab.value = 'find'
}

// Browser/saved tab recipe detail panel (fetches full recipe from API)
const browserSelectedRecipe = ref<RecipeSuggestion | null>(null)

const browserRecipeError = ref<string | null>(null)

async function openRecipeById(recipeId: number) {
  browserRecipeError.value = null
  try {
    browserSelectedRecipe.value = await recipesAPI.getRecipe(recipeId)
  } catch {
    browserRecipeError.value = 'Could not load this recipe. Please try again.'
  }
}

// Collapsible panel open state — kept in sync with DOM toggle event so
// :aria-expanded on <summary> reflects the actual open/closed state
const dietaryOpen = ref(false)
const advancedOpen = ref(false)

// Per-recipe swap section open tracking (Set of recipe IDs whose swap <details> are open)

// Human-readable level labels for filter chips (avoids "Lv1" etc. for screen readers)
const levelLabels: Record<number, string> = {
  1: 'Use What I Have',
  2: 'Allow Swaps',
  3: 'Get Creative',
  4: 'Surprise Me',
}

// Local input state for tags
const constraintInput = ref('')
const allergyInput = ref('')
const excludeIngredientInput = ref('')
const categoryInput = ref('')
const isLoadingMore = ref(false)

// Recipe detail panel (Find tab)
const selectedRecipe = ref<RecipeSuggestion | null>(null)

// Filter state (#21)
const filterText = ref('')
const filterLevel = ref<number | null>(null)
const filterMissing = ref<number | null>(null)
const filterComplexity = ref<string | null>(null)
const spotlightRecipe = ref<RecipeSuggestion | null>(null)

const availableLevels = computed(() => {
  if (!recipesStore.result) return []
  return [...new Set(recipesStore.result.suggestions.map((r) => r.level))].sort()
})

const filteredSuggestions = computed(() => {
  if (!recipesStore.result) return []
  let items = recipesStore.result.suggestions
  const q = filterText.value.trim().toLowerCase()
  if (q) {
    items = items.filter((r) =>
      r.title.toLowerCase().includes(q) ||
      r.matched_ingredients.some((i) => i.toLowerCase().includes(q)) ||
      r.missing_ingredients.some((i) => i.toLowerCase().includes(q))
    )
  }
  if (filterLevel.value !== null) {
    items = items.filter((r) => r.level === filterLevel.value)
  }
  if (filterMissing.value !== null) {
    items = items.filter((r) => r.missing_ingredients.length <= filterMissing.value!)
  }
  if (filterComplexity.value !== null) {
    items = items.filter((r) => r.complexity === filterComplexity.value)
  }
  return items
})

const hasActiveFilters = computed(
  () => filterText.value.trim() !== '' || filterLevel.value !== null || filterMissing.value !== null || filterComplexity.value !== null
)

function clearFilters() {
  filterText.value = ''
  filterLevel.value = null
  filterMissing.value = null
  filterComplexity.value = null
}

function pickSurprise() {
  const pool = filteredSuggestions.value
  if (!pool.length) return
  const exclude = spotlightRecipe.value?.id
  const candidates = pool.length > 1 ? pool.filter((r) => r.id !== exclude) : pool
  spotlightRecipe.value = candidates[Math.floor(Math.random() * candidates.length)] ?? null
}

function pickBest() {
  const pool = filteredSuggestions.value
  if (!pool.length) return
  spotlightRecipe.value = pool[0] ?? null
}

const selectedGroceryLinks = computed<GroceryLink[]>(() => {
  if (!selectedRecipe.value || !recipesStore.result) return []
  const missing = new Set(selectedRecipe.value.missing_ingredients.map((s) => s.toLowerCase()))
  return recipesStore.result.grocery_links.filter((l) => missing.has(l.ingredient.toLowerCase()))
})

function openRecipe(recipe: RecipeSuggestion) {
  selectedRecipe.value = recipe
}

const lastCookedRecipe = ref<{ id: number; title: string } | null>(null)
let undoTimer: ReturnType<typeof setTimeout> | null = null

function onCooked(recipe: RecipeSuggestion) {
  recipesStore.logCook(recipe.id, recipe.title)
  recipesStore.dismiss(recipe.id)
  lastCookedRecipe.value = { id: recipe.id, title: recipe.title }
  if (undoTimer) clearTimeout(undoTimer)
  undoTimer = setTimeout(() => {
    lastCookedRecipe.value = null
  }, 12000)
}

function undoCooked() {
  if (!lastCookedRecipe.value) return
  recipesStore.undismiss(lastCookedRecipe.value.id)
  if (undoTimer) clearTimeout(undoTimer)
  lastCookedRecipe.value = null
}

onUnmounted(() => { if (undoTimer) clearTimeout(undoTimer) })

const levels = [
  { value: 1, label: 'Use What I Have',  description: 'Finds recipes you can make right now using exactly what\'s in your pantry.' },
  { value: 2, label: 'Allow Swaps',      description: 'Same as above, plus recipes where one or two ingredients can be substituted.' },
  { value: 3, label: 'Get Creative',     description: 'AI builds recipes in your chosen cuisine style from what you have. Requires paid tier.' },
  { value: 4, label: 'Surprise Me 🎲',   description: 'Fully AI-generated — open-ended and occasionally unexpected. Requires paid tier.' },
]

const dietaryPresets = [
  { label: 'Vegetarian', value: 'vegetarian' },
  { label: 'Vegan',      value: 'vegan' },
  { label: 'Gluten-free', value: 'gluten-free' },
  { label: 'Dairy-free', value: 'dairy-free' },
  { label: 'Keto',       value: 'keto' },
  { label: 'Low-carb',   value: 'low-carb' },
  { label: 'Halal',      value: 'halal' },
  { label: 'Kosher',     value: 'kosher' },
]

const allergenPresets = [
  { label: 'Peanuts',    value: 'peanuts' },
  { label: 'Tree nuts',  value: 'tree nuts' },
  { label: 'Shellfish',  value: 'shellfish' },
  { label: 'Fish',       value: 'fish' },
  { label: 'Milk',       value: 'milk' },
  { label: 'Eggs',       value: 'eggs' },
  { label: 'Wheat',      value: 'wheat' },
  { label: 'Soy',        value: 'soy' },
  { label: 'Sesame',     value: 'sesame' },
]

function toggleConstraint(value: string) {
  if (recipesStore.constraints.includes(value)) {
    recipesStore.constraints = recipesStore.constraints.filter((c) => c !== value)
  } else {
    recipesStore.constraints = [...recipesStore.constraints, value]
  }
}

function toggleAllergy(value: string) {
  if (recipesStore.allergies.includes(value)) {
    recipesStore.allergies = recipesStore.allergies.filter((a) => a !== value)
  } else {
    recipesStore.allergies = [...recipesStore.allergies, value]
  }
}

const dietaryActive = computed(() =>
  recipesStore.constraints.length > 0 ||
  recipesStore.allergies.length > 0 ||
  recipesStore.excludeIngredients.length > 0 ||
  recipesStore.shoppingMode
)

const advancedActive = computed(() =>
  Object.values(recipesStore.nutritionFilters).some((v) => v !== null) ||
  recipesStore.maxMissing !== null ||
  !!recipesStore.category ||
  !!recipesStore.styleId
)

const FILTERS_OPEN_KEY = 'kiwi:find_filters_open'
const filtersOpen = ref(localStorage.getItem(FILTERS_OPEN_KEY) !== 'false')

function toggleFilters() {
  filtersOpen.value = !filtersOpen.value
  localStorage.setItem(FILTERS_OPEN_KEY, String(filtersOpen.value))
}

const activeFilterCount = computed(() => {
  let n = 0
  if (recipesStore.constraints.length > 0) n++
  if (recipesStore.allergies.length > 0) n++
  if (recipesStore.excludeIngredients.length > 0) n++
  if (recipesStore.shoppingMode) n++
  if (recipesStore.pantryMatchOnly) n++
  if (recipesStore.hardDayMode) n++
  if (recipesStore.maxActiveMin !== null) n++
  if (recipesStore.maxTotalMin !== null) n++
  if (recipesStore.maxMissing !== null) n++
  if (recipesStore.styleId !== null) n++
  if (recipesStore.category !== null) n++
  n += Object.values(recipesStore.nutritionFilters).filter((v) => v !== null).length
  return n
})

function clearAllFindFilters() {
  recipesStore.clearConstraints()
  recipesStore.clearAllergies()
  recipesStore.clearExcludeIngredients()
  recipesStore.shoppingMode = false
  recipesStore.pantryMatchOnly = false
  recipesStore.hardDayMode = false
  recipesStore.maxActiveMin = null
  recipesStore.maxTotalMin = null
  recipesStore.maxMissing = null
  recipesStore.styleId = null
  recipesStore.category = null
  recipesStore.nutritionFilters = { max_calories: null, max_sugar_g: null, max_carbs_g: null, max_sodium_mg: null }
  constraintInput.value = ''
  allergyInput.value = ''
  excludeIngredientInput.value = ''
  categoryInput.value = ''
}

// Pantry items sorted expiry-first (available items only)
const pantryItems = computed(() => {
  const sorted = [...inventoryStore.items]
    .filter((item) => item.status === 'available')
    .sort((a, b) => {
      if (!a.expiration_date && !b.expiration_date) return 0
      if (!a.expiration_date) return 1
      if (!b.expiration_date) return -1
      return new Date(a.expiration_date).getTime() - new Date(b.expiration_date).getTime()
    })
  return sorted.map((item) => item.product_name).filter(Boolean) as string[]
})

// Secondary-state items: expired but still usable in specific recipes.
// Maps product_name → secondary_state label (e.g. "Bread" → "stale").
// Sent alongside pantry_items so the recipe engine can boost relevant recipes.
const secondaryPantryItems = computed<Record<string, string>>(() => {
  const result: Record<string, string> = {}
  for (const item of inventoryStore.items) {
    if (item.secondary_state && item.product_name) {
      result[item.product_name] = item.secondary_state
    }
  }
  return result
})

// ── Inverted flow: auto-suggest + live re-suggest (closes #132) ─────────────

function _debounce(fn: () => void, ms: number): () => void {
  let t: ReturnType<typeof setTimeout>
  return () => { clearTimeout(t); t = setTimeout(fn, ms) }
}

// Stable key over all filter state — one watcher instead of a dozen.
const filterKey = computed(() => JSON.stringify({
  constraints: recipesStore.constraints,
  allergies: recipesStore.allergies,
  excludeIngredients: recipesStore.excludeIngredients,
  shoppingMode: recipesStore.shoppingMode,
  pantryMatchOnly: recipesStore.pantryMatchOnly,
  hardDayMode: recipesStore.hardDayMode,
  maxActiveMin: recipesStore.maxActiveMin,
  maxTotalMin: recipesStore.maxTotalMin,
  maxMissing: recipesStore.maxMissing,
  styleId: recipesStore.styleId,
  category: recipesStore.category,
  nutritionFilters: recipesStore.nutritionFilters,
  level: recipesStore.level,
}))

// Flag prevents double-firing on initial pantry load + tab switch within same render cycle.
let _autoSuggestFired = false

// Auto-suggest on Find tab activation (L1/L2 only — L3/L4 require explicit user intent).
watch(
  [activeTab, pantryItems] as const,
  ([tab, items]) => {
    if (tab !== 'find') return
    if (recipesStore.level > 2) return
    if (items.length === 0) return
    if (recipesStore.result || recipesStore.loading) return
    if (_autoSuggestFired) return
    _autoSuggestFired = true
    handleSuggest().then(() => {
      filtersOpen.value = false
      localStorage.setItem(FILTERS_OPEN_KEY, 'false')
    })
  },
)

// Live re-suggest when any filter changes while on Find tab with existing results (L1/L2).
const _debouncedResuggest = _debounce(async () => {
  if (activeTab.value !== 'find') return
  if (recipesStore.level > 2) return
  if (pantryItems.value.length === 0) return
  if (!recipesStore.result) return
  await handleSuggest()
}, 1200)

watch(filterKey, () => {
  if (activeTab.value !== 'find') return
  if (recipesStore.level > 2) return
  if (!recipesStore.result) return
  _debouncedResuggest()
})

// #46 — count of active nutrition filters so the summary is informative when collapsed
const activeNutritionFilterCount = computed(() =>
  Object.values(recipesStore.nutritionFilters ?? {}).filter((v) => v !== null).length
)

const activeLevel = computed(() => levels.find(l => l.value === recipesStore.level))

// Time budget buckets for the time-first entry selector (kiwi#52)
// Active = hands-on cooking; total = active + passive waits (marinating, baking, etc.)
const activeTimeBuckets = [
  { label: '15 min', value: 15 },
  { label: '30 min', value: 30 },
  { label: '45 min', value: 45 },
  { label: '1 hr', value: 60 },
]

const totalTimeBuckets = [
  { label: '30 min', value: 30 },
  { label: '1 hr', value: 60 },
  { label: '90 min', value: 90 },
  { label: '2 hr', value: 120 },
  { label: '3 hr', value: 180 },
  { label: '4+ hr', value: 240 },
]

const cuisineStyles = [
  { id: 'italian',           label: 'Italian' },
  { id: 'mediterranean',     label: 'Mediterranean' },
  { id: 'east_asian',        label: 'East Asian' },
  { id: 'latin',             label: 'Latin' },
  { id: 'eastern_european',  label: 'Eastern European' },
]

// Tag input helpers — constraints
function addConstraint(value: string) {
  const tag = value.trim().toLowerCase()
  if (tag && !recipesStore.constraints.includes(tag)) {
    recipesStore.constraints = [...recipesStore.constraints, tag]
  }
  constraintInput.value = ''
}

function onConstraintKey(e: KeyboardEvent) {
  if (e.key === 'Enter' || e.key === ',') {
    e.preventDefault()
    addConstraint(constraintInput.value)
  }
}

function commitConstraintInput() {
  if (constraintInput.value.trim()) {
    addConstraint(constraintInput.value)
  }
}

// Tag input helpers — allergies
function addAllergy(value: string) {
  const tag = value.trim().toLowerCase()
  if (tag && !recipesStore.allergies.includes(tag)) {
    recipesStore.allergies = [...recipesStore.allergies, tag]
  }
  allergyInput.value = ''
}

function removeAllergy(tag: string) {
  recipesStore.allergies = recipesStore.allergies.filter((a) => a !== tag)
}

function onAllergyKey(e: KeyboardEvent) {
  if (e.key === 'Enter' || e.key === ',') {
    e.preventDefault()
    addAllergy(allergyInput.value)
  }
}

function commitAllergyInput() {
  if (allergyInput.value.trim()) {
    addAllergy(allergyInput.value)
  }
}

function addExcludeIngredient(value: string) {
  const tag = value.trim().toLowerCase()
  if (tag && !recipesStore.excludeIngredients.includes(tag)) {
    recipesStore.excludeIngredients = [...recipesStore.excludeIngredients, tag]
  }
  excludeIngredientInput.value = ''
}

function removeExcludeIngredient(tag: string) {
  recipesStore.excludeIngredients = recipesStore.excludeIngredients.filter((i) => i !== tag)
}

function onExcludeIngredientKey(e: KeyboardEvent) {
  if (e.key === 'Enter' || e.key === ',') {
    e.preventDefault()
    addExcludeIngredient(excludeIngredientInput.value)
  }
}

function commitExcludeIngredientInput() {
  if (excludeIngredientInput.value.trim()) {
    addExcludeIngredient(excludeIngredientInput.value)
  }
}

// Max missing number input
function onMaxMissingInput(e: Event) {
  const target = e.target as HTMLInputElement
  const val = parseInt(target.value)
  recipesStore.maxMissing = isNaN(val) ? null : val
}

// Nutrition filter inputs
type NutritionKey = 'max_calories' | 'max_sugar_g' | 'max_carbs_g' | 'max_sodium_mg'
function onNutritionInput(key: NutritionKey, e: Event) {
  const target = e.target as HTMLInputElement
  const val = parseFloat(target.value)
  recipesStore.nutritionFilters[key] = isNaN(val) ? null : val
}

// Streaming recipe generation
async function streamRecipe(level: 3 | 4, wildcardConfirmed = false) {
  isStreaming.value = true
  streamChunks.value = ''
  streamError.value = null

  // Try cf-orch warm vllm path first (returns a direct stream URL)
  let tokenData: StreamTokenResponse | null = null
  try {
    tokenData = await recipesAPI.getRecipeStreamToken({ level, wildcard_confirmed: wildcardConfirmed })
  } catch { /* cf-orch unavailable — fall through to native SSE */ }

  if (tokenData) {
    const url = `${tokenData.stream_url}?token=${encodeURIComponent(tokenData.token)}`
    const es = new EventSource(url)
    es.onmessage = (e: MessageEvent) => {
      try {
        const data = JSON.parse(e.data)
        if (data.done) { es.close(); isStreaming.value = false }
        else if (data.error) { es.close(); isStreaming.value = false; streamError.value = data.error }
        else if (data.chunk) { streamChunks.value += data.chunk }
      } catch { /* ignore malformed events */ }
    }
    es.onerror = () => { es.close(); isStreaming.value = false; streamError.value = 'Stream connection lost' }
    return
  }

  // Native SSE fallback: Kiwi backend streams directly from Ollama
  await recipesStore.streamSuggest(
    pantryItems.value,
    secondaryPantryItems.value,
    (chunk) => { streamChunks.value += chunk },
    () => { isStreaming.value = false },
    (err) => { isStreaming.value = false; streamError.value = err },
  )
}

// Suggest handler
async function handleSuggest() {
  isLoadingMore.value = false
  await recipesStore.suggest(pantryItems.value, secondaryPantryItems.value)
}

async function handleLoadMore() {
  isLoadingMore.value = true
  await recipesStore.loadMore(pantryItems.value, secondaryPantryItems.value)
  isLoadingMore.value = false
}

// ── Ask tab state ────────────────────────────────────────────────────────────

interface AskHistoryEntry {
  question: string
  result: AskResponse
}

const askQuestion = ref('')
const askLoading = ref(false)
const askError = ref<string | null>(null)
const askResult = ref<AskResponse | null>(null)
const askHistory = ref<AskHistoryEntry[]>([])

const ASK_EXAMPLES = [
  'What can I make with chicken and pasta?',
  'Easy vegetarian dinners under 30 minutes',
  'Recipes using overripe bananas',
  'Quick breakfasts with eggs',
]

async function handleAsk(question?: string) {
  const q = (question ?? askQuestion.value).trim()
  if (!q) return
  askQuestion.value = q
  askLoading.value = true
  askError.value = null
  askResult.value = null
  try {
    const result = await recipesAPI.ask(q, pantryItems.value)
    askResult.value = result
    askHistory.value = [{ question: q, result }, ...askHistory.value].slice(0, 3)
  } catch {
    askError.value = 'Could not search recipes. Please try again.'
  } finally {
    askLoading.value = false
  }
}

function onAskKeydown(e: KeyboardEvent) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    handleAsk()
  }
}

onMounted(async () => {
  if (inventoryStore.items.length === 0) {
    await inventoryStore.fetchItems()
  }
  // Pre-load saved recipes so we know immediately whether to redirect
  await savedStore.load()
})

// If Saved tab is empty after loading, bounce to Build Your Own.
// No immediate: true — the immediate fire happens before onMounted runs load(),
// so loading=false and count=0 is the initial unloaded state, not "empty after load".
watch(
  () => ({ loading: savedStore.loading, count: savedStore.saved.length }),
  ({ loading, count }) => {
    if (!loading && count === 0 && activeTab.value === 'saved') {
      activeTab.value = 'build'
    }
  },
)
</script>

<style scoped>
.byo-nudge {
  padding: var(--spacing-sm) 0;
}

.btn-link {
  background: none;
  border: none;
  color: var(--color-primary);
  cursor: pointer;
  padding: 0;
  text-decoration: underline;
  font-size: inherit;
}

.tab-bar {
  border-bottom: 1px solid var(--color-border);
  padding-bottom: var(--spacing-sm);
}

/* Prevent theme's mobile .btn white-space:normal from wrapping tab labels */
@media (max-width: 480px) {
  .tab-btn {
    white-space: nowrap;
    flex-shrink: 0;
  }
}

.tab-btn {
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  border-bottom: none;
}

.mb-controls {
  margin-bottom: var(--spacing-md);
}

.mb-md {
  margin-bottom: var(--spacing-md);
}

.mb-sm {
  margin-bottom: var(--spacing-sm);
}

.mt-xs {
  margin-top: var(--spacing-xs);
}

.ml-xs {
  margin-left: var(--spacing-xs);
}

.level-description {
  font-style: italic;
  line-height: 1.4;
}

.wildcard-warning {
  display: block;
  margin-bottom: var(--spacing-md);
  padding: var(--spacing-sm) var(--spacing-md);
}

.hard-day-toggle {
  cursor: pointer;
  user-select: none;
}

.shopping-toggle {
  cursor: pointer;
  user-select: none;
}

.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
}

.chip-remove {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  font-size: 14px;
  line-height: 1;
  color: inherit;
  opacity: 0.7;
  transition: opacity 0.15s;
  min-width: 24px;
  min-height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.chip-remove:hover {
  opacity: 1;
  transform: none;
}

.inline-spinner {
  display: inline-block;
  vertical-align: middle;
  margin-right: var(--spacing-xs);
}

.rate-limit-banner {
  display: block;
  padding: var(--spacing-sm) var(--spacing-md);
}

.recipe-title {
  flex: 1;
  margin-right: var(--spacing-sm);
}

.btn-dismiss {
  background: transparent;
  border: none;
  cursor: pointer;
  /* WCAG 2.2 2.5.8: minimum 24×24px touch target */
  min-width: 24px;
  min-height: 24px;
  padding: 4px 6px;
  font-size: 12px;
  line-height: 1;
  color: var(--color-text-muted);
  border-radius: 4px;
  transition: color 0.15s, background 0.15s;
  flex-shrink: 0;
}

.btn-dismiss:hover {
  color: var(--color-error, #dc2626);
  background: var(--color-error-bg, #fee2e2);
  transform: none;
}

.btn-bookmark {
  background: transparent;
  border: none;
  cursor: pointer;
  /* WCAG 2.2 2.5.8: minimum 24×24px touch target */
  min-width: 24px;
  min-height: 24px;
  padding: 4px 6px;
  font-size: 14px;
  line-height: 1;
  color: var(--color-text-muted);
  border-radius: 4px;
  transition: color 0.15s, background 0.15s;
  flex-shrink: 0;
}

.btn-bookmark:hover,
.btn-bookmark.active {
  color: var(--color-warning, #ca8a04);
  background: var(--color-warning-bg, #fef9c3);
  transform: none;
}

/* Saved recipes section */
.saved-header {
  user-select: none;
}

.saved-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.saved-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--spacing-sm);
  padding: var(--spacing-xs) 0;
  border-bottom: 1px solid var(--color-border);
}

.saved-row:last-child {
  border-bottom: none;
}

.saved-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-primary);
}

.saved-title:hover {
  text-decoration: underline;
}

/* Filter bar */
.active-filter-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-xs) var(--spacing-sm);
  margin-bottom: var(--spacing-sm);
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
}

/* ── Refine collapsible ──────────────────────────────────────────────────── */
.refine-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-xs);
  margin-bottom: var(--spacing-xs);
  font-size: var(--font-size-sm);
}

.refine-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.3rem;
  border-radius: 9999px;
  background: var(--color-primary);
  color: white;
  font-size: var(--font-size-xs, 0.72rem);
  font-weight: 600;
}

.refine-body {
  margin-bottom: var(--spacing-xs);
}

.filter-bar {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.filter-search {
  width: 100%;
}

.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-xs);
}

.filter-chip {
  background: var(--color-bg-secondary, #f5f5f5);
  border: 1px solid var(--color-border);
  border-radius: 16px;
  padding: 2px 10px;
  font-size: var(--font-size-xs);
  cursor: pointer;
  color: var(--color-text-secondary);
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  white-space: nowrap;
}

.filter-chip:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
  background: var(--color-bg-secondary);
  transform: none;
}

.filter-chip.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

.filter-chip-clear {
  border-color: var(--color-error, #dc2626);
  color: var(--color-error, #dc2626);
}

.filter-chip-clear:hover {
  background: var(--color-error-bg, #fee2e2);
  border-color: var(--color-error, #dc2626);
  color: var(--color-error, #dc2626);
}

.suggest-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

.btn-ghost {
  background: transparent;
  border: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  cursor: pointer;
  padding: var(--spacing-xs) var(--spacing-sm);
  white-space: nowrap;
}

.btn-ghost:hover {
  color: var(--color-primary);
  background: transparent;
  transform: none;
}

.btn-sm {
  padding: var(--spacing-xs) var(--spacing-sm);
  font-size: var(--font-size-sm);
}

.load-more-row {
  display: flex;
  justify-content: center;
  margin-bottom: var(--spacing-md);
}

.collapsible {
  border-top: 1px solid var(--color-border);
  padding-top: var(--spacing-sm);
}

.collapsible-summary {
  cursor: pointer;
  list-style: none;
  padding: var(--spacing-xs) 0;
  color: var(--color-primary);
}

.collapsible-summary::-webkit-details-marker {
  display: none;
}

.collapsible-summary::before {
  content: '▶ ';
  font-size: 10px;
}

details[open] .collapsible-summary::before {
  content: '▼ ';
}

.filter-summary {
  display: flex;
  align-items: center;
  gap: var(--spacing-xs);
  font-weight: 500;
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
}

.filter-active-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-primary);
  flex-shrink: 0;
}

.collapsible-body {
  padding-top: var(--spacing-sm);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
}

/* Hard Day Mode button */
.hard-day-btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  justify-content: flex-start;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
  font-weight: 500;
  transition: background 0.15s, color 0.15s;
}

.hard-day-active {
  background: var(--color-success, #2d7a4f);
  color: white;
  border-color: var(--color-success, #2d7a4f);
}

.hard-day-sub {
  font-size: var(--font-size-xs, 0.75rem);
  font-weight: 400;
  opacity: 0.85;
  margin-left: auto;
}

/* Time bucket selector (kiwi#52) */
.time-bucket-group {
  margin-top: var(--spacing-sm, 0.5rem);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.time-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
}

.time-row-label {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  white-space: nowrap;
  min-width: 6rem;
  font-weight: 500;
}

.time-bucket-btn {
  min-width: 4rem;
  border-radius: var(--radius-full, 9999px);
  font-weight: 500;
}

.time-bucket-active {
  background: var(--color-primary, #1a6b4a);
  color: white;
  border-color: var(--color-primary, #1a6b4a);
}

.time-bucket-clear {
  font-style: italic;
  opacity: 0.7;
}

.time-bucket-clear:hover {
  opacity: 1;
}

@media (max-width: 480px) {
  .time-row {
    flex-direction: column;
    align-items: flex-start;
  }

  .time-row-label {
    min-width: unset;
  }
}

/* Preset grid — auto-fill 2+ columns */
.preset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: var(--spacing-xs);
}

.preset-btn {
  justify-content: center;
  text-align: center;
}

.preset-active {
  background: var(--color-primary);
  color: white;
  border-color: var(--color-primary);
}

.allergen-btn {
  justify-content: center;
  text-align: center;
}

.allergen-active {
  background: var(--color-error, #c0392b);
  color: white;
  border-color: var(--color-error, #c0392b);
}

.swap-row {
  padding: var(--spacing-xs) 0;
  border-bottom: 1px solid var(--color-border);
}

.swap-row:last-child {
  border-bottom: none;
}

.prep-notes-list {
  padding-left: var(--spacing-lg);
  list-style-type: disc;
}

.prep-note-item {
  margin-bottom: var(--spacing-xs);
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.ingredient-section {
  border-top: 1px solid var(--color-border);
  padding-top: var(--spacing-sm);
}

.ingredient-label {
  margin-bottom: 0;
}

.ingredient-label-have {
  color: var(--color-success, #16a34a);
}

.ingredient-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: var(--font-size-xs);
  white-space: nowrap;
}

.ingredient-chip-have {
  background: var(--color-success-bg, #dcfce7);
  color: var(--color-success, #16a34a);
}

.directions-section {
  border-top: 1px solid var(--color-border);
  padding-top: var(--spacing-sm);
  margin-top: var(--spacing-xs);
}

.directions-label {
  color: var(--color-text-secondary);
  text-transform: uppercase;
  font-size: var(--font-size-xs);
  letter-spacing: 0.05em;
  margin-bottom: var(--spacing-xs);
}

.directions-list {
  padding-left: var(--spacing-lg);
}

.direction-step {
  margin-bottom: var(--spacing-sm);
  line-height: 1.6;
}

.grocery-link {
  text-decoration: none;
  cursor: pointer;
  transition: opacity 0.2s;
}

.grocery-link:hover {
  opacity: 0.8;
}

.card-actions {
  border-top: 1px solid var(--color-border);
  padding-top: var(--spacing-sm);
  margin-top: auto;
  display: flex;
  justify-content: flex-end;
}

.btn-make {
  font-size: var(--font-size-sm);
  padding: var(--spacing-xs) var(--spacing-md);
}

/* Compact recipe card — summary only; detail in RecipeDetailPanel */
.recipe-card-compact {
  display: flex;
  flex-direction: column;
  padding: var(--spacing-sm) var(--spacing-md);
}

.recipe-card-meta {
  min-height: 1.25em; /* prevent layout shift when both fields absent */
}

.chip-overflow {
  opacity: 0.75;
  font-style: italic;
}

.spotlight-card {
  border: 2px solid var(--color-primary);
  background: linear-gradient(135deg, var(--color-bg-elevated) 0%, rgba(232, 168, 32, 0.06) 100%);
}

.results-section {
  margin-top: var(--spacing-md);
}

.nutrition-summary {
  cursor: pointer;
}

.nutrition-filters-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--spacing-sm);
}

.nutrition-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-xs);
}

.nutrition-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: var(--font-size-xs);
  background: var(--color-bg-secondary, #f5f5f5);
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.nutrition-chip-sugar {
  background: var(--color-warning-bg);
  color: var(--color-warning);
}

.nutrition-chip-servings {
  background: var(--color-info-bg);
  color: var(--color-info-light);
}

.nutrition-chip-estimated {
  font-style: italic;
  opacity: 0.7;
}

/* Mobile adjustments */
@media (max-width: 480px) {
  .flex-between {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--spacing-xs);
  }

  .recipe-title {
    margin-right: 0;
  }

  .nutrition-filters-grid {
    grid-template-columns: 1fr;
  }
}

.undo-toast {
  position: fixed;
  bottom: var(--spacing-lg);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--spacing-sm) var(--spacing-md);
  box-shadow: var(--shadow-md);
  font-size: var(--font-size-sm);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

.undo-toast .btn-link {
  min-height: 24px;
  padding: 2px 4px;
}

.stream-panel {
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md, 8px);
  padding: var(--spacing-md);
  margin-bottom: var(--spacing-md);
}

.stream-status {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

.stream-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-warning);
  animation: stream-pulse 1.2s ease-in-out infinite;
}

@keyframes stream-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

.stream-error {
  color: var(--color-danger, #e05c5c);
  margin-bottom: 0.5rem;
}

/* ── Stream recipe card — skeleton + progressive reveal ─────────────────── */
.stream-recipe-card {
  margin-top: var(--spacing-sm);
}

.stream-section-label {
  display: block;
  color: var(--color-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--font-size-xs, 0.72rem);
  margin-bottom: var(--spacing-xs);
}

.stream-ingredients {
  padding-left: 1.2rem;
  list-style: disc;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.stream-steps {
  padding-left: 1.4rem;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.stream-notes {
  border-top: 1px solid var(--color-border);
  padding-top: var(--spacing-xs);
  margin-top: var(--spacing-xs);
}

/* ── Skeleton shimmer ───────────────────────────────────────────────────── */
@keyframes skeleton-shimmer {
  0% { background-position: -200px 0; }
  100% { background-position: calc(200px + 100%) 0; }
}

.skeleton-line {
  height: 0.8rem;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    var(--color-bg-secondary, #f5f5f5) 0%,
    var(--color-border, #e0e0e0) 50%,
    var(--color-bg-secondary, #f5f5f5) 100%
  );
  background-size: 200px 100%;
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
  margin-bottom: var(--spacing-xs);
}

.skeleton-title { height: 1.2rem; width: 55%; }
.skeleton-short { width: 38%; }
.skeleton-medium { width: 65%; }
.skeleton-long { width: 88%; }

@media (prefers-reduced-motion: reduce) {
  .skeleton-line {
    animation: none;
    background: var(--color-bg-secondary, #f5f5f5);
  }
}

/* ── Ask tab ──────────────────────────────────────────────────────────── */
.ask-panel {
  padding-top: var(--spacing-sm);
}

.ask-input-row {
  display: flex;
  gap: var(--spacing-xs);
  align-items: flex-start;
}

.ask-input {
  flex: 1;
  min-width: 0;
}

.ask-submit {
  flex-shrink: 0;
  white-space: nowrap;
}

.ask-examples {
  margin-top: var(--spacing-md);
}

.ask-example-btn {
  white-space: normal;
  text-align: left;
  max-width: 100%;
}

.ask-answer {
  border-left: 3px solid var(--color-primary);
  background: var(--color-bg-secondary, #f8f8f8);
  padding: var(--spacing-sm) var(--spacing-md);
}

.ask-hit-card {
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}

.ask-hit-card:hover,
.ask-hit-card:focus-visible {
  box-shadow: 0 0 0 2px var(--color-primary);
  outline: none;
}

.ask-history-btn {
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>
