# Hogares design system

Source of truth for the live UI. Canonical copy: repo root `/design.md`.
Published at **https://hogares.buenalabs.io/design.md** via `app/public/design.md`
(`output: "export"` copies `public/` to the site root). **Keep both files identical.**

Use this doc for every UI PR. If a change disagrees with this file, update the
product first, then this file in the same PR.

---

## Product

Read-only search for families looking for **casa, PH or departamento** in
**CABA and GBA** (alquiler or venta). Locale **es-AR**. We aggregate already
published listings. There is **no** publish, edit, admin, or account UI.

Copy is Argentine Spanish. Prices use `es-AR` grouping. Do not invent
English UI, fake dates, guests, or similarity scores.

### Catalog freshness

The browse UI is a **static export**. Listings come from `data/listings.json`
baked at Cloudflare Pages build time (not scraped per request). A scheduled
GitHub Action refreshes that file daily (~11:00 UTC) from ZonaProp live HTML
and Mercado Libre’s official API when `MELI_ACCESS_TOKEN` is configured.
If live sources return nothing (WAF / missing token), the previous catalog is
kept. Cadence and secrets: `NOTES.md` / `.github/workflows/ingest-catalog.yml`.

---

## Brand

| Rule | Do | Don’t |
| --- | --- | --- |
| Wordmark | **Hogares** only | “Hogares BA”, “para familias” eyebrow, logo lockups |
| Placement | Centered at the top of the page, generous padding, destination homepage | Left-aligned header, fat toolbar, site-nav chrome |
| Navbar | **None.** No Avisos / Acerca row | Any top link row that reads as site navigation |
| Legal | Acerca / ToS from the **footer** only | Header or search-bar links to legal |

The wordmark uses the signature accent (`text-primary`, `#00deb6`). Large brand
lettering is exempt from WCAG text contrast (logo / brand name). Body text and
prices are not — see Contrast.

---

## Signature accent

**`#00deb6`** is `--primary` in light, dark, and system themes.

Use it for:

- Wordmark
- Primary search CTA
- Focus rings (`--ring`)
- Selected filter chips (`variant="chip"`)
- Checked controls and the listing-card arrow disc

Do **not** leave lavender / purple as primary, ring, or chart-1. Surfaces may
keep a faint mint wash (hue ~175), never a violet wash.

### Contrast

`#00deb6` on white is **~1.7:1** — fail for body text and for large text.
White on `#00deb6` is also ~1.7:1 — fail.

**Text on primary** (CTA, selected chips, icon discs): dark ink
`--primary-foreground` ≈ `oklch(0.22 0.045 175)` (~10:1 on `#00deb6`).

**Mint as text** (prices, small labels, links that are not on a mint fill):
use `--primary-text`, not `--primary`.

| Theme | `--primary-text` | Why |
| --- | --- | --- |
| Light | `oklch(0.36 0.09 175)` | ≥ 4.5:1 on background / secondary |
| Dark | `#00deb6` | ~7:1 on the dark surface |

Hover for filled primary: `bg-primary/80` (existing button token). Do not
reintroduce a second brand hue for hover.

---

## Themes

Default preference is **system**. Persist with `localStorage` key `theme`.
The blocking init script in `layout.tsx` must keep setting `color-scheme` so
native controls match.

| Value | Label (a11y only) | Icon (SF) |
| --- | --- | --- |
| `system` | Sistema | `display` |
| `light` | Claro | `sun.max` |
| `dark` | Oscuro | `moon` |

**Theme toggle lives in the footer**, icons only (visible label is
`aria-label`, never “Sistema / Claro / Oscuro” in the chrome). It must not
read as a navbar. Hide until mounted to avoid hydration flash.

Light, dark, and system must all keep `#00deb6` as `--primary`.

---

## Typography

- UI font: **Inter** with system fallbacks (`-apple-system`, `BlinkMacSystemFont`,
  `SF Pro Text`, `Helvetica Neue`, Helvetica, Arial, sans-serif).
- Headings and body share the same family (`--font-heading` = `--font-text`).
- Wordmark: `text-4xl` / `md:text-5xl`, `font-semibold`, `tracking-tight`.
- Slot labels on the search bar: 11px, semibold, uppercase.
- Slot values: `text-sm`; muted when empty, `font-medium` when set.

Do not add a display serif or a second webfont without updating this file.

---

## Spacing and radius

| Token | Value | Use |
| --- | --- | --- |
| `--radius` | `1.15rem` | Cards, sheets, popovers |
| Search bar | `rounded-full` | Desktop pill and mobile compact trigger |
| Filter chips | `rounded-full` | Selected / unselected options |
| Page header | `pt-14 md:pt-20` | Air above the centered wordmark |
| Content width | `max-w-6xl` | Page; search pill `max-w-4xl` |
| Card grid | `gap-5`, 1 / 2 / 3 cols | Browse |

Prefer `gap-*` flex/grid. Do not use `space-x-*` / `space-y-*`.

---

## Semantic tokens

Edit `app/src/app/globals.css` only. Do not sprinkle raw hex except `#00deb6`
on `--primary` / `--ring`.

| Token | Role |
| --- | --- |
| `--primary` | Mint fill (`#00deb6`) |
| `--primary-foreground` | Dark text/icons on mint |
| `--primary-text` | Accessible mint-adjacent text |
| `--background` / `--foreground` | Page |
| `--card` / `--popover` | Surfaces |
| `--muted` / `--muted-foreground` | Placeholders, catalog meta |
| `--secondary` | Soft chips, price pill track |
| `--ring` | Focus (`#00deb6`) |
| `--highlight` | Photo badge (operation · source) |
| `--mark` | “Qué destaca” label (warm, not lavender) |
| `--destructive` | Errors only |

