# Rediseño Bento del prototipo Pulso · plan de implementación

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** aplicar el estilo Bento Box Grid a las 14 pantallas del prototipo Pulso manteniendo los tokens del TDS, con el gráfico de Ventas nuevo, según la spec `docs/superpowers/specs/2026-09-22-pulso-bento-design.md`.

**Architecture:** una capa CSS nueva en `css/app.css` (rejilla `.bento`, tiles, anatomía, gráfico) y la migración de cada pantalla de markup inline a clases. Lo mecánico lo hace un script Python de la stdlib (`bentofy.py`) que sustituye la anatomía inline por clases, desenvuelve los grids viejos, envuelve las tarjetas en `.bento` con sus spans y genera el gráfico; lo específico de cada pantalla (tile alta de Salud, Canales ancha, listas de avisos, KPI anidados, Copiloto) se retoca a mano.

**Tech Stack:** HTML + CSS sin build, Python 3 stdlib para el script de migración, servidor `python3 -m http.server` (entrada `proto-pulso` de `.claude/launch.json`) y la pestaña del navegador para verificar.

## Global Constraints

- Sin git en este proyecto por decisión del usuario: no hay commits. Cada tarea termina con una verificación en el navegador y un punto de control (copia de seguridad en el scratchpad).
- Tokens del TDS intactos: ningún hex en las pantallas, solo `var(--…)`. Rubik 300 y 400 únicamente. Sin emoji. Etiquetas en versalitas.
- Sin sombras en TPV; jerarquía por `bg-canvas → bg-surface → bg-surface-raised → bg-surface-highlight` y borde.
- Rejilla de 6 columnas con `grid-auto-flow: dense`, gap `--space-16`, tile con padding `--space-16`, radio `--radius-xl`, alto mínimo 132 px.
- Inline permitido dentro de una tile solo para `--i`, anchos de barra (`width:45%`), colores de segmento de barra y de punto de leyenda (`background:var(--…)`), posición del tooltip y geometría de SVG. Todo lo demás, clase.
- Cifras: hero 44/52, tile 28/36, unidad 15/20, rótulo 14/16 con `letter-spacing: 1.4px`, nota 13/18, texto 14/20, cuerpo 16/22. Igual que el prototipo actual.
- Gráfico de Ventas: variante B (media móvil de 3 días con anclaje gradual en los tres últimos puntos), punto destacado en hoy, escala Y ajustada con 6 % de aire, rejilla en los lunes 4, 11, 18 y 25 de mayo.
- Verificación por pantalla a 390 px en `tpv-dark` y `light`, sin errores de consola y sin `class="card`.
- Idioma de textos, comentarios y documentación: español de España, tuteando.

---

### Tarea 1: capa Bento y gráfico en `css/app.css`

**Files:**
- Modify: `css/app.css` (variables `--app-*` en las líneas 10-32, `.content` en 96-100, `.chip` en 113-129)

**Interfaces:**
- Produces: las clases `.bento`, `.bento__heading`, `.bento__full`, `.tile`, `.tile--wide`, `.tile--third`, `.tile--tall`, `.tile--hero`, `.tile--plain`, `.tile__head`, `.tile__label`, `.tile__chevron`, `.tile__figure`, `.tile__figure--center`, `.tile__figure--m`, `.tile__unit`, `.tile__note` (+ `--ok`, `--warn`, `--bad`, `--light`, `--primary`, `--right`, `--center`), `.tile__text`, `.tile__body` (+ `--regular`), `.tile__row` (+ `--tight`), `.grow`, `.tile__divider` (+ `--push`), `.tile__bar`, `.tile__legend`, `.tile__dot`, `.tile__strong`, `.tile__kv`, `.tile__kv-label`, `.tile__kv-value`, `.tile__kv-track`, `.tile__kv-fill` (+ `--ink`), `.chart*`, y la variable `--app-tip-bg`. Las usan las tareas 2 a 6.

- [ ] **Paso 1: copia de seguridad de los originales**

```bash
SP="/private/tmp/claude-501/-Users-maguro-Ticksy-AIWork-Proto-Pulso/b5a4b1cd-aa06-47a7-b1cf-f3bcf839e99c/scratchpad"
mkdir -p "$SP/backup-pre-bento" && cp "/Users/maguro/Ticksy/AIWork/Proto Pulso/"*.html "/Users/maguro/Ticksy/AIWork/Proto Pulso/css/app.css" "$SP/backup-pre-bento/" && ls "$SP/backup-pre-bento" | wc -l
```

Esperado: `15`.

- [ ] **Paso 2: añadir `--app-tip-bg` a los dos bloques de variables**

En el bloque `:root, :root[data-theme="tpv-dark"]` añadir tras `--app-hero-shadow`:

```css
  --app-tip-bg: var(--bg-surface-highlight);          /* tooltip del gráfico */
```

En el bloque `:root[data-theme="light"], :root[data-theme="brand"]` añadir tras `--app-hero-shadow`:

```css
  --app-tip-bg: var(--bg-surface);
```

`--app-hero-shadow` se conserva hasta la tarea 8, porque `index.html` lo usa hasta que se migra.

- [ ] **Paso 3: `.content` con gap 16 y `.chip` a 44 px**

Cambiar `.content` a:

```css
.content {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  padding: var(--space-16);
  flex-grow: 1;
}
```

En `.chip`, cambiar `height: 36px;` por `height: 44px;`.

- [ ] **Paso 4: pegar la capa Bento**

Añadir al final de `css/app.css`, antes del bloque `.ds`, el bloque de la spec §5.2 completo (desde `/* ---- Rejilla Bento` hasta el cierre de `@keyframes tile-in`), sin las dos líneas de variables ni la de `.chip` que ya están hechas, y a continuación estas clases auxiliares que la spec permite añadir:

```css
/* Auxiliares de la migración */
.bento__full { grid-column: span 6; }                       /* bloque no-tile dentro de la rejilla */
.grow { flex-grow: 1; }
.tile--plain { background: transparent; border-color: transparent; padding: 0 var(--space-4); min-height: 0; }
.tile__figure--center { align-items: center; }
.tile__note--primary { color: var(--text-primary); }
.tile__note--right { text-align: right; }
.tile__note--center { text-align: center; }
.tile__body--regular { font-weight: 400; }
```

- [ ] **Paso 5: pegar el gráfico**

Añadir, detrás del bloque anterior, el CSS de la spec §6.2 completo (`.chart` … `.chart__tip-value`).

- [ ] **Paso 6: verificar que nada se rompe y que las clases existen**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && for c in '\.bento ' '\.tile ' '\.tile--hero' '\.tile__kv-label' '\.chart__tip ' '\.bento__full' '--app-tip-bg'; do printf '%s: ' "$c"; grep -cE "$c" css/app.css; done && grep -c 'height: 44px' css/app.css
```

Esperado: cada clase `1` (o `2` para `--app-tip-bg`), y `1` para el alto de 44.

Abrir `http://localhost:8765/index.html` en el navegador a 390 px: se ve igual que antes salvo los chips más altos y el gap de 16 entre bloques. Sin errores de consola.

