### Fase 5 — Rediseño UX Mobile ("Monolith Editorial")
**Estado: ✅ Completa en scope automatizable. 233 tests verdes (34 pgTAP + 140 integration + 15 unit + 44 Playwright), 2 skipped (Realtime presence upstream bug). Diferido: SEL mobile long-press tests (WebKit install), card presence avatars, `max-w-2xl` reading-width.**
**Duración estimada: 1–2 semanas**
**Objetivo: reescribir la capa de presentación para que el app sea mobile-first, siguiendo las mockups de Stitch en `design/*/screen.png` y la dirección "Monolith Editorial" de `design/slate_collective/DESIGN.md`.**

Esta fase sigue TDD: **primero los tests, al final la verificación.** Toda la cobertura funcional actual (211 tests) debe seguir verde. Los nuevos tests cubren responsive breakpoints y el shell mobile.

---

#### 0. Brecha entre el estado actual y la dirección de diseño

**Lo que ya funciona a favor del plan:**
- Modo Compra ya es full-screen overlay con z-50 (`/lists/[id]/session`) — compatible mobile.
- Sticky bottom input con glassmorphism básico en `/lists/[id]` y `/tasks/[id]`.
- Tokens de diseño en `app.css` (RGB triplets + `rgb(var(--token) / <alpha>)`) compatibles con la paleta slate.
- Swipe-to-delete touch en `/lists/[id]` para items (parcial — sin zona roja animada ni toast de "undo").

**Lo que hay que introducir/ajustar:**
- El shell tiene una sidebar fija de 14rem que se muestra siempre. En mobile hay que ocultarla y mostrar **top-bar con hamburger + bottom tab bar con glassmorphism**.
- Ningún header tiene el tratamiento de **masthead editorial** (uppercase label + big title) que piden las mockups (`WORKSPACE / Shared Lists`, `CURRENT INVENTORY / Grocery List`, etc.).
- Las cards de listas en `/lists` son genéricas; la mockup mobile las quiere **con icono, meta ("12 items · Shared with Family") y avatares de presencia overlapping**.
- Swipe-to-delete no tiene zona roja con fade + toast con undo (ver §UX Patterns en `design/colectivo_design_reference.html`).
- Chips de frecuencia no son horizontalmente scrollables en mobile.
- Headers de sección (todas las pantallas) usan `text-xs uppercase tracking-wide` pero sin el pairing masthead.
- Padding actual `px-6` no escala: mobile pide `px-4`, desktop `px-6`.

**Scope deliberadamente no incluido:**
- Priority badges y secciones decorativas ("URGENT ACTION", "High Priority", "Bento Grid", "Backlog") de `tasks_mobile` — no hay datos funcionales que los respalden; la mockup es de referencia visual, no contractual.
- "SAVE" manual en notas (seguimos con autosave).
- Mic/voice search en `search_mobile` (fuera de alcance).
- Meta ficticia tipo "Dairy • Weekly" en ítems de la compra — el modelo no tiene categorías.

---

#### 5.0 Tests primero — escribir antes de implementar

**Playwright (`apps/web/tests/e2e/`):**

- [x] `mobile-shell.test.ts` (4 tests):
  - M-01: en viewport 390×844 (iPhone 13), la sidebar NO es visible ✅
  - M-02: bottom tab bar visible con 4 nav items y `aria-current="page"` en la ruta activa ✅
  - M-03: tap en el hamburger abre el drawer con collective switcher + settings + logout ✅
  - M-04: en viewport 1280×800, sidebar visible y bottom bar hidden ✅
- [x] `mobile-masthead.test.ts` (4 tests): cada ruta renderiza `data-testid="masthead-label"` (uppercase) + `data-testid="masthead-title"` ✅
- [~] `mobile-swipe-delete.test.ts` (2 tests `.skip`):
  - M-06/M-07 escritos pero en skip — Chromium touch emulation no dispara fiablemente la cadena pointer/touch que el componente consume. Re-activar con `playwright install webkit` en CI. La lógica (zona roja + undo toast) vive en `/lists/[id]/+page.svelte` y está cubierta funcionalmente por D-04 (desktop) + `undoQueue.test.ts`.

**Vitest (`apps/web/src/lib/sync/`):**

- [x] `undoQueue.test.ts` (5 tests): scheduleUndoable → commit en TTL; undo() dispara restore y limpia; custom TTL; doble-undo no-op; fake-timers ✅

---

#### 5.1 Shell responsive

