📄 src/routes/guide/+page.svelte
<script lang="ts">
  import { resolve } from "$app/paths";
  import { historyStore } from "$lib/history";
  import { labelsStore } from "$lib/labels";
  import PatchBanner from "$lib/components/PatchBanner.svelte";

  let search = $state("");

  const detectedSet = $derived(new Set($historyStore.map((e) => e.labelIndex)));

  const allBirds = $derived($labelsStore.map((label, index) => ({ label, index })));

  const filtered = $derived(
    search.trim() === ""
      ? allBirds
      : allBirds.filter(
          ({ label }) =>
            label.commonName.toLowerCase().includes(search.toLowerCase()) ||
            label.scientificName.toLowerCase().includes(search.toLowerCase()),
        ),
  );
</script>

<PatchBanner text="Field Guide" />

<div class="search-wrap">
  <input
    type="search"
    bind:value={search}
    placeholder="Search {$labelsStore.length} species…"
    aria-label="Search species"
  />
</div>

{#if $labelsStore.length === 0}
  <p class="loading">Loading guide…</p>
{:else}
  <div class="grid">
    {#each filtered as { label, index } (index)}
      {@const detected = detectedSet.has(index)}
      {#if detected}
        <a href={resolve(`/guide/${index}`)} class="card">
          <span class="num">#{String(index + 1).padStart(4, "0")}</span>
          <span class="name">{label.commonName}</span>
        </a>
      {:else}
        <div class="card unseen">
          <span class="num">#{String(index + 1).padStart(4, "0")}</span>
          <span class="name">{label.commonName}</span>
        </div>
      {/if}
    {/each}
  </div>
{/if}

<style>
  .search-wrap {
    padding: 0.75rem 1rem 0;
  }

  input[type="search"] {
    width: 100%;
    background: var(--bg-paper);
    border: 2px solid var(--bg-near-black);
    border-radius: 8px;
    padding: 0.6rem 0.75rem;
    color: var(--bg-near-black);
    font-size: 1rem;
    font-family: inherit;
    outline: none;

    &::placeholder {
      color: var(--accent-rust);
      opacity: 0.7;
    }

    &:focus {
      outline: 2px solid var(--brand-red);
      outline-offset: -2px;
    }
  }

  .loading {
    padding: 3rem 1.5rem;
    text-align: center;
    color: var(--accent-rust);
  }

  .grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.6rem;
    padding: 0.75rem 1rem;
    padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom) + 1rem);
  }

  .card {
    background: var(--bg-paper);
    border: 2px solid var(--bg-near-black);
    border-radius: 8px;
    padding: 0.6rem 0.75rem;
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    text-decoration: none;
    color: var(--bg-near-black);
    content-visibility: auto;
    contain-intrinsic-block-size: 5rem;

    &::before {
      content: "";
      position: absolute;
      inset: 4px;
      border-radius: 4px;
      border: 1.5px dashed var(--accent-amber);
      pointer-events: none;
    }

    &.unseen {
      opacity: 0.35;
      pointer-events: none;
    }

    &:not(.unseen):hover {
      background: var(--bg-cream);
    }

    .num {
      font-size: 0.6rem;
      font-variant-numeric: tabular-nums;
      color: var(--accent-rust);
      font-weight: 700;
      letter-spacing: 0.05em;
    }

    .name {
      font-size: 0.8rem;
      font-weight: 700;
      line-height: 1.3;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }
  }
</style>