---

### Tarea 2: script de migración `bentofy.py`

**Files:**
- Create: `<scratchpad>/tools/bentofy.py`

**Interfaces:**
- Consumes: las clases de la tarea 1.
- Produces: `python3 bentofy.py <pantalla.html>` imprime un informe de estilos inline residuales y no toca nada; con `--write` reescribe la pantalla. Lo usan las tareas 3, 4 y 5.

- [ ] **Paso 1: crear el script**

```python
#!/usr/bin/env python3
"""bentofy.py · migra una pantalla del prototipo Pulso a la capa Bento.

Uso:
  python3 bentofy.py <ruta/pantalla.html>           informe, sin escribir
  python3 bentofy.py <ruta/pantalla.html> --write   reescribe la pantalla

Hace, en este orden:
  1. desenvuelve los <div style="display:grid;…"> que agrupaban tarjetas;
  2. convierte cada <a|div class="card…"> en <… class="tile [span]" style="--i:N">
     con el span de SPANS[pantalla] en orden DOM;
  3. sustituye la anatomía inline repetida por clases (SUBS);
  4. resuelve los contenedores ambiguos (cifra con flecha frente a leyenda);
  5. envuelve el tramo de tiles de <main> en <div class="bento"> y marca con
     bento__full los hermanos que no son tiles;
  6. si hay sparkline de 326x72, la cambia por el gráfico nuevo (variante B).
Al final lista los style="…" que quedan, con su recuento, para el retoque manual.
"""
import re, sys, pathlib

# ---- spans por pantalla, en orden DOM de las tarjetas ------------------------
SPANS = {
    "index.html":      ["hero", "tall", "", "wide", "", "", "", "", ""],
    "ventas.html":     ["hero", "third", "third", "third", "wide", "wide", "wide"],
    "salud.html":      ["hero", "wide"],
    "margenes.html":   ["hero", "wide", "wide", "wide"],
    "canales.html":    ["hero", "wide", "wide"],
    "control.html":    ["hero", "", "", "", "", "wide", "wide"],
    "prevision.html":  ["hero", "wide"],
    "carta.html":      ["hero", "wide", "wide"],
    "clientes.html":   ["hero", "", "", "wide", "wide", "wide", "wide"],
    "ritmo.html":      ["hero", "wide", "wide", "wide"],
    "aviso.html":      ["hero", "wide"],
    "decisiones.html": ["hero", "", "", "wide"],
    "costes.html":     ["wide", "wide", "wide", "wide", "wide"],
}

VOID = {"br", "img", "input", "path", "circle", "polyline", "polygon", "line", "meta", "link", "stop", "hr"}
TAG = re.compile(r"<(/?)([a-zA-Z][\w-]*)([^>]*?)(/?)>")

def find_close(html, open_start):
    """Índice justo después del cierre del elemento que abre en open_start."""
    depth = 0
    for m in TAG.finditer(html, open_start):
        closing, tag, _, selfclose = m.group(1), m.group(2).lower(), m.group(3), m.group(4)
        if tag in VOID or selfclose:
            continue
        depth += -1 if closing else 1
        if depth == 0:
            return m.end()
    raise ValueError("cierre no encontrado")

def children(html, start, end):
    """Rangos (ini, fin) de los hijos directos del elemento cuyo contenido va de start a end."""
    out, pos = [], start
    while True:
        m = TAG.search(html, pos, end)
        if not m or m.group(1):
            break
        tag = m.group(2).lower()
        if tag in VOID or m.group(4):
            pos = m.end(); continue
        close = find_close(html, m.start())
        out.append((m.start(), close)); pos = close
    return out

# ---- anatomía inline → clases (orden importa) ------------------------------
HEAD_INNER = '<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;">'
SUBS = [
    (HEAD_INNER + HEAD_INNER, '<div class="tile__head">'),
    ('<p style="font-size:14px;line-height:16px;font-weight:400;color:var(--text-accent);margin:0;letter-spacing:1.4px;text-transform:uppercase;">', '<p class="tile__label">'),
    ('</div><div style="flex-grow:1;"></div><span class="ds" data-ds="Icon" data-props=\'{"name": "nav_forward", "size": 20}\' style="color:var(--text-subtle);"></span></div>',
     '<span class="tile__chevron ds" data-ds="Icon" data-props=\'{"name": "nav_forward", "size": 20}\'></span></div>'),
    ('</div><div style="flex-grow:1;"></div>', '<span class="grow"></span>'),
    (HEAD_INNER, '<div class="tile__kv">'),
    ('<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:14px;flex-wrap:wrap;">', '<div class="tile__row tile__row--tight">'),
    ('<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:16px;">', '<div class="tile__row">'),
    ('<div style="display:flex;align-items:baseline;gap:6px;">', '<div class="tile__figure">'),
    ('<span style="font-size:44px;line-height:52px;font-weight:300;color:var(--text-primary);margin:0;">', '<span>'),
    ('<span style="font-size:33px;line-height:40px;font-weight:300;color:var(--text-primary);margin:0;">', '<span class="tile__figure--m">'),
    ('<span style="font-size:28px;line-height:36px;font-weight:300;color:var(--text-primary);margin:0;">', '<span>'),
    ('<span style="font-size:15px;line-height:20px;font-weight:300;color:var(--app-text-support);margin:0;">', '<span class="tile__unit">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;width:96px;flex-shrink:0;">', '<p class="tile__kv-label">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;width:56px;text-align:right;flex-shrink:0;color:var(--text-primary);">', '<p class="tile__kv-value">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-ok-text);margin:0;">', '<p class="tile__note tile__note--ok">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-warn-text);margin:0;">', '<p class="tile__note tile__note--warn">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-bad-text);margin:0;">', '<p class="tile__note tile__note--bad">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-warn-text);margin:0;text-align:right;">', '<p class="tile__note tile__note--warn tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-bad-text);margin:0;text-align:right;">', '<p class="tile__note tile__note--bad tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:300;color:var(--app-text-support);margin:0;text-align:right;">', '<p class="tile__note tile__note--light tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;color:var(--text-primary);">', '<p class="tile__note tile__note--primary">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:300;color:var(--app-text-support);margin:0;text-align:center;">', '<p class="tile__note tile__note--light tile__note--center">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:300;color:var(--app-text-support);margin:0;">', '<p class="tile__note tile__note--light">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;">', '<p class="tile__note">'),
    ('<p style="font-size:16px;line-height:22px;font-weight:400;color:var(--text-primary);margin:0;">', '<p class="tile__body tile__body--regular">'),
    ('<p style="font-size:16px;line-height:22px;font-weight:300;color:var(--text-primary);margin:0;">', '<p class="tile__body">'),
    ('<div style="display:flex;flex-direction:row;height:8px;border-radius:6px;overflow:hidden;background:var(--border-subtle);">', '<div class="tile__bar">'),
    ('<div style="height:1px;background:var(--border-subtle);"></div>', '<div class="tile__divider"></div>'),
    ('<span style="color:var(--text-primary);font-weight:400;">', '<span class="tile__strong">'),
]
# icono de cabecera con el color inline del acento: el color lo hereda de .tile__head
ICON_ACCENT = re.compile(r'(<span class="ds" data-ds="Icon" data-props=\'[^\']*\') style="color:var\(--text-accent\);"></span> ?')
# segmento de barra: quitar el alto, dejar ancho y color
BAR_SEG = re.compile(r'<div style="width:([0-9.]+%);height:8px;background:(var\(--[a-z-]+\));"></div>')
# punto de leyenda
DOT = re.compile(r'<div style="width:10px;height:10px;border-radius:5px;background:(var\(--[a-z-]+\));"></div>')
CARD = re.compile(r'<(a|div) class="card(?: card--link)?"((?: href="[^"]+")?) style="[^"]*">')
GRID = re.compile(r'<div style="display:grid;grid-template-columns:repeat\(\d, minmax\(0, 1fr\)\);gap:12px;">')
AMBIG = '<div style="display:flex;align-items:center;gap:6px;">'

def unwrap_grids(html):
    while True:
        m = GRID.search(html)
        if not m:
            return html
        close = find_close(html, m.start())
        inner = html[m.end():close - len("</div>")]
        html = html[:m.start()] + inner + html[close:]

def convert_cards(html, spans):
    n = 0
    def rep(m):
        nonlocal n
        span = spans[n] if n < len(spans) else ""
        cls = "tile" + (f" tile--{span}" if span else "")
        out = f'<{m.group(1)} class="{cls}"{m.group(2)} style="--i:{n};">'
        n += 1
        return out
    html = CARD.sub(rep, html)
    return html, n

def resolve_ambiguous(html):
    pos = 0
    while True:
        i = html.find(AMBIG, pos)
        if i < 0:
            return html
        close = find_close(html, i)
        inner = html[i + len(AMBIG):close]
        big = re.search(r"font-size:(44|33|28)px", inner)
        new = '<div class="tile__figure tile__figure--center">' if big else '<div class="tile__legend">'
        html = html[:i] + new + html[i + len(AMBIG):]
        pos = i + len(new)

def wrap_bento(html):
    m_open = re.search(r'<main class="content"[^>]*>', html)
    m_close = html.find("</main>", m_open.end())
    kids = children(html, m_open.end(), m_close)
    tiles = [k for k in kids if re.match(r'<(a|div) class="tile', html[k[0]:k[0] + 40])]
    if not tiles:
        return html
    first, last = tiles[0][0], tiles[-1][1]
    out, pos = [], first
    for k in kids:
        if first <= k[0] < last and k not in tiles:
            seg = html[k[0]:k[1]]
            seg = re.sub(r'^<(\w+) class="', r'<\1 class="bento__full ', seg, count=1) if re.match(r'<\w+ class="', seg) else re.sub(r'^<(\w+)', r'<\1 class="bento__full"', seg, count=1)
            out.append(html[pos:k[0]] + seg); pos = k[1]
    out.append(html[pos:last])
    return html[:first] + '<div class="bento">' + "".join(out) + '</div>' + html[last:]

def clean_main_gap(html):
    return html.replace('<main class="content" style="gap:12px;">', '<main class="content">')

# ---- gráfico de Ventas (variante B), portado de gen-chart.py ------------------
W, H, PAD_T, PAD_B, PLOT_R = 326.0, 120.0, 10.0, 10.0, 318.0
TICKS = [(6, "4 may"), (13, "11 may"), (20, "18 may"), (27, "25 may")]

def smooth(vals, win=3):
    half = win // 2
    out = [sum(vals[max(0, i - half):i + half + 1]) / len(vals[max(0, i - half):i + half + 1]) for i in range(len(vals))]
    for k in range(min(3, len(vals))):
        i = len(vals) - 1 - k; w = 1 - k / 3
        out[i] = w * vals[i] + (1 - w) * out[i]
    return out

def monotone_path(xs, ys):
    k = len(xs)
    d = [(ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]) for i in range(k - 1)]
    m = [0.0] * k; m[0], m[-1] = d[0], d[-1]
    for i in range(1, k - 1):
        if d[i - 1] * d[i] <= 0: m[i] = 0.0
        else:
            h0, h1 = xs[i] - xs[i - 1], xs[i + 1] - xs[i]
            m[i] = 3 * (h0 + h1) / ((2 * h1 + h0) / d[i - 1] + (h1 + 2 * h0) / d[i])
    parts = [f"M{xs[0]},{ys[0]}"]
    for i in range(k - 1):
        h = xs[i + 1] - xs[i]
        parts.append(f"C{xs[i] + h / 3:.1f},{ys[i] + m[i] * h / 3:.1f} {xs[i + 1] - h / 3:.1f},{ys[i + 1] - m[i + 1] * h / 3:.1f} {xs[i + 1]},{ys[i + 1]}")
    return " ".join(parts)

def chart_block(svg_old, gid="grad-ventas"):
    polys = re.findall(r'<polyline points="([^"]+)"', svg_old)
    pts = lambda s: [float(p.split(",")[1]) for p in s.split()]
    expected, real = pts(polys[0]), pts(polys[1]); n = len(real)
    xs = [round(i / (n - 1) * PLOT_R, 1) for i in range(n)]
    rs, es = smooth(real), expected
    lo, hi = min(rs + es), max(rs + es); pad = (hi - lo) * 0.06
    fit = lambda v: round(PAD_T + (v - (lo - pad)) / ((hi + pad) - (lo - pad)) * (H - PAD_T - PAD_B), 1)
    ry, ey = [fit(v) for v in rs], [fit(v) for v in es]
    line = monotone_path(xs, ry); area = f"{line} L{xs[-1]},{H:g} L{xs[0]},{H:g} Z"; exp = monotone_path(xs, ey)
    grid = "".join(f'<line class="chart__grid" x1="{xs[i]}" y1="0" x2="{xs[i]}" y2="{H:g}"/>' for i, _ in TICKS)
    labels = "".join(f'<span style="left:{xs[i] / W * 100:.1f}%;">{t}</span>' for i, t in TICKS)
    dx, dy = xs[-1], ry[-1]
    svg = (f'<svg viewBox="0 0 {W:g} {H:g}" preserveAspectRatio="none" aria-label="Ventas de los últimos 30 días frente a lo esperado">'
           f'<defs><linearGradient id="{gid}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" class="chart__grad-a"/><stop offset="1" class="chart__grad-b"/></linearGradient></defs>'
           f'{grid}<path d="{area}" fill="url(#{gid})"/><path class="chart__expected" d="{exp}"/><path class="chart__line" d="{line}"/>'
           f'<circle class="chart__halo" cx="{dx}" cy="{dy}" r="9"/><circle class="chart__dot" cx="{dx}" cy="{dy}" r="4"/></svg>')
    tip = (f'<div class="chart__tip" style="right:calc(100% - {dx / W * 100:.1f}% + 16px);top:{max(0, dy - 27):.0f}px;">'
           f'<span class="chart__tip-date">Hoy · 27 may</span><span class="chart__tip-value">2.957 €</span></div>')
    legend = ('<div class="tile__row tile__row--tight">'
              '<span class="tile__legend"><span class="tile__dot" style="background:var(--app-chart-accent);"></span>venta real · 30 días</span>'
              '<span class="tile__legend"><span class="tile__dot" style="background:var(--text-secondary);"></span>esperado</span></div>')
    return f'<div class="chart">{svg}{tip}<div class="chart__axis">{labels}</div></div>{legend}'

def replace_chart(html):
    m = re.search(r'<svg viewBox="0 0 326 72"[^>]*>.*?</svg>', html, re.S)
    if not m:
        return html
    # la leyenda vieja es el siguiente hermano: la fila con gap 14 ya convertida a tile__row--tight
    end = m.end()
    if html.startswith('<div class="tile__row tile__row--tight">', end):
        end = find_close(html, end)
    return html[:m.start()] + chart_block(m.group(0)) + html[end:]

def migrate(path):
    html = path.read_text(encoding="utf-8")
    html = unwrap_grids(html)
    html, n = convert_cards(html, SPANS.get(path.name, []))
    html = resolve_ambiguous(html)
    for a, b in SUBS:
        html = html.replace(a, b)
    html = ICON_ACCENT.sub(r"\1></span>", html)
    html = BAR_SEG.sub(r'<div style="width:\1;background:\2;"></div>', html)
    html = DOT.sub(r'<span class="tile__dot" style="background:\1;"></span>', html)
    html = clean_main_gap(html)
    html = wrap_bento(html)
    html = replace_chart(html)
    return html, n

def report(html):
    styles = re.findall(r'style="([^"]*)"', re.search(r"<body>(.*)</body>", html, re.S).group(1))
    keep = re.compile(r"^(--i:\d+;|width:[0-9.]+%;background:var\(--[a-z-]+\);|background:var\(--[a-z-]+\);|right:calc[^;]+;top:[0-9-]+px;|left:[0-9.]+%;|display:block;width:100%;)$")
    counts = {}
    for s in styles:
        if not keep.match(s):
            counts[s] = counts.get(s, 0) + 1
    return sorted(counts.items(), key=lambda kv: -kv[1])

if __name__ == "__main__":
    path = pathlib.Path(sys.argv[1]); write = "--write" in sys.argv
    html, n = migrate(path)
    print(f"{path.name}: {n} tarjetas → tiles · class=\"card queda {html.count('class=\"card')} veces")
    for s, c in report(html):
        print(f"  {c:3d} × style=\"{s[:140]}\"")
    if write:
        path.write_text(html, encoding="utf-8"); print("escrito")
```