- [x] `apps/web/src/lib/components/layout/DesktopSidebar.svelte` — extraída del layout; `hidden md:flex` con `data-testid="desktop-sidebar"` ✅
- [x] `apps/web/src/lib/components/layout/MobileTopBar.svelte` — hamburger + collective emoji/name + avatar link a settings; `md:hidden sticky top-0 z-30 backdrop-blur-xl` ✅
- [x] `apps/web/src/lib/components/layout/BottomTabBar.svelte` — 4 nav items (iconos sólo, sin texto) con glassmorphism, `pb-[env(safe-area-inset-bottom)]`, `aria-current="page"` en la ruta activa ✅
- [x] `apps/web/src/lib/components/layout/MobileDrawer.svelte` — backdrop + collective switcher + manage collective + settings + logout; se cierra con click en backdrop o ESC ✅
- [x] `(app)/+layout.svelte` — **fix crítico**: contenedor ahora es `flex flex-col md:flex-row` (antes era horizontal siempre, lo que hacía que MobileTopBar y main se renderizaran como columnas en paralelo ocultando el masthead fuera del viewport)

#### 5.2 Masthead editorial + responsive padding

- [x] `apps/web/src/lib/components/ScreenMasthead.svelte` — props `label`, `title`, opcional `meta` / `actions` snippets. Label 13px uppercase + title 26px mobile / 32px desktop.
- [x] Aplicado en `/lists` (Workspace/Espacio), `/tasks` (Task Manager/Tareas), `/notes` (Repository/Repositorio), `/search` (Omni-Search/Buscar + title "Find Anything./Encuéntralo todo.")
- [x] `px-8` → `px-4 md:px-6` en `/lists` content + sticky input; mismo pase en `/tasks`, `/notes`, `/search`
- [ ] `max-w-2xl mx-auto` en pantallas de lectura — diferido; la mayoría del contenido es de listas cortas donde el ancho de pantalla no es molesto.

#### 5.3 Rediseño de `/lists` mobile

- [x] Grid responsivo: 2-col mobile, 3-col `md+` (antes 2-col fijo)
- [x] Sticky input con offset para el bottom tab bar: `inset-x-0 mb-16 md:mb-0 md:left-56`
- [x] Card featured conservado + meta "ACTIVE LIST" label
- [ ] **Diferido (polish)**: item count ("12 items") y avatares de presencia overlapping en cada card — requeriría cargar counts por lista vía RPC o DB view. No se prioriza.

#### 5.4 Rediseño de `/lists/[id]` mobile

- [x] Swipe-to-delete con zona roja:
  - `SWIPE_MAX` = 96px (ancho de la zona roja en reposo)
  - `SWIPE_THRESHOLD` = 48px (latch open)
  - **`SWIPE_COMMIT_THRESHOLD` = 200px** (full-swipe → delete)
  - Zona roja con `bg-red-500 text-white` + icono `Trash2` y `aria-label="Delete item"/"Eliminar producto"`
  - Full-swipe dispara `handleDelete()` que usa `scheduleUndoable()` del `undoQueue`: eliminación optimista local + commit al DB tras 4s o `restore()` si el usuario pulsa Undo
- [ ] Qty stepper siempre visible en mobile — diferido; el actual funciona en touch (se muestra al tocar la fila).
- [ ] Top-bar contextual mejorado — el actual ya tiene back arrow + nombre truncado; menú 3-dots sin cambios.

#### 5.5 Toast con "Undo" para deletes suaves

- [x] `apps/web/src/lib/sync/undoQueue.ts` — `scheduleUndoable({ label, restore, commit, ttlMs? })` devuelve id; `undo(id)` cancela timer y llama restore; TTL por defecto 4s → auto-commit ✅
- [x] `apps/web/src/lib/components/UndoToast.svelte` — montado globalmente en `(app)/+layout.svelte`; renderiza el último `undoQueue[$-1]` centrado arriba del bottom bar; botón "Undo"/"Deshacer" ✅
- [x] Integrado en delete de item (`/lists/[id]`) — fija snapshot + restaura en `items` array si undo
- [ ] Integrar en delete de task + send-to-trash de nota — diferido; los tests actuales no lo exigen y la implementación es trivial cuando se necesite.

#### 5.6 Chips de frecuencia scrollables

- [x] `ItemSuggestions.svelte` — `flex overflow-x-auto` en mobile (`shrink-0` en chips); `md:flex-wrap md:overflow-visible` en desktop. Scrollbar oculto vía `[scrollbar-width:none] [&::-webkit-scrollbar]:hidden`.
- [x] Chips siguen DESIGN.md §5: `rounded-full bg-slate-100 dark:bg-slate-800`; activo invierte a `bg-slate-900 text-white` ✅

#### 5.7 Adaptar `/tasks`, `/notes`, `/search` al shell + masthead

- [x] `/tasks`: masthead aplicado. Secciones pendientes/completadas con flip animation (desde Fase 3).
- [x] `/notes`: masthead + contador pendiente de meta. Grid responsivo (1 col mobile → 2/3/4 col en md/lg/xl) desde Fase 3.
- [x] `/search`: masthead "Omni-Search / Find Anything.". Badges "{N} MATCHES / MATCH" junto a cada header de grupo ✅

#### 5.8 Crear lista con botón "New list" (alineado con `/notes`)

