
  /*
   * Nunito, self-hosted (see src/assets/font-nunito.ts for why not a live
   * Google Fonts <link>). One variable-weight file covers the whole 400-800
   * range this stylesheet uses, so a single @font-face with a weight RANGE
   * is correct here — no need for one @font-face per weight.
   */
  @font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('/fonts/nunito.188dsto.woff2') format('woff2');
  }
  :root {
    color-scheme: light;
    /*
     * Purple/violet identity, sampled from Marco's reference
     * (prompts/ui-proposal.png) and contrast-corrected against white text
     * (WCAG relative-luminance formula, computed, not eyeballed) — see the
     * plan file for the exact ratios. Cool, lavender-leaning neutrals
     * throughout instead of the previous warm cream palette, so the whole
     * system reads as one family with the new primary.
     */
    --color-bg: #f7f6fb;
    --color-surface: #ffffff;
    --color-text: #211c2e;
    --color-text-muted: #6b6478;
    --color-border: #e7e4f0;
    --color-photo-bg: #f1eef9;
    --color-primary: #7657cc;
    --color-primary-dark: #5c3db0;
    --color-primary-contrast: #ffffff;
    --color-perdida: #c23a32;
    /*
     * Distinct from --color-perdida on purpose: "perdí mascota" and "mascota
     * extraviada" read as two different situations at a glance, not just two
     * shades of the same warm tone — color is a second signal alongside the
     * text and emoji, never the only one (CLAUDE.md §14). One shared token
     * for every place ENCONTRADA shows up (feed/detail/mis-publicaciones
     * status badges AND the "Mascota extraviada" card in the create form)
     * — a prior version split these into two tokens (a green badge color vs.
     * this form's own orange), which read as two different post kinds
     * depending on which screen you were on, and didn't even match the 🟠
     * this same kind's label already carries (POST_KIND_LABELS). Same 30°
     * hue as the brand's pre-redesign orange, darkened from it (~0.89× value,
     * hue/saturation unchanged) so white badge text still clears 4.5:1 AA at
     * this size/weight — the original brand orange only reached 4.04:1.
     */
    --color-encontrada: #ad5b09;
    /* Teal, not blue: a blue read fine next to the old warm terracotta
       primary, but sits too close to the new purple primary on the color
       wheel to stay easily distinguishable from it at a glance. */
    --color-reunida: #0b6e70;
    /* Semantic alias, not a new color: destructive actions and error text
       happened to reuse --color-perdida by coincidence (it's a red), not by
       design — this name says what it means regardless of what it's built
       from. */
    --color-danger: var(--color-perdida);
    /*
     * Admin panel only (Marco, 2026-08-28): "suspender" (reversible, softer)
     * needs to read as visually distinct from "banear"/"eliminar"
     * (--color-danger) at a glance, not just a smaller version of the same
     * red — an amber reads as "caution" where red reads as "destructive".
     */
    --color-warning: #a3670a;
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 1rem;
    --space-4: 1.5rem;
    --space-5: 2.5rem;
    --radius: 14px;
    /* The smaller radius used on inputs, cards and thumbnails — kept
       distinct from the pill buttons' 999px and from --radius (14px, used
       on larger surfaces like cards/dialogs/the reunited counter). */
    --radius-sm: 10px;
    /* Softer than --radius, for the one surface that should read as a
       floating card rather than a panel: the cookie notice. */
    --radius-lg: 20px;
    /*
     * How much room the page reserves at its end while the cookie notice is
     * pinned over it, so nothing (the "Publicar" button, once) ends up
     * unreachable underneath. Lives here as a token because the bottom-nav
     * block further down has to add it to its own height — two places, one
     * number.
     */
    --cookie-banner-reserve: 21rem;
    --thumb-size: 84px;
    --thumb-empty-padding: 14px;
    --max-width: 720px;
    --bottom-nav-height: 64px;
    /*
     * Wider than the public site's 720px — the admin user table needs the
     * room. Scoped to .admin-layout/.admin-main only (see below), never
     * changes --max-width itself, so no public page is affected.
     */
    --admin-max-width: 1180px;
  }
  * { box-sizing: border-box; }
  /*
   * Reserve the scrollbar's width on every page, whether or not that page
   * actually scrolls. Without this, a long page (the feed) is ~15px narrower
   * than a short one (login) on any platform with classic scrollbars, and the
   * header's nav row can fit on one line on one page and wrap on the other —
   * reported as "en inicio el header es diferente". Also stops the whole page
   * shifting sideways when navigating between the two.
   */
  html { scrollbar-gutter: stable; }
  html, body { margin: 0; padding: 0; }
  body {
    background: var(--color-bg);
    color: var(--color-text);
    font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }
  a { color: var(--color-primary); }
  img { max-width: 100%; display: block; }
  .skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    background: var(--color-primary);
    color: var(--color-primary-contrast);
    padding: var(--space-2) var(--space-3);
    z-index: 100;
  }
  .skip-link:focus { left: var(--space-2); top: var(--space-2); }
  /*
   * Two tiers, both centred, at every width — the design calls for this on
   * desktop too, not just as a narrow-screen fallback: logo+name stacked on
   * top, the nav row underneath. flex-wrap on the nav row is what keeps this
   * from overflowing sideways once it does not fit one line (this has broken
   * three separate times as items were added to the header); centring the
   * wrapped lines, not just the row, is what keeps that fallback looking
   * intentional instead of ragged.
   */
  header.site-header {
    position: sticky;
    top: 0;
    z-index: 10;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
  }
  /* Logo and name on one line: the stacked version cost an extra ~30px of
     header height on every page for no added clarity. */
  .wordmark {
    font-weight: 800;
    text-decoration: none;
    color: var(--color-text);
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
  }
  .wordmark-logo { width: 2rem; height: auto; flex: 0 0 auto; }
  .wordmark-text { font-size: 1.125rem; line-height: 1; }
  /* Feed-only brand row (Marco, 2026-08-25) — reuses .wordmark and the
     footer's .footer-social-nav/.footer-social-link styles rather than
     duplicating them, since it's the same mark and the same social icons. */
  /*
   * Misma caja que <main>: mismo max-width, mismo padding lateral y mismo
   * centrado. Cuando esta fila vivía dentro de home.tsx heredaba todo eso de
   * <main> sin pedirlo; al subirla al Layout (para que aparezca en todas las
   * páginas) quedó suelta a lo ancho del viewport, con el logo pegado al
   * borde izquierdo y la red social al derecho. Alinearla con main es lo que
   * la vuelve a poner en la misma columna que el resto del contenido.
   *
   * El padding vertical es simétrico a propósito: el hueco de arriba y el de
   * abajo tienen que verse iguales. Eso solo funciona porque main ya no lleva
   * padding-top (ver su regla): antes, el espacio de abajo era este padding
   * MÁS el de main, y por eso se veía casi el triple que el de arriba.
   */
  .feed-brand-row {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: var(--space-3);
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-2);
  }
  .feed-brand-row .footer-social-nav { justify-content: flex-end; margin-top: 0; }
  nav.site-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-1) var(--space-3);
    align-items: center;
  }
  /*
   * Scoped to .nav-link on purpose, not every nav.site-nav a/button: an
   * actual .button (e.g. "Crear cuenta") needs its own background/border/
   * padding to win, and this selector's specificity (nav.site-nav + element)
   * is higher than a single .button-* class — matching every nav link/button
   * unconditionally silently overrode the button styling instead of
   * composing with it.
   */
  nav.site-nav a.nav-link, nav.site-nav button.nav-link {
    font-size: 0.9375rem;
    text-decoration: none;
    color: var(--color-text);
    padding: var(--space-2);
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-family: inherit;
    background: none;
    border: 0;
    cursor: pointer;
  }
  nav.site-nav .nav-link svg { width: 18px; height: 18px; flex-shrink: 0; }
  /*
   * Icon-only items still have to be a 44px touch target: with the label
   * hidden, padding around an 18px glyph would leave a ~34px-wide tap area.
   * The icon itself goes up a notch too — alone it carries the whole meaning,
   * so it should not read as small chrome.
   */
  nav.site-nav .nav-link-icon, .legal-menu > summary.legal-menu-summary-icon {
    min-width: 44px;
    justify-content: center;
  }
  nav.site-nav .nav-link-icon svg { width: 21px; height: 21px; }
  nav.site-nav a.nav-link:hover, nav.site-nav button.nav-link:hover { color: var(--color-primary); }
  nav.site-nav a.nav-link[aria-current='page'] {
    color: var(--color-primary);
    font-weight: 700;
  }
  /*
   * Donaciones: a soft-lilac pill so it stands apart from the plain text
   * links either side of it without shouting over them — filled, not
   * outlined, but in the same photo-bg lilac the rest of the app already
   * uses for quiet emphasis (the required-fields note, the kind-card icon
   * background), not a loud saturated purple.
   */
  /*
   * Written at the same specificity as the base nav.site-nav rule and placed
   * after it, so it simply wins on order — the earlier version leaned on
   * !important, which is a shortcut that makes the next override harder.
   */
  nav.site-nav a.nav-link-donate {
    background: var(--color-photo-bg);
    border-radius: 999px;
    padding: var(--space-2) var(--space-3);
    color: var(--color-primary);
    font-weight: 700;
  }
  nav.site-nav a.nav-link-donate:hover {
    background: color-mix(in srgb, var(--color-primary) 14%, var(--color-photo-bg));
    color: var(--color-primary);
  }
  /*
   * The session control reads as a real button — outlined rather than filled,
   * so it stays clearly secondary to the Donaciones pill next to it instead
   * of competing with it.
   */
  nav.site-nav a.nav-link-session, nav.site-nav button.nav-link-session {
    border: 1px solid var(--color-border);
    border-radius: 999px;
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface);
    font-weight: 700;
  }
  nav.site-nav a.nav-link-session:hover, nav.site-nav button.nav-link-session:hover {
    border-color: var(--color-primary);
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  nav.site-nav a.nav-link-session:active, nav.site-nav button.nav-link-session:active {
    background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  }
  /*
   * A <details> in the nav row so the "más información" links don't crowd
   * the header at 320px — same accessible-without-JS pattern already used
   * for the search filter panel.
   */
  .legal-menu { position: relative; }
  .legal-menu > summary {
    list-style: none;
    cursor: pointer;
    font-size: 0.9375rem;
    padding: var(--space-2);
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--color-text);
  }
  .legal-menu > summary::-webkit-details-marker { display: none; }
  /* Anchors .notification-bell-badge to the bell icon itself, not the whole <details>. */
  .notification-bell > summary { position: relative; }
  .legal-menu > summary svg { width: 18px; height: 18px; flex-shrink: 0; }
  /* Info glyph matched to the other icon-only items; the chevron stays small
     so it reads as a modifier rather than a second icon of equal weight. */
  .legal-menu > summary.legal-menu-summary-icon > svg { width: 21px; height: 21px; }
  .legal-menu-summary-icon .legal-menu-chevron svg { width: 14px; height: 14px; }
  .legal-menu-chevron { transition: transform 0.15s ease; }
  .legal-menu[open] .legal-menu-chevron { transform: rotate(180deg); }
  /*
   * display:flex here unconditionally would override the browser's own
   * "hide everything after <summary> while <details> is closed" behavior —
   * an author rule wins over that default regardless of when the closed
   * <details> is collapsed, so the panel would sit permanently in the page
   * (just visually tucked away via position:absolute), still wide enough at
   * 320px to blow out the page's scrollable width. It only becomes flex
   * once the [open] attribute is actually there.
   */
  .legal-menu-links { display: none; }
  .legal-menu[open] .legal-menu-links {
    position: absolute;
    z-index: 20;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    min-width: 12rem;
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 4px 12px rgba(33, 28, 46, 0.16);
    padding: var(--space-1) 0;
  }
  .legal-menu-links a {
    padding: var(--space-2) var(--space-3);
    min-height: 44px;
    display: flex;
    align-items: center;
    color: var(--color-text);
    text-decoration: none;
  }
  .legal-menu-links a:hover, .legal-menu-links a:focus-visible {
    background: var(--color-bg);
  }
  .legal-menu-links a[aria-current='page'] { color: var(--color-primary); font-weight: 700; }
  /*
   * The account menu is the last item in the nav row, hard against the right
   * edge of the header. The base .legal-menu-links panel centers itself under
   * its own summary (left:50%/translateX(-50%)) — fine for "Más información"
   * in the middle of the row, but here it would push half of a 14rem panel
   * past the right edge of a 320px viewport and force horizontal scroll.
   * Right-aligning it keeps it flush with the icon that opened it instead.
   *
   * Selector has to match .legal-menu[open] .legal-menu-links's own
   * specificity (class + attribute + class) — a plain .account-menu-links
   * rule loses that tie and the centering rule keeps winning regardless of
   * source order, which is exactly what shipped broken the first time and
   * overflowed at 320px (caught by the E2E overflow check, not by eye).
   */
  .account-menu[open] .account-menu-links { left: auto; right: 0; transform: none; min-width: 12rem; }
  /* Same shape as the <a> siblings above it in the panel: the only reason
     this is a <button> is that logging out is a POST, not a navigation. */
  .account-menu-logout {
    width: 100%;
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
    display: flex;
    align-items: center;
    border: none;
    background: none;
    color: var(--color-text);
    font: inherit;
    font-weight: 700;
    text-align: left;
    cursor: pointer;
  }
  .account-menu-logout:hover, .account-menu-logout:focus-visible {
    background: var(--color-bg);
    color: var(--color-primary);
  }
  /*
   * Notification bell (Marco, 2026-09-02) — same right-aligned panel
   * treatment as .account-menu[open] .account-menu-links above, for the
   * same reason: it sits far enough right in the nav row that the base
   * centered panel would overflow a 320px viewport.
   */
  .notification-bell[open] .notification-bell-panel {
    left: auto;
    right: 0;
    transform: none;
    /*
     * Ensanchado (Marco, 2026-09-03: "podemos ocupar un poco más") — el
     * calc() sigue acotando a viewports angostos de verdad (320px de
     * CLAUDE.md §14: min() nunca deja que esto exceda el ancho disponible
     * menos los márgenes), así que el aumento solo se nota donde ya sobraba
     * espacio.
     */
    min-width: 19rem;
    max-width: min(25rem, calc(100vw - 2 * var(--space-2)));
    padding: 0;
  }
  .notification-bell-badge {
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 16px;
    height: 16px;
    padding: 0 3px;
    border-radius: 999px;
    background: var(--color-danger);
    color: #fff;
    font-size: 0.625rem;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
  }
  .notification-bell-read-all {
    margin: 0;
    padding: var(--space-1) var(--space-2);
    border-bottom: 1px solid var(--color-border);
  }
  .notification-bell-read-all button {
    width: 100%;
    min-height: 40px;
    border: none;
    background: none;
    color: var(--color-primary);
    font: inherit;
    font-weight: 700;
    font-size: 0.8125rem;
    cursor: pointer;
    text-align: left;
  }
  .notification-bell-read-all button:hover, .notification-bell-read-all button:focus-visible {
    text-decoration: underline;
  }
  .notification-bell-empty {
    margin: 0;
    padding: var(--space-3) var(--space-2);
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    text-align: center;
  }
  /*
   * "Unas cuantas notificaciones... con scroll para subir y bajar tanto en
   * web, tablet y mobile" (Marco) — a fixed max-height with its own scroll,
   * not the whole page. 5 items' worth is a reasonable phone-sized panel;
   * it scrolls past that on every viewport, wide screens included.
   */
  .notification-bell-list {
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: 20rem;
    overflow-y: auto;
  }
  .notification-bell-item + .notification-bell-item { border-top: 1px solid var(--color-border); }
  .notification-bell-item-body {
    display: block;
    width: 100%;
    padding: var(--space-2);
    color: inherit;
    text-decoration: none;
  }
  a.notification-bell-item-body:hover, a.notification-bell-item-body:focus-visible { background: var(--color-bg); }
  /* Unread state is never color-only (CLAUDE.md §14): bold title + a leading dot, not just a tint. */
  .notification-bell-item-unread .notification-bell-title::before {
    content: '';
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 6px;
    border-radius: 999px;
    background: var(--color-primary);
    vertical-align: middle;
  }
  .notification-bell-title { margin: 0; font-size: 0.875rem; font-weight: 600; color: var(--color-text); }
  .notification-bell-item-unread .notification-bell-title { font-weight: 800; }
  .notification-bell-text { margin: 0.125rem 0 0; font-size: 0.8125rem; color: var(--color-text-muted); line-height: 1.4; }
  .notification-bell-time { margin: 0.25rem 0 0; font-size: 0.75rem; color: var(--color-text-muted); }
  /*
   * At 320px — the narrowest phone the product must support — five nav items
   * plus the two-line wordmark are a lot of vertical space; tighten padding
   * and let more of the row wrap rather than shrinking text to the point of
   * being hard to read. Touch targets stay at 44px throughout — nothing here
   * lowers min-height.
   */
  /*
   * Phone widths: tighten the row so all five items stay on ONE line down to
   * about 400px, which covers the common portrait phones (390/393/412/430).
   * Below that it still wraps — five controls simply do not fit across 320px
   * without shrinking touch targets, and the bottom nav already repeats the
   * main destinations there.
   */
  @media (max-width: 560px) {
    header.site-header { padding: var(--space-2) var(--space-2) var(--space-3); gap: var(--space-2); }
    nav.site-nav { gap: var(--space-1); }
    nav.site-nav a.nav-link, nav.site-nav button.nav-link { font-size: 0.875rem; padding: var(--space-1); }
    nav.site-nav a.nav-link-donate,
    nav.site-nav a.nav-link-session,
    nav.site-nav button.nav-link-session {
      padding: var(--space-1) var(--space-2);
    }
    /* The icon-only targets deliberately keep their full 44px here: shaving
       them was the easy way to buy another ~12px of row, and it is exactly
       the accessibility floor this project holds everywhere else. The row gets
       its space back from the rule below instead. */
  }
  /*
   * Drop the two destinations the fixed bottom nav already shows — Inicio and
   * Mis publicaciones. Nothing becomes unreachable: this is exactly the window
   * where that bar is on screen on every page, so these are a duplicate of it.
   *
   * The breakpoint is 768px and not a pixel less BECAUSE that is the bottom
   * nav's own breakpoint (see .bottom-nav further down) — the two have to be
   * the same number or the trade stops holding. It used to be 560px, which
   * left 561–768px showing both navigations at once, offering the same two
   * destinations twice.
   *
   * Below 560px this also frees ~96px, which is what lets the remaining items
   * stay on ONE aligned line all the way down to 320px instead of wrapping.
   */
  @media (max-width: 768px) {
    nav.site-nav a.nav-link[href='/'], nav.site-nav a.nav-link[href='/mis-publicaciones'] {
      display: none;
    }
  }
  /*
   * The trophy shortcut is the one header item that does not survive the
   * narrowest phones. Measured: with it present the row wraps at 320px and
   * 360px and only fits from 390px up, and the budget here has no slack left
   * (the rule below already took the last ~16px out of the pills' padding).
   * So at these widths it is the trophy or the word "Donaciones" — and
   * Donaciones is a real call to action, while /casos-de-exito is still one
   * tap away from the feed's counter card and from the footer.
   */
  @media (max-width: 389px) {
    nav.site-nav a.nav-link[href='/casos-de-exito'] { display: none; }
  }
  @media (max-width: 380px) {
    header.site-header { padding: var(--space-1) var(--space-1) var(--space-2); }
    .wordmark-logo { width: 1.75rem; }
    .wordmark-text { font-size: 1rem; }
    /* The last ~16px needed to keep the row on one line at 320px, taken from
       the pills' side padding rather than from their height or hit area. */
    nav.site-nav a.nav-link-donate,
    nav.site-nav a.nav-link-session,
    nav.site-nav button.nav-link-session {
      padding: var(--space-1);
    }
  }
  /*
   * margin:0 and flex, not the browser default. A <form> carries a UA
   * margin-block-end of 1em, which here made the flex line 60px tall instead
   * of 44 — so every OTHER nav item got centred 8px lower while this one
   * stayed put, and the logout control read as misaligned. (Same UA-margin
   * trap already fixed on form.auth-form/form.post-form.) display:flex also
   * removes the line box a blockified inline form would otherwise wrap the
   * button in.
   */
  .logout-form { display: flex; margin: 0; }
  main {
    max-width: var(--max-width);
    margin: 0 auto;
    /* Sin padding-top: .feed-brand-row va justo encima en TODAS las páginas
       (ver Layout) y su propio padding-bottom ya separa del contenido. Con
       los dos, el hueco bajo la fila de marca sumaba 2.5rem contra 1rem
       arriba, que es exactamente la desproporción que se veía.

       Bottom matches the gap between components inside a form (--space-3)
       instead of --space-5: the extra 24px read as an unexplained hole
       between the last control and the footer. The footer brings its own
       top padding, so there is still real separation. */
    padding: 0 var(--space-3) var(--space-3);
  }
  footer.site-footer {
    text-align: center;
    padding: var(--space-4) var(--space-3);
    color: var(--color-text-muted);
    font-size: 0.8125rem;
  }
  .footer-legal-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-1) var(--space-3);
    margin-top: var(--space-2);
  }
  .footer-legal-nav a { color: var(--color-text-muted); }
  .footer-social-nav {
    display: flex;
    justify-content: center;
    gap: var(--space-2);
    margin-top: var(--space-3);
  }
  /* 44px touch target (CLAUDE.md §14) even though the icon itself is smaller. */
  .footer-social-link {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
  }
  .footer-social-link svg { width: 24px; height: 24px; }
  .footer-social-link:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .footer-copyright { margin: var(--space-2) 0 0; }

  /*
   * /acerca-de. Reads as a landing, not a legal document: the same soft
   * lavender surfaces, wide radii and faint shadows the feed uses, with a
   * measure capped per block instead of one 65ch column for the whole page.
   * Mobile-first — every multi-column arrangement is a min-width addition.
   */
  .about-page { display: flex; flex-direction: column; gap: var(--space-5); }
  .about-page h2 { font-size: 1.375rem; line-height: 1.25; margin: 0; color: var(--color-text); }
  .about-page h3 { font-size: 1.0625rem; margin: 0; color: var(--color-text); }
  .about-page p { margin: 0; color: var(--color-text-muted); line-height: 1.6; }
  /* :not(.button) is load-bearing — this selector (0,2,0) outranks
     .button-primary (0,1,0), so without it the CTA's own white text and icon
     were repainted purple on a purple pill and vanished. */
  .about-page a:not(.button) { color: var(--color-primary); font-weight: 600; }
  .about-section { display: flex; flex-direction: column; gap: var(--space-2); max-width: 62ch; }
  .about-section-wide { max-width: none; }
  .about-section-wide > h2, .about-section-wide > p { max-width: 62ch; }
  .about-list { margin: 0; padding-left: 1.15rem; display: flex; flex-direction: column; gap: var(--space-1); color: var(--color-text-muted); line-height: 1.55; }
  /*
   * Mismo héroe que el feed (.hero, más abajo): titular e ilustración lado a
   * lado desde el ancho más pequeño, no apilados. Es una grilla y no un flex
   * en fila porque aquí el texto es mucho más largo que en el feed — el
   * titular comparte fila con el dibujo, y los párrafos y botones bajan a una
   * fila propia a todo el ancho, que a 320px es la diferencia entre una
   * columna de prosa legible y una cinta de ~190px.
   */
  .about-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 38%;
    grid-template-areas:
      "headline art"
      "body     body";
    column-gap: var(--space-3);
    row-gap: var(--space-2);
    overflow: hidden;
    padding: var(--space-4) var(--space-3);
    border-radius: 22px;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
  }
  .about-hero-headline { grid-area: headline; align-self: center; display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
  .about-hero-body { grid-area: body; display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
  /* Escala por tramos como el titular del feed: a 320px la columna de texto
     es angosta, y 1.75rem fijos la convertían en siete renglones. */
  .about-page h1 { margin: 0; font-size: 1.25rem; line-height: 1.12; font-weight: 800; color: var(--color-text); }
  .about-lead { font-size: 0.9375rem; color: var(--color-text); font-weight: 600; }
  /*
   * align-self: stretch + height auto es lo que hace que el dibujo llene el
   * alto de su fila en vez de quedar como una miniatura centrada (misma razón
   * que en .hero-illustration: si se fija la altura, deja de estirarse).
   * object-position: bottom apoya a los animales en el piso de su celda y el
   * margen negativo cancela el padding derecho de la tarjeta para que toquen
   * ese borde exacto, con overflow: hidden recortando dentro del radio.
   */
  .about-hero-illustration {
    grid-area: art;
    align-self: stretch;
    /* justify-self: end es lo que hace efectivo el margen negativo: con un
       ancho definido, la caja se alinea al borde derecho de su celda y solo
       entonces el margen la corre hacia afuera, hasta el borde de la tarjeta. */
    justify-self: end;
    width: 100%;
    height: auto;
    min-height: 0;
    margin-right: calc(var(--space-3) * -1);
    object-fit: contain;
    object-position: bottom center;
  }
  @media (min-width: 375px) {
    .about-page h1 { font-size: 1.4375rem; }
  }
  @media (min-width: 430px) {
    .about-page h1 { font-size: 1.5rem; }
    .about-hero { grid-template-columns: minmax(0, 1fr) 40%; }
  }
  @media (min-width: 560px) {
    .about-page h1 { font-size: 1.75rem; }
    .about-lead { font-size: 1rem; }
  }
  .about-cta-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-1); }
  .about-cta { gap: var(--space-2); }
  .about-cta svg { width: 20px; height: 20px; flex-shrink: 0; }
  /*
   * 404. Mismo degradado lavanda y mismo radio ancho que el héroe de
   * /acerca-de, para que se lea como una pantalla más del sitio y no como una
   * página de error del servidor. Centrada y en una sola columna: aquí no hay
   * nada que comparar, solo una salida que tomar.
   */
  .not-found {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding: var(--space-5) var(--space-3);
    border-radius: 22px;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
  }
  /* Ancho por porcentaje y no fijo: a 320px una ilustración de 400px se
     saldría de la tarjeta. */
  .not-found-illustration { width: 60%; max-width: 260px; height: auto; }
  .not-found h1 { margin: 0; font-size: 1.375rem; line-height: 1.25; color: var(--color-text); }
  .not-found-lead {
    margin: 0;
    max-width: 46ch;
    color: var(--color-text-muted);
    line-height: 1.55;
  }
  /* Columna en móvil para que cada botón sea un blanco cómodo; en fila desde
     que hay ancho, igual que .cta-row del feed. */
  .not-found-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    width: 100%;
    max-width: 320px;
  }
  .not-found-cta { gap: var(--space-2); }
  .not-found-cta svg { width: 20px; height: 20px; flex-shrink: 0; }
  .not-found-help { margin: 0; font-size: 0.875rem; color: var(--color-text-muted); }
  /* El ícono acompaña al enlace dentro de la misma línea de texto. */
  .not-found-help a { display: inline-flex; align-items: center; gap: 0.25rem; font-weight: 700; }
  .not-found-help svg { width: 16px; height: 16px; flex-shrink: 0; }
  @media (min-width: 480px) {
    .not-found-actions { flex-direction: row; justify-content: center; max-width: none; }
    .not-found h1 { font-size: 1.75rem; }
  }
  @media (min-width: 768px) {
    .not-found { padding: var(--space-5) var(--space-4); }
    .not-found h1 { font-size: 2rem; }
    .not-found-illustration { max-width: 300px; }
  }

  /* Pulled quote — the one line per section that should stop the eye. */
  .about-callout {
    padding: var(--space-3);
    border-left: 3px solid var(--color-primary);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    color: var(--color-text);
    font-weight: 600;
  }
  .about-highlight { color: var(--color-text); font-weight: 700; }
  .about-card {
    padding: var(--space-4) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
    max-width: none;
  }
  .about-card > * { max-width: 62ch; }
  .about-card h2 { display: flex; align-items: center; gap: var(--space-2); }
  .about-card-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    border-radius: 999px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .about-card-icon svg { width: 18px; height: 18px; }
  .about-card-trust { border-color: color-mix(in srgb, var(--color-primary) 30%, var(--color-border)); }
  /* Numbered steps: list-style is off because each step renders its own badge. */
  .about-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
  .about-step {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  .about-step-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    border-radius: 999px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .about-step-icon svg { width: 20px; height: 20px; }
  .about-step-number {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    font-size: 1.5rem;
    font-weight: 800;
    line-height: 1;
    color: color-mix(in srgb, var(--color-primary) 28%, var(--color-surface));
  }
  .about-final {
    padding: var(--space-4) var(--space-3);
    border-radius: 22px;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
    max-width: none;
  }
  .about-final > * { max-width: 62ch; }
  @media (min-width: 700px) {
    /* Desde tablet el dibujo pasa a ocupar las dos filas: el texto completo
       queda en una columna a la izquierda y la ilustración centrada a su
       derecha, que es la disposición que ya se veía bien aquí. */
    .about-hero {
      grid-template-columns: minmax(0, 1fr) 40%;
      grid-template-areas:
        "headline art"
        "body     art";
      column-gap: var(--space-4);
      padding: var(--space-5) var(--space-4);
    }
    .about-hero-headline { align-self: end; }
    .about-hero-illustration { align-self: center; margin-right: 0; }
    .about-page h1 { font-size: 2.25rem; }
    .about-lead { font-size: 1.0625rem; }
    .about-page h2 { font-size: 1.5rem; }
    .about-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .about-card, .about-final { padding: var(--space-5) var(--space-4); }
  }
  /*
   * /donaciones. Same visual family as /acerca-de — lavender gradient bands,
   * white cards with a faint shadow, circular artwork — but its own classes:
   * the about page's rules are tuned to a page of prose, and reusing them here
   * would tie two unrelated pages together for the sake of saving a few lines.
   *
   * Mobile-first like the rest of the sheet: every arrangement below is what a
   * 320px phone gets, and the only additions are the min-width tiers at the
   * end of the block.
   */
  .donate-page { display: flex; flex-direction: column; gap: var(--space-4); }
  .donate-page h2 { margin: 0; font-size: 1.25rem; line-height: 1.25; color: var(--color-text); }
  .donate-page h3 { margin: 0; font-size: 1rem; color: var(--color-primary-dark); }
  .donate-page p { margin: 0; color: var(--color-text-muted); line-height: 1.55; }
  /* Same grid recipe as .about-hero: the artwork shares a row with the
     headline instead of being pushed under the whole text block, which is what
     keeps this hero reading like the feed's on a phone. */
  .donate-hero {
    display: grid;
    /* Wider art column than the feed hero's 38%: that one is a photo bled off
       the card, so part of it lives in the padding, while this one has to fit
       whole inside its cell — at 38% the logo read as a small sticker lost
       beside a tall column of text. */
    grid-template-columns: minmax(0, 1fr) 44%;
    /* En móvil el logo acompaña al titular y el párrafo baja a todo el ancho:
       a 320px la columna de texto mide ~140px, y meter ahí también el párrafo
       lo partía en ocho renglones de tres palabras. */
    grid-template-areas:
      "headline art"
      "lead     lead";
    column-gap: var(--space-3);
    row-gap: var(--space-2);
    overflow: hidden;
    padding: var(--space-4) var(--space-3);
    border-radius: 22px;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
  }
  .donate-hero-headline { grid-area: headline; align-self: center; min-width: 0; }
  .donate-lead { grid-area: lead; min-width: 0; }
  .donate-page h1 {
    margin: 0;
    font-size: 1.25rem;
    line-height: 1.14;
    font-weight: 800;
    color: var(--color-primary-dark);
  }
  /* The reference's little heart-on-a-line divider between title and lead. */
  .donate-hero-rule {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-2) 0;
    color: var(--color-perdida);
  }
  .donate-hero-rule::before, .donate-hero-rule::after {
    content: "";
    flex: 1 1 auto;
    height: 2px;
    border-radius: 999px;
    background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--color-primary) 45%, transparent));
  }
  .donate-hero-rule::after { background: linear-gradient(90deg, color-mix(in srgb, var(--color-perdida) 45%, transparent), transparent); }
  .donate-hero-rule svg { width: 16px; height: 16px; flex-shrink: 0; }
  .donate-lead { font-size: 0.875rem; }
  .donate-lead strong { color: var(--color-primary-dark); }
  /*
   * A sticker-style logo, so it gets the opposite treatment to the feed's
   * .hero-illustration: shown whole and centred in its cell, with no bleed off
   * the card edge and nothing resting on the card's floor. A photograph cut by
   * the edge it touches still reads as a photograph; a logo cut by that same
   * edge just looks broken.
   */
  .donate-hero-illustration {
    grid-area: art;
    align-self: center;
    justify-self: center;
    width: 100%;
    height: auto;
    object-fit: contain;
  }
  /* The page's one primary action, so it gets more presence than .button's
     default: full width, deeper purple, and a shadow that lifts it off the
     page the way the reference's pill does. */
  .donate-paypal {
    width: 100%;
    min-height: 52px;
    gap: var(--space-2);
    font-size: 1.0625rem;
    background: var(--color-primary-dark);
    border-color: var(--color-primary-dark);
    box-shadow: 0 6px 18px color-mix(in srgb, var(--color-primary-dark) 32%, transparent);
  }
  .donate-paypal svg { width: 20px; height: 20px; flex-shrink: 0; }
  /* Button and its reassurance line belong together, so they get their own
     tighter gap instead of the page's. */
  .donate-action { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
  .donate-redirect-note {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    font-size: 0.8125rem;
    color: var(--color-text-muted);
  }
  .donate-redirect-note svg { width: 14px; height: 14px; flex-shrink: 0; }
  .donate-card {
    padding: var(--space-4) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  /* Icon and text share the top row; the QR sits beside them and, on a phone,
     drops to its own full-width row so it never shrinks below a scannable
     size. */
  .donate-scan {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "icon text"
      "qr   qr";
    align-items: center;
    gap: var(--space-3);
  }
  .donate-scan-icon { grid-area: icon; width: 64px; height: auto; }
  .donate-scan-text { grid-area: text; display: flex; flex-direction: column; gap: var(--space-1); }
  .donate-scan-text h2 { color: var(--color-primary-dark); }
  .donate-scan-text p { font-size: 0.875rem; }
  /* White plate and a quiet zone around the code: a QR printed straight onto
     a tinted surface, or crowded by its neighbours, is harder for a camera to
     lock onto. */
  .donate-qr {
    grid-area: qr;
    justify-self: center;
    width: 128px;
    height: 128px;
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: #ffffff;
    image-rendering: pixelated;
    /*
     * A forced square box stretched a non-square QR crop enough to break
     * scanning (found by Marco, 2026-08-25, on the Bre-B QR — fixed by
     * re-cropping it square, unscaled). object-fit keeps this from
     * happening again for any future QR whose crop isn't pixel-perfect.
     */
    object-fit: contain;
  }
  /*
   * El QR de Bre-B va a TAMAÑO NATIVO (292x279), nunca reescalado — pedido
   * repetido de Marco, y no es estética: reducirlo lo vuelve ilegible.
   *
   * La caja de 128px de arriba dejaba 110px útiles (descontando marco), o sea
   * una reducción de 2.65x; con image-rendering: pixelated eso es
   * vecino-más-cercano, que descarta filas y columnas enteras de módulos y
   * deja al lector sin retícula que reconstruir.
   *
   * Se le quitan padding y borde a propósito: la tarjeta que lo contiene ya es
   * blanca (--color-surface) y la imagen trae su propio margen, así que ese
   * marco era decorativo — y sus 18px eran justo los que impedían que los
   * 292px nativos cupieran en un teléfono de 360px.
   *
   * max-width es la única concesión: por debajo de ~358px no hay sitio físico
   * para 292px, y ahí vale más encogerlo que provocar scroll horizontal
   * (CLAUDE.md §14). De 360px en adelante — o sea cualquier teléfono real —
   * se pinta 1:1. Verificado en tests/e2e/donations.spec.ts comparando el
   * ancho pintado contra naturalWidth.
   */
  .donate-qr-breb {
    width: auto;
    height: auto;
    max-width: 100%;
    padding: 0;
    border: 0;
  }
  /* Único tramo donde max-width sí lo reduce; ahí el vecino-más-cercano es lo
     peor posible, así que se deja interpolar suavemente. */
  @media (max-width: 359px) {
    .donate-qr-breb { image-rendering: auto; }
  }
  .donate-reasons-title { text-align: center; color: var(--color-primary-dark); }
  .donate-reasons {
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
  }
  .donate-reason {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-top: 1px solid var(--color-border);
  }
  .donate-reason:first-child { border-top: 0; padding-top: 0; }
  .donate-reason:last-child { padding-bottom: 0; }
  .donate-reason img { width: 56px; height: auto; flex-shrink: 0; }
  .donate-reason p { margin-top: 2px; font-size: 0.875rem; }
  .donate-band {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3);
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  }
  .donate-band img { width: 44px; height: auto; flex-shrink: 0; }
  .donate-band p { font-size: 0.875rem; color: var(--color-text); }
  .donate-band strong { color: var(--color-primary-dark); }
  .donate-thanks {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-3);
    border-radius: 22px;
    text-align: center;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
  }
  .donate-thanks h2 { color: var(--color-primary-dark); }
  @media (min-width: 430px) {
    .donate-page h1 { font-size: 1.4375rem; }
    .donate-hero { grid-template-columns: minmax(0, 1fr) 46%; }
    /* From here the card is wide enough for the code to sit beside its label
       without either being squeezed. */
    .donate-scan {
      grid-template-columns: auto minmax(0, 1fr) auto;
      grid-template-areas: "icon text qr";
    }
    .donate-qr { justify-self: end; }
  }
  /*
   * El de Bre-B se queda en su propia fila hasta más tarde que el de PayPal.
   * A 430px la fila de tres columnas sería icono(64) + texto + QR(292) = más
   * ancho que la tarjeta, y el texto quedaría aplastado o se desbordaría la
   * página. Desde 640px sí caben los tres (al texto le quedan ~186px, y en
   * escritorio ~266px), así que a partir de ahí hereda la fila de arriba.
   * Solo afecta a esta tarjeta: la de PayPal conserva su breakpoint de 430px.
   */
  @media (max-width: 639px) {
    .donate-scan.donate-scan-breb {
      grid-template-columns: auto minmax(0, 1fr);
      grid-template-areas:
        "icon text"
        "qr   qr";
    }
    .donate-scan-breb .donate-qr-breb { justify-self: center; }
  }
  @media (min-width: 560px) {
    .donate-page h1 { font-size: 1.75rem; }
    .donate-lead { font-size: 1rem; }
    .donate-page h2 { font-size: 1.375rem; }
    .donate-reason img { width: 64px; }
    /* Desde aquí la columna de texto ya es lo bastante ancha para el párrafo,
       así que el logo pasa a ocupar las dos filas y queda centrado frente al
       bloque completo, como en la referencia. */
    .donate-hero {
      grid-template-areas:
        "headline art"
        "lead     art";
    }
  }
  @media (min-width: 700px) {
    .donate-hero { grid-template-columns: minmax(0, 1fr) 42%; padding: var(--space-5) var(--space-4); }
    .donate-page h1 { font-size: 2rem; }
    .donate-card, .donate-thanks { padding: var(--space-5) var(--space-4); }
  }
  .legal-page { max-width: 65ch; }
  .legal-page h1 { font-size: 1.5rem; margin: 0 0 var(--space-3); }
  .legal-page h2 { font-size: 1.125rem; margin: var(--space-4) 0 var(--space-1); }
  .legal-page p, .legal-page li { color: var(--color-text-muted); }
  .legal-page ul { padding-left: 1.25rem; }

  /*
   * /mi-cuenta, redesigned to match prompts/mi-cuenta-page-reference.png
   * (Marco, 2026-08-27) — visual only, no route/page other than this one
   * reads these classes. Card shell still follows .about-card's language
   * (border, wide radius, soft shadow) without reusing that class name
   * directly — a reader of account.tsx should not have to know this page
   * borrows /acerca-de's CSS.
   */
  /*
   * Sin max-width propio: hereda los 720px de <main> (--max-width), igual que
   * el feed, /donaciones y el resto del sitio. Antes cortaba en 65ch (~520px)
   * y eso era justo lo que descuadraba la fila de marca del Layout — esa fila
   * sí mide los 720px completos, así que sus iconos sociales quedaban por
   * fuera del borde derecho de estas tarjetas en tablet y escritorio. La fila
   * es un componente compartido y no se toca (Marco, 2026-08-28): lo que se
   * corrige es esta página, que era la única fuera de la caja común.
   */
  .account-page { display: flex; flex-direction: column; gap: var(--space-4); }
  /*
   * Mismo bloque superior que el héroe del feed y el de /donaciones (Marco,
   * 2026-08-28): degradado lavanda, esquinas de 22px y el dibujo apoyado en
   * el piso de la tarjeta. Clases propias en vez de reutilizar .hero: esa
   * está calibrada para un titular de tres renglones más un botón, y aquí
   * solo hay título y una línea de apoyo.
   *
   * min-height es lo que le da cuerpo a la banda: con align-self: stretch la
   * altura del dibujo la marca el texto, y dos renglones lo dejaban como una
   * miniatura perdida en una franja delgada.
   */
  .account-header {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: row;
    align-items: stretch;
    gap: 10px;
    min-height: 116px;
    padding: 14px;
    border-radius: 22px;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
  }
  .account-header-text { flex: 1 1 auto; min-width: 0; align-self: center; display: flex; flex-direction: column; gap: var(--space-1); }
  .account-page h1 { margin: 0; font-size: 1.375rem; line-height: 1.12; font-weight: 800; }
  .account-lead { margin: 0; font-size: 0.875rem; line-height: 1.4; color: var(--color-text-muted); }
  /*
   * gato-feed-logo.png, el mismo dibujo del héroe del feed (FEED_HERO_URL) y
   * con el mismo tratamiento: se estira al alto de la tarjeta y el margen
   * negativo cancela el padding para que se apoye exactamente en el borde;
   * overflow: hidden en .account-header recorta dentro del radio. height NO
   * se fija — con una altura pinchada dejaría de estirarse.
   */
  .account-header-illustration {
    flex: 0 0 34%;
    align-self: stretch;
    width: 34%;
    height: auto;
    min-height: 0;
    margin: 0 -14px -14px 0;
    object-fit: contain;
    object-position: bottom center;
  }
  .account-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  .account-card h2 { margin: 0; font-size: 1.125rem; color: var(--color-text); }
  .account-card > p { margin: 0; color: var(--color-text-muted); }
  .account-card-greeting { display: flex; align-items: center; gap: var(--space-3); }
  /* min-width:0 so a long BuscaMascotaN pseudonym can wrap/break instead of
     forcing the flex item (and the whole page) wider than the viewport. */
  .account-card-greeting-text { min-width: 0; }
  .account-card-greeting-text h2 { overflow-wrap: anywhere; }
  .account-card-greeting p { margin: 0.125rem 0 0; color: var(--color-text-muted); }
  .account-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    flex-shrink: 0;
    border-radius: 999px;
    background: var(--color-primary);
    color: var(--color-primary-contrast);
  }
  .account-avatar svg { width: 26px; height: 26px; }
  .account-divider { border: none; border-top: 1px solid var(--color-border); margin: 0; }
  .account-card-heading { display: flex; align-items: flex-start; gap: var(--space-3); }
  .account-card-heading-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    border-radius: 14px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .account-card-heading-icon svg { width: 20px; height: 20px; }
  .account-card-heading p { margin: var(--space-1) 0 0; color: var(--color-text-muted); }
  .account-info { display: flex; flex-direction: column; gap: var(--space-3); margin: 0; }
  .account-info-row { display: flex; gap: var(--space-2); align-items: flex-start; }
  .account-info-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    /* Rounded square, not a circle, to match the reference's icon tiles. */
    border-radius: 14px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .account-info-icon svg { width: 20px; height: 20px; }
  .account-info-text { min-width: 0; }
  .account-info dt { font-size: 0.8125rem; color: var(--color-text-muted); font-weight: 700; }
  .account-info dd { margin: 0.125rem 0 0.25rem; color: var(--color-text); font-weight: 700; overflow-wrap: anywhere; }
  .account-info-text .field-help { margin: 0; }
  /*
   * Un cuadro con borde propio por fila, no el separador de línea superior
   * de antes — así se ve en la referencia. .account-username-form (más
   * abajo) resuelve el input+botón de la fila "Nombre visible"; este es el
   * de las dos acciones de la cuenta (cerrar sesión / eliminar cuenta).
   */
  .account-action {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
  }
  .account-action-main { display: flex; align-items: center; gap: var(--space-2); flex: 1 1 220px; min-width: 0; }
  .account-action-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    border-radius: 14px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .account-action-icon svg { width: 20px; height: 20px; }
  /* Decorative accent colours (not semantic state like --color-danger), used
     only for this row's icon tile — matches the reference's mint-green. */
  .account-action-icon-neutral { background: color-mix(in srgb, #1a9c56 14%, var(--color-surface)); color: #1a9c56; }
  .account-action-icon-danger { background: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface)); color: var(--color-danger); }
  .account-action-text { min-width: 0; }
  .account-action-text h3 { margin: 0; font-size: 1rem; color: var(--color-text); }
  .account-action-danger .account-action-text h3 { color: var(--color-danger); }
  .account-action-text p { margin: var(--space-1) 0 0; font-size: 0.875rem; color: var(--color-text-muted); }
  /*
   * Lilac-bordered, purple-text pill — same visual language as
   * .reunited-counter-cta, kept as its own standalone class (not combined
   * with .button) rather than reused or generalised: .button's own base
   * rule is declared later in this file and would win the min-height/
   * padding/border cascade on any element carrying both classes, and
   * .button-secondary itself is shared sitewide (header account menu,
   * my-posts.tsx) so restyling it would leak everywhere.
   */
  .account-action-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 44px;
    padding: 0 var(--space-3);
    border: 1.5px solid color-mix(in srgb, var(--color-primary) 40%, var(--color-surface));
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-primary);
    font-weight: 600;
    font-size: 0.9375rem;
    cursor: pointer;
    font-family: inherit;
    text-decoration: none;
  }
  .account-action-button svg { width: 16px; height: 16px; }
  .account-action-button:hover { background: var(--color-photo-bg); }
  .account-action-button:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .account-danger-note {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    margin: 0;
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface));
    color: var(--color-text);
    font-size: 0.875rem;
    line-height: 1.5;
  }
  .account-danger-note svg { width: 20px; height: 20px; flex-shrink: 0; color: var(--color-danger); }
  /*
   * Layout only — input/button colours and sizing already come from
   * form.auth-form's shared rules not applying here (this form isn't
   * form.auth-form/form.post-form), so both are spelled out below to match
   * that same look rather than duplicating a class list onto the form tag.
   */
  .account-username-form { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
  .account-username-form input {
    flex: 1 1 200px;
    min-width: 0;
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
    font-family: inherit;
    background: var(--color-surface);
    color: var(--color-text);
  }
  .account-username-form input:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .account-username-form .button { flex-shrink: 0; }
  .account-username-form .form-error { flex-basis: 100%; margin: 0; }
  /* Scoped to .account-page so it never touches .button-danger elsewhere
     (e.g. /publicar's delete-photo button, which has no icon anyway). */
  .account-page .button { gap: 6px; }
  .account-page .button svg { width: 16px; height: 16px; flex-shrink: 0; }
  .account-card > p a[href='/privacidad'] { display: inline-flex; align-items: center; gap: 4px; }
  .account-card > p a[href='/privacidad'] svg { width: 14px; height: 14px; }
  /* Mismos tramos que el héroe del feed, en la misma proporción. */
  @media (min-width: 375px) {
    .account-header { gap: 12px; min-height: 132px; padding: 16px; }
    .account-page h1 { font-size: 1.5rem; }
    .account-header-illustration { flex-basis: 36%; width: 36%; margin: 0 -16px -16px 0; }
  }
  @media (min-width: 560px) {
    .account-header { min-height: 156px; }
    .account-page h1 { font-size: 1.75rem; }
    .account-lead { font-size: 0.9375rem; }
    .account-header-illustration { flex-basis: 38%; width: 38%; }
  }
  @media (min-width: 700px) {
    .account-header { gap: 20px; min-height: 184px; padding: 24px; }
    .account-page h1 { font-size: 2rem; }
    .account-lead { font-size: 1rem; }
    .account-header-illustration { flex-basis: 40%; width: 40%; margin: 0 -8px -24px 0; }
  }

  /*
   * Anclado al borde inferior de la pantalla y acompañando el scroll, como
   * en cualquier web: suelto en el flujo del documento solo se veía al
   * llegar al pie de página, cuando el aviso ya no sirve de nada.
   *
   * El riesgo conocido de esto es tapar lo que quede debajo — una versión
   * anterior dejó el botón "Publicar" literalmente inalcanzable. La defensa
   * NO es renunciar a fijarlo, sino reservar espacio equivalente al final
   * de la página (body.has-cookie-banner), para que cualquier contenido
   * pueda desplazarse por encima del aviso. Sin JavaScript: la reserva es
   * CSS puro y el aviso se sirve ya renderizado desde el servidor.
   */
  /*
   * The fixed layer is a transparent positioning wrapper; the visible card
   * floats inside it. Keeping them separate is what buys the "no pegado a
   * los bordes" margin without the shadow getting clipped, and it keeps this
   * element's own box flush with the bottom offset the bottom-nav stacking
   * below depends on.
   */
  .cookie-banner {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 50;
    padding: var(--space-3);
    pointer-events: none;
  }
  .cookie-banner-card {
    pointer-events: auto;
    max-width: var(--max-width);
    margin: 0 auto;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    /* Soft and wide rather than dark and tight — it should read as lifted
       off the page, not as a heavy modal dropped on top of it. */
    box-shadow: 0 12px 32px rgba(33, 28, 46, 0.18), 0 2px 8px rgba(33, 28, 46, 0.08);
    padding: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }
  .cookie-banner-body { display: flex; align-items: flex-start; gap: var(--space-3); }
  .cookie-banner-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 999px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .cookie-banner-icon svg { width: 26px; height: 26px; }
  .cookie-banner-content { min-width: 0; }
  .cookie-banner-title {
    margin: 0 0 var(--space-1);
    font-size: 1.0625rem;
    font-weight: 800;
    color: var(--color-text);
  }
  .cookie-banner-text { margin: 0; font-size: 0.875rem; line-height: 1.5; color: var(--color-text-muted); }
  .cookie-banner-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    margin-top: var(--space-2);
    /* Tall enough to be a comfortable touch target on its own row. */
    min-height: 44px;
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--color-primary);
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  .cookie-banner-link:hover { color: var(--color-primary-dark); }
  .cookie-banner-link-icon svg { width: 17px; height: 17px; display: block; }
  .cookie-banner-link-arrow svg { width: 15px; height: 15px; display: block; }
  .cookie-banner-link:hover .cookie-banner-link-arrow { transform: translateX(2px); }
  .cookie-banner-link-arrow { transition: transform 0.15s ease; }
  .cookie-banner-separator {
    margin: 0;
    border: 0;
    border-top: 1px solid var(--color-border);
  }
  /*
   * Mobile-first: two equal columns that fill the row. From 560px up they
   * shrink to their content and split to the two ends, which is the desktop
   * arrangement asked for (Rechazar left, Aceptar right).
   */
  .cookie-banner-actions { display: flex; gap: var(--space-3); }
  .cookie-banner-button {
    flex: 1 1 0;
    min-width: 0;
    min-height: 48px;
    padding: var(--space-2) var(--space-4);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    border-radius: 999px;
    font-family: inherit;
    font-size: 0.9375rem;
    font-weight: 700;
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
  }
  .cookie-banner-button-reject {
    background: var(--color-surface);
    border: 2px solid var(--color-primary);
    color: var(--color-primary);
  }
  .cookie-banner-button-reject:hover { background: var(--color-photo-bg); }
  .cookie-banner-button-reject:active { background: color-mix(in srgb, var(--color-primary) 14%, var(--color-surface)); }
  .cookie-banner-button-accept {
    background: var(--color-primary);
    border: 2px solid var(--color-primary);
    color: var(--color-primary-contrast);
  }
  .cookie-banner-button-accept:hover {
    background: var(--color-primary-dark);
    border-color: var(--color-primary-dark);
  }
  .cookie-banner-button-accept:active { background: var(--color-primary-dark); }
  .cookie-banner-button:disabled { opacity: 0.55; cursor: not-allowed; }
  /* The small white disc with a purple tick, sitting after the word. */
  .cookie-banner-button-check {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 999px;
    background: var(--color-primary-contrast);
    color: var(--color-primary);
    flex-shrink: 0;
  }
  .cookie-banner-button-check svg { width: 12px; height: 12px; }
  .cookie-banner-button:focus-visible, .cookie-banner-link:focus-visible {
    outline: 3px solid var(--color-primary-dark);
    outline-offset: 2px;
  }
  @media (min-width: 560px) {
    .cookie-banner-actions { justify-content: space-between; }
    .cookie-banner-button { flex: 0 0 auto; min-width: 9.5rem; }
  }
  @media (max-width: 400px) {
    .cookie-banner-card { padding: var(--space-3); }
    .cookie-banner-icon { width: 36px; height: 36px; }
    .cookie-banner-icon svg { width: 21px; height: 21px; }
    .cookie-banner-button { padding: var(--space-2); }
  }
  /*
   * La reserva es generosa a propósito y cubre el caso más alto (el aviso
   * envuelto en tres líneas a 320px). Solo existe mientras el aviso está
   * arriba: al aceptar o rechazar, la clase desaparece con él.
   */
  @media (max-width: 560px) {
    :root { --cookie-banner-reserve: 23rem; }
  }
  body.has-cookie-banner { padding-bottom: var(--cookie-banner-reserve); }

  /*
   * Reduced app-style bottom nav: only the 3 destinations that already exist
   * (Inicio/Publicar/Mis publicaciones) — no Mensajes/Guardados/Perfil, those
   * are MVP 2. Phone/small-tablet only (≤768px); the header nav above
   * already covers navigation on larger screens, and a permanent fixed bar
   * pinned to a desktop browser window would look out of place.
   *
   * Real interaction with the cookie banner, both use position:fixed;bottom:0
   * and would overlap: this bar is PERMANENT (every page, always), the
   * banner is TEMPORARY (first visit only). body always reserves this bar's
   * height while it can show; the banner sits ABOVE it (not under it) and
   * body reserves the sum of both while the banner is also showing — same
   * "reserve space, never let fixed content hide something reachable"
   * reasoning as the banner's own comment above (a prior version made
   * "Publicar" unreachable by skipping this).
   */
  .bottom-nav { display: none; }
  @media (max-width: 768px) {
    .bottom-nav {
      display: flex;
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      z-index: 40;
      height: var(--bottom-nav-height);
      padding-bottom: env(safe-area-inset-bottom);
      background: var(--color-surface);
      border-top: 1px solid var(--color-border);
      /* Lifts the bar off whatever content ends up right above it (the
         footer, most of the time) — a plain 1px border read as the footer
         and the nav running straight into each other with no separation. */
      box-shadow: 0 -4px 12px rgba(33, 28, 46, 0.12);
      align-items: stretch;
      justify-content: space-around;
    }
    body { padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom)); }
    body.has-cookie-banner {
      padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom) + var(--cookie-banner-reserve));
    }
    .cookie-banner { bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom)); }
  }
  .bottom-nav-item {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.125rem;
    text-decoration: none;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
  }
  .bottom-nav-item svg { width: 22px; height: 22px; }
  .bottom-nav-item[aria-current='page'] { color: var(--color-primary-dark); }
  .bottom-nav-item:focus-visible { outline: 3px solid var(--color-primary); outline-offset: -3px; }
  /*
   * The center "Publicar" tab gets a filled circle behind its icon for a
   * touch of emphasis (it's the primary action), but sized to sit in the
   * same normal flow as "Inicio"/"Mis publicaciones" — not floating,
   * raised, or dramatically bigger. It previously used position:absolute
   * at 52px, well over double the plain 22px icons next to it, and a
   * .bottom-nav-item-fab span rule that (being a plain descendant
   * selector) matched BOTH the circle and the "Publicar" label span,
   * pushing the label out of place and tinting it the wrong color.
   */
  .bottom-nav-item-fab-circle {
    width: 34px;
    height: 34px;
    border-radius: 999px;
    background: var(--color-primary);
    color: var(--color-primary-contrast);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 6px rgba(33, 28, 46, 0.25);
  }
  .bottom-nav-item-fab-circle svg { width: 18px; height: 18px; }
  .bottom-nav-item-fab[aria-current='page'] .bottom-nav-item-fab-circle { background: var(--color-primary-dark); }
  .button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 48px;
    padding: var(--space-2) var(--space-4);
    border-radius: 999px;
    font-weight: 600;
    font-size: 1rem;
    text-decoration: none;
    border: 2px solid transparent;
    cursor: pointer;
    font-family: inherit;
  }
  .button-primary { background: var(--color-primary); color: var(--color-primary-contrast); }
  .button-primary:hover { background: var(--color-primary-dark); }
  .button-secondary { background: var(--color-surface); color: var(--color-text); border-color: var(--color-border); }
  .button-danger { color: var(--color-danger); border-color: var(--color-danger); background: var(--color-surface); }
  /* Generic gap: no .button anywhere had a disabled look before — it just
     fell back to the browser default, which barely reads as "inactive". */
  .button:disabled { opacity: 0.6; cursor: not-allowed; }
  .button-block { width: 100%; }
  .cta-row { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-4); }
  @media (min-width: 480px) {
    .cta-row { flex-direction: row; }
  }
  /*
   * Featured horizontal card matching the approved reference
   * (prompts/feed-reference.png): trophy | big number over a two-line
   * caption | outlined CTA, all on one compact row.
   *
   * The CTA shares that row from 480px up — the width at which the caption
   * still keeps ~170px, enough for the two lines the reference shows. Below
   * that it drops to its own row: inline at 320-430px would leave the
   * caption 50-100px, spilling 44 characters into 4+ lines and making the
   * card TALLER than the wrap does. Trophy and text stay side by side at
   * every width, so the horizontal composition itself never collapses.
   */
  .reunited-counter {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      'trophy text'
      'cta cta';
    align-items: center;
    gap: 8px 12px;
    min-height: 78px;
    /* No margin-top: main's own var(--space-4) padding already separates this
       from the header, and the extra var(--space-3) on top of it just pushed
       the feed down. Only this card's own margin — main's padding is shared
       with every other page and stays as it is. */
    margin: 0;
    padding: 12px 14px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 20px;
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  /* height, not width: the source art is wider than it is tall, so sizing by
     height is what actually controls how much room it takes in the row. */
  .reunited-counter-trophy { grid-area: trophy; width: auto; height: 44px; }
  .reunited-counter-text { grid-area: text; min-width: 0; margin: 0; }
  .reunited-counter-number {
    display: block;
    font-size: 1.625rem;
    line-height: 1.1;
    font-weight: 800;
    color: var(--color-primary);
  }
  .reunited-counter-caption {
    display: block;
    margin-top: 2px;
    font-size: 0.8125rem;
    line-height: 1.3;
    color: var(--color-text);
  }
  /*
   * Its own outlined treatment instead of .button/.button-secondary: that
   * shared pair is grey-bordered with dark text and a fixed 48px floor,
   * where the reference wants a lilac-bordered, purple-text pill that drops
   * to 40px on a phone. Restyling .button-secondary itself would leak into
   * every other component using it.
   */
  .reunited-counter-cta {
    grid-area: cta;
    justify-self: start;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 38px;
    padding: 0 14px;
    border: 1.5px solid color-mix(in srgb, var(--color-primary) 40%, var(--color-surface));
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-primary);
    font-weight: 600;
    font-size: 0.8125rem;
    text-decoration: none;
    white-space: nowrap;
  }
  .reunited-counter-cta svg { width: 16px; height: 16px; flex-shrink: 0; }
  .reunited-counter-cta:hover { border-color: var(--color-primary); }
  .reunited-counter-cta:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  @media (min-width: 480px) {
    .reunited-counter {
      grid-template-columns: auto minmax(0, 1fr) auto;
      grid-template-areas: 'trophy text cta';
      gap: 12px;
      padding: 14px 16px;
    }
    .reunited-counter-trophy { height: 48px; }
    .reunited-counter-number { font-size: 1.75rem; }
    .reunited-counter-cta { justify-self: end; min-height: 40px; }
  }
  /*
   * 768px, not 700: below it the caption wraps to two lines at the larger
   * type and the card grows to ~121px, past the ~95-115px this card is
   * meant to hold. 768 is also the tablet width responsive.spec.ts already
   * exercises, so the tier boundary lands where the suite looks.
   */
  @media (min-width: 768px) {
    .reunited-counter { gap: 20px; min-height: 100px; padding: 18px 22px; }
    .reunited-counter-trophy { height: 58px; }
    .reunited-counter-number { font-size: 2.25rem; }
    .reunited-counter-caption { font-size: 1rem; }
    .reunited-counter-cta { min-height: 48px; padding: 0 22px; font-size: 0.9375rem; }
    .reunited-counter-cta svg { width: 18px; height: 18px; }
  }
  /*
   * Gradient banner card matching the approved reference
   * (prompts/feed-reference.png): heading, lead paragraph and the primary
   * button stacked on the left, illustration filling the right side and
   * resting on the card's bottom edge.
   *
   * Text-left/art-right at EVERY width, phones included — stacking the art
   * underneath on mobile was the old behaviour and read as a separate blob
   * below the card instead of part of it. Keeping the row also narrows the
   * text column, which is what makes the heading fall into the three lines
   * the reference shows without hard-coding line breaks.
   *
   * The artwork (prompts/gato-feed-logo.png) has a real alpha channel, so it
   * sits straight on this gradient with no plate of its own — see
   * src/assets/feed-hero.ts for why keeping that alpha through the encode
   * matters.
   */
  .hero {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: row;
    align-items: stretch;
    gap: 10px;
    margin-top: var(--space-3);
    padding: 14px;
    border-radius: 22px;
    background: linear-gradient(135deg, #efe9fb 0%, #f7f1fb 60%, #fbeff6 100%);
  }
  .hero-text { flex: 1 1 auto; min-width: 0; align-self: center; }
  /* 1.1875rem is what keeps the heading at the three lines the reference
     shows once the art takes its column — at 1.25rem the 320px text column
     is ~149px and "Ayudemos a que" no longer fits on one line, spilling to
     four. */
  .hero h1 { margin: 0; font-size: 1.1875rem; line-height: 1.1; font-weight: 800; color: var(--color-text); }
  .hero-lead { margin: 8px 0 0; font-size: 0.8125rem; line-height: 1.4; color: var(--color-text-muted); }
  /* Overrides .cta-row's shared var(--space-4); .cta-row is only ever used by
     this hero, so this is not reaching into another component's spacing. */
  .hero .cta-row { margin-top: 12px; }
  .hero-cta { gap: var(--space-1); min-height: 44px; padding: 0 12px; font-size: 0.8125rem; }
  .hero-cta svg { width: 16px; height: 16px; flex-shrink: 0; }
  /*
   * align-self: stretch is what makes the art fill the card's full height
   * rather than sitting as a small centred thumbnail — it only applies while
   * height stays auto, so height must NOT be pinned here (the width/height
   * attributes on the <img> still reserve layout space before it loads).
   * object-fit: contain keeps the aspect ratio, object-position: bottom rests
   * the animals on the floor of the card, and the negative bottom margin
   * cancels the card's own padding so they touch that edge exactly as in the
   * reference. .hero's overflow: hidden clips anything that runs past it.
   */
  .hero-illustration {
    flex: 0 0 38%;
    align-self: stretch;
    width: 38%;
    height: auto;
    min-height: 0;
    /* Bleeds into the card's own padding on both edges so a square image
       still reads as generous next to a narrow phone text column — .hero's
       overflow: hidden keeps the bleed inside the rounded corners. */
    margin: 0 -14px -14px 0;
    object-fit: contain;
    object-position: bottom center;
  }
  @media (max-width: 359px) {
    .hero-illustration { flex-basis: 36%; width: 36%; }
  }
  @media (min-width: 375px) {
    .hero { gap: 12px; padding: 16px; }
    .hero h1 { font-size: 1.375rem; }
    .hero-lead { font-size: 0.875rem; }
    .hero-cta { padding: 0 14px; font-size: 0.875rem; }
    .hero-illustration { flex-basis: 40%; width: 40%; margin: 0 -16px -16px 0; }
  }
  @media (min-width: 430px) {
    .hero h1 { font-size: 1.5rem; }
    .hero-cta { padding: 0 16px; }
    .hero-illustration { flex-basis: 42%; width: 42%; }
  }
  @media (min-width: 560px) {
    .hero h1 { font-size: 1.75rem; }
    .hero-lead { margin-top: 10px; }
  }
  @media (min-width: 700px) {
    .hero { gap: 20px; padding: 24px; min-height: 340px; }
    .hero-text { flex: 0 1 54%; align-self: center; }
    .hero-illustration { flex: 0 0 46%; width: 46%; margin: 0 -8px -24px 0; }
    .hero h1 { font-size: 2.375rem; }
    .hero-lead { margin-top: 12px; max-width: 34ch; font-size: 1rem; }
    .hero .cta-row { margin-top: 18px; }
    .hero-cta { gap: var(--space-2); min-height: 50px; padding: 0 24px; font-size: 1rem; }
    .hero-cta svg { width: 20px; height: 20px; }
  }
  /*
   * On a phone the hero pushed every pet below the fold — the opposite of
   * what compact cards were for. The hero's own sizing now lives in the
   * min-width tiers above (mobile-first), so what is left here is the
   * tightening the filter panel and the post list want on a small screen.
   */
  @media (max-width: 480px) {
    details.filter-panel { margin-top: var(--space-2); }
    .post-list { margin-top: var(--space-2); gap: var(--space-2); }
  }
  .empty-state {
    margin-top: var(--space-5);
    padding: var(--space-5) var(--space-3);
    text-align: center;
    background: var(--color-surface);
    border: 1px dashed var(--color-border);
    border-radius: var(--radius);
    color: var(--color-text-muted);
  }
  /*
   * margin block/inline written out separately: the block side stays 0 (the
   * default form margin added a 16th px below the submit button that had
   * nothing to do with this layout's own rhythm), while the inline side
   * centres the column.
   *
   * On a phone the form fills the width, so centring changes nothing there.
   * It only shows up once the page is wider than 480px, where the old
   * "margin: 0" pinned the form to the left edge and left a growing band of
   * empty space to its right — on a tablet, ~240px of it. .login-page already
   * centred itself, so /publicar was the odd one out, not a deliberate look.
   */
  form.auth-form, form.post-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    max-width: 480px;
    margin-block: 0;
    margin-inline: auto;
  }
  form.auth-form label, form.post-form label:not(.kind-card):not(.photo-dropzone):not(.checkbox-row), .form-legend,
  form.filter-bar label, form.filter-bar legend, .close-case-note label {
    font-weight: 600;
    font-size: 0.9375rem;
    display: block;
    margin-bottom: var(--space-1);
  }
  form.auth-form input, form.post-form input, form.post-form textarea {
    width: 100%;
    min-height: 48px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
    font-family: inherit;
    background: var(--color-surface);
    color: var(--color-text);
  }
  /*
   * /iniciar-sesion AND /crear-cuenta, both scoped under .login-page
   * throughout (the "login-" prefix predates the register redesign reusing
   * it — kept as-is rather than churning class names across an already
   * shipped, already-tested page for a rename with no behavioural upside).
   * Everything form.auth-form/input above still applies underneath, this
   * only adds the card, icons and layout around it. Column capped at 480px
   * (the same width form.auth-form already used on its own) so the form
   * never becomes a wide, bare desktop strip; the rest of the viewport is
   * left as visual breathing room, centred via margin:auto rather than a
   * second max-width on main.
   */
  .login-page { max-width: 480px; margin: 0 auto; }
  .login-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    /* Same barely-there shadow token as .report-hero — a floating card, not a modal. */
    box-shadow: 0 6px 20px rgba(33, 28, 46, 0.07), 0 1px 3px rgba(33, 28, 46, 0.04);
    padding: var(--space-4);
    text-align: center;
  }
  .login-illustration {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 220px;
    height: 220px;
    margin: 0 auto var(--space-3);
    border-radius: 999px;
    background: var(--color-photo-bg);
  }
  /* contain, never cover — object-fit:cover would crop the illustration; see the same rule on .report-hero-illustration for why that matters here. */
  .login-illustration img { width: 84%; height: 84%; object-fit: contain; display: block; }
  .login-title { margin: 0; font-size: 1.625rem; font-weight: 800; color: var(--color-text); }
  .login-subtitle { margin: var(--space-1) 0 var(--space-4); font-size: 0.9375rem; color: var(--color-text-muted); }
  .login-form { text-align: left; }
  .login-input-wrap { position: relative; }
  /*
   * .login-page prefixed on purpose, not just .login-input-wrap input: the
   * shared "form.auth-form input" padding rule above has specificity
   * (0,1,2) — one class plus two elements — which already beats a bare
   * class+element selector like (0,1,1). Without the extra .login-page
   * class here this padding silently lost and the icon sat on top of the
   * placeholder text (confirmed by screenshot, not by inspection).
   */
  .login-page .login-input-wrap input { padding-left: 2.75rem; }
  .login-page .login-input-wrap input.login-input-has-trailing { padding-right: 2.75rem; }
  .login-input-icon {
    position: absolute;
    left: var(--space-3);
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    color: var(--color-text-muted);
    pointer-events: none;
  }
  .login-input-icon svg { width: 18px; height: 18px; }
  .login-password-toggle {
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 0;
    border-radius: 999px;
    color: var(--color-text-muted);
    cursor: pointer;
  }
  .login-password-toggle:hover { color: var(--color-primary); background: var(--color-photo-bg); }
  .login-password-toggle svg { width: 19px; height: 19px; }
  /* Only one of the two eye states shows at a time — enhance-forms.js toggles this class, the input's own type, and aria-pressed together. */
  .login-password-toggle .login-eye-hide { display: none; }
  .login-password-toggle[aria-pressed='true'] .login-eye-show { display: none; }
  .login-password-toggle[aria-pressed='true'] .login-eye-hide { display: block; }
  .login-forgot { text-align: right; margin: var(--space-1) 0 0; }
  .login-forgot-link {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    font-size: 0.875rem;
    color: var(--color-primary);
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  .login-forgot-link:disabled { color: var(--color-text-muted); opacity: 0.8; cursor: not-allowed; text-decoration: none; }
  /*
   * /crear-cuenta's terms/privacy notice. Left-aligned (unlike .login-trust
   * below, which is short and centred) — this is a full running sentence
   * with two inline links, and centring several lines of prose like that
   * reads worse than a plain left-aligned paragraph. The icon gets its own
   * class rather than reusing .login-trust-icon: that one's lilac circle is
   * meant to sit on a WHITE card, and this box's own background already IS
   * lilac, so the circle needs to be white here to still read as a distinct
   * badge instead of blending into the box behind it.
   */
  .register-terms {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-2) var(--space-3);
    background: var(--color-photo-bg);
    border-radius: var(--radius-sm);
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--color-text);
    text-align: left;
  }
  .register-terms a { color: var(--color-primary); font-weight: 600; }
  .register-terms-icon {
    flex-shrink: 0;
    margin-top: 1px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-primary);
  }
  .register-terms-icon svg { width: 14px; height: 14px; }
  .login-submit { margin-top: var(--space-1); gap: var(--space-2); position: relative; }
  .login-submit svg { width: 18px; height: 18px; }
  .login-submit-spinner {
    display: none;
    width: 18px;
    height: 18px;
    border: 2px solid rgba(255, 255, 255, 0.45);
    border-top-color: #fff;
    border-radius: 999px;
  }
  .login-submit:disabled svg:not(.login-submit-spinner) { display: none; }
  .login-submit:disabled .login-submit-spinner { display: inline-block; animation: login-spin 0.7s linear infinite; }
  @keyframes login-spin { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) {
    .login-submit-spinner { animation: none; }
  }
  .login-divider {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-4) 0 var(--space-3);
    color: var(--color-text-muted);
    font-size: 0.8125rem;
  }
  .login-divider::before, .login-divider::after { content: ''; flex: 1; height: 1px; background: var(--color-border); }
  .login-social-label { margin: 0 0 var(--space-1); font-size: 0.875rem; color: var(--color-text-muted); text-align: center; }
  .login-soon-badge {
    display: inline-block;
    margin-left: var(--space-1);
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
    font-size: 0.6875rem;
    font-weight: 700;
  }
  .login-social-row { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
  .login-social-button {
    flex: 1;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 48px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-text-muted);
    font-weight: 600;
    font-size: 0.9375rem;
    font-family: inherit;
    cursor: not-allowed;
  }
  .login-social-button:disabled { opacity: 0.6; }
  .login-social-button svg { width: 18px; height: 18px; flex-shrink: 0; }
  /* 320-340px is tight for two pill buttons with an icon and a word each — stack rather than let text wrap or truncate. */
  @media (max-width: 340px) {
    .login-social-row { flex-direction: column; }
  }
  .login-signup { margin: var(--space-4) 0 0; font-size: 0.9375rem; text-align: center; }
  /* Colour and emoji stay (the global a-element rule already sets the purple) — only the underline goes. */
  .login-signup-link { text-decoration: none; }
  .login-trust {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    margin: var(--space-3) 0 0;
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 0.8125rem;
    color: var(--color-text);
    text-align: center;
  }
  .login-trust-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 999px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
  }
  .login-trust-icon svg { width: 15px; height: 15px; }
  @media (max-width: 380px) {
    .login-illustration { width: 150px; height: 150px; }
    .login-title { font-size: 1.375rem; }
    .login-card { padding: var(--space-3); }
  }

  form.post-form textarea { min-height: 96px; resize: vertical; }
  form.post-form select {
    width: 100%;
    min-height: 48px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
    font-family: inherit;
    background: var(--color-surface);
    color: var(--color-text);
  }
  /* Persistent guidance, not a placeholder that vanishes once typing starts. */
  .field-help {
    margin: var(--space-1) 0 0;
    font-size: 0.875rem;
    font-weight: 400;
    color: var(--color-text-muted);
  }
  /*
   * Visually-hidden-but-focusable — the standard accessible pattern for a
   * custom-styled radio/checkbox/file input: stays in the DOM, keyboard-
   * reachable, announced by a screen reader via its <label>, just not
   * rendered as the native widget. Shared by the "¿Qué pasó?" cards and the
   * photo dropzone below.
   */
  .sr-only-input {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  /*
   * Same visually-hidden technique, for plain text rather than a focusable
   * control — the explanatory sentence under each "¿Qué pasó?" card, kept
   * for screen readers via aria-describedby without cluttering the compact
   * visual card.
   */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  /*
   * Two equal-size cards, always side by side (a CSS grid, not flex-wrap)
   * — with only two options and a compact icon+label layout, staying 2-up
   * even at 320px reads better than stacking, and trivially guarantees
   * "same size" without extra rules.
   */
  /*
   * Bold and in the danger red so it is legible as a marker rather than a
   * stray punctuation glyph. Never the ONLY signal: the fields also carry a
   * real required attribute for assistive tech, and the legend below spells
   * out what the symbol means for everyone else (CLAUDE.md §14 — no meaning
   * by colour alone).
   */
  .required-mark { color: var(--color-danger); font-weight: 800; }
  /*
   * Header card for /publicar. Mobile-first: illustration stacked above the
   * text and centred, since side-by-side at 320px leaves the title about
   * eight characters of width. From 480px up it becomes the row the design
   * asks for — art on the left, a hairline divider, text on the right.
   */
  .report-hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4);
    margin-bottom: var(--space-3);
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    /* Barely-there, per the brief: enough to lift the card off the lavender
       page behind it without reading as a modal. */
    box-shadow: 0 6px 20px rgba(33, 28, 46, 0.07), 0 1px 3px rgba(33, 28, 46, 0.04);
    text-align: center;
  }
  .report-hero-figure {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 108px;
    height: 108px;
    border-radius: 999px;
    background: var(--color-photo-bg);
  }
  /* contain, never cover: the artwork must not be cropped or stretched. */
  .report-hero-illustration {
    width: 78%;
    height: 78%;
    object-fit: contain;
    display: block;
  }
  .report-hero-text { min-width: 0; }
  .report-hero h1 {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 800;
    line-height: 1.2;
    color: var(--color-text);
  }
  .report-hero-subtitle {
    margin: var(--space-2) 0 0;
    font-size: 0.9375rem;
    line-height: 1.5;
    color: var(--color-text-muted);
  }
  @media (min-width: 480px) {
    .report-hero {
      flex-direction: row;
      align-items: center;
      gap: var(--space-4);
      padding: var(--space-5) var(--space-4);
      text-align: left;
    }
    .report-hero-figure { width: 132px; height: 132px; }
    /* The subtle vertical rule from the reference, as a border on the text
       rather than an extra empty element in the markup. */
    .report-hero-text {
      border-left: 1px solid var(--color-border);
      padding-left: var(--space-4);
    }
    .report-hero h1 { font-size: 1.875rem; }
  }
  /*
   * Informative, not an alert: soft lilac and an info icon, deliberately not
   * the red of .form-error. The asterisk keeps .required-mark so it is the
   * same red as the one beside each required label, and the sentence spells
   * the rule out in words so the colour is never carrying it alone.
   */
  .required-note {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-4);
    padding: var(--space-2) var(--space-3);
    background: var(--color-photo-bg);
    border-radius: var(--radius-sm);
    font-size: 0.875rem;
    color: var(--color-text);
  }
  .required-note-icon {
    flex-shrink: 0;
    display: flex;
    color: var(--color-primary);
  }
  .required-note-icon svg { width: 18px; height: 18px; display: block; }
  .kind-choice { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
  .kind-option { display: flex; flex-direction: column; }
  .kind-card {
    /* Positioning context for .sr-only-input's top:0;left:0 — without this,
       the absolutely-positioned radio has no local anchor and can land
       anywhere up the ancestor chain, ending up behind unrelated elements
       (this broke real clicks/checks in Playwright — "header intercepts
       pointer events" — until this was added). */
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 128px;
    padding: var(--space-3);
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease;
  }
  .kind-card-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    border-radius: 999px;
    background: var(--color-photo-bg);
    color: var(--color-text-muted);
  }
  .kind-card-icon svg { width: 24px; height: 24px; }
  .kind-card-label { font-weight: 700; }
  /* Selected state: solid-fill icon circle + tinted border, same "solid fill
     reads as a confident status signal" language as the status badges. */
  .kind-card-perdida:has(.sr-only-input:checked) {
    border-color: var(--color-perdida);
    background: color-mix(in srgb, var(--color-perdida) 8%, var(--color-surface));
  }
  .kind-card-perdida:has(.sr-only-input:checked) .kind-card-icon {
    background: var(--color-perdida);
    color: var(--color-primary-contrast);
  }
  .kind-card-encontrada:has(.sr-only-input:checked) {
    border-color: var(--color-encontrada);
    background: color-mix(in srgb, var(--color-encontrada) 8%, var(--color-surface));
  }
  .kind-card-encontrada:has(.sr-only-input:checked) .kind-card-icon {
    background: var(--color-encontrada);
    color: var(--color-primary-contrast);
  }
  .kind-card:has(.sr-only-input:focus-visible), .photo-dropzone:has(.sr-only-input:focus-visible) {
    outline: 3px solid var(--color-primary);
    outline-offset: 2px;
  }
  .kind-option .field-help { text-align: center; }
  /*
   * "Ubicación actual" solo aplica a "Mascota extraviada" — oculto por
   * defecto (cubre también "todavía no eligió nada"), revelado por CSS puro
   * cuando esa tarjeta queda marcada. Sin JavaScript: sigue funcionando con
   * scripts desactivados, igual que el resto de este formulario. display:none
   * no impide que el campo se envíe si tuviera un valor — solo empieza vacío
   * en modo creación, que es el único modo donde esta regla aplica.
   */
  .field-only-encontrada { display: none; }
  form.post-form:has(.kind-card-encontrada .sr-only-input:checked) .field-only-encontrada {
    display: block;
  }

  /*
   * Big dropzone-style upload target. A <label for="images"> — clicking it
   * opens the native file picker with no JavaScript needed, same as the
   * card selector above. The 4 thumbnail slots below are a JS-only preview
   * layered on top (see enhance-forms.js): without JS this box still opens
   * the picker and the form still submits normally, just without the live
   * preview.
   */
  .photo-dropzone {
    /* Positioning context for .sr-only-input — see the comment on .kind-card. */
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
    padding: var(--space-5) var(--space-3);
    border: 2px dashed var(--color-border);
    border-radius: var(--radius);
    background: var(--color-photo-bg);
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease;
  }
  .photo-dropzone:hover { border-color: var(--color-primary); }
  .photo-dropzone-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-primary);
  }
  .photo-dropzone-icon svg { width: 28px; height: 28px; }
  .photo-dropzone-title { font-weight: 800; font-size: 1.0625rem; color: var(--color-text); }
  .photo-dropzone-subtitle { font-size: 0.875rem; color: var(--color-text-muted); }
  /*
   * Four across, as the design asked — except under 360px, where that left
   * each thumbnail about 65px wide with nowhere to put the quitar/portada
   * controls at a size a thumb can hit. Only the narrowest phones drop to
   * two columns; every common width keeps the single row.
   */
  .photo-thumb-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-2);
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
  }
  @media (min-width: 360px) {
    .photo-thumb-grid { grid-template-columns: repeat(4, 1fr); }
  }
  /* Room is tight at four across on a phone, so the controls shrink rather
     than the thumbnail: the remove button keeps a usable disc and the cover
     pill drops to its shortest legible form. */
  @media (min-width: 360px) and (max-width: 520px) {
    .photo-thumb-remove { width: 26px; height: 26px; font-size: 1rem; }
    .photo-thumb-action { font-size: 0.5625rem; min-height: 22px; }
  }
  .photo-thumb-slot {
    position: relative;
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-photo-bg);
    color: var(--color-text-muted);
    overflow: hidden;
  }
  /* A faint "+" on the still-empty slots hints there's room for more —
     pure CSS, so it shows even before any JS enhancement runs. */
  .photo-thumb-slot:not(.photo-thumb-slot-filled)::after {
    content: '+';
    font-size: 1.5rem;
    opacity: 0.35;
  }
  .photo-thumb-slot-filled { border-style: solid; }
  .photo-thumb-slot img { width: 100%; height: 100%; object-fit: contain; display: block; }
  .photo-thumb-cover-badge {
    position: absolute;
    bottom: 2px;
    left: 2px;
    right: 2px;
    background: var(--color-primary);
    color: var(--color-primary-contrast);
    font-size: 0.625rem;
    font-weight: 700;
    text-align: center;
    border-radius: 999px;
    padding: 1px 4px;
    line-height: 1.4;
  }
  /*
   * "Portada" on a photo that is not the cover yet: same pill in the same
   * place as the badge it would replace, so choosing one visibly promotes it
   * rather than moving the eye somewhere else. Outlined instead of filled so
   * the current cover still reads as the selected one.
   */
  .photo-thumb-action {
    position: absolute;
    bottom: 2px;
    left: 2px;
    right: 2px;
    min-height: 24px;
    background: var(--color-surface);
    color: var(--color-primary);
    border: 1px solid var(--color-primary);
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 700;
    text-align: center;
    border-radius: 999px;
    padding: 1px 4px;
    line-height: 1.4;
    cursor: pointer;
  }
  .photo-thumb-action:hover { background: var(--color-photo-bg); }
  .photo-thumb-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 999px;
    background: var(--color-danger);
    color: var(--color-primary-contrast);
    font-family: inherit;
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
  }
  .photo-thumb-remove:hover { background: var(--color-text); }
  .photo-thumb-action:focus-visible, .photo-thumb-remove:focus-visible {
    outline: 3px solid var(--color-primary-dark);
    outline-offset: 2px;
  }
  /*
   * The dial code and the plain number sit side by side — [+57] [3001234567]
   * — instead of stacked, so the pairing reads the way people actually think
   * about a phone number. The dial code box only needs to fit "+" plus a
   * handful of digits; the number field takes whatever space is left.
   */
  .phone-row { display: flex; gap: var(--space-2); align-items: flex-start; }
  .phone-dial-code { flex: 0 0 6rem; position: relative; }
  .phone-number { flex: 1 1 auto; min-width: 0; }
  /*
   * Basic, deliberately: a plain bordered list, nothing more, so this isn't
   * something to keep polishing right now. Capped height keeps it from
   * dwarfing the form on a small screen — the rest scrolls.
   */
  .phone-dial-menu {
    position: absolute;
    z-index: 20;
    top: calc(100% + 4px);
    left: 0;
    min-width: 14rem;
    max-height: 12rem;
    overflow-y: auto;
    margin: 0;
    padding: var(--space-1) 0;
    list-style: none;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 4px 12px rgba(33, 28, 46, 0.16);
    font-size: 0.875rem;
  }
  .phone-dial-menu[hidden] { display: none; }
  .phone-dial-menu li {
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
  }
  .phone-dial-menu li:hover { background: var(--color-bg); }
  /* Same searchable-dropdown widget as the phone dial code above, applied to "País" — see the JS for why. */
  .location-country-field { position: relative; }
  .location-country-menu {
    position: absolute;
    z-index: 20;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    max-height: 12rem;
    overflow-y: auto;
    margin: 0;
    padding: var(--space-1) 0;
    list-style: none;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 4px 12px rgba(33, 28, 46, 0.16);
    font-size: 0.875rem;
  }
  .location-country-menu[hidden] { display: none; }
  .location-country-menu li {
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
  }
  .location-country-menu li:hover { background: var(--color-bg); }
  /*
   * Excluded from the shared label rule above only because of its
   * display:block — a checkbox and its text belong on one row. The size is
   * repeated here on purpose: adding this class back to that rule would drag
   * display:block along with them, and since :not() carries specificity,
   * out-ranking it turns into an arms race instead of a fix. align-items is
   * flex-start, not center, so the box stays level with the FIRST line when
   * the text wraps rather than drifting between the lines.
   *
   * font-weight is 700, not the shared rule's 600: measured side by side in
   * Nunito (the site's font), 600 is nearly indistinguishable from 400 for
   * this specific string — the "wght" axis barely moves between them, and it
   * only reads as visibly bold from 700 up. Matching the other labels'
   * declared 600 would have kept this looking unbolded.
   */
  .checkbox-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-weight: 700;
    font-size: 0.9375rem;
  }
  .checkbox-row:has(input:disabled) { color: var(--color-text-muted); }
  .checkbox-row input { width: auto; min-height: 0; }
  /*
   * Smaller than the default browser checkbox on purpose — this is a single
   * secondary toggle next to its own field, not a primary action. The
   * clickable area stays large regardless: the whole label row (text
   * included) toggles it, not just the 18px box itself.
   */
  /*
   * Has to outrank "form.post-form input" (0,1,2), which gives every field in
   * the form a 48px min-height for touch. Applied to a checkbox that produced
   * an 18px-wide, 48px-TALL control whose checkmark floated far from the text
   * it belongs to — the row stays easy to tap via the full-width label, not
   * by stretching the box itself.
   */
  form.post-form label.checkbox-row input[type='checkbox'] {
    width: 18px;
    height: 18px;
    min-height: 0;
    /*
     * Centers the 18px box against the FIRST line of the label (15px text at
     * the body's 1.5 line-height = a 22.5px line box), so it stays put
     * whether the text fits on one line or wraps to two.
     */
    margin: 2px 0 0;
    accent-color: var(--color-primary);
    flex-shrink: 0;
  }
  form.auth-form input:focus-visible, form.post-form input:focus-visible, form.post-form textarea:focus-visible,
  .button:focus-visible, nav.site-nav a.nav-link:focus-visible, nav.site-nav button.nav-link:focus-visible,
  .photo-carousel-arrow:focus-visible, .legal-menu > summary:focus-visible, .legal-menu-links a:focus-visible,
  .account-menu-logout:focus-visible,
  .photo-carousel-open:focus-visible, .photo-lightbox-close:focus-visible,
  .login-password-toggle:focus-visible {
    outline: 3px solid var(--color-primary);
    outline-offset: 2px;
  }
  .form-error {
    color: var(--color-danger);
    font-size: 0.9375rem;
    min-height: 1.2em;
    /* The browser's default paragraph margin was adding 15px above AND below
       an element that is empty almost all the time, on top of the flex gap
       the form already provides. */
    margin: 0;
  }
  /*
   * Empty means no error to show, so it should occupy nothing — min-height
   * above was reserving a blank line permanently, which read as a hole
   * between the WhatsApp checkbox and the text under it. Collapsed with
   * display:none rather than a zero height because a zero-height flex item
   * still costs one full gap. The element stays in the markup (only its box
   * goes away), so the role="alert" live region is there in the DOM from the
   * start and the announcement fires when text is written into it.
   */
  .form-error:empty { display: none; }
  details.filter-panel {
    margin-top: var(--space-4);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  details.filter-panel > summary {
    list-style: none;
    cursor: pointer;
    padding: var(--space-3);
    font-weight: 700;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
  }
  details.filter-panel > summary::-webkit-details-marker { display: none; }
  details.filter-panel > summary:focus-visible { outline: 3px solid var(--color-primary); outline-offset: -3px; }
  /* Same rotate-on-[open] treatment as .legal-menu-chevron, own class since it lives on a different <details>. */
  .filter-panel-chevron { display: flex; flex-shrink: 0; color: var(--color-text-muted); transition: transform 0.15s ease; }
  .filter-panel-chevron svg { width: 18px; height: 18px; }
  details.filter-panel[open] .filter-panel-chevron { transform: rotate(180deg); }
  .filter-active-dot { color: var(--color-primary); }
  form.filter-bar {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: 0 var(--space-3) var(--space-3);
  }
  form.filter-bar input[type="search"] {
    width: 100%;
    min-height: 48px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
    font-family: inherit;
    background: var(--color-bg);
    color: var(--color-text);
  }
  form.filter-bar fieldset { border: 0; margin: 0; padding: 0; }
  form.filter-bar legend { padding: 0; }
  /*
   * Wraps instead of scrolling: at 320px six categories must still fit
   * without pushing the page sideways.
   */
  .filter-species-options { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
  .filter-species-options .checkbox-row { min-height: 44px; font-weight: 500; }
  .filter-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
  form.filter-bar input:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .post-list { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
  .post-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--space-3);
  }
  /*
   * flex-wrap is the actual fix for a real 320px overflow bug: a long post
   * title (or the page-level "Mis publicaciones"/detail heading, which
   * reuses this same row) pushed the status badge/CTA past the viewport
   * edge instead of dropping to its own line.
   */
  .post-card-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
  .post-card h3 { margin: 0; font-size: 1.0625rem; }
  /*
   * Solid fill (color background + white text), not the outline style this
   * had before (colored border + colored text on a plain surface) — matches
   * the reference Marco shared, and reads as a clearer, more confident
   * status signal at a glance. White text needs each status color to be
   * dark enough for AA contrast on its own, not just legible as a border.
   */
  .status-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
    font-size: 0.8125rem;
    font-weight: 800;
    padding: 0.3em 0.75em;
    border-radius: 999px;
    color: var(--color-primary-contrast);
  }
  /*
   * A fixed square box keeps every card the same height regardless of the
   * photo's orientation, and object-fit contain guarantees the whole animal
   * stays visible. object-fit cover looked tidier but cropped to fill — on
   * the portrait photos phones actually take, that cut off heads, tails and
   * legs, exactly the features someone needs to recognise their pet.
   */
  .post-card-photo {
    display: block;
    width: 100%;
    /*
     * height:auto is required, not cosmetic: the img carries width/height
     * attributes (so the browser reserves space and the layout never jumps).
     * Those attributes make both dimensions definite, and aspect-ratio is
     * ignored whenever neither dimension is auto — which rendered every card
     * at the photo's full pixel height instead of the intended square.
     */
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: contain;
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-2);
    background: var(--color-photo-bg);
  }
  .status-badge.kind-perdida { background-color: var(--color-perdida); }
  .status-badge.kind-encontrada { background-color: var(--color-encontrada); }
  .status-badge.status-reunida { background-color: var(--color-reunida); }
  .post-card p { margin: var(--space-2) 0 0; color: var(--color-text-muted); }
  .post-card-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
  .post-card-actions form { display: inline; }

  /*
   * Vertical, photo-forward card — replaces the previous horizontal-compact
   * layout (thumbnail + text side by side), which was chosen deliberately
   * for density ("fit several pets on one screen"). This trades some of
   * that density for visual/emotional impact, on purpose, to match the
   * reference Marco asked for: a big photo is what makes someone stop and
   * look at a specific animal.
   */
  .post-card-compact {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
  }
  /*
   * object-fit: contain, not cover — a portrait photo cropped to fill this
   * box would cut off a head, ears or a collar, the exact details someone
   * needs to recognise their pet (this bit the Boxer photo before, which is
   * tall and got its top chopped off). Same reasoning as .post-detail-photo
   * — the reference's cards crop to fill, but that bug is exactly why this
   * project doesn't.
   */
  /*
   * No plate behind the photo. object-fit: contain necessarily leaves bars
   * whenever the photo's aspect does not match the box, and painting those
   * bars in --color-photo-bg read as "picture stuck onto a coloured block".
   * Transparent lets the card's own white show through instead, so the
   * letterboxing stops being visible rather than being decorated — the same
   * result as a truly transparent box, since every card these sit on is
   * --color-surface. The radius moves to the <img>, now the only painted
   * thing here.
   */
  .post-card-photo-wrap {
    position: relative;
    aspect-ratio: 4 / 5;
    background: transparent;
  }
  .post-card-thumb {
    width: 100%;
    height: 100%;
    object-fit: contain;
    border-radius: var(--radius-sm);
  }
  .post-card-thumb-empty {
    padding: var(--space-5);
    opacity: 0.55;
  }
  .post-card-badge-overlay {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
  }
  .post-card-body { min-width: 0; padding: var(--space-3); }
  .post-card-meta { margin: var(--space-1) 0 0; color: var(--color-text-muted); font-size: 0.9375rem; }
  .post-card-compact h3 { font-size: 1.0625rem; min-width: 0; overflow-wrap: anywhere; margin: 0; }
  .post-card-compact h3 a { color: var(--color-text); text-decoration: none; }
  .post-card-compact h3 a:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  /*
   * Feed-only look from here down: my-posts.tsx renders a plain
   * .post-card-body (h3 + postSubtitle + its own actions row) and never
   * emits .post-card-line, so scoping these to .post-card-feed keeps that
   * page's cards exactly as they were — this is a feed redesign, not a
   * shared-shell one.
   *
   * A CSS grid on the <li> itself, matching the approved reference
   * (prompts/feed-reference.png): photo | info, dense enough that several
   * posts show on one screen instead of one big photo per scroll. WhatsApp
   * lives inside the info column on the badge's own line (see
   * .post-card-topline) rather than in a track of its own, so a contactable
   * card is exactly as tall as one without a number.
   */
  .post-card-feed {
    display: grid;
    grid-template-columns: 96px minmax(0, 1fr);
    column-gap: var(--space-2);
    align-items: start;
    padding: var(--space-2);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  .post-card-feed .post-card-photo-wrap {
    grid-column: 1;
    grid-row: 1;
    width: 96px;
    height: 96px;
    aspect-ratio: auto;
    border-radius: var(--radius-sm);
    overflow: hidden;
  }
  /*
   * contain, never cover. A tall portrait photo — the Boxer shot is the
   * standing example — loses its head to a cover crop, and the head is the
   * single thing someone scanning the feed recognises their pet by. The
   * letterboxing that contain leaves is filled by .post-card-photo-wrap's
   * --color-photo-bg, which is the deliberate trade: some empty box beats a
   * decapitated dog. Same reasoning as .post-detail-photo.
   */
  .post-card-feed .post-card-thumb { object-fit: contain; }
  .post-card-feed .post-card-thumb-empty { padding: var(--space-2); }
  .post-card-feed .post-card-body {
    /* Spans the photo row AND the like/comment row beside it, so the info
       column keeps the card's full height instead of stopping level with the
       photo. */
    grid-column: 2;
    grid-row: 1 / span 2;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0;
    min-width: 0;
  }
  /*
   * space-between, so the badge keeps the left edge and WhatsApp is pinned
   * to the far right of the card whatever the badge's own width.
   */
  .post-card-topline {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    min-width: 0;
  }
  .post-card-feed .post-card-topline > .status-badge {
    font-size: 0.6875rem;
    padding: 0.2em 0.6em;
    /* "Perdí mascota" reads as one unit or not at all — breaking it across
       two lines was what made the topline taller than the button on a phone. */
    white-space: nowrap;
  }
  .post-card-feed h3 { font-size: 1rem; }
  .post-card-line { display: flex; align-items: center; gap: 0.3em; margin: 0; font-size: 0.8125rem; line-height: 1.3; color: var(--color-text-muted); overflow-wrap: anywhere; }
  .post-card-line svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--color-text-muted); }
  /*
   * 44px min-height is the CLAUDE.md §14 touch floor and is NOT negotiable
   * for shrinking this into the badge's line — instead the row it shares
   * with the badge is simply 44px tall, which is still shorter than the
   * separate button row this replaced (44px + its own gap).
   */
  .post-card-whatsapp {
    flex-shrink: 0;
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-height: 44px;
    /* Square while icon-only so it reads as a round button, not a stub. */
    min-width: 44px;
    padding: 0;
    border: 1.5px solid #25d366;
    border-radius: 999px;
    background: var(--color-surface);
    color: #128c3e;
    font-weight: 700;
    font-size: 0.875rem;
    text-decoration: none;
    white-space: nowrap;
  }
  .post-card-whatsapp svg { width: 18px; height: 18px; flex-shrink: 0; }
  .post-card-whatsapp:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .post-card-whatsapp-label { display: none; }
  /*
   * Like + comment count (pre-live set, item 3 follow-up: liking and reading
   * the comment count from the feed thumbnail, not only from the detail
   * page). Placed in the thumbnail's own column, on the row under it, rather
   * than inside .post-card-body: that column is a fixed 96px tall while the
   * body beside it is almost always taller, so this row lands in space the
   * card was already paying for. The body spans both rows to keep filling it.
   *
   * The explicit placement is load-bearing, not decorative — with three
   * children in a two-column grid, auto-placement would drop this next to the
   * photo and push the body onto a row of its own.
   */
  .post-card-feed .post-card-engagement {
    grid-column: 1;
    grid-row: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-width: 0;
  }
  /*
   * Feed size for the shared .engagement-pill (defined once, further down
   * with the detail page's copy): visually compact so it never competes with
   * the photo or the badge, but the tap target is expanded back past the 44px
   * floor by .engagement-pill::after — the glyph stays small, the hit area
   * does not (CLAUDE.md §14).
   */
  .post-card-engagement .engagement-pill {
    min-height: 30px;
    padding: 0 0.5rem;
    gap: 0.3rem;
    font-size: 0.75rem;
  }
  .post-card-engagement .engagement-pill svg { width: 15px; height: 15px; }
  @media (min-width: 375px) {
    .post-card-feed { grid-template-columns: 104px minmax(0, 1fr); padding: 0.625rem; }
    .post-card-feed .post-card-photo-wrap { width: 104px; height: 104px; }
  }
  /* Room for the word from 430px up; below that the glyph alone keeps the
     badge on one line (the aria-label carries the name regardless). */
  @media (min-width: 430px) {
    .post-card-whatsapp { min-width: 0; padding: 0 var(--space-3); }
    .post-card-whatsapp-label { display: inline; }
  }
  /*
   * Roomier thumbnail from 560px up — "aprovechar mejor el ancho" on
   * tablet/desktop without turning the overall feed into a grid: the outer
   * <ul class="post-list"> stays a single vertical column at every width,
   * only the photo column grows. Square, not 174x140: with contain, a
   * letterboxed portrait wastes far less of a square box than of a wide one,
   * so the pet is drawn bigger for the same card height.
   */
  @media (min-width: 560px) {
    .post-card-feed {
      grid-template-columns: 140px minmax(0, 1fr);
      align-items: center;
      column-gap: var(--space-3);
      padding: 0.75rem;
    }
    .post-card-feed .post-card-photo-wrap { width: 140px; height: 140px; }
    .post-card-feed h3 { font-size: 1.1875rem; }
  }
  /* Tighter than .post-list's shared var(--space-3) gap — the dense list both
     the feed and Mis publicaciones now use. */
  .feed-post-list { gap: 0.625rem; }

  /*
   * Mis publicaciones: the same compact list card as the feed, plus the two
   * things only an owner sees — the manage buttons and the testimonial
   * field. Phone stacks thumb+info, then the buttons, then the note; from
   * 560px the buttons move into a third column and only the note keeps a
   * full-width row of its own (it holds a textarea, which is unusable
   * squeezed into a side column).
   */
  .post-card-mine {
    display: grid;
    grid-template-columns: 88px minmax(0, 1fr);
    grid-template-areas:
      'thumb info'
      'note note';
    column-gap: var(--space-2);
    row-gap: var(--space-2);
    align-items: start;
    padding: var(--space-2);
    box-shadow: 0 2px 10px rgba(33, 28, 46, 0.05);
  }
  .post-card-mine .post-card-photo-wrap { grid-area: thumb; width: 88px; height: 88px; aspect-ratio: auto; }
  .post-card-mine .post-card-body {
    grid-area: info;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0;
    min-width: 0;
  }
  .post-card-mine .post-card-topline > .status-badge { font-size: 0.6875rem; padding: 0.2em 0.6em; white-space: nowrap; }
  /* No plate: the filled green pill was drowning the words. Bold text does
     the job and the ✅ still carries the resolved colour. */
  /* The .post-card-mine prefix is load-bearing: the shared post-card
     paragraph rule (0,1,1) would otherwise beat a bare .post-card-closed
     (0,1,0) and grey the text out — the opposite of making it readable. */
  .post-card-mine .post-card-closed { margin: var(--space-1) 0 0; font-size: 0.8125rem; font-weight: 700; color: var(--color-text); }
  .post-card-mine h3 { font-size: 1rem; }
  /*
   * Instagram-style "Publicando…" placeholder (Marco, 2026-09-02) and its
   * REJECTED counterpart — not by color alone (CLAUDE.md §14): PENDING has
   * its own icon+word, REJECTED an explicit alert-role sentence, so neither
   * relies solely on the border/background tint below to communicate status.
   */
  .post-card-pending { border-color: var(--color-border); background: var(--color-bg); }
  .post-card-pending-status {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--color-text-muted);
  }
  .post-card-pending-status svg { width: 16px; height: 16px; flex-shrink: 0; }
  .post-card-pending-hint { margin: 0; font-size: 0.8125rem; color: var(--color-text-muted); }
  .post-card-rejected { border-color: var(--color-danger); }
  .post-card-rejected-reason { margin: 0; font-size: 0.8125rem; font-weight: 600; color: var(--color-danger); }
  .post-card-rejected-actions { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
  .post-card-rejected-actions form { margin: 0; display: flex; }
  /*
   * wrap, because the labels under the glyphs make each tile ~70px wide and
   * two of them plus the badge need ~230px — more than the info column has
   * on a phone. So on narrow screens they drop to their own line directly
   * under the badge, still as small tiles rather than the full-width stacked
   * buttons this replaced; from 560px up they sit beside it as intended.
   */
  .post-card-mine .post-card-topline { flex-wrap: wrap; }
  .post-card-mine .post-card-manage { display: flex; align-items: stretch; gap: var(--space-1); flex-shrink: 0; }
  .post-card-mine .post-card-manage form { margin: 0; display: flex; }
  /*
   * Square tile: glyph on top, its word underneath. Rounded-rect rather than
   * a pill because the label sets the width, and a pill that wide reads as a
   * stretched lozenge. 44px floor on both axes (CLAUDE.md §14) — "icono
   * pequeño" is the 18px drawing, never a shrunken target.
   */
  .post-card-icon-button {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-width: 44px;
    min-height: 44px;
    max-width: 76px;
    padding: var(--space-1) 0.375rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: inherit;
    font-weight: 600;
    line-height: 1.15;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
  }
  .post-card-icon-button svg { width: 18px; height: 18px; flex-shrink: 0; }
  .post-card-icon-button:hover { border-color: var(--color-primary); color: var(--color-primary); }
  .post-card-icon-button:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .post-card-icon-button-danger { color: var(--color-danger); }
  .post-card-icon-button-danger:hover { border-color: var(--color-danger); color: var(--color-danger); }
  .post-card-icon-label { display: block; font-size: 0.625rem; overflow-wrap: anywhere; }
  @media (min-width: 560px) {
    .post-card-icon-label { font-size: 0.6875rem; }
  }
  /*
   * Confirmation after the testimonial round-trips through its redirect.
   * --color-reunida, the project's "case resolved" token — NOT
   * --color-encontrada, which since the badge fix is the orange that means
   * "mascota extraviada" and would read as a category, not a success.
   */
  /*
   * .status-banner is the same treatment under a generic name — added when
   * the home page needed its own "¡Bienvenido!" confirmation after email
   * verification and there was no reason to duplicate this block for it.
   */
  .my-posts-saved, .status-banner {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-reunida);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--color-reunida) 8%, var(--color-surface));
    color: var(--color-text);
    font-size: 0.875rem;
    font-weight: 600;
  }
  .my-posts-saved svg, .status-banner svg { width: 18px; height: 18px; flex-shrink: 0; color: var(--color-reunida); }
  /*
   * Tinted panel rather than loose controls on the card: it groups the label,
   * the textarea and the button that submits them into one obvious block,
   * and separates owner-only editing from the post's own information above.
   */
  .post-card-mine .close-case-form {
    grid-area: note;
    gap: var(--space-2);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
  }
  .post-card-mine .close-case-note label {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--color-text-muted);
  }
  .post-card-mine .close-case-note textarea { background: var(--color-surface); font-size: 0.875rem; }
  .post-card-mine .close-case-form > button { align-self: flex-end; }
  @media (min-width: 375px) {
    .post-card-mine { grid-template-columns: 104px minmax(0, 1fr); padding: 0.625rem; }
    .post-card-mine .post-card-photo-wrap { width: 104px; height: 104px; }
  }
  @media (min-width: 560px) {
    .post-card-mine {
      grid-template-columns: 150px minmax(0, 1fr);
      column-gap: var(--space-3);
      padding: 0.75rem;
    }
    .post-card-mine .post-card-photo-wrap { width: 150px; height: 130px; }
    .post-card-mine h3 { font-size: 1.1875rem; }
  }
  .my-posts-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
  }
  .my-posts-header h1 { margin: 0; font-size: 1.5rem; }
  .my-posts-new { gap: var(--space-2); }
  .my-posts-new svg { width: 20px; height: 20px; flex-shrink: 0; }
  /*
   * Makes the whole card tappable without nesting one link inside another,
   * which is invalid HTML and confusing for screen readers. Anything that
   * must stay independently clickable is raised above this overlay.
   */
  .stretched-link::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: var(--radius);
  }
  /* 44px, not smaller — the touch-target floor CLAUDE.md §14 requires; this used to be 40px. */
  .button-compact { min-height: 44px; padding: var(--space-1) var(--space-3); font-size: 0.9375rem; }
  .post-card-compact .post-card-actions { margin-top: var(--space-2); }
  /* var(--space-2), not the full var(--space-3) main forms use — this sits
     inside a compact card action row, a deliberately denser context. */
  .close-case-form { display: flex; flex-direction: column; gap: var(--space-2); align-items: flex-start; }
  .close-case-note { width: 100%; }
  .close-case-note textarea {
    width: 100%;
    min-height: 48px;
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 0.9375rem;
    font-family: inherit;
    background: var(--color-surface);
    color: var(--color-text);
    resize: vertical;
  }

  .post-detail-photo {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: contain;
    border-radius: var(--radius);
    background: var(--color-photo-bg);
    margin-bottom: var(--space-3);
  }
  .post-detail-photo-empty {
    padding: var(--space-5);
    opacity: 0.55;
  }

  /*
   * The track scrolls natively (touch swipe, trackpad, shift+wheel) even
   * with JavaScript off — the arrows and the live position count are a
   * enhance-forms.js addition on top of that, not a requirement for it.
   */
  .photo-carousel { margin-bottom: var(--space-3); }
  .photo-carousel-nav {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
  }
  .photo-carousel-arrow {
    min-width: 44px;
    min-height: 44px;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: 1.25rem;
    line-height: 1;
    cursor: pointer;
  }
  .photo-carousel-arrow:hover { background: var(--color-photo-bg); }
  .photo-carousel-position { font-size: 0.9375rem; color: var(--color-text-muted); min-width: 3rem; text-align: center; }
  .photo-carousel-track {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    border-radius: var(--radius);
    background: var(--color-photo-bg);
  }
  .photo-carousel-slide {
    flex: 0 0 100%;
    scroll-snap-align: start;
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .photo-carousel-slide img { width: 100%; height: 100%; object-fit: contain; display: block; }
  @media (prefers-reduced-motion: reduce) {
    .photo-carousel-track { scroll-behavior: auto; }
  }

  /* Opens the fullscreen lightbox below — a plain button wrapping the thumbnail, reset to look like the image itself. */
  .photo-carousel-open {
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
  }

  /*
   * Native <dialog>: showModal() renders it in the browser's top-layer, above
   * everything on the page with no z-index bookkeeping, and gives Escape-to-
   * close and focus return to the trigger button for free. Closed state is
   * the UA default (display: none) — only the open state is styled here.
   */
  .photo-lightbox {
    padding: 0;
    border: none;
    margin: 0;
    width: 100vw;
    height: 100vh;
    max-width: 100vw;
    max-height: 100vh;
    background: #000;
  }
  .photo-lightbox[open] {
    display: flex;
    flex-direction: column;
  }
  .photo-lightbox::backdrop { background: rgba(0, 0, 0, 0.92); }
  .photo-lightbox-close {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    z-index: 1;
    min-width: 44px;
    min-height: 44px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.4);
    background: rgba(0, 0, 0, 0.4);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
  }
  .photo-carousel-lightbox {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-bottom: 0;
    /*
     * Breathing room around the photo — running it flush to every edge read
     * as the app taking over the screen rather than a photo viewer (Marco,
     * 2026-08-30: "algo como en Facebook"). Extra top clearance keeps the
     * photo clear of the close button, which floats above this padded area.
     */
    padding: 4.5rem var(--space-3) var(--space-4);
  }
  .photo-carousel-lightbox .photo-carousel-track {
    flex: 1;
    min-height: 0;
    background: transparent;
    border-radius: 0;
    /*
     * Overrides .photo-carousel-track's own scroll-behavior: smooth —
     * opening the lightbox on, say, photo 3 must jump straight there, not
     * visibly slide across every photo in between (regression Marco
     * reported, 2026-08-30). The prev/next arrows still ask for behavior:
     * 'smooth' explicitly in JS, which wins over this either way.
     */
    scroll-behavior: auto;
  }
  /* Full available height instead of the thumbnail's cropped 1:1 square — the point of opening this is to see the whole photo. */
  .photo-carousel-lightbox .photo-carousel-slide { aspect-ratio: unset; height: 100%; }
  .photo-carousel-lightbox .photo-carousel-nav { margin: var(--space-3) 0; }
  .photo-carousel-lightbox .photo-carousel-position { color: rgba(255, 255, 255, 0.85); }
  .photo-carousel-lightbox .photo-carousel-arrow {
    background: rgba(255, 255, 255, 0.12);
    border-color: rgba(255, 255, 255, 0.4);
    color: #fff;
  }
  .photo-carousel-lightbox .photo-carousel-arrow:hover { background: rgba(255, 255, 255, 0.24); }

  .photo-gallery-edit { max-width: 480px; margin: var(--space-3) auto; }
  .photo-gallery-edit-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    list-style: none;
    margin: var(--space-2) 0 0;
    padding: 0;
  }
  .photo-gallery-edit-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    width: 92px;
  }
  .photo-gallery-edit-thumb {
    width: var(--thumb-size);
    height: var(--thumb-size);
    object-fit: contain;
    border-radius: var(--radius-sm);
    background: var(--color-photo-bg);
  }
  .photo-gallery-cover-badge {
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    text-align: center;
  }
  .photo-gallery-edit-item form { margin: 0; }
  .photo-gallery-add-form {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--color-border);
  }

  .detail-back { margin: 0 0 var(--space-3); font-size: 0.9375rem; }
  .post-detail h1 { font-size: 1.5rem; margin: 0; }
  .post-detail h2 { font-size: 1.0625rem; margin: var(--space-4) 0 var(--space-1); }
  .detail-facts { display: grid; grid-template-columns: auto 1fr; gap: var(--space-1) var(--space-3); margin: var(--space-3) 0 0; }
  .detail-facts dt { font-weight: 700; font-size: 0.9375rem; }
  .detail-facts dd { margin: 0; color: var(--color-text-muted); }
  .detail-contact { margin-top: var(--space-4); }
  .detail-contact-number { margin: var(--space-2) 0 0; color: var(--color-text-muted); font-size: 0.9375rem; }

  .story-list { display: flex; flex-direction: column; gap: var(--space-3); }
  .story-card { display: flex; gap: var(--space-3); align-items: flex-start; }
  .story-card-photo {
    flex: 0 0 auto;
    width: var(--thumb-size);
    height: var(--thumb-size);
    object-fit: contain;
    border-radius: var(--radius-sm);
    background: var(--color-photo-bg);
  }
  .story-card-photo-empty { padding: var(--thumb-empty-padding); opacity: 0.55; }
  .story-card-body { min-width: 0; }
  .story-card h3 { margin: 0; font-size: 1rem; }
  .story-card-note { margin: var(--space-1) 0 0; font-style: italic; color: var(--color-text-muted); }
  .story-card-date {
    display: flex;
    align-items: center;
    gap: 0.3em;
    margin: var(--space-1) 0 0;
    font-size: 0.875rem;
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
  }
  .story-card-date svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--color-text-muted); }
  .fraud-warning {
    margin-top: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: 0.9375rem;
  }

  /*
   * Likes and comments (pre-live set, item 3). Like button uses the same
   * bordered-pill shape at every state; "active" fills it so a repeat
   * visitor can see at a glance they already liked it, without relying on
   * color alone (CLAUDE.md §14) — the label text itself also changes
   * ("Me gusta" -> "Te gusta").
   */
  .detail-engagement { margin-top: var(--space-5); }
  .like-form { margin: 0; }
  .detail-engagement-actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
  }
  /*
   * One shared social control for the feed card and the detail page: same
   * pill, same border, same proportions, only the scale differs (the feed
   * shrinks it near .post-card-engagement above; the detail keeps this size,
   * where these are the screen's primary actions).
   *
   * Deliberately no box-shadow — these sit on cards that already carry one,
   * and stacking two reads as heavy.
   */
  .engagement-pill {
    position: relative;
    /* Above the feed card's stretched-link overlay (.stretched-link::after),
       so each pill is really clickable instead of the card swallowing it —
       the same trick .post-card-whatsapp uses. */
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    font-weight: 600;
    font-size: 0.9375rem;
    line-height: 1;
    text-decoration: none;
    /* A <button> defaults to cursor: default, unlike an <a>; without this the
       like was the one spot on a card where the pointer turned back into an
       arrow while everything around it showed a hand. */
    cursor: pointer;
  }
  /*
   * Restores the 44px touch floor wherever the visible pill is smaller than
   * that (the feed's compact size). Invisible, centred on the pill, and
   * pointer-events land on the control itself since it is its own
   * pseudo-element.
   */
  .engagement-pill::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    min-height: 44px;
    height: 100%;
  }
  .engagement-pill:hover { border-color: var(--color-primary); background: var(--color-photo-bg); }
  .engagement-pill:active { background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)); }
  .engagement-pill:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  .engagement-pill:disabled { opacity: 0.55; cursor: not-allowed; }
  .engagement-pill:disabled:hover { border-color: var(--color-border); background: var(--color-surface); }
  .engagement-pill svg { width: 20px; height: 20px; flex-shrink: 0; }
  /*
   * Outline heart by default, filled when the visitor already liked it. The
   * icon component itself ships fill="currentColor" as a presentation
   * attribute, which CSS outranks — so one icon covers both states and the
   * existing JS keeps toggling nothing but the class.
   */
  .like-button svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; }
  .like-button-active { border-color: var(--color-perdida); color: var(--color-perdida); }
  .like-button-active svg { fill: currentColor; stroke: currentColor; }
  .like-button-active:hover { border-color: var(--color-perdida); background: color-mix(in srgb, var(--color-perdida) 8%, var(--color-surface)); }
  .like-count, .comment-count { color: var(--color-text-muted); font-weight: 700; }
  .like-button-active .like-count { color: inherit; }
  /*
   * Secondary navigation, not a dominant button: the purple is the text and
   * the border, never a filled background.
   */
  .detail-back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-primary);
    font-weight: 600;
    font-size: 0.9375rem;
    line-height: 1;
    text-decoration: none;
  }
  .detail-back-link svg { width: 18px; height: 18px; flex-shrink: 0; }
  .detail-back-link:hover { border-color: var(--color-primary); background: var(--color-photo-bg); color: var(--color-primary-dark); }
  .detail-back-link:active { background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)); }
  .detail-back-link:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }

  .comment-cta {
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    color: var(--color-text);
  }
  .comment-cta a { color: var(--color-primary); font-weight: 700; }

  .comment-form { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-3); }
  .comment-form textarea {
    width: 100%;
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font: inherit;
    resize: vertical;
  }

  .comment-empty { color: var(--color-text-muted); }

  .comment-list { display: flex; flex-direction: column; gap: var(--space-3); list-style: none; margin: 0; padding: 0; }
  .comment-item {
    position: relative;
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }
  .comment-author { margin: 0 0 var(--space-1); font-weight: 700; font-size: 0.875rem; color: var(--color-text-muted); }
  .comment-body { margin: 0; color: var(--color-text); white-space: pre-wrap; }
  .comment-delete-form { position: absolute; top: var(--space-2); right: var(--space-2); }
  .comment-delete-button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    color: var(--color-text-muted);
  }
  .comment-delete-button:hover, .comment-delete-button:focus-visible { color: var(--color-danger); }
  .comment-delete-button svg { width: 16px; height: 16px; }

  .app-dialog {
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--space-4);
    max-width: 22rem;
    width: calc(100% - 2 * var(--space-3));
    background: var(--color-surface);
    color: var(--color-text);
  }
  .app-dialog::backdrop { background: rgba(33, 28, 46, 0.55); }
  .app-dialog-message { margin: 0 0 var(--space-4); font-size: 1rem; }
  .app-dialog-donate-note {
    margin: 0 0 var(--space-2);
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-photo-bg);
    font-size: 0.9rem;
  }
  .app-dialog-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: flex-end; }

  /*
   * Admin panel (Marco, 2026-08-28). Own chrome (see AdminLayout), own
   * page-width token (--admin-max-width, wider than the public site's 720px
   * --max-width) — deliberately not reusing .site-header/.site-footer/.page
   * classes, since /admin/* has no public nav, bottom nav or cookie banner.
   */
  .admin-header {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
  }
  .admin-header-brand { display: flex; align-items: center; gap: var(--space-2); text-decoration: none; color: var(--color-text); flex-shrink: 0; }
  /* The vertical rule between the wordmark and the panel title in the
     reference — a border on the title block, so it disappears together with
     the block when the header wraps at narrow widths. */
  .admin-header-title {
    color: var(--color-text-muted);
    padding-left: var(--space-3);
    border-left: 1px solid var(--color-border);
    min-width: 0;
  }
  .admin-header-title h1 { margin: 0; font-size: 1.0625rem; color: var(--color-text); }
  .admin-header-title p { margin: 0; font-size: 0.8125rem; }
  /* Pushed hard right, the way the reference places the signed-in admin. */
  .admin-header-account { margin-left: auto; }
  .admin-header-account > summary { gap: var(--space-2); }
  .admin-header-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--color-photo-bg);
    color: var(--color-primary);
    font-weight: 800;
    font-size: 0.9375rem;
    flex-shrink: 0;
  }
  .admin-header-account-label { font-size: 0.9375rem; }
  @media (max-width: 560px) {
    /* The word "Administrador" is the first thing worth dropping when the
       header runs out of room. Clipped, never display:none — display:none
       would take it out of the accessibility tree too and leave the control
       with no name at all on a phone. */
    .admin-header-account-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
    /* Brand and account share the top line; the title drops to a full-width
       second line rather than leaving the avatar stranded on a row of its own. */
    .admin-header { gap: var(--space-2); }
    .admin-header-account { order: 2; }
    .admin-header-title { order: 3; flex-basis: 100%; padding-left: 0; border-left: none; }
  }
  .admin-main {
    max-width: var(--admin-max-width);
    margin: 0 auto;
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }
  .admin-page { display: flex; flex-direction: column; gap: var(--space-3); }
  /* Squared-off, not the public site's 999px pills: an admin table row is a
     dense line of data, and pill buttons in it read as oversized chips. */
  .admin-main .button { border-radius: var(--radius-sm); }
  .admin-main .button svg { width: 16px; height: 16px; flex-shrink: 0; }
  .admin-main .button-compact { gap: 6px; }
  .admin-page-title { margin: 0; font-size: 1.25rem; }
  .admin-breadcrumb { font-size: 0.875rem; color: var(--color-text-muted); }
  .admin-breadcrumb a { color: var(--color-text-muted); }

  /*
   * Five metric cards across on a wide screen, then progressively fewer —
   * auto-fit alone collapsed them to three uneven columns at laptop widths,
   * so the count is pinned per breakpoint instead.
   */
  .admin-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
  @media (min-width: 620px) { .admin-stats { grid-template-columns: repeat(3, 1fr); } }
  @media (min-width: 920px) { .admin-stats { grid-template-columns: repeat(5, 1fr); } }
  .admin-stat-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 1px 2px rgba(33, 28, 46, 0.04);
    padding: var(--space-3);
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
  }
  .admin-stat-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    border-radius: 12px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
    flex-shrink: 0;
  }
  .admin-stat-icon svg { width: 21px; height: 21px; }
  .admin-stat-body { display: flex; flex-direction: column; min-width: 0; }
  /* Three of the five cards are shortcuts into their own listing. */
  .admin-stat-card-link { text-decoration: none; color: inherit; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
  .admin-stat-card-link:hover {
    border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
    box-shadow: 0 2px 8px rgba(33, 28, 46, 0.08);
  }
  .admin-stat-card-link:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
  /* A post kept alive by an anonymized account — say so instead of printing the placeholder address. */
  .admin-owner-deleted { color: var(--color-text-muted); font-style: italic; }
  .admin-stat-value { font-size: 1.5rem; font-weight: 800; color: var(--color-text); line-height: 1.15; }
  .admin-stat-label { font-size: 0.8125rem; color: var(--color-text-muted); }

  /* The one white surface that holds search + bulk actions + table + pagination. */
  .admin-panel-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 1px 2px rgba(33, 28, 46, 0.04);
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }

  .admin-search-form { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  /* Wrapper exists only to host the magnifier: an icon inside the field
     needs a positioning context that the bare <input> can't provide. */
  .admin-search-field { position: relative; display: flex; flex: 1 1 240px; min-width: 0; }
  .admin-search-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    color: var(--color-text-muted);
    pointer-events: none;
  }
  .admin-search-icon svg { width: 18px; height: 18px; }
  .admin-search-form input[type="search"] {
    width: 100%;
    min-height: 44px;
    padding: var(--space-2) var(--space-3) var(--space-2) 2.4rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: 0.9375rem;
    font-family: inherit;
    background: var(--color-surface);
    color: var(--color-text);
  }
  .admin-search-submit { min-height: 44px; padding: var(--space-2) var(--space-4); flex-shrink: 0; }
  /* Once the button drops below the field it may as well span it — a narrow
     button floating under a full-width input reads as unfinished. */
  @media (max-width: 560px) { .admin-search-submit { flex: 1 1 100%; } }

  .admin-section { display: flex; flex-direction: column; gap: var(--space-2); }
  .admin-section-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
  .admin-section-header h2 { margin: 0; font-size: 1.0625rem; }
  .admin-section-title { font-size: 1.25rem !important; font-weight: 800; }
  .admin-empty { color: var(--color-text-muted); padding: var(--space-3) 0; }

  /*
   * Bulk bar (Marco, 2026-08-28). The dashboard's copy is persistent (it
   * shows "0 seleccionados" before anything is picked, per the reference);
   * elsewhere it stays hidden via the HTML hidden attribute until at least
   * one row is checked, which enhance-forms.js toggles from the live
   * selection count. Reason is required for every bulk action, so
   * the select/note live inside it rather than in a confirm dialog, which
   * only takes yes/no.
   */
  .admin-bulk-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    padding: var(--space-3);
    background: color-mix(in srgb, var(--color-primary) 5%, var(--color-surface));
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
  }
  .admin-bulk-count { font-size: 0.875rem; font-weight: 700; white-space: nowrap; padding-bottom: 0.7rem; }
  .admin-bulk-reason { display: flex; flex-wrap: wrap; gap: var(--space-2); flex: 1 1 320px; }
  .admin-bulk-reason label { display: flex; flex-direction: column; gap: 0.2em; font-size: 0.8125rem; color: var(--color-text-muted); flex: 1 1 160px; }
  .admin-bulk-reason select, .admin-bulk-reason textarea {
    min-height: 44px;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: 0.875rem;
    background: var(--color-surface);
    color: var(--color-text);
  }
  .admin-bulk-reason textarea { resize: vertical; min-height: 44px; }
  .admin-bulk-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); padding-bottom: 1px; }
  .admin-bulk-error { flex-basis: 100%; margin: 0; }

  /*
   * Desktop: a real table. Below 720px: each row becomes a stacked card and
   * each cell shows its column header via data-label (CSS content, not a
   * duplicated DOM node) — same data the desktop <thead> already has, so
   * there is only one server-rendered markup for both layouts.
   */
  .admin-table-wrap { overflow-x: auto; border: 1px solid var(--color-border); border-radius: var(--radius-sm); }
  .admin-table { width: 100%; border-collapse: collapse; background: var(--color-surface); }
  /* Tight horizontal padding on purpose: eight columns at --space-3 each side
     spent ~250px on gutters alone, which pushed "Acciones" out of view on a
     1280px screen once addresses stopped wrapping. */
  .admin-table th, .admin-table td { padding: 0.7rem var(--space-2); text-align: left; border-bottom: 1px solid var(--color-border); font-size: 0.875rem; vertical-align: middle; }
  .admin-table th:first-child, .admin-table td:first-child { padding-left: var(--space-3); }
  .admin-table th:last-child, .admin-table td:last-child { padding-right: var(--space-3); }
  /* A faintly tinted head row so the column names read as chrome, not as a first record. */
  .admin-table thead th { background: var(--color-bg); color: var(--color-text-muted); font-weight: 600; font-size: 0.8125rem; white-space: nowrap; }
  .admin-table tbody tr:last-child td { border-bottom: none; }
  .admin-table tbody tr:hover { background: color-mix(in srgb, var(--color-primary) 3%, var(--color-surface)); }
  .admin-table td.admin-col-actions { white-space: nowrap; }
  /* Bold, not just a color tint (CLAUDE.md §14 — never color alone), so a rejected-post count still reads as a flag with color vision off. */
  .admin-table td.admin-col-rejected-flagged { font-weight: 800; color: var(--color-danger); }
  /* AdminPostTable's "Moderación" column (Marco, 2026-09-03) — bold + explicit words, never a color-only signal (CLAUDE.md §14). */
  .admin-table td.admin-col-moderation { max-width: 22ch; }
  .admin-moderation-pending { font-weight: 700; color: var(--color-text-muted); }
  .admin-moderation-rejected { font-weight: 800; color: var(--color-danger); }
  /*
   * "Caracterizada" (Marco, 2026-09-04): publicaciones que se quedaron sin
   * vector por completo — el texto "Sí"/"No" ya lo dice, el color es
   * refuerzo, nunca la única señal (CLAUDE.md §14).
   */
  .admin-table td.admin-col-characterized { max-width: 20ch; }
  .admin-characterized-yes { font-weight: 700; color: var(--color-text-muted); }
  /* El botón de reintentar ahora vive junto al Sí/No en vez de debajo (Marco, 2026-09-04: el mismo botoncito aplica a los dos estados). */
  .admin-characterized-cell { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  .admin-characterized-no { font-weight: 800; color: var(--color-danger); }
  /* Enlace a /admin/posts/:postId/characterization (Marco, 2026-09-04: "agregarla a publicaciones... para poderla ver y leer"). Padding generoso para el piso táctil de CLAUDE.md §14 aunque el texto sea corto. */
  .admin-characterized-view-link { padding: var(--space-1) 0; font-weight: 600; }
  .admin-recharacterize-form { display: contents; }
  /* Mismo patrón que .account-username-form .form-error: si aparece un error, toma su propia línea en vez de apretarse junto al botón. */
  .admin-recharacterize-form .form-error { flex-basis: 100%; margin: var(--space-1) 0 0; }
  /*
   * Botón de solo ícono — 44px es el piso táctil de CLAUDE.md §14, no
   * negociable aunque el ícono en sí sea chico. El nombre accesible vive en
   * el aria-label del botón, no en texto visible (Marco pidió explícitamente
   * "un botoncito de refresh", no un botón con etiqueta).
   */
  .admin-icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    flex-shrink: 0;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-text-muted);
    cursor: pointer;
  }
  .admin-icon-button:hover, .admin-icon-button:focus-visible { background: var(--color-bg); color: var(--color-text); }
  .admin-icon-button svg { width: 18px; height: 18px; }
  .admin-table .admin-col-select { width: 1%; padding-right: 0; }
  /* One address per line, as in the reference. Long ones (the e2e fixtures
     run 40+ characters) widen the table rather than wrapping into two-line
     rows, and .admin-table-wrap's own overflow-x absorbs the extra width —
     the page itself never gains a horizontal scrollbar. */
  .admin-table td.admin-col-email { color: var(--color-text); white-space: nowrap; }
  .admin-table input[type="checkbox"], .admin-bulk-bar input[type="checkbox"] { width: 18px; height: 18px; }
  .admin-view-activity { color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 35%, var(--color-border)); }
  .admin-view-activity:hover { background: var(--color-photo-bg); }

  /*
   * Panel de "Coincidencias" (Marco, 2026-09-03) — /admin/matches y el aviso
   * debajo de Usuarios en el panel principal. Mobile-first: la pareja de
   * publicaciones se apila en una columna angosta y pasa a fila en pantallas
   * más anchas (CLAUDE.md §14, sin scroll horizontal a 320px).
   */
  .admin-matches-teaser-text { margin: var(--space-2) 0 0; color: var(--color-text-muted); font-size: 0.875rem; }

  /* Resumen de pesos en /admin/matches, con enlace a su propia página (Marco, 2026-09-03). */
  .admin-weights-summary { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
  .admin-weights-summary-values { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
  .admin-weights-summary-chip { font-size: 0.875rem; color: var(--color-text-muted); }
  .admin-weights-summary-chip strong { color: var(--color-text); }

  .admin-matches-settings-intro { margin: 0 0 var(--space-3); color: var(--color-text-muted); font-size: 0.875rem; max-width: 60ch; }

  /* La fórmula con los valores actuales (Marco: "quiero que se vea la fórmula utilizada"), recalculada en vivo por el bloque data-matching-calculator en este mismo archivo. */
  .admin-formula-card { margin-bottom: var(--space-3); text-align: center; }
  .admin-formula-label { margin: 0 0 var(--space-1); font-size: 0.8125rem; color: var(--color-text-muted); }
  .admin-formula-text { margin: 0; font-size: 1.0625rem; font-weight: 700; font-family: var(--font-mono, monospace); word-break: break-word; }

  /*
   * Cada grupo de pesos en su propia tarjeta, no anidados dentro de un solo
   * formulario largo (Marco: "sigue siendo confuso") — separación visual
   * real entre "los tres grupos principales" y sus repartos internos.
   */
  .admin-weights-form { display: flex; flex-direction: column; gap: var(--space-3); }
  .admin-weight-group-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--space-3); }
  .admin-weight-group-title { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-3); font-size: 1rem; }
  .admin-weight-group-fields { display: grid; grid-template-columns: 1fr; gap: var(--space-3); }
  @media (min-width: 640px) {
    .admin-weight-group-fields { grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); }
  }
  .admin-weight-field { display: flex; flex-direction: column; gap: var(--space-1); font-size: 0.875rem; }
  .admin-weight-field-label { color: var(--color-text); font-weight: 700; }
  .admin-weight-field-help { color: var(--color-text-muted); font-size: 0.75rem; }
  .admin-weight-field-percent-row { display: flex; align-items: center; gap: var(--space-1); }
  .admin-weight-field input[type="number"] {
    width: 100%;
    max-width: 8rem;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    font: inherit;
  }
  /*
   * Indicador de suma por tarjeta (Marco, 2026-09-04: "Los tres grupos
   * principales no deben sumar mas que 100... deben sumar 100%") — nunca
   * solo color (CLAUDE.md §14): el propio texto ya dice "✓", "no pueden
   * sumar más de 100%" o "deben sumar 100%", el color es refuerzo.
   */
  .admin-weight-group-sum { margin: var(--space-3) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--color-border); font-size: 0.8125rem; font-weight: 700; }
  .admin-weight-group-sum-ok { color: var(--color-text-muted); }
  .admin-weight-group-sum-bad { color: var(--color-danger); }

  /*
   * Simulador (Marco: "que se autocalcule el umbral de detección", "todo se
   * vea muy gráfico") — controles de ejemplo que nunca se envían al
   * servidor, solo recalculan en el navegador contra los pesos/umbral que
   * estén escritos arriba en ese momento.
   */
  .admin-sim-card { display: flex; flex-direction: column; gap: var(--space-3); }
  .admin-sim-title { margin: 0; font-size: 1rem; }
  .admin-sim-help { margin: 0; color: var(--color-text-muted); font-size: 0.8125rem; }
  .admin-sim-slider { display: flex; flex-direction: column; gap: var(--space-1); font-size: 0.875rem; }
  .admin-sim-slider-label { display: flex; justify-content: space-between; font-weight: 700; }
  .admin-sim-slider input[type="range"] { width: 100%; }
  /*
   * Ubicación y Detalles del simulador ahora se arman con casillas + días
   * (Marco, 2026-09-03), no con un número inventado — cada grupo en su
   * propia caja para que se lea como "un mini-formulario", igual que las
   * tarjetas de configuración de arriba.
   */
  .admin-sim-group { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); display: flex; flex-direction: column; gap: var(--space-1); }
  .admin-sim-group-title { margin: 0 0 var(--space-1); font-weight: 700; font-size: 0.8125rem; color: var(--color-text-muted); }
  .admin-sim-checkbox { display: flex; align-items: center; gap: var(--space-2); font-size: 0.875rem; }
  .admin-sim-days { display: flex; flex-direction: column; gap: var(--space-1); font-size: 0.875rem; margin-top: var(--space-1); }
  /*
   * Marco (2026-09-04): el placeholder "sin fecha en alguna..." no cabía en
   * el recuadro angosto. Se acortó el texto Y se ensanchó la caja — las dos
   * cosas, no solo una, para que quepa cómodo incluso en una pantalla chica.
   */
  .admin-sim-days input { width: 100%; max-width: 16rem; padding: var(--space-1) var(--space-2); border: 1px solid var(--color-border); border-radius: 8px; font: inherit; }
  .admin-sim-subscore { margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--color-border); font-size: 0.8125rem; color: var(--color-text-muted); }
  .admin-sim-subscore strong { color: var(--color-text); }
  .admin-sim-result { margin-top: var(--space-2); }
  .admin-sim-result-bar-track { display: block; height: 14px; border-radius: 999px; background: var(--color-bg); overflow: hidden; }
  .admin-sim-result-bar-fill { display: block; height: 100%; width: 0%; background: var(--color-primary); border-radius: inherit; transition: width 0.15s ease-out; }
  .admin-sim-result-text { margin: var(--space-2) 0 0; font-weight: 700; text-align: center; }
  /* El texto ya dice "notificaría"/"no notificaría" — el color es refuerzo, nunca la única señal (CLAUDE.md §14). */
  .admin-sim-result-notify { color: var(--color-primary); }
  .admin-sim-result-no-notify { color: var(--color-text-muted); }

  .admin-match-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
  .admin-match-row { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--space-3); }
  /* Nunca solo color (CLAUDE.md §14) — el propio texto "notificaría hoy" en admin-match-score-total ya lo dice; este borde es un refuerzo visual, no la única señal. */
  .admin-match-row-would-notify { border-color: var(--color-primary); }
  .admin-match-pair { display: flex; flex-direction: column; gap: var(--space-2); align-items: stretch; }
  .admin-match-pair-sep { align-self: center; color: var(--color-text-muted); font-size: 1.25rem; }
  .admin-match-side {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    color: var(--color-text);
    text-decoration: none;
    flex: 1;
    min-width: 0;
  }
  .admin-match-side:hover, .admin-match-side:focus-visible { background: var(--color-bg); }
  .admin-match-side-info { min-width: 0; display: flex; flex-direction: column; }
  .admin-match-side-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .admin-match-side-meta { font-size: 0.8125rem; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Envuelve MatchSide + su caracterización, uno debajo del otro (Marco, 2026-09-04: "abajo de cada miniatura... quiero ver como esta caracterizando la foto"). */
  .admin-match-side-wrap { display: flex; flex-direction: column; gap: var(--space-2); flex: 1; min-width: 0; }
  .admin-match-characterization { margin: 0; padding: var(--space-2); border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-bg); font-size: 0.8125rem; }
  .admin-match-characterization-field { margin: 0 0 var(--space-1); }
  .admin-match-characterization-field strong { color: var(--color-text-muted); font-weight: 700; }
  .admin-match-characterization-description { margin: var(--space-1) 0 0; padding-top: var(--space-1); border-top: 1px solid var(--color-border); color: var(--color-text); }
  .admin-match-characterization-empty { margin: 0; padding: var(--space-2); font-size: 0.8125rem; color: var(--color-text-muted); font-style: italic; }
  /*
   * "La huella de identificación" es lo primero y más grande que se lee —
   * es el texto que de verdad alimenta el embedding (characterizationText(),
   * queue-consumer.ts), así que es lo más útil para diagnosticar por qué dos
   * publicaciones sí/no se parecieron (Marco, 2026-09-04).
   */
  .admin-match-characterization-signature { margin: var(--space-1) 0; font-weight: 600; }
  .admin-match-characterization-features { margin: 0 0 var(--space-1); padding-left: 1.25rem; }
  .admin-match-characterization-features li { margin-bottom: 2px; }
  .admin-match-characterization-empty-features { font-style: italic; color: var(--color-text-muted); }
  /*
   * Marco (2026-09-04, segunda vuelta): "not_visible" y "none_observed" son
   * dos centinelas DISTINTOS (ver PetCharacterization) — "no se pudo ver"
   * contra "se vio y no hay nada" — y nunca deben mostrarse como el mismo
   * "ninguna" genérico, porque eso ocultaría justo el problema que esta
   * vista existe para exponer. El texto en español ya distingue los dos
   * casos; la cursiva es solo refuerzo visual, nunca la única señal
   * (CLAUDE.md §14).
   */
  .admin-match-characterization-notvisible { font-style: italic; color: var(--color-text-muted); }
  .admin-match-characterization-noneobserved { font-style: italic; color: var(--color-text); }
  .admin-match-characterization-details { margin-top: var(--space-2); border-top: 1px solid var(--color-border); padding-top: var(--space-1); }
  .admin-match-characterization-details summary { cursor: pointer; font-weight: 700; color: var(--color-primary); font-size: 0.8125rem; }
  .admin-match-characterization-full { margin-top: var(--space-2); display: flex; flex-direction: column; gap: var(--space-2); }
  .admin-match-characterization-group { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--space-2); background: var(--color-surface); }
  .admin-match-characterization-group-title { margin: 0 0 var(--space-1); font-size: 0.75rem; font-weight: 700; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.02em; }
  .admin-match-scores { margin-top: var(--space-3); display: flex; flex-direction: column; gap: var(--space-1); }
  /*
   * Marco (2026-09-04): "quita esas barras gigantes de /matches que tienen
   * porcentajes. Con que muestres el porcentaje es suficiente." — antes era
   * un grid de tres columnas con una barra de progreso en medio; ahora es
   * solo la etiqueta a la izquierda y el número a la derecha.
   */
  .admin-match-score-line { display: flex; justify-content: space-between; gap: var(--space-2); font-size: 0.8125rem; }
  .admin-match-score-line-label { color: var(--color-text-muted); }
  .admin-match-score-line-value { font-weight: 700; }
  .admin-match-score-total { margin-top: var(--space-1); font-size: 0.875rem; }
  .admin-match-meta { margin: var(--space-2) 0 0; font-size: 0.75rem; color: var(--color-text-muted); }
  .admin-match-no-vector { margin-top: var(--space-3); padding: var(--space-2) 0; }
  @media (min-width: 640px) {
    .admin-match-pair { flex-direction: row; }
    .admin-match-pair-sep { align-self: auto; }
  }

  /* Enlace "¿Por qué no notificó?" en /admin/matches, y la página de comparación en sí (Marco, 2026-09-04). */
  .admin-match-compare-link { margin: var(--space-3) 0; }
  .admin-compare-form { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }
  .admin-compare-result { margin-top: var(--space-3); }
  .admin-compare-sides { display: flex; flex-direction: column; gap: var(--space-2); align-items: stretch; }
  .admin-compare-side { display: flex; flex-direction: column; gap: var(--space-1); flex: 1; min-width: 0; }
  .admin-compare-side-label { font-weight: 700; font-size: 0.8125rem; color: var(--color-text-muted); }
  .admin-compare-side-missing { border: 1px dashed var(--color-border); border-radius: var(--radius-sm); padding: var(--space-3); }
  /* El texto ya dice "Tiene vector"/"Sin vector" — el símbolo ✓/✗ es refuerzo, nunca la única señal (CLAUDE.md §14). */
  .admin-compare-vector-status { margin: 0; font-size: 0.8125rem; color: var(--color-text-muted); }
  .admin-compare-vector-missing { color: var(--color-danger); }
  .admin-compare-explain { margin: var(--space-3) 0 0; font-size: 0.875rem; }
  .admin-compare-explain-filtered { padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-bg); }
  /* Botón "Reintentar emparejamiento" por lado, y la confirmación tras volver aquí redirigido (Marco, 2026-09-04). */
  .admin-compare-retry-form { margin-top: var(--space-2); }
  .admin-compare-retry-flash { margin: var(--space-1) 0 0; font-size: 0.8125rem; color: var(--color-primary); }
  @media (min-width: 640px) {
    .admin-compare-sides { flex-direction: row; }
  }

  /*
   * Sortable column headers are real links (plain GET, works without JS), so
   * they inherit the header cell's muted colour instead of the site's link
   * blue — they should read as part of the table chrome until hovered.
   */
  .admin-sort-link {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
    /* Comfortable pointer target across the whole header cell height. */
    padding: var(--space-1) 0;
  }
  .admin-sort-link:hover { color: var(--color-primary); }
  .admin-sort-link-active { color: var(--color-primary); font-weight: 700; }
  .admin-sort-indicator { display: inline-flex; opacity: 0.45; }
  .admin-sort-link-active .admin-sort-indicator { opacity: 1; }
  .admin-sort-indicator svg { width: 13px; height: 13px; }
  /* Same chevron, flipped, rather than a second near-identical icon. */
  .admin-sort-indicator-asc { transform: rotate(180deg); }

  .admin-select-all { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; }
  /* Visually hidden on desktop (the column needs no caption there) but always
     present, so the checkbox has a real accessible name at every width. */
  .admin-select-all-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  /*
   * Dashboard "Usuarios" table only (Marco, 2026-09-02) — column values
   * centered instead of left-aligned, and the derived-count headers
   * ("Casos de éxito", "Publicaciones rechazadas", "Comentarios
   * rechazados") wrap onto two lines via an explicit <br/> in
   * admin-dashboard.tsx, so those columns stay narrow enough that the table
   * doesn't need horizontal scroll at common desktop widths — .admin-table-
   * wrap's overflow-x stays in place as a fallback for narrower ones.
   * Correo/Usuario are deliberately excluded ("correo y usuario si puedes
   * dejarlo tal cual como esta") — left-aligned, unwrapped, same as before.
   * Scoped to .admin-users-table only; the other admin tables (posts,
   * comments, rejected comments) are untouched.
   */
  .admin-users-table th,
  .admin-users-table td { text-align: center; }
  .admin-users-table th.admin-col-email,
  .admin-users-table th.admin-col-username,
  .admin-users-table td.admin-col-email,
  .admin-users-table td.admin-col-username { text-align: left; }
  /*
   * "Ver actividad" is the eye alone on desktop (Marco, 2026-09-02), so the
   * Acciones column costs as little width as possible. The caption stays in
   * the DOM as the link's accessible name — hidden the same way
   * .admin-select-all-text is, and shown again below 720px.
   */
  .admin-view-activity-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  /* No gap to collapse and no text to pad around once the caption is hidden. */
  .admin-users-table .admin-view-activity { gap: 0; padding-left: 0.5rem; padding-right: 0.5rem; }
  @media (max-width: 720px) {
    .admin-table-wrap { border: none; border-radius: 0; }
    /*
     * The header row can't just be clipped here: it holds the "seleccionar
     * todo" checkbox, and hiding it left that control unreachable on a phone
     * (it stayed in the DOM at 1px, under the rows, so taps never landed on
     * it). Instead the row stays, every column caption but the first is
     * dropped, and the checkbox gets its visible label back.
     */
    .admin-table thead { display: block; }
    .admin-table thead tr { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
    .admin-table thead th { display: none; }
    .admin-table thead th.admin-col-select {
      display: block;
      flex-basis: 100%;
      background: none;
      border-bottom: none;
      padding: 0 0 var(--space-1);
    }
    /*
     * The stacked layout drops the header row, which would take the sort
     * controls with it and make ordering desktop-only — the same trap the
     * select-all checkbox fell into. The sortable headers stay, re-drawn as
     * a wrapped row of chips above the cards.
     */
    .admin-table thead th.admin-col-sortable {
      display: inline-flex;
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-bottom: 1px solid var(--color-border);
      border-radius: 999px;
      padding: 0.2rem 0.7rem;
      margin-bottom: var(--space-2);
    }
    .admin-sort-link { padding: 0; }
    .admin-select-all-text { position: static; width: auto; height: auto; clip: auto; font-size: 0.875rem; font-weight: 700; color: var(--color-text); }
    .admin-table, .admin-table tbody, .admin-table tr, .admin-table td { display: block; width: 100%; }
    .admin-table tr {
      border: 1px solid var(--color-border);
      border-radius: var(--radius-sm);
      padding: var(--space-2) var(--space-3);
      margin-bottom: var(--space-2);
    }
    .admin-table td { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); border: none; padding: var(--space-1) 0; text-align: right; }
    /* In the stacked card layout there is no table width to protect, so the
       address wraps instead of overflowing its own card. */
    .admin-table td.admin-col-email { white-space: normal; word-break: break-word; }
    /* nowrap + no shrink: without it a long value squeezed the caption until
       it broke mid-word ("Corr / eo"). The value wraps instead. */
    .admin-table td[data-label]::before {
      content: attr(data-label);
      font-weight: 700;
      text-align: left;
      color: var(--color-text-muted);
      margin-right: var(--space-2);
      white-space: nowrap;
      flex-shrink: 0;
    }
    .admin-table td.admin-col-actions .button { width: 100%; }
    /* The card layout has no table width to save, and a full-width button
       showing only an eye would read as a mystery control — the caption comes
       back, exactly as .admin-select-all-text's does. */
    .admin-view-activity-text { position: static; width: auto; height: auto; clip: auto; }
    .admin-users-table .admin-view-activity { gap: var(--space-2); }
  }

  /*
   * Soft-tinted pills for account state, not the solid fills the post-kind
   * badges use: a table of twenty rows with twenty saturated blocks in it is
   * unreadable. Icon + text on every one, so colour is a second cue and never
   * the only one (CLAUDE.md §14). The base .status-badge sets white text for
   * its solid variants, so each of these has to restate its own colour.
   */
  .status-badge.user-active,
  .status-badge.user-suspended,
  .status-badge.user-banned { font-size: 0.75rem; font-weight: 700; }
  .status-badge.user-active {
    background-color: color-mix(in srgb, var(--color-reunida) 14%, var(--color-surface));
    color: var(--color-reunida);
  }
  .status-badge.user-suspended {
    background-color: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface));
    color: var(--color-warning);
  }
  .status-badge.user-banned {
    background-color: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface));
    color: var(--color-danger);
  }
  .status-badge svg { width: 13px; height: 13px; }

  .admin-pagination { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--space-3); }
  .admin-pagination-info { color: var(--color-text-muted); font-size: 0.875rem; }
  .admin-page-link[aria-disabled='true'] { opacity: 0.5; pointer-events: none; }

  /* Back link with its arrow, on both the profile and the three list pages. */
  .admin-back-link { display: inline-flex; align-items: center; gap: 6px; color: var(--color-primary); text-decoration: none; font-size: 0.875rem; }
  .admin-back-link:hover { text-decoration: underline; }
  .admin-back-link svg { width: 15px; height: 15px; }

  /* User summary: avatar, name + state, then the six read-only figures. */
  .admin-profile-card { flex-direction: row; align-items: center; gap: var(--space-4); padding: var(--space-4) var(--space-3); }
  .admin-profile-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 84px;
    height: 84px;
    border-radius: 50%;
    background: var(--color-photo-bg);
    color: var(--color-primary);
    flex-shrink: 0;
  }
  .admin-profile-avatar svg { width: 44px; height: 44px; }
  .admin-profile-body { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; flex: 1; }
  .admin-profile-title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  .admin-profile-title h1 { margin: 0; font-size: 1.375rem; }
  .admin-profile-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--space-3); margin: 0; font-size: 0.875rem; }
  .admin-profile-meta > div { min-width: 0; }
  .admin-profile-meta dt { color: var(--color-text-muted); margin: 0 0 0.15em; font-size: 0.8125rem; }
  .admin-profile-meta dd { margin: 0; font-weight: 700; word-break: break-word; }
  .admin-profile-meta-flagged { color: var(--color-danger); font-weight: 800; }
  @media (max-width: 620px) {
    .admin-profile-card { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
  }

  /* Section card: icon + title on the left, "Ver todas" hard right. */
  .admin-content-section { gap: var(--space-2); }
  .admin-content-heading { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: 1.0625rem; }
  .admin-section-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background: var(--color-photo-bg);
    color: var(--color-primary);
    flex-shrink: 0;
  }
  .admin-section-icon svg { width: 18px; height: 18px; }

  /* Row thumbnail — small, square, never distorted, with a neutral bed for transparent placeholders. */
  .admin-thumb {
    width: 34px;
    height: 34px;
    border-radius: 8px;
    object-fit: cover;
    background: var(--color-photo-bg);
    flex-shrink: 0;
  }
  .admin-thumb-empty { object-fit: contain; padding: 4px; }
  .admin-name-cell { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
  .admin-name-link { color: var(--color-primary); font-weight: 600; text-decoration: none; }
  .admin-name-link:hover { text-decoration: underline; }
  .admin-content-table td.admin-col-name { min-width: 12rem; }
  /* Comment bodies are free text: clamp the column so one long comment can't
     stretch the row, but never truncate to the point of hiding what was said. */
  .admin-col-comment { color: var(--color-text); max-width: 26rem; }

  .admin-list-header { flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2); }
  .admin-list-header h1 { font-size: 1.25rem; }
  .admin-list-header-meta { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; font-size: 0.875rem; }
  .admin-list-header-user { color: var(--color-text-muted); word-break: break-word; }

  /* Suspender/Banear/Eliminar cuenta: three visually distinct blocks, never just three same-looking buttons in a row (explicit requirement). */
  .admin-actions-block { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-3); }
  .admin-action-card { border-radius: var(--radius); padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-3); border: 1px solid var(--color-border); background: var(--color-surface); }
  .admin-action-card-warning { border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border)); background: color-mix(in srgb, var(--color-warning) 6%, var(--color-surface)); }
  .admin-action-card-danger { border-color: color-mix(in srgb, var(--color-danger) 40%, var(--color-border)); background: color-mix(in srgb, var(--color-danger) 5%, var(--color-surface)); }
  .admin-action-head { display: flex; align-items: flex-start; gap: var(--space-2); }
  .admin-action-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    flex-shrink: 0;
    color: var(--color-primary-contrast);
  }
  .admin-action-icon svg { width: 20px; height: 20px; }
  .admin-action-icon-warning { background: var(--color-warning); }
  .admin-action-icon-danger { background: var(--color-danger); }
  .admin-action-icon-neutral { background: var(--color-primary); }
  .admin-action-card h3 { margin: 0 0 0.2em; font-size: 0.9375rem; }
  .admin-action-card p { margin: 0; font-size: 0.8125rem; color: var(--color-text-muted); line-height: 1.4; }
  .admin-reason-form { display: flex; flex-direction: column; gap: var(--space-2); }
  .admin-reason-form label { display: flex; flex-direction: column; gap: 0.2em; font-size: 0.8125rem; color: var(--color-text-muted); }
  .admin-reason-form select, .admin-reason-form textarea {
    min-height: 44px;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font: inherit;
    background: var(--color-bg);
    color: var(--color-text);
  }
  .admin-reason-form textarea { resize: vertical; }
  .button-warning { color: var(--color-warning); border-color: var(--color-warning); background: var(--color-surface); }

  .admin-history-table td, .admin-history-table th { font-size: 0.8125rem; }
  .admin-result-failure { color: var(--color-danger); font-weight: 700; }

  @media (prefers-reduced-motion: reduce) {
    * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  }