- [ ] **Paso 2: probar en seco con el Dashboard**

```bash
SP="/private/tmp/claude-501/-Users-maguro-Ticksy-AIWork-Proto-Pulso/b5a4b1cd-aa06-47a7-b1cf-f3bcf839e99c/scratchpad"
python3 "$SP/tools/bentofy.py" "/Users/maguro/Ticksy/AIWork/Proto Pulso/index.html"
```

Esperado: `index.html: 9 tarjetas → tiles · class="card queda 0 veces` y una lista corta de estilos residuales (barra superior, línea de contexto, insignia de la campana, flecha de Márgenes). Si aparece anatomía de tile repetida (cifras, notas, filas), añadir su cadena exacta a `SUBS` con la clase que corresponda y repetir hasta que solo queden los estilos no repetidos.

- [ ] **Paso 3: probar en seco con las otras 13 pantallas**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && for f in ventas salud margenes canales control prevision carta clientes ritmo aviso decisiones costes copiloto; do python3 "$SP/tools/bentofy.py" "$f.html" | head -12; done
```

Esperado: cada pantalla con su número de tarjetas igual a la longitud de su lista en `SPANS` (copiloto: 0). Ampliar `SUBS` con las cadenas repetidas que aparezcan (tracks de barra de las filas de dato, textos de 12/16 de tablas, etc.). Regla: si una cadena aparece 3 o más veces en el conjunto, va a `SUBS` con una clase de la tarea 1 o una nueva que se añade a `css/app.css` en ese momento, documentada con un comentario.

---

### Tarea 3: Dashboard (`index.html`)

**Files:**
- Modify: `index.html`
- Uses: `bentofy.py` de la tarea 2

**Interfaces:**
- Produces: `index.html` como patrón de referencia de tile para el resto de tareas.

- [ ] **Paso 1: migrar con el script**

```bash
python3 "$SP/tools/bentofy.py" "/Users/maguro/Ticksy/AIWork/Proto Pulso/index.html" --write
```

Esperado: `9 tarjetas → tiles`, `escrito`.

- [ ] **Paso 2: contenido de la tile alta de Salud**

Sustituir el cuerpo de la tile `salud.html` (tras su `tile__head`) por:

```html
<div class="tile__figure"><span>3</span><span class="tile__unit">avisos</span></div>
<div><span class="ds" data-ds="Badge" data-props='{"label": "1 URGENTE", "tone": "danger"}'></span></div>
<div class="tile__divider"></div>
<p class="tile__text">Vendiste más y ganaste menos</p>
<p class="tile__note">371 € de comisión · te quedaste 71 cts de cada euro, tu normal son 75</p>
<div class="tile__divider tile__divider--push"></div>
<p class="tile__note tile__note--light">2 avisos más · 2 para saber</p>
```

- [ ] **Paso 3: contenido de la tile ancha de Canales**

Sustituir el cuerpo de la tile `canales.html` (tras su `tile__head`) por:

```html
<div class="tile__row">
  <div class="tile__figure"><span>43 %</span><span class="tile__unit">por marketplace</span></div>
  <span class="grow"></span>
  <div class="tile__note--right">
    <p class="tile__note">mejor canal</p>
    <p class="tile__body tile__body--regular">Webapp</p>
  </div>