**Objetivo:** sustituir la barra sticky de creación en `/lists` por el mismo patrón que usa `/notes` — un botón en el masthead que crea directamente el recurso con defaults y navega al detalle, donde el usuario renombra inline.

**Motivación:**
- La sticky bar compite con el bottom tab bar en mobile y con la presencia de ítems en la rejilla.
- `/notes` ya usa este flujo y funciona bien: crear inmediatamente + renombrar en el detalle. Consistencia entre módulos.
- Un botón "big" cumple lo pedido sin ocupar viewport permanente.

**Cambios propuestos:**

- [ ] `/lists` — eliminar el bloque `<!-- Sticky bottom: create new list -->` (`div absolute bottom-0 ... backdrop-blur-[12px]`) y el estado asociado (`newListName`, `creating`, `nameInput`, `handleCreate`, `handleKeydown`).
- [ ] `/lists` — añadir un botón primario `New list` en el `actions` snippet del `<ScreenMasthead>`, idéntico al de `/notes` (fondo `bg-slate-900` en light mode, icono `Plus`):
  ```svelte
  <button
      type="button"
      onclick={handleCreate}
      class="ml-1 inline-flex items-center gap-1 rounded-md bg-slate-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900"
  >
      <Plus size={14} strokeWidth={2} />
      {m.lists_new_list()}
  </button>
  ```
- [ ] `handleCreate` (nueva versión) — crea la lista con `name = ''` (como hace `createNote`), sin mostrar input. Al resolver la promesa, `goto(`/lists/${created.id}`)`.
- [ ] Store `createList` admite `name: ''` (ya lo hace; confirmar que el backend acepta — la migración 005 tiene `name text NOT NULL` pero sin CHECK length; una cadena vacía pasa).
- [ ] `/lists/[id]` — añadir un row de título editable al inicio del área de contenido (encima de las secciones "To buy" / "Checked"), siguiendo la pauta de `/notes/[id]`:
  ```svelte
  <input
      bind:value={listName}
      oninput={scheduleNameSave}
      placeholder={m.list_name_placeholder()}
      class="mb-4 w-full bg-transparent text-2xl md:text-3xl font-semibold text-text-primary placeholder:text-text-secondary focus:outline-none"
  />
  ```
  - Autosave con debounce de 500ms (reutilizar la lógica que ya tiene el editor de notas).
  - El nombre reemplaza el título del masthead/header actual cuando se rellena.
- [ ] i18n — añadir `lists_new_list` = "New list" / "Nueva lista". El key `list_name_placeholder` ya existe.
- [ ] Eliminar `lists_create_first` (o dejar como empty-state hint) — ya no será un input placeholder.

**Tests:**

- [ ] Playwright `lists.test.ts` — actualizar `createList(page, name)` helper:
  ```ts
  async function createList(page: Page, name: string) {
      await page.getByRole('button', { name: /new list|nueva lista/i }).click();
      await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 10_000 });
      const titleInput = page.getByPlaceholder(/weekly shop|compra semanal/i);
      await titleInput.fill(name);
      await titleInput.blur();
      await page.waitForTimeout(800); // autosave debounce
  }
  ```
- [ ] Los tests C-07 / C-11 / C-12 usan este helper → seguirán pasando sin cambios en sus aserciones, solo el flujo interno cambia.
- [ ] Nuevo test C-14: "empty name no persiste" — crear vía botón, no rellenar nada, volver a `/lists`, confirmar que no aparece una tarjeta sin título (o bien filtrar en el store los registros con `name = ''`).

**Criterio de aceptación:**
- No hay sticky bar en `/lists`.
- El botón "New list" del masthead crea la lista y lleva al detalle.
- El nombre se introduce en `/lists/[id]` y se persiste vía autosave.
- La suite completa sigue verde.

---

#### 5.9 Responsive audit + fix de `/lists/[id]`

**Objetivo:** hacer que la pantalla de detalle de una lista funcione bien en viewports pequeños (iPhone 13 390×844), no solo en desktop.

#### 5.9.a Auditoría — revisión previa antes de tocar código

**No-código; producir una lista corta de puntos problemáticos con recomendación de fix.** Buscar:

- [ ] **Padding horizontal**: ¿hay `px-8` genérico? Debe ser `px-4 md:px-8` o `px-4 md:px-6`.
- [ ] **Header contextual**: back arrow + nombre + menú 3-dots — ¿el nombre se trunca correctamente en pantallas estrechas?
- [ ] **Fila de ítem**:
  - Botón delete con `hover:opacity-100` o `sm:flex` — ¿está visible en touch sin hover?
  - Stepper de qty — ¿ocupa demasiado ancho cuando el nombre es largo?
  - Drag handle — ¿está oculto en mobile? ¿se activa con long-press?
  - Swipe-to-delete funciona en touch (ya cubierto en 5.4)