shadcn components use these tokens. Compose with variants (`default`,
`outline`, `chip`, `ghost`, `highlight`) before adding custom color classes.

---

## Icons

**SF Symbols (Apple) names and metrics**, implemented as an SF-compatible SVG
set in `app/src/components/icons.tsx`.

- 24×24 viewBox, 1.75 stroke, round caps/joins, `currentColor`.
- No Lucide leftover in chrome, filters, theme, or shadcn primitives we
  touch (`select`, `checkbox`, `sheet`). Open SF-compatible SVGs only —
  do not embed Apple’s proprietary `.ttf` / `.otf`.
- Map metaphors, do not invent decorative icons.

| SF name | Component | Where |
| --- | --- | --- |
| `magnifyingglass` | `MagnifyingglassIcon` | Search CTA, empty results |
| `sun.max` | `SunMaxIcon` | Theme: claro |
| `moon` | `MoonIcon` | Theme: oscuro |
| `desktopcomputer` | `DesktopcomputerIcon` | Theme: sistema |
| `xmark` | `XmarkIcon` | Sheet close |
| `chevron.down` / `chevron.up` | `ChevronDownIcon` / `ChevronUpIcon` | Select |
| `checkmark` | `CheckmarkIcon` | Select / checkbox |
| `arrow.right` / `arrow.left` | `ArrowRightIcon` / `ArrowLeftIcon` | Card / back |
| `square.and.arrow.up` | `SquareAndArrowUpIcon` | External listing |
| `bed.double` / bed single | `BedDoubleIcon` / `BedSingleIcon` | Ambientes / dorms |
| `ruler` | `RulerIcon` | m² |
| `drop` | `DropIcon` | Baños |
| `calendar` | `CalendarIcon` | Antigüedad |
| `tree` | `TreeIcon` | Aire libre |
| `location.north` | `LocationNorthIcon` | Orientación |

Icons inside `Button` use `data-icon="inline-start" | "inline-end"`. Do not
put sizing classes on icons inside shadcn controls.

---

## Search

Airbnb-inspired: **one pill**, a few slots, details in a popover (desktop) or
bottom sheet (mobile). Not a stacked filter form. Not dates or guests.

### Slots

1. **Operación** — todas / alquiler / venta
2. **Tipo** — casa / PH / depto (multi)
3. **Zona** — CABA, Norte, Oeste, Sur + barrio/calle text (`q`)
4. **Presupuesto** — **Precio máximo** `Select` (not a text field or slider)
5. **Más** — ambientes mín. chips, **Dormitorios máximo** `Select`, extras, orden

Keep existing query semantics (`schema/search.schema.yaml` / `parseSearchParams`).
Do not add pets, dates, or guests unless the catalog grows a real field.

### Filter selectors

Price max and bedrooms max are **Select** controls only.

| Control | Options | URL | Notes |
| --- | --- | --- | --- |
| Precio máximo | `Sin máximo`, then **Alquiler (ARS)** presets and **Venta (USD)** presets | `precio_max` + `moneda` | Picking a preset sets both amount and currency. Labels: `Hasta $ 1.500.000` / `Hasta USD 200.000` |
| Dormitorios máximo | `Sin máximo`, `Hasta 1` … `Hasta 5` | `dorms_max` | Caps `features.bedrooms`. es-AR: dormitorios, not “beds” |

ARS presets: 500 mil → 5 millones. USD presets: 80 mil → 1 millón. No free-text, no slider.

GET form, URL is source of truth. Hidden inputs live in the form; portaled
sheet/popover controls stay controlled React state. Sheet submit uses
`form="{id}"`.

### States

- Empty slot: muted placeholder (“Cualquiera”, “CABA y GBA”, “Sin tope”…).
- Filled slot: medium weight summary (`lib/search-query.ts`).
- **Limpiar** is obvious: per-popover, under the bar when anything is set,
  in the mobile sheet footer, and on the empty results card (`href="/"`).
- Primary **Buscar** is mint (`Button` default).

Desktop: floating `rounded-full` bar, vertical separators, CTA on the right.
Mobile: compact bar (mint search disc + summary) → bottom sheet of chip groups.

---

## Cards

Se Vienen Casitas–inspired, at home with mint:

- `rounded-2xl` card, 16/10 photo, highlight badge (operation · source)
- Title = barrio (or address); subtitle = tipo · calle
- Fact pills (ambientes, dorms, m², baños, aire libre) — no fake scores
- “Por qué la elegimos” / “Qué destaca” from real description and family tags
- Price pill + mint arrow disc

Do not add match percentages, heat, or invented family scores.

---

## Layout

```
[ centered wordmark: Hogares ]

[ one-line subtitle on home ]

[ search pill ]

[ result count + catalog source ]

[ card grid ]

[ footer: legal + design.md + icon theme toggle ]
```

Inner pages (aviso, acerca) keep the same header chrome. No extra nav.

---

## Constraints for future PRs

1. `cd app && npm run build` must pass (static export).
2. No secrets, no write/admin surfaces.
3. es-AR only in user-facing strings.
4. If you change tokens, search slots, or brand rules, update **both**
   `design.md` files in the same PR.
5. Prefer shadcn composition (`Field`, `ToggleGroup`, `Popover`, `Sheet`,
   `Empty`) over custom markup.