</div>
<div class="tile__bar" aria-hidden="true">
  <div style="width:45%;background:var(--channel-web);"></div>
  <div style="width:43%;background:var(--channel-uber-eats);"></div>
  <div style="width:12%;background:var(--app-chart-accent);"></div>
</div>
<div class="tile__row tile__row--tight">
  <span class="tile__legend"><span class="tile__dot" style="background:var(--channel-web);"></span>canal propio <span class="tile__strong">45 %</span></span>
  <span class="tile__legend"><span class="tile__dot" style="background:var(--channel-uber-eats);"></span>marketplace <span class="tile__strong">43 %</span></span>
  <span class="tile__legend"><span class="tile__dot" style="background:var(--app-chart-accent);"></span>directo <span class="tile__strong">12 %</span></span>
</div>
```

- [ ] **Paso 4: quitar el inline que quede en la hero**

La hero tenía `border-style:none;box-shadow:var(--app-hero-shadow);` en su `style`: el script ya lo cambió por `--i:0`. Comprobar que la línea de contexto y la barra superior conservan su inline (no son tiles; se dejan como están).

- [ ] **Paso 5: verificar**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && grep -c 'class="card' index.html; grep -c 'class="bento"' index.html; grep -o 'tile--tall\|tile--wide\|tile--hero' index.html | sort | uniq -c
```