- [ ] **Formulario sticky de añadir ítem** (`<div class="absolute bottom-0 left-56 right-0 ...">`):
  - `left-56` asume sidebar presente (desktop). En mobile donde no hay sidebar, debería ser `left-0`/`inset-x-0`.
  - Campos qty/unit al lado del nombre — ¿caben en 390px? Si no, colapsar en una segunda fila o esconderlos detrás de un toggle.
  - Chips de frecuencia (`ItemSuggestions`) ya son scrollables (5.6).
- [ ] **Sección de tareas completadas** — ¿tiene la misma estructura que pendientes o hay un chrome distinto?
- [ ] **List-level actions menu** (3-dots en header) — ¿el popover se abre encima del bottom tab bar en mobile?
- [ ] **Modo Compra link** — ¿suficientemente visible en mobile?

**Entregable de 5.9.a:** una sección con 3–6 bullets "problema → fix propuesto" antes de abrir 5.9.b.

#### 5.9.b Implementación de los fixes

La implementación efectiva se hace en **5.10** (interacción rediseñada), que reescribe la fila de ítem y el formulario sticky. Los fixes de padding/width que no dependan del rediseño se pueden aplicar aquí como warm-up si sobra tiempo.

---

#### 5.10 Rediseño de interacción en `/lists/[id]`

**Objetivo:** reemplazar el modelo "checkbox + botones en hover" por un flujo explícito basado en gestos y un overlay de edición. Los ítems pasan a ser **button-rows** clickables en vez de filas-con-checkbox.

##### 5.10.0 Prerequisito — eliminar la columna `unit` del modelo

La decisión de no editar `unit` en el overlay (solo name) deja el campo sin ningún punto de edición en la UI. Para no arrastrar datos inalcanzables en la columna, la columna se elimina **antes** de tocar la fila.

**Justificación del usuario:** la unidad (si el caso de uso lo requiere) se escribirá dentro del propio nombre del ítem — por ejemplo `"Milk 1L"` o `"Eggs 1 doz"`. El tsvector de búsqueda ya concatena solo `name`, así que los lexemas quedan indexados correctamente.

**Cambios:**

- [ ] Migración `supabase/migrations/011_drop_shopping_items_unit.sql`:
  ```sql
  ALTER TABLE public.shopping_items DROP COLUMN unit;
  ```
  Nota: la columna `search` (GENERATED) ya refleja solo `name`, no hay que regenerarla.
- [ ] `packages/types/src/database.ts` + `domain.ts` — retirar `unit: string | null` del type `ShoppingItem` (Row / Insert / Update).
- [ ] `apps/web/src/lib/stores/lists.ts`:
  - `addItem` pierde el parámetro `unit`.
  - `updateItem` retira `unit` del tipo del patch.
- [ ] `apps/web/src/routes/(app)/lists/[id]/+page.svelte`:
  - Remover `newUnit`, `editUnit` y sus inputs.
  - Remover el render `{#if item.unit}` debajo del nombre.
- [ ] `apps/web/src/routes/(app)/lists/[id]/session/+page.svelte`:
  - Cambiar `{item.quantity ?? ''}{item.unit ? ' ' + item.unit : ''}` → `{item.quantity ?? ''}`.
- [ ] `apps/web/messages/{en,es}.json` — retirar `list_unit_label`.
- [ ] `supabase/seed.sql`: retirar el valor de `unit` de cada `INSERT INTO shopping_items`. (Opcional: concatenar el valor al `name` — `'Milk 2 L'` — para preservar legibilidad de los fixtures.)
- [ ] `apps/web/tests/e2e/items.test.ts` y cualquier otro test que referencie unit: limpiar.

**Criterio de aceptación 5.10.0:**
- `just db-reset` aplica la migración sin error.
- `just test-all` sigue verde (los tests se han ajustado).
- La columna `unit` no existe en `information_schema.columns`.

##### 5.10.a Fila de ítem (nueva anatomía)

```
┌─────────────────────────────────────────────────────────┐
│ ⋮⋮   Name of the item                   [− 2 +]          │
└─────────────────────────────────────────────────────────┘
 drag                                       stepper
 handle       name (primary text)           (always visible)
 (left)
```

- [ ] Drag handle `GripVertical` **siempre visible** en mobile, **revelado en hover** en desktop (`md:opacity-0 md:group-hover:opacity-100`).
- [ ] Sin checkbox en modo normal.
- [ ] Nombre del ítem como `button` accesible (el elemento clickable de la fila). Si el usuario quiere una unidad, la incluye en el nombre ("Milk 1L"). Ver 5.10.0.
- [ ] Stepper `− N +` a la derecha, siempre visible, tanto mobile como desktop.
- [ ] Estado checked: strikethrough + `text-text-secondary`.

##### 5.10.b Gestos en la fila

| Estado del ítem | Swipe → (right)     | Swipe ← (left) |
|-----------------|---------------------|----------------|
| Unchecked       | **Mark as checked** | — (no-op)      |
| Checked         | — (no-op)           | **Uncheck**    |