Esperado: `0`, `1`, y `1 tile--hero`, `1 tile--tall`, `1 tile--wide`.

En el navegador, `http://localhost:8765/index.html` a 390 px en `tpv-dark` y `light`: el mapa coincide con la spec §7 y con el mockup (Salud alta con Control debajo de Márgenes, Canales ancha, gráfico nuevo con tooltip "Hoy · 27 may · 2.957 €"). Sin errores de consola. Copia de control: `cp index.html "$SP/backup-pre-bento/index.bento-ok.html"`.

---

### Tarea 4: secciones de nivel 1 (9 pantallas)

**Files:**
- Modify: `ventas.html`, `salud.html`, `margenes.html`, `canales.html`, `control.html`, `prevision.html`, `carta.html`, `clientes.html`, `ritmo.html`

**Interfaces:**
- Consumes: `bentofy.py`, clases de la tarea 1, patrón de `index.html`.

- [ ] **Paso 1: migrar las nueve con el script**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && for f in ventas salud margenes canales control prevision carta clientes ritmo; do python3 "$SP/tools/bentofy.py" "$f.html" --write | head -1; done
```

Esperado: una línea por pantalla con `class="card queda 0 veces` y `escrito`.

- [ ] **Paso 2: `ventas.html`**

Comprobar que la hero lleva el gráfico nuevo (el script lo sustituye porque tiene la sparkline de 326 × 72) y que la fila de KPI son tres `tile tile--third` con `tile__label` arriba ("PEDIDOS", "TICKET", "NETO"). La tarjeta "Por franja" usa `tile__kv` con `tile__kv-label`, track y `tile__kv-value`: si el track sigue inline, sustituir su apertura por `<div class="tile__kv-track"><div class="tile__kv-fill" style="width:NN%;"></div></div>` conservando el porcentaje, y `tile__kv-fill--ink` donde el relleno era `--app-chart-ink`.

- [ ] **Paso 3: `salud.html`**

El bloque de avisos es un `<div class="bento__full" style="display:flex;flex-direction:column;gap:8px;">` con los rótulos "Avisos" y "Para saber" y cinco elementos. Sustituirlo por hijos directos de `.bento`:

```html
<p class="bento__heading">Avisos</p>
<a class="tile tile--wide" href="aviso.html" style="--i:2;">
  <p class="tile__text">Vendiste más y ganaste menos</p>
  <p class="tile__note">El 43 % del día salió por marketplace y pagaste 371 € de comisión. Te quedaste 71 cts de cada euro; tu normal son 75.</p>
</a>
<a class="tile tile--wide" href="aviso.html" style="--i:3;">
  <p class="tile__text">Repercutes mal la comisión de Glovo y Uber</p>
  <p class="tile__note">Subes los precios un 28 %, pero con un 30 % de comisión necesitas un 43 %. Se te escapan 16.123 € al año.</p>
</a>
<a class="tile tile--wide" href="aviso.html" style="--i:4;">
  <p class="tile__text">La cocina cierra 21 min antes que la caja</p>
  <p class="tile__note">Última comanda a las 21:12, último cobro a las 21:33. Esa ventana existe y quizá no la sabías.</p>
</a>
<p class="bento__heading">Para saber</p>
<div class="tile tile--wide" style="--i:5;">
  <p class="tile__text">Temporal el viernes: vendes un 12 % menos</p>
  <p class="tile__note">Con 3 días de aviso. No evitas el día; evitas comprar de más y poner un turno de más.</p>
</div>
<div class="tile tile--wide" style="--i:6;">
  <p class="tile__text">3 reseñas de Google sin responder</p>
  <p class="tile__note">Una de ellas de 2 estrellas. Conecta Google para responder desde aquí.</p>
</div>
```

Conservar los Badges o iconos que cada aviso tuviera dentro (mantener su `data-ds` dentro de la tile, antes del `tile__text`). Renumerar `--i` de la tile "Hiciste, ganaste" a `7`. Si un aviso enlazaba a otra pantalla distinta de `aviso.html`, respetar su `href`.

- [ ] **Paso 4: `prevision.html`**

Igual que Salud: el bloque `bento__full` "Avisos de la semana" pasa a `<p class="bento__heading">Avisos de la semana</p>` seguido de una `<div class="tile tile--wide">` por aviso con `tile__text` (título) y `tile__note` (explicación), `--i` consecutivos, y "En el radar" renumerada.

- [ ] **Paso 5: `clientes.html`**

La tarjeta "Google" contiene anidadas las de 4,3, +14 y 3 sin responder. Aplanar: cerrar la hero justo después de su `tile__head` y su Badge, y sacar las tres tiles como hermanas dentro de `.bento`:

- 4,3 → `<div class="tile" style="--i:1;">` con `tile__label` "Nota media", `tile__figure` 4,3 y su nota.
- +14 → `<div class="tile" style="--i:2;">` con `tile__label` "Reseñas este mes", `tile__figure` +14 y su nota.
- 3 sin responder → `<div class="tile tile--wide" style="--i:3;">` con `tile__label` "Sin responder", `tile__figure` 3, `tile__note tile__note--warn` con el párrafo y el enlace o Button "Responder" tal como estaba.

Renumerar `--i` del resto (4, 5, 6). El `<span class="ds" style="display:block;width:100%;">` final (Button del bundle) queda fuera de `.bento`, después de ella, como está.

- [ ] **Paso 6: `margenes.html`, `canales.html`, `control.html`, `carta.html`, `ritmo.html`**

Revisar el informe del script de cada una. Filas de dato con track inline → `tile__kv-track` / `tile__kv-fill` como en Ventas. En `control.html` los cuatro KPI son `tile` (mitad) en dos filas: comprobar que cada uno lleva `tile__label` arriba. En `canales.html` la hero conserva su contenido (43 %, "tu normal 42 % · máximo 52 %", barra y leyenda canal propio / marketplace / directo, párrafo en `tile__note`).