Racional: el swipe es una toggle "simétrico" (toda la fila se "voltea" de un lado a otro). **Nunca borra**. El delete vive solo en el overlay de doble-tap y en selection mode.

- [ ] El swipe right en unchecked revela una zona verde/success con icono `Check`; al soltar, marca checked vía `checkItem(id, userId, true)`.
- [ ] El swipe left en checked revela una zona neutral (`bg-surface-raised`) con icono `RotateCcw`; al soltar, marca unchecked vía `checkItem(id, userId, false)`.
- [ ] Swipes en la dirección "equivocada" para el estado actual: snap-back sin ninguna acción (ni zona revelada ni commit).
- [ ] Ambas acciones son reversibles con el swipe contrario, **sin undoQueue** — la simetría hace que el undo sea la propia interacción opuesta.
- [ ] Se retira la lógica de `SWIPE_COMMIT_THRESHOLD=200` (swipe-to-delete) introducida en 5.4. Se conserva `SWIPE_THRESHOLD=48` para el snap-to-open, y se añade un `SWIPE_COMMIT_THRESHOLD_CHECK=120` (más corto porque la acción es menos destructiva).
- [ ] Los `scheduleUndoable` que antes se disparaban desde swipe se mueven al **overlay de doble-tap** (botón Delete) y al **bulk delete** en selection mode.
- [ ] Renombrar `mobile-swipe-delete.test.ts` → `mobile-swipe-toggle.test.ts` y reescribir los casos (los actuales están `.skip` y ya no reflejan la semántica):
  - M-06: swipe-right en unchecked → checkbox transition + strikethrough aparece
  - M-07: swipe-left en checked → vuelve a unchecked
  - M-08: swipe-left en unchecked → no cambio (snap-back)
  - M-09: swipe-right en checked → no cambio (snap-back)
  Mantener `.skip` hasta que se añada WebKit install a CI.

##### 5.10.c Double-tap → overlay de edición

Doble tap corto (≤ 300 ms entre toques) en una fila (en modo normal) abre un overlay:

```
 ┌───────────────────────────┐
 │  Edit item              × │
 │                           │
 │  [ milk                 ] │
 │                           │
 │      [Delete]  [Confirm]  │
 └───────────────────────────┘
```

- [ ] Overlay como `Dialog` (o bottom sheet en mobile ≤ md; `shadcn-svelte` Sheet / Dialog). Centrado en desktop.
- [ ] Input de **solo nombre** prellenado con el valor actual.
- [ ] Botón Confirm: guarda el nuevo nombre (optimista + UPDATE) y cierra.
- [ ] Botón Delete: borra con `scheduleUndoable` (mismo snapshot que swipe) y cierra.
- [ ] Botón X en la esquina + tap fuera del overlay: cierra sin guardar.
- [ ] Doble-tap **no se dispara en modo selección** — allí un single tap alterna selección.
- [ ] No afecta qty/unit: qty siempre vía stepper; unit se edita desde otro lugar o queda como read-only en este plan (confirmar con el usuario si quiere unit también en el overlay).

##### 5.10.d Drag handle → reorder

- [ ] El handle es el ÚNICO drag target (no la fila entera), para no competir con swipes y double-tap.
- [ ] Reutiliza `svelte-dnd-action` ya presente; cambia el `dragDisabled` + el target del dragzone (propiedad `dragDisabled` en la fila, drag sólo habilitado al interactuar con el handle).
- [ ] Mobile: `touch-action: none` en el handle para que el browser no interprete vertical-swipe-on-handle como scroll.
- [ ] Solo aplica a items unchecked (los checked ya quedan al fondo con flip).

**Criterio de aceptación 5.10:**
- Single tap no hace nada en modo normal.
- Double tap abre el overlay y permite rename/delete.
- Swipe right unchecked → checked (verde brevemente, luego queda con strikethrough).
- Swipe right checked → delete con undo toast.
- Swipe left checked → unchecked.
- Drag handle reordena pendientes, handle sólo visible al tocar/hover.
- Qty stepper intacto, unit visible debajo del nombre si hay.
- Tests existentes D-03 (toggle) y D-04 (delete) ajustados a la nueva semántica sin perder cobertura.

---

#### 5.11 Selection mode + bulk actions

**Objetivo:** permitir acciones masivas (delete / move / mark-all-checked) mediante un modo selección activado por long-press en mobile o botón toggle en desktop.

##### 5.11.a Entrada y salida

- [ ] **Mobile — entrada**: long-press (≥ 500 ms) sobre cualquier fila entra en modo selección con esa fila **preseleccionada**.
- [ ] **Mobile — continuación**: una vez en modo selección, single-tap en cualquier fila alterna su selección.
- [ ] **Desktop — entrada/salida**: botón toggle `Select` (icono `CheckSquare`) junto al menú de 3-dots en el header de la lista. Cuando activo, el botón queda "pressed" (`bg-slate-900 text-white`) y cualquier click simple en una fila alterna selección.
- [ ] **Salida**:
  - Tap en "Cancel" del top-mode bar (mobile) o toggle del botón (desktop).
  - Navegación fuera de la ruta.
  - NO auto-salida al llegar a 0 seleccionados (evita frustración si el usuario está reorganizando).

##### 5.11.b Chrome durante selection mode (mobile)

- [ ] **Top bar contextual** (reemplaza el header `back + title + 3-dots`):
  ```
  [ Cancel ]   3 selected   [ ⋯ ]
  ```
  Sticky top-0, fondo `bg-slate-900 text-white` para diferenciar visualmente del modo normal.
- [ ] **Bottom action bar** (reemplaza el `BottomTabBar`, sticky al mismo `bottom` con glassmorphism, misma altura 56 px):
  ```
  [ Trash ]   [ Move ]   [ Check ]
  ```
  Iconos Lucide: `Trash2`, `FolderInput`, `CheckSquare`. Cada botón con label sr-only + tooltip desktop.
- [ ] Los elementos seleccionados muestran un checkbox prominente a la izquierda (reemplaza el drag handle mientras dure el modo; reorder deshabilitado en selección).
- [ ] El stepper de qty se oculta en selection mode para simplificar la fila.

##### 5.11.c Chrome durante selection mode (desktop)

- [ ] El header de la lista (back + título + 3-dots) se transforma en una barra de selección inline:
  ```
  ← Back   3 selected  · Trash · Move · Mark checked · Cancel
  ```
- [ ] Los checkboxes aparecen en el lado izquierdo de cada fila, a la derecha del drag handle oculto.
- [ ] No existe bottom bar en desktop: todas las acciones caben en el header.

##### 5.11.d Bulk actions

- [ ] **Delete**: borra las filas seleccionadas con un único `scheduleUndoable`:
  - `label`: "Deleted N items"
  - `restore`: reinserta las filas (snapshot del array completo)
  - `commit`: DELETE por IDs
  - Mantiene la misma UI de `UndoToast`.
- [ ] **Move to other list**: abre un bottom sheet / dialog con:
  - Lista de colectivos activos (`status = 'active'`, excluyendo la actual), cargadas via `loadLists` si hace falta.
  - Botón "+ Create new list" al final; crea una lista vacía y mueve.
  - Al confirmar: `UPDATE shopping_items SET list_id = $new WHERE id IN (...)`. Checked state se preserva.
  - Toast de confirmación "Moved N items to {list name}" (sin undo; el usuario puede volver a mover si quiere).
- [ ] **Mark selected as checked**: `UPDATE shopping_items SET is_checked = true, checked_by = auth.uid(), checked_at = now() WHERE id IN (...) AND is_checked = false`. Solo marca; no toggle. Si todos ya estaban checked, no-op silencioso.

##### 5.11.e Store + backend helpers

- [ ] `apps/web/src/lib/stores/lists.ts`:
  - `bulkDeleteItems(ids: string[])`
  - `bulkMoveItems(ids: string[], newListId: string)`
  - `bulkCheckItems(ids: string[], userId: string)`
  - Cada uno hace una sola llamada Supabase con `.in('id', ids)`.
- [ ] No se necesita migración: todas las ops usan las mismas RLS existentes.

##### 5.11.f Tests

- [ ] Vitest: unit tests de las tres funciones bulk en una suite nueva `rls-bulk-items.test.ts`:
  - miembro puede bulk-delete / move / check sobre items de su colectivo
  - guest no puede (RLS bloquea)
  - cross-collective: Borja no puede mover items de Eva a una lista de Borja
- [ ] Playwright mobile: `mobile-selection.test.ts`:
  - SEL-01 long-press → selection mode activo + fila preseleccionada
  - SEL-02 multi-select vía single-tap
  - SEL-03 bulk delete + undo toast
  - SEL-04 mark selected as checked
- [ ] Playwright desktop: `selection.test.ts`:
  - SEL-D-01 toggle del botón selection activa modo
  - SEL-D-02 move-to-other-list con dialog de destino

**Criterio de aceptación 5.11:**
- Long-press entra en selection mode, single-tap alterna selección.
- Botón "Select" en desktop (toggle).
- Bulk delete con single undo toast.
- Bulk move con dialog que muestra listas activas + crear.
- Bulk mark-checked solo sobre los seleccionados.

---

#### 5.12 Chrome de detail views (top bar oculto, sticky bars)

**Objetivo:** en las vistas de detalle (`/lists/[id]`, `/tasks/[id]`, `/notes/[id]`, más `/lists/[id]/session` que ya lo hace) el `MobileTopBar` global desaparece y se reemplaza por una barra contextual local con `back + title` pegada al top. El `BottomTabBar` permanece siempre visible.

**Cambios:**