- [ ] **Paso 7: verificar las nueve**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && for f in ventas salud margenes canales control prevision carta clientes ritmo; do printf '%s card=%s bento=%s hero=%s\n' "$f" "$(grep -c 'class="card' $f.html)" "$(grep -c 'class="bento"' $f.html)" "$(grep -c 'tile--hero' $f.html)"; done
```

Esperado: `card=0 bento=1 hero=1` en las nueve.

Navegador, cada pantalla a 390 px en los dos temas: hero arriba, spans según la spec §8, sin solapes ni textos cortados, sin errores de consola. Copia de control de las nueve en `$SP/backup-pre-bento/*.bento-ok.html`.

---

### Tarea 5: nivel 2 (`aviso.html`, `decisiones.html`, `costes.html`)

**Files:**
- Modify: `aviso.html`, `decisiones.html`, `costes.html`

- [ ] **Paso 1: migrar con el script**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && for f in aviso decisiones costes; do python3 "$SP/tools/bentofy.py" "$f.html" --write | head -1; done
```

- [ ] **Paso 2: `aviso.html`**

El bloque final de acciones ("Hecho, mídelo", "Ahora no" y su nota) queda marcado `bento__full` dentro de la rejilla: mantenerlo así (ocupa las 6 columnas) o, si el script lo dejó fuera, dejarlo fuera. Los párrafos sueltos entre tarjetas, si los hay, van en `<div class="tile tile--wide tile--plain"><p class="tile__body">…</p></div>`.

- [ ] **Paso 3: `decisiones.html`**

La hero "12 jun · subiste el precio de marketplace" contiene anidadas las tiles +7,4 % y +1.240 €. Aplanar como en Clientes: la hero conserva cabecera, Badge y el SVG antes/después tal cual; las dos cifras salen como `<div class="tile" style="--i:1;">` y `--i:2` con `tile__label`, `tile__figure` y su nota; "Tu registro de decisiones" pasa a `--i:3`.

- [ ] **Paso 4: `costes.html`**

Cinco tiles anchas. Las dos tarjetas de entrada (Personal 14.000 €, Food cost 30 %) conservan sus bandas y sus tres cifras; si sus barras siguen inline, usar `tile__bar` con segmentos `width:NN%`. El Toggle y el Button del bundle se conservan con su `data-ds`.

- [ ] **Paso 5: verificar las tres**

Mismo comando de la tarea 4 con `aviso decisiones costes`; esperado `card=0 bento=1` y `hero=1` en aviso y decisiones, `hero=0` en costes. Navegador en los dos temas, sin errores.

---

### Tarea 6: Copiloto (`copiloto.html`)

**Files:**
- Modify: `copiloto.html`, `css/app.css` (bloque nuevo al final)

- [ ] **Paso 1: CSS del override**

Añadir al final de `css/app.css`, antes del bloque `.ds`:

```css
/* ---- Copiloto · override AI-Native ------------------------------------- */
.chat { display: flex; flex-direction: column; gap: var(--space-12); flex-grow: 1; }
.msg { max-width: 88%; padding: var(--space-12) var(--space-16); font-size: 16px; line-height: 22px; font-weight: 300; color: var(--text-primary); border-radius: var(--radius-lg); }
.msg p { margin: 0; }
.msg--user { align-self: flex-end; background: var(--action-secondary); border-bottom-right-radius: var(--radius-sm); }
.msg--bot { align-self: flex-start; background: var(--bg-surface); border: 1px solid var(--border-default); border-bottom-left-radius: var(--radius-sm); display: flex; flex-direction: column; gap: var(--space-8); }
.cite { padding: var(--space-8) var(--space-12); background: var(--bg-surface-raised); border-left: 2px solid var(--text-accent); border-radius: var(--radius-sm); }
.cite__source { margin: 0; font-size: 11px; line-height: 14px; font-weight: 400; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-accent); }
.cite__body { margin: 0; font-size: 13px; line-height: 18px; font-weight: 300; color: var(--app-text-support); }
.attachment { display: inline-flex; align-items: center; gap: var(--space-8); padding: var(--space-8) var(--space-12); border-radius: var(--radius-sm); background: var(--bg-surface); border: 1px solid var(--border-default); color: var(--text-primary); font-size: 13px; line-height: 18px; }
.suggestions { display: flex; gap: var(--space-8); overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.suggestions::-webkit-scrollbar { display: none; }
.suggestions .chip { flex-grow: 0; flex-shrink: 0; white-space: nowrap; text-transform: none; letter-spacing: 0; font-size: 14px; }
.composer { position: sticky; bottom: calc(64px + env(safe-area-inset-bottom, 0px)); z-index: 2; display: flex; align-items: center; gap: var(--space-8); padding: var(--space-8) var(--space-16); background: var(--bg-surface); border-top: 1px solid var(--border-default); }
.composer__input { flex-grow: 1; height: 44px; padding: 0 var(--space-16); border-radius: 22px; background: var(--bg-canvas); border: 1px solid var(--border-default); color: var(--text-primary); font: 300 16px/22px var(--font-sans); }
.composer__input::placeholder { color: var(--text-secondary); }
.composer__input:focus-visible { outline: 2px solid var(--action-primary); outline-offset: 1px; }
.composer__btn { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border: 0; border-radius: 22px; background: transparent; color: var(--text-primary); cursor: pointer; }
.composer__btn--send { background: var(--action-primary); color: var(--text-on-accent); }
.composer__btn:focus-visible { outline: 2px solid var(--action-primary); outline-offset: 2px; }
```

- [ ] **Paso 2: reestructurar el HTML**

Dentro de `<main class="content">`:

- El primer hijo (columna del hilo) pasa a `<div class="chat">`. Dentro: la marca de hora en `<p class="tile__note tile__note--light tile__note--center">Hoy · 8:04</p>`; la pregunta del usuario en `<div class="msg msg--user"><p>¿Por qué ayer vendí más y gané menos?</p></div>`; la respuesta en `<div class="msg msg--bot">` con sus párrafos (`<p>` con las cifras en `<span class="tile__strong">`) y, dentro, cada tarjeta de cita como `<div class="cite"><p class="cite__source">Ticksy · 155 pedidos del 27 may · comisiones que declaraste</p><p class="cite__body">…</p></div>`; el chip del PDF como `<div class="attachment"><span class="ds" data-ds="Icon" data-props='{"name": "menu_bar_invoice", "size": 20}'></span><span>Liquidación Glovo · mayo.pdf</span><span class="tile__note tile__note--light">subido hace un momento</span></div>` dentro del mensaje del usuario al que acompañaba.
- El segundo hijo (fila de sugerencias) pasa a `<nav class="suggestions" aria-label="Sugerencias">` con sus tres `<a class="chip" href="#">`.
- El aviso "El copiloto solo habla de lo ya calculado…" pasa al final de `.chat` como `<p class="tile__note tile__note--light tile__note--center">`.

Después de `</main>` y antes de la tabbar, el compositor:

```html
<form class="composer" onsubmit="return false;">
  <button type="button" class="composer__btn" aria-label="Subir un documento o una foto"><span class="ds" data-ds="Icon" data-props='{"name": "menu_bar_invoice", "size": 24}'></span></button>
  <input class="composer__input" type="text" placeholder="Pregunta lo que quieras" aria-label="Pregunta al copiloto">
  <button type="submit" class="composer__btn composer__btn--send" aria-label="Enviar"><span class="ds" data-ds="Icon" data-props='{"name": "nav_forward", "size": 24}'></span></button>
</form>
```

Usar los mismos nombres de icono que tenía el compositor actual si eran otros. Eliminar el compositor y el aviso antiguos.

- [ ] **Paso 3: verificar**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && grep -c 'class="chat"' copiloto.html; grep -c 'msg--bot' copiloto.html; grep -c 'class="composer"' copiloto.html; grep -oE 'style="[^"]*"' copiloto.html | grep -v 'display:inline-flex' | sort | uniq -c | sort -rn | head
```

Esperado: `1`, `≥1`, `1`, y ningún inline de tipografía o color.

Navegador en los dos temas: burbuja del usuario a la derecha, respuesta con citas con borde izquierdo cian, sugerencias desplazables, compositor pegado encima de la tabbar al hacer scroll, foco visible en el campo. Sin errores de consola.

---

### Tarea 7: `design-system/pulso/MASTER.md` y `pages/copiloto.md`

**Files:**
- Create: `design-system/pulso/MASTER.md`
- Create: `design-system/pulso/pages/copiloto.md`

- [ ] **Paso 1: MASTER.md**

```markdown
# Design System Master File

> **LOGIC:** When building a specific page, first check `design-system/pulso/pages/[page-name].md`.
> If that file exists, its rules **override** this Master file.

- **Proyecto:** Pulso · Dashboard Ticksy para hosteleros (prototipo HTML, móvil 390 px)
- **Estilo:** Bento Box Grid (`bento-box-grid`, ui-ux-pro-max 2.13.0) sobre el Ticksy Design System 2.0
- **Origen:** escrito a mano el 22-09-2026 siguiendo la plantilla del plugin. El generador `--design-system` propone landing pages, paleta y tipografía ajenas, así que no se usa. Spec: `docs/superpowers/specs/2026-09-22-pulso-bento-design.md`.

## Global Rules

### Color Palette
Ninguna paleta nueva. Todo el color son tokens del TDS (`css/tokens.css`) o la capa `--app-*` de `css/app.css`. Prohibido el hex en pantallas.

| Rol | Token |
|---|---|
| Fondo de pantalla | `--bg-canvas` |
| Tile | `--bg-surface` |
| Tile hero | `--bg-surface-raised` |
| Tooltip, seleccionado | `--bg-surface-highlight` (oscuro) · `--bg-surface` (claro) vía `--app-tip-bg` |
| Texto principal | `--text-primary` |
| Rótulos e iconos de tile | `--text-accent` |
| Texto de apoyo | `--app-text-support` |
| Estado en texto | `--app-status-ok-text`, `--app-status-warn-text`, `--app-status-bad-text` |
| Acción principal | `--action-primary` + `--text-on-accent` |
| Bordes | `--border-default` (tile), `--border-subtle` (divisores), `--border-strong` (hover) |
| Gráficos | `--app-chart-accent`, `--app-chart-ink`, `--text-secondary` (esperado), `--channel-*` |

### Typography
Rubik 300 y 400 desde Google Fonts. Escala móvil: 44/52 hero, 33/40 cifra grande, 28/36 cifra de tile, 20/24 título de pantalla, 16/22 cuerpo, 15/20 unidad, 14/16 rótulo (letter-spacing 1.4 px, versalitas), 14/20 texto, 13/18 nota, 12/16 fecha de tooltip, 11/14 fuente de cita y tabbar.

### Spacing Variables
Tokens `--space-4 … --space-96`. Rejilla: gap `--space-16`; tile: padding `--space-16`, gap interno `--space-8`; radio de tile `--radius-xl`; radio de tooltip y citas `--radius-sm`.

### Shadow Depths
Ninguna sombra en TPV. Profundidad por capas de fondo (`bg-canvas → bg-surface → bg-surface-raised → bg-surface-highlight`) y borde de 1 px.

## Component Specs

### Cards (tiles)
Rejilla `.bento` de 6 columnas, `grid-auto-flow: dense`, gap 16. Spans: `.tile` mitad (3), `.tile--third` (2), `.tile--wide` (6), `.tile--tall` (2 filas), `.tile--hero` (6 + fondo raised). Anatomía: `tile__head` (icono 20 + `tile__label` + `tile__chevron` si navega) → `tile__figure` (+ `tile__unit`) → `tile__note` con estado → visualización opcional (gráfico, `tile__bar`, filas `tile__kv`). Hover: borde `--border-strong`. Pulsación: `scale(0.98)` 150 ms. Foco: outline 2 px `--action-primary`.

Regla de tamaño: el dinero es hero; la sección con avisos activos crece en alto y muestra el aviso principal; la que lleva gráfico de barras ocupa el ancho; el resto cuadrado. En una sección: primera tarjeta hero, trío de KPI en tercios, cuatro KPI en dos filas de mitades, lo demás ancho.

### Buttons
Los del bundle del TDS (`data-ds="Button"`). Los enlaces que navegan son `<a>` con estilo de tile o de chip. Mínimo táctil en el prototipo 44 × 44 px con 8 px o más de separación.

### Inputs
Compositor del Copiloto: campo de 44 px, radio 22, fondo `--bg-canvas`, borde `--border-default`, foco con outline `--action-primary`.

### Modals
Los del TDS (claros, cabecera cian). No hay modales en el prototipo.

## Style Guidelines
- Rejilla de 6 columnas con spans variados; nunca una fila de tiles todas iguales si hay jerarquía que mostrar.
- Radios consistentes: todas las tiles `--radius-xl`.
- Sin sombra; jerarquía por fondo y borde.
- El contenido cabe en la tile: nada se recorta; los textos envuelven.
- Una sola cifra grande por tile, siempre con unidad o línea de estado.
- El color de estado acompaña siempre a una palabra; nunca viaja solo.
- Iconos del set del TDS por nombre; sin emoji.

## Page Pattern
Dashboard: Ventas hero (gráfico de 30 días con tooltip de hoy) · Salud alta · Márgenes · Control · Canales ancha (barra y leyenda) · Previsión · Carta · Clientes · Ritmo. Orden DOM igual al orden de lectura; el flujo `dense` sube Control junto a Salud.

## Motion
Dial 3 (sutil). Aparición escalonada de tiles al cargar: `tile-in` 250 ms ease-out, 30 ms entre tiles vía `--i`, `animation-fill-mode: backwards`. Pulsación 150 ms. Todo dentro de `@media (prefers-reduced-motion: no-preference)`; con movimiento reducido, estado final inmediato.

## Anti-Patterns (Do NOT Use)
- Emoji como icono. Hex en pantallas. Sombras en TPV.
- Texto de apoyo por debajo de 13 px (salvo la fecha del tooltip a 12 y las fuentes de cita a 11, ambas con etiqueta contextual).
- Cifras sin unidad ni línea de estado. Estado solo por color.
- Inline para tipografía, color o layout de tile: se añade una clase.
- Rubik Medium o Bold. Fuentes distintas de Rubik.

## Pre-Delivery Checklist
- [ ] Captura a 390 px en `tpv-dark` y `light` de cada pantalla
- [ ] Sin errores de consola ni avisos de `ds-mount`
- [ ] `grep -c 'class="card' *.html` = 0 · `grep -oE '#[0-9a-fA-F]{6}\b' *.html` vacío
- [ ] Chips de 44 px; tiles con gap 16; foco visible; `prefers-reduced-motion` respetado
- [ ] Gráfico: punto de hoy sobre la línea, tooltip "Hoy · 27 may · 2.957 €", fechas sin solapar
- [ ] Contraste 4,5:1 del texto de apoyo en claro
```

- [ ] **Paso 2: pages/copiloto.md**

```markdown
# Copiloto Page Overrides

> **Page Type:** AI-Native UI (conversational)
> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/pulso/MASTER.md`).

## Page-Specific Rules

### Layout Overrides
Sin rejilla Bento. Columna `.chat` con gap 12. Mensajes con ancho máximo del 88 %: los del usuario a la derecha (`msg--user`, fondo `--action-secondary`), los del copiloto a la izquierda (`msg--bot`, fondo `--bg-surface` con borde). Esquina inferior del lado del emisor en `--radius-sm`, el resto `--radius-lg`. Citas `.cite` dentro del mensaje del copiloto con borde izquierdo de 2 px en `--text-accent`. Sugerencias en fila desplazable de chips de 44 px. Compositor `sticky` encima de la tabbar (`bottom: 64px + safe-area`).

### Spacing Overrides
Padding de mensaje 12 × 16. Padding de cita 8 × 12. Compositor 8 × 16.

### Typography Overrides
Mensajes 16/22 Rubik 300 con cifras en 400 (`tile__strong`). Fuente de cita 11/14 versalitas en `--text-accent`. Cuerpo de cita 13/18. Chips de sugerencia sin versalitas (14 px, frase natural).

### Color Overrides
Ninguno nuevo. Botón de enviar en `--action-primary` con icono en `--text-on-accent`.

### Patrones documentados, no implementados en el prototipo
- Indicador de escritura: tres puntos de 6 px en `--text-secondary` con pulso de 900 ms escalonado 150 ms; estático con movimiento reducido.
- Texto en streaming: aparece por frases, sin animar la altura del contenedor.
```

- [ ] **Paso 3: verificar**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && ls design-system/pulso design-system/pulso/pages && grep -c '^## ' design-system/pulso/MASTER.md
```