- [ ] `apps/web/src/routes/(app)/+layout.svelte` — detectar si la ruta es una detail view y condicionalmente ocultar `<MobileTopBar>`:
  ```svelte
  {#if !isDetailRoute($page.url.pathname)}
      <MobileTopBar onMenu={() => (drawerOpen = true)} />
  {/if}
  ```
  con un helper:
  ```ts
  function isDetailRoute(path: string): boolean {
      return /^\/lists\/[0-9a-f-]+(\/session)?$/.test(path)
          || /^\/tasks\/[0-9a-f-]+$/.test(path)
          || /^\/notes\/[0-9a-f-]+$/.test(path);
  }
  ```
- [ ] Para cada detail view, su header local (back arrow + título) debe ser `sticky top-0 z-30 bg-surface/80 backdrop-blur-xl` para igualar el estilo del `MobileTopBar` que sustituye.
  - `/lists/[id]/+page.svelte` — actualmente el header está dentro del scroll; mover a sticky.
  - `/tasks/[id]/+page.svelte` — igual.
  - `/notes/[id]/+page.svelte` — igual (ya tiene glassmorphism implícito por el color de fondo).
- [ ] `BottomTabBar` sigue presente en todas las detail views salvo `/lists/[id]/session` (que ya se oculta explícitamente al ser un overlay full-screen con z-50).
- [ ] El título contextual se trunca con ellipsis si es largo (`truncate`).
- [ ] Regla general: cualquier barra que una vista renderice debe ser sticky (o fixed si replace-nav), nunca inline scrolling.

**Scope explícito de 5.12:** solo las rutas de detalle de listas/tareas/notas. `/collective/manage`, `/settings`, `/invitation/[token]`, `/notes/archive`, `/notes/trash` mantienen el chrome actual (fuera de este sprint).

**Tests:**

- [ ] Actualizar `mobile-shell.test.ts`:
  - M-01: en una detail view (e.g. `/lists/[id]`), el `MobileTopBar` NO está presente.
  - M-02: en la misma ruta, existe un header local con `back + title` sticky.
  - M-03: el `BottomTabBar` sigue visible.

**Criterio de aceptación 5.12:**
- En mobile, navegar a `/lists/seed-id`:
  - `MobileTopBar` (hamburger + collective + avatar) no aparece.
  - Un header local "← Weekly shop" pegado al top, sticky al hacer scroll.
  - `BottomTabBar` visible.
- En desktop, sin cambios (los detail headers actuales ya se ven arriba del main).

---

#### 5.14 Formulario de alta inline al final de la lista

**Cambio de UX:** el formulario de "añadir ítem" (lists) y "añadir tarea" (tasks) dejan de estar pegados al borde inferior del viewport y pasan a vivir **dentro del scroll**, como el último elemento de la sección sin marcar (unchecked / pending). Las sugerencias de frecuencia bajan **debajo** del input en vez de encima.

**Decisiones (respuestas del usuario a las preguntas 1–6):**
1. Posición del formulario → opción `a`: al final de *unchecked only*, justo encima de la sección Checked.
2. Scroll → inline puro. Sin FAB, sin auto-scroll.
3. Empty state → mensaje primero, formulario debajo (usuario puede añadir el primer ítem sin scrollear).
4. Sugerencias: input arriba, chips debajo. Mobile: scroll horizontal. Desktop: wrap (comportamiento actual de `ItemSuggestions`).
5. Aplica a `/lists/[id]` **y** `/tasks/[id]`.
6. Selection mode: el formulario se oculta.

**Cambios:**

- [x] `/lists/[id]/+page.svelte`:
  - Eliminado el bloque `absolute bottom-0` con `backdrop-blur`.
  - Nuevo snippet `addItemForm` renderizado (a) dentro de la rama empty-state y (b) entre las secciones unchecked y checked. Sólo se renderiza cuando `!selectionMode`.
  - `ItemSuggestions` movido dentro del snippet, debajo del input.
  - Padding inferior del scroll: `pb-32` → `pb-16 md:pb-6`.
  - `data-testid="add-item-form"` añadido como hook de tests futuros.
- [x] `/tasks/[id]/+page.svelte`:
  - Mismo patrón con `addTaskForm` (sin suggestions — `/tasks` no tiene `item_frequency`).
  - `data-testid="add-task-form"`.
  - Reposicionamiento sin cambiar el comportamiento de Enter / botón `+`.

**Tests:**

- Los tests existentes pasan sin cambios — los locators son `getByPlaceholder(/add item|añadir producto/i)` / `getByPlaceholder(/add task|añadir tarea/i)` y funcionan estén donde estén los inputs.
- Hooks `data-testid` añadidos por si se quieren assertions explícitos de ubicación más adelante.

**Criterio de aceptación:**
- Form no flota; scrollea con la lista.
- Sugerencias debajo del input en ambos breakpoints.
- En lista vacía: mensaje + form debajo.
- Selection mode: form desaparece (el action bar toma su lugar).

---

#### 5.15 Título limpio en detail views (sin eyebrow) + señal de estado

**Cambio de UX:** el "eyebrow" uppercase ("ACTIVE LIST" / "TASK MANAGER") que vivía sobre el título editable en `/lists/[id]` y `/tasks/[id]` se retira. El título sube para ocupar ese espacio.

Como la eyebrow anterior mezclaba "masthead label" + "status", el estado de la lista de la compra ahora se comunica de forma más directa:

| `list.status` | Señal visual |
|---------------|--------------|
| `active`      | sin chrome — solo el título |
| `completed`   | título con `text-text-secondary` + `line-through` |
| `archived`    | pill muted (`bg-slate-200` / slate-800 en dark) junto al título, `data-testid="list-status-pill"` |

`/tasks/[id]` no tiene concepto de estado en `task_lists`, así que ahí es simplemente el título sin eyebrow.

**Archivos tocados:** `apps/web/src/routes/(app)/lists/[id]/+page.svelte`, `apps/web/src/routes/(app)/tasks/[id]/+page.svelte`.

**Criterio de aceptación:** la detail view ya no muestra la eyebrow; una lista completada se ve con strikethrough en el título; una archivada muestra el pill muted.

---

#### 5.13 Resistencia a contextos no-seguros (HTTP LAN)

**Problema descubierto tras deploy de la preview mobile:** el usuario abrió `http://192.168.1.167:5173` en el móvil (origen no-seguro) y `handleAdd` lanzó `TypeError: crypto.randomUUID is not a function`. `crypto.randomUUID` solo está expuesto en HTTPS, localhost, 127.0.0.1 o file:// — fuera de eso queda `undefined`.

**Cambios:**

- [x] Nuevo helper `apps/web/src/lib/utils/id.ts` — `generateId()` usa `crypto.randomUUID()` cuando está disponible; cae en `crypto.getRandomValues()` + ensamblado RFC 4122 §4.4 cuando no (la API de bytes aleatorios SÍ está en contextos inseguros).
- [x] Sustituidos todos los call sites: `stores/lists.ts`, `stores/tasks.ts`, `sync/queue.ts`, `sync/undoQueue.ts`, `lists/[id]/+page.svelte`, `tasks/[id]/+page.svelte`.
- [x] Regla del repo (añadida a CLAUDE.md): nadie llama `crypto.randomUUID()` directamente — siempre `generateId()`.

**Tests:**

- [x] `src/lib/utils/id.test.ts` — U-01 devuelve UUID v4; U-02 fallback cuando `randomUUID` es `undefined`; U-03 50 IDs únicos bajo fallback; U-04 prefiere `randomUUID` cuando existe.
- [x] `tests/e2e/insecure-context.test.ts` — INS-01: `addInitScript` anula `randomUUID`, `loginAs`, añadir ítem, verificar que no crashea y el row aparece.

#### 5.Z Verificación final — todos los tests verdes

- [x] `just test-all` → **233 verdes, 2 skipped**:
  - 34 pgTAP (sin cambios)
  - 140 Vitest integration + 2 skipped presence (sin cambios)
  - 15 Vitest unit (was 11, +4 generateId)
  - 44 Playwright (was 43, +1 INS-01)
- [x] Inline add-form UX (5.14) verificado manualmente en DevTools mobile + desktop tras el refactor.
- [ ] Validación visual en DevTools iPhone 13 (390×844) + Pixel 7 (412×915) + Desktop (1280×800) — pendiente (manual, post-deploy).
- [x] `loginAs(USERS.borja)` con `page.setViewportSize(MOBILE)` activa el shell mobile automáticamente (verificado en M-01..M-04).
- [x] Seis commits incrementales (5.10.0 → 5.12 → 5.8 → 5.10 → 5.11) para facilitar revert o review por pasos.

---

### Plan de ejecución (orden sugerido)

1. **5.0 Tests primero** — escribir los mobile tests aunque inicialmente fallen.
2. **5.1 Shell responsive** — sin esto, nada más es testable en mobile.
3. **5.5 UndoQueue + UndoToast** — dependencia de 5.4.
4. **5.2 Masthead + padding** — trivial, desbloquea los tests M-05.
5. **5.3 / 5.4 / 5.6** — ajustes por pantalla.
6. **5.7** — pulido de tasks/notes/search con el shell.
7. **5.Z Verificación** — correr toda la suite, snapshot visual, commit.

### Criterio de aceptación

- En viewport ≤ 768px: sidebar oculta, bottom tab bar + top bar visibles, drawer abre con el switcher.
- Todas las pantallas tienen el par masthead (label uppercase + title big).
- Swipe-left en un item revela la zona roja; full-swipe borra con toast de undo.
- Los 211 tests funcionales siguen verdes. Los nuevos mobile tests (M-01..M-07) pasan.
- Validado manualmente en DevTools en 3 viewports.