Esperado: `MASTER.md`, `pages/` con `copiloto.md`, y `7` secciones de nivel 2 en el Master.

---

### Tarea 8: README, limpieza y verificación final

**Files:**
- Modify: `README.md`, `css/app.css`, `.claude/launch.json`

- [ ] **Paso 1: README**

En "Estructura" añadir:

```
design-system/pulso/    MASTER.md (reglas Bento sobre el TDS) y pages/copiloto.md (override)
docs/superpowers/       Spec y plan del rediseño Bento, mockup aprobado y generador del gráfico
```

En "Cómo está hecho", sustituir el párrafo de tipografía y layout inline por:

```
- **Layout**: rejilla Bento de 6 columnas (`.bento`) con tiles de tamaño variable
  (`.tile`, `.tile--third`, `.tile--wide`, `.tile--tall`, `.tile--hero`). La anatomía de cada
  tile (`tile__head → tile__figure → tile__note`) va en clases de `css/app.css`; el inline
  se reserva para valores de dato (anchos de barra, `--i` de la animación, geometría de SVG).
  Regla de tamaño: el dinero es hero, la sección con avisos crece en alto, la que lleva barras
  ocupa el ancho, el resto cuadrado.
- **Gráfico de Ventas**: curva suavizada (media móvil de 3 días) con área degradada, línea de
  lo esperado, rejilla semanal y tooltip de hoy. Se genera con
  `docs/superpowers/specs/assets/2026-09-22-pulso-bento-gen-chart.py` a partir de los 30
  valores diarios.
```

Quitar la frase sobre la banda del rango esperado en "Gráficos".

- [ ] **Paso 2: limpiar `css/app.css`**

Comprobar y borrar lo que ya no se usa:

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && grep -c 'class="card' *.html | grep -v ':0' ; grep -c 'app-hero-shadow' *.html | grep -v ':0'
```

Esperado: sin salida. Entonces borrar de `css/app.css` las reglas `.card`, `.card--link:hover`, `.card--link:focus-visible` y las dos líneas `--app-hero-shadow`.

- [ ] **Paso 3: limpiar `.claude/launch.json`**

Quitar la configuración `pulso-mockups` (apunta al scratchpad de esta sesión) y dejar solo `proto-pulso`.

- [ ] **Paso 4: verificación final**

```bash
cd "/Users/maguro/Ticksy/AIWork/Proto Pulso" && echo "card: $(grep -c 'class="card' *.html | awk -F: '{s+=$2} END {print s}')" && echo "hex: $(grep -oE '#[0-9a-fA-F]{6}\b' *.html | wc -l | tr -d ' ')" && echo "bento: $(grep -l 'class="bento"' *.html | wc -l | tr -d ' ')" && grep -ohE 'style="[^"]*"' *.html | grep -vE 'style="(--i:[0-9]+;|width:[0-9.]+%;background:var\(--[a-z-]+\);|background:var\(--[a-z-]+\);|right:calc[^"]+|left:[0-9.]+%;|display:block;width:100%;|display:inline-flex;)"' | sort | uniq -c | sort -rn | head -20
```

Esperado: `card: 0`, `hex: 0`, `bento: 13`, y la lista de inline restante solo con la barra superior, la línea de contexto, la insignia de la campana, las flechas SVG y los valores de dato.

Navegador: las 14 pantallas a 390 px en `tpv-dark` y en `light`, sin errores de consola; con `prefers-reduced-motion: reduce` emulado, las tiles aparecen sin animación; al tabular, el foco es visible en tiles, chips y compositor.
