/* common.css — design system tokens, shared components (buttons, chips, sections, helpers) */

/* Public Sans se carga desde el <head> de cada página junto a JetBrains
   Mono (link combinado + preconnect, QA #56). El @import que vivía aquí
   era una cadena en serie (HTML → common.css → CSS de Google → fuente)
   que retrasaba el primer render ~300-500ms en 4G. */

:root {

  /* ──────────────────────────────────────────────────────────────────
     PRIMITIVES — full color names (2026-04 DS-Web rename)
     · Each ramp is 9 steps. Step 5 is the brand canonical for that color.
     · Los atajos cortos (--g-1, --y-5…) de la primera época YA NO EXISTEN:
       se retiraron y este comentario decía lo contrario (oct 2026). Si ves
       uno en una hoja o en la documentación, es deuda: el nombre bueno es
       el largo.
     ────────────────────────────────────────────────────────────────── */

  /* Gray — photographic neutral · 13 steps (white + 11 + black) */
  --gray-white: #FFFFFF;
  --gray-1:     #F2F2F2;
  --gray-2:     #CCCCCC;
  --gray-3:     #B3B3B3;
  --gray-4:     #999999;
  --gray-5:     #808080;
  --gray-6:     #666666;
  /* PELDAÑO AÑADIDO (Irene, 14-10-2026) · entre el 6 y el 7 había un salto
     mucho mayor que entre los demás (#666 → #3A3A3A) y justo ahí caía el
     valor que necesitábamos: el texto secundario sobre claro tiene que dar
     7:1 y el 6 solo daba 5,74. El 7 lo cumple de sobra (11,4) pero se
     confunde con el cuerpo del texto (1,74 entre ellos). Este da **7,0
     clavados** sobre blanco y mantiene 2,83 contra el cuerpo: cumple y
     sigue leyéndose como secundario. */
  --gray-6b:    #595959;
  --gray-7:     #3A3A3A;
  --gray-8:     #282828;
  --gray-9:     #1F1F1F;
  --gray-10:    #141414;
  --gray-11:    #0A0A0A;
  --gray-black: #000000;

  /* Yellow — brand + action · step 5 is the logo yellow #FED925 */
  --yellow-1: #FFF7D3;
  --yellow-2: #FFF0A8;
  --yellow-3: #FEE87C;
  --yellow-4: #FEDD3B;
  --yellow-5: #FED925;
  --yellow-6: #CBAE1E;
  --yellow-7: #988216;
  --yellow-8: #66570F;
  --yellow-9: #332B07;

  /* Red — alert · step 5 is the logo orange #FF4D00 */
  --red-1: #FFDBCC;
  --red-2: #FFB899;
  --red-3: #FF9466;
  --red-4: #FF7133;
  --red-5: #FF4D00;
  --red-6: #CC3E00;
  --red-7: #992E00;
  --red-8: #661F00;
  --red-9: #330F00;

  /* Lightblue — text links + interactive accents · step 6 is canonical */
  --lightblue-1: #DBF3F9;
  --lightblue-2: #B7E6F3;
  --lightblue-3: #92DAEC;
  --lightblue-4: #6ECDE6;
  --lightblue-5: #4AC1E0;
  --lightblue-6: #3B9AB3;
  --lightblue-7: #2C7486;
  --lightblue-8: #1E4D5A;
  --lightblue-9: #0F272D;

  /* Blue — logo only · step 5 is the logo blue #2689F8 (never used in UI) */
  --blue-1: #D4E7FE;
  --blue-2: #A8D0FC;
  --blue-3: #7DB8FB;
  --blue-4: #51A1F9;
  --blue-5: #2689F8;
  --blue-6: #1E6EC6;
  --blue-7: #175295;
  --blue-8: #0F3763;
  --blue-9: #081B32;

  /* Orange — warm accent · reserved, rarely surfaced */
  --orange-1: #FFE4D0;
  --orange-2: #FFC9A1;
  --orange-3: #FFAD73;
  --orange-4: #FF9244;
  --orange-5: #FF7715;
  --orange-6: #CC5F11;
  --orange-7: #99470D;
  --orange-8: #663008;
  --orange-9: #331804;

  /* Green — success · reserved for form/checkout flows */
  --green-1: #CDF3E1;
  --green-2: #9BE6C2;
  --green-3: #6ADAA4;
  --green-4: #38CD85;
  --green-5: #06C167;
  --green-6: #059A52;
  --green-7: #04743E;
  --green-8: #024D29;
  --green-9: #012715;

  /* Short aliases (--g-/--y-/--r-/--lb-/--b-/--o-/--gr-) RETIRED 2026-05.
     Cada consumer migrado al nombre completo (--gray-/--yellow-/--red-/
     --lightblue-/--blue-/--orange-/--green-). Si algo todavía referencia
     un alias corto, fallará en silencio — re-añadir solo si hay razón. */

  /* ── Semantic — Backgrounds ──
     Trimmed 2026-05 to only the slots with active consumers. Retired:
     --bg-neutral-subtle/-soft/-strong, --bg-primary-weak, --bg-tertiary-soft.
     Re-add a slot when a real component needs it (don't preempt). */
  --bg-neutral-default:   var(--gray-10);   /* .section-dark · cards · containers */
  --bg-neutral-bold:      var(--gray-11);   /* page canvas (body) */
  --bg-primary-default:   var(--yellow-5);    /* brand fill — yellow */
  --bg-primary-strong:    var(--yellow-6);    /* .btn-primary:active */
  --bg-tertiary-default:  var(--gray-8);    /* tertiary surface (used in 1 component) */

  /* ── Semantic — Text ──
     Note: `--content-text-accent` (cyan) was retired 2026-04. Cyan is for
     LINKS only — not for highlighting text. To emphasise a word inside a
     paragraph use yellow (--yellow-5) or white (--gray-white). */
  --content-text-default:          var(--gray-white);
  /* Bumped from g-4 (#999) to g-3 (#B3B3B3) on 2026-05 — same role,
     better contrast on dark surfaces (~10:1 vs ~7.4:1). */
  --content-text-default-weak:     var(--gray-3);
  --content-text-default-inverted: var(--gray-9);
  --content-text-label:            var(--gray-3);
  --content-text-label-weak:       var(--gray-4);
  --content-text-subtitle:         var(--gray-2);
  --content-text-highlight:        var(--yellow-5);    /* yellow highlight inside body copy */

  /* ── Semantic — Links ── */
  /* Links — yellow + underlined (2026-05). Cyan retired as link colour;
     yellow now owns both Action and Interactive text. */
  --content-link-default:  var(--yellow-5);
  --content-link-hover:    var(--yellow-4);

  /* ── Semantic — Borders ── */
  /* `--border-neutral-default` (opaque --g-6) retired 2026-04 — only had
     1 usage. The 50%-transparent --border-neutral-soft below is the
     canonical hairline used across the system. */
  /* White-on-dark hairline at 16% opacity. Bumped from 12% — the lower
     value rendered visibly thinner at rounded corners on some browsers
     because alpha-blending compounds at the antialiased edge. 16% reads
     as a clean, even 1px line all the way around. */
  /* ESCALA DE TRANSPARENCIAS (Irene, 2026-10-16) · blanco y negro con
     transparencia, en escalones fijos. Sustituye a los ~115 rgba() escritos a
     mano en las reglas (había 41 tonos distintos: 6, 8, 9, 10 %…). Blanco
     sobre oscuro (filetes, velos, texto atenuado); negro sobre foto o claro
     (discos de iconos, sombras, velos). Validada en
     design-system/pruebas/escala-transparencias.html. */
  --white-a04: color-mix(in srgb, var(--gray-white) 4%, transparent);
  --white-a08: color-mix(in srgb, var(--gray-white) 8%, transparent);
  --white-a12: color-mix(in srgb, var(--gray-white) 12%, transparent);
  --white-a20: color-mix(in srgb, var(--gray-white) 20%, transparent);
  --white-a32: color-mix(in srgb, var(--gray-white) 32%, transparent);
  --white-a50: color-mix(in srgb, var(--gray-white) 50%, transparent);
  --white-a65: color-mix(in srgb, var(--gray-white) 65%, transparent);
  --white-a78: color-mix(in srgb, var(--gray-white) 78%, transparent);
  --white-a90: color-mix(in srgb, var(--gray-white) 90%, transparent);
  --black-a04: color-mix(in srgb, var(--gray-black) 4%, transparent);
  --black-a20: color-mix(in srgb, var(--gray-black) 20%, transparent);
  --state-hover-dark: var(--black-a20);   /* hover que OSCURECE lo pulsable sobre una superficie (campos, filas, chips de app) · Irene, 10-11-2026 */
  --black-a40: color-mix(in srgb, var(--gray-black) 40%, transparent);
  --black-a55: color-mix(in srgb, var(--gray-black) 55%, transparent);
  --black-a75: color-mix(in srgb, var(--gray-black) 75%, transparent);
  --black-a92: color-mix(in srgb, var(--gray-black) 92%, transparent);

  --border-neutral-soft:     color-mix(in srgb, var(--gray-white) 16%, transparent);
  /* Stroke de CONTROL interactivo (2026-10) · el filete soft da 1.44:1 sobre
     el lienzo y la norma WCAG 1.4.11 pide ≥3:1 para el contorno de algo con
     lo que se interactúa (QA #19/#29). No había token para eso, así que dos
     componentes se lo inventaron por separado y casi igual: el acordeón a
     blanco 40% y la casilla de Missions a 42%. Este es ese valor, promovido:
     40% = 3.66:1. Para casillas, interruptores y contornos de control —
     NO para filetes decorativos, que siguen en soft. */
  --border-neutral-strong:   color-mix(in srgb, var(--gray-white) 40%, transparent);
  --border-neutral-faint:    var(--white-a08);   /* divisor que separa sin ruido: contorno de campos de app, filetes de hoja y listas (Irene, 10-11-2026) */
  --border-secondary-accent: var(--yellow-5);
  /* `--border-primary-accent` (cyan) retired 2026-04 — never used in
     any real component. Yellow (--border-secondary-accent) is the only
     accent border the system needs. */
  /* `--border-tertiary-accent` retired 2026-04 — its single usage
     (.info-tip border) migrated to --border-neutral-soft. */

  /* ── Semantic — Brand ── */
  --brand-yellow: var(--yellow-5);

  /* ── Semantic — Aliases (signal/success/warning/error/info) ── */
  --success-default: var(--green-5);
  --warning-default: var(--yellow-5);
  --error-default:   var(--red-5);
  --info-default:    var(--blue-5);

  /* ── Semantic — Magic hours (fases de luz) ──
     Colores de marca de PhotoPills para las fases de luz del día. NO son
     roles de UI (acción/alerta/etc.): son una PALETA DE DATO. Cualquier
     visualización o pieza que represente Day / Golden hour / Blue hour /
     Night DEBE usar estos tokens — no re-elegir a ojo. Mapean 1:1 a
     primitivas existentes. (Decisión de Irene · 2026-07-22.) */
  --lightphase-day:    var(--lightblue-3);  /* Day light   · #92DAEC */
  --lightphase-golden: var(--red-5);        /* Golden hour · #FF4D00 */
  --lightphase-blue:   var(--blue-5);       /* Blue hour   · #2689F8 */
  --lightphase-night:  var(--gray-black);   /* Night       · #000000 */

  /* ── Semantic — Datos y gráficos (--chart-*) ──
     PALETA DE DATO, como magic hours: no son roles de UI. Valores para
     fondo CLARO (artículos y guías); .chart-figure--dark los cambia por
     pasos de las MISMAS rampas, nunca un flip automático. Validada para
     daltonismo y contraste con design-system/charts/validate_palette.js:
     si cambias un paso, vuelve a pasarlo. Pieza: charts.css. */
  --chart-surface:   var(--gray-white);        /* lienzo de la figura */
  --chart-ink:       var(--gray-11);           /* títulos, valores */
  --chart-muted:     var(--gray-6);            /* ejes, subtítulo, fuente · 5,4:1 */
  --chart-grid:      color-mix(in srgb, var(--gray-11) 8%, transparent);
  --chart-baseline:  color-mix(in srgb, var(--gray-11) 22%, transparent);
  --chart-neutral:   var(--bg-light-default);  /* casilla vacía + terreno · gris que vira al azul */
  --chart-cell-off:  var(--chart-neutral);
  --chart-tier-edge: var(--yellow-2);          /* temporada · rampa secuencial de un tono */
  --chart-tier-in:   var(--yellow-4);
  --chart-tier-best: var(--yellow-6);
  --chart-sun:       var(--yellow-7);          /* sol */
  --chart-bluehour:  var(--lightblue-6);       /* chroma justo bajo el mínimo: siempre con etiqueta directa */
  --chart-night:     var(--blue-6);            /* noche / Vía Láctea */
  --chart-aurora:    var(--green-6);
  --chart-moon:      var(--gray-1);
  --chart-moon-line: var(--gray-4);
  --chart-blood:     var(--red-7);             /* luna de sangre · par validado con la ruta */
  --chart-umbra:     var(--gray-7);
  --chart-route:     var(--yellow-5);          /* ruta y puntos de disparo · el rojo es alerta */
  --chart-water:     var(--lightblue-2);
  --chart-terrain:   var(--chart-neutral);
  --chart-watermark: var(--red-6);             /* MOCKUP · 4,9:1 */

  /* ──────────────────────────────────────────────────────────────────
     DESIGNER ALIASES (2026-04 · DS-Web rename)
     A friendlier vocabulary for design-time thinking. These are pure
     aliases — they layer on top of the primitives + semantic tokens
     above. Devs can use either; designers should reach for these.
     ──────────────────────────────────────────────────────────────────

     Naming rules:
     · `--color-<role>-<intensity>` for accents (action, link, alert)
     · Intensity scale: subtle → soft → default → strong → bold
     · `--bg-<surface>` for the tonal cascade of section backgrounds
     ────────────────────────────────────────────────────────────────── */

  /* Action — yellow ramp · covers BOTH primary CTAs and inline text links
     (unified 2026-05: yellow owns Action + Identity + Interactive text). */
  --color-action-subtle:  var(--yellow-2);
  --color-action-soft:    var(--yellow-3);
  --color-action-default: var(--yellow-5);   /* brand yellow ★ */
  --color-action-strong:  var(--yellow-6);
  --color-action-bold:    var(--yellow-7);

  /* `--color-link-*` retired 2026-05 — use `--color-action-*` instead.
     Links and CTAs share the same hue family in this system. */

  /* Alert — red ramp · errors, validation, attention */
  --color-alert-subtle:  var(--red-2);
  --color-alert-soft:    var(--red-3);
  --color-alert-default: var(--red-5);   /* brand alert ★ */
  --color-alert-strong:  var(--red-6);
  --color-alert-bold:    var(--red-7);

  /* ─────────────────────────────────────────────────────────────────
     SURFACE FAMILIES (2026-04 DS-Web rename)

     Three orthogonal layers compose any section's atmosphere:

     1. `--bg-*`        Solid backgrounds (no gradient).
     2. `--gradient-*`  Directional vertical fades (light top → dark
                        bottom). Each one creates a clear seam against
                        the next section.
     3. `--vignette-*`  Atmospheric overlays applied via ::before.
                        Don't change the base tone — only add character
                        (warm-right, cool-left, central glow, band…).

     A section = surface + (optional) vignette.
     ───────────────────────────────────────────────────────────────── */

  /* ── Backgrounds — solids ─────────────────────────────────────── */
  --bg-bold: var(--gray-black);   /* Solid #000 — anchor (footer, statements) */

  /* Light surface · off-white con tinte azul-gris sutil. Para sales
     letters largas y secciones de lectura sostenida (Overview, How It
     Works, Itinerary, FAQs). El tilt azul (B>G>R, +5 puntos sobre rojo)
     arma con los gradients dark del DS (blue tilt B>G>R) y evita el
     blanco puro #FFF que genera fatiga. Patrón validado en Linear,
     Figma marketing, Vercel docs. */
  --bg-light-default:                #F5F7FA;
  /* Un paso más hundido, para cajas que tienen que despegarse del papel
     blanco (avisos, notas). Añadido 15-10-2026: el #F5F7FA sobre blanco
     apenas se distinguía (Irene). */
  --bg-light-sunk:                   #ECEFF4;
  /* Un paso más ALTO: el blanco puro, para tarjetas que se levantan sobre
     una banda clara (la tarjeta blanca `.card--light`). Añadido 15-10-2026
     (Irene): Dolomites usaba el blanco tres veces escrito a mano. */
  --bg-light-raised:                 var(--gray-white);
  --content-text-on-light-default:   var(--gray-11);   /* near-black */
  --content-text-on-light-weak:      var(--gray-6b);   /* 7,0:1 sobre blanco · ver el peldaño 6b */
  --content-text-on-light-data:      var(--gray-9);    /* 16,5:1 · datos: filas de tabla y leyenda de gráfico, a 17px (Irene, 27-10-2026 · QA 929) */
  --border-on-light:                 rgba(10, 10, 10, 0.12);

  /* ── Gradients — directional vertical fades ───────────────────── */
  /* Each gradient ranges from a "light top" to a "dark bottom".
     Stacked sections produce a visible "lift" at every seam.
     All three share a clear blue tilt (B > G > R in every stop). */
  --gradient-subtle: linear-gradient(180deg, #25282e 0%, #16181e 100%);
                     /* range #25 → #16 — card on dark surface */
  --gradient-soft:   linear-gradient(180deg, #181c26 0%, #0c1018 100%);
                     /* range #18 → #0c — content band, blue-tinted, gentle delta */
  --gradient-strong: linear-gradient(180deg, #14161e 0%, #02030a 100%);
  /* Claro · la card blanca: arriba un toque del gris azulado de las bandas
     claras, abajo blanco. Solo para `.card--light`. */
  --gradient-light-subtle: linear-gradient(180deg, color-mix(in srgb, var(--bg-light-default) 50%, var(--gray-white)) 0%, var(--gray-white) 100%);
                     /* range #14 → #02 — deep band, near-black bottom */

  /* Campo de formulario claro (.input--light) · fondo con un toque de gris
     azulado y borde pizarra (gris + azul profundo). El borde da ≥3:1 sobre
     el campo; bajarlo de --gray-5 lo deja por debajo del mínimo. */
  --bg-field-on-light:           color-mix(in srgb, var(--gray-white) 75%, var(--bg-light-default));
  --border-field-on-light:       color-mix(in srgb, var(--gray-5) 82%, var(--blue-8));
  --border-field-on-light-hover: color-mix(in srgb, var(--gray-6b) 82%, var(--blue-8));

  /* ── Vignettes — atmospheric overlay color (single, blue-gray) ──
     One vignette family with two intensities. Position is set by
     modifier classes (.has-vignette--right/--left/--top/--bottom)
     on the consumer element. See `.has-vignette` utility in common.css. */
  --vignette-soft:   rgba(95, 105, 128, 0.20);  /* atmospheric ambiental */
  --vignette-strong: rgba(95, 105, 128, 0.50);  /* character pronunciado */

  /* ── Scrims — legibility overlays over photography (2026-07 audit) ──
     Distintos de --gradient-* (fondos de sección) y de --vignette-*
     (carácter atmosférico): un scrim oscurece la foto para que el texto
     encima pase contraste AA. Se aplican en el ::after/overlay del media.
     · hero  → oscurece leve arriba (nav) + fuerte abajo (title block).
     · soft  → fade inferior medio, para captions/diptychs sobre foto.
     · strong→ cobertura completa creciente, para heroes con mucho copy. */
  --scrim-hero:         linear-gradient(180deg, rgba(0, 0, 0, 0.10) 0%, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.75) 100%);
  --scrim-photo-soft:   linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 45%, rgba(0, 0, 0, 0.65) 100%);
  --scrim-photo-strong: linear-gradient(180deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.55) 55%, rgba(0, 0, 0, 0.82) 100%);
  /* Fundido de corte · una MÁSCARA, no un color: desvanece el propio
     contenido hasta transparente en sus últimos 4,5em, así que vale sobre
     cualquier fondo. Para listas cortadas que siguen debajo (Contents).
     Se usa con `mask-image`, nunca como `background`. */
  --mask-fade-bottom:   linear-gradient(to bottom, #000 calc(100% - 4.5em), transparent);

  /* `--gradient-default`, `--bg-subtle/-soft/-default/-strong/-elevated/
     -loop/-deep/-atmospheric/-pre-anchor/-anchor` were aliases retired
     2026-05 (zero consumers). Use `--gradient-subtle/-soft/-strong` and
     `--bg-bold/-neutral-bold/-neutral-default` directly. */

  /* ── Shadows ── */
  /* `--shadow-md` retirado el 16-10-2026: lo usaba UNA sola pieza en todo el
     proyecto (el comparador plan/foto) y no estaba en el catálogo de tres
     elevaciones. Esa pieza pasó a `--shadow-soft`. */
  --shadow-device-light: 0 50px 80px -20px color-mix(in srgb, var(--gray-black) 22%, transparent);
  --shadow-device-dark:  0 60px 100px -20px color-mix(in srgb, var(--gray-black) 65%, transparent);
  /* ── Typography ── */
  /* Font families (the only two used across the site) */
  --font-sans: 'Public Sans', -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
  /* `--font-brand` · solo para el wordmark "PhotoPills" (logo lockup).
     Gibson es la tipografía oficial del wordmark; cae a Public Sans si
     no está disponible. NO usar fuera del lockup — para todo lo demás,
     `--font-sans`. */
  --font-brand: Gibson, 'Public Sans', -apple-system, BlinkMacSystemFont, sans-serif;

  /* ──────────────────────────────────────────────────────────────────
     TEXT — typography scale (2026-04 DS-Web rename)
     · Prefix `--text-*` for everything readable.
     · Suffix tells the role (display/heading/body/mono) + size (xs–xl).
     · `--fs-*` shortnames RETIRED 2026-05 — every consumer migrated to `--text-*`.
     ────────────────────────────────────────────────────────────────── */

  /* Display — hero / statement */
  --text-display-xl: 96px;   /* hero h1 */
  --text-display-lg: 78px;   /* secondary display */
  --text-display-md: 64px;   /* tertiary display */

  /* Heading — section titles */
  --text-heading-xl: 56px;   /* big section title */
  --text-heading-lg: 40px;   /* default section title */
  --text-heading-md: 32px;   /* subheading */
  --text-heading-sm: 24px;   /* compact subheading */

  /* Body — editorial copy */
  --text-lead:    28px;      /* hero subtitle / lead paragraph */
  --text-body-lg: 22px;      /* large body */
  --text-body-md: 18px;      /* default body (md) — minimum legible for prose */
  --text-body-data: 17px;    /* datos: filas de tabla · entre sm y md (Irene, 27-10-2026 · QA 928) */
  --text-body-sm: 16px;      /* small body — still safe for reading */
  --text-body-xs: 14px;      /* legal / smallest body — captions, never paragraphs */
  --text-body-2xs: 12px;     /* SOLO app (hoja del Planner): títulos de campo, subtítulos de fila, pistas · nunca en la web (Irene, 10-11-2026) */

  /* Mono — metadata, technical labels */
  --text-mono-xs: 10px;      /* tiny mono — timer digits */
  --text-mono-sm: 12px;      /* photo credits, footer meta */
  --text-mono-md: 14px;      /* eyebrows */
  --text-mono-lg: 16px;      /* author credit, paginator */
  --text-mono-xl: 18px;      /* author name (avatar+autor) · mono destacado */

  /* Editorial fluid sizes — FUENTE ÚNICA del subtítulo/body editorial
     (section-header / editorial-stack / product-copy · patrón "mirror de la
     home"). Esta receta estaba copiada por todo el repo; ahora vive aquí.
     Cambiar el tamaño editorial = tocar solo estas 2 líneas. */
  --text-editorial-subtitle: clamp(var(--text-heading-sm), 3vw, var(--text-heading-md));
  --text-editorial-body:     clamp(var(--text-body-md), 1.7vw, var(--text-body-lg));

  /* Product hero title — tamaño del H1 "PhotoPills Premium Beta" (Pricing +
     Premium comparten el componente `.pricing-intro__title-v2`). Tokenizado
     (petición Irene 2026-08): tocar el tamaño del hero = estas 2 líneas.
     Desktop: display-md (64) → display-lg (78). Móvil: heading-xl (56) → 78. */
  --text-hero-title:    clamp(var(--text-display-md), 7vw, var(--text-display-lg));
  /* EL MÓVIL DEL HERO · «punto intermedio» (QA #33). Este token valía
     56→78px y NO LO USABA NADIE: los heroes llevaban el clamp escrito a mano,
     porque Irene corrigió el tamaño DESPUÉS de crear el token y la corrección
     no llegó aquí. Resultado: la norma mandaba una cosa y el sitio hacía otra,
     que es la peor manera de empezar páginas nuevas.
     El valor bueno es el de la corrección: ~45px a 375 y empalma con 56 en
     tablet sin salto. (Puesto al día el 16-10-2026.) */
  --text-hero-title-sm: clamp(var(--text-heading-lg), 12vw, var(--text-heading-xl));

  /* Line-heights + tracking (shared across the scale) */
  --lh-display:       1.08;
  --lh-heading:       1.18;
  --lh-body:          1.47;
  --tracking-display: -0.022em;
  --tracking-heading: -0.018em;

  /* `--fs-*` aliases retired (mayo 2026). Every consumer migrated to the
     canonical `--text-*` tokens. If something still references --fs-...
     it'll fail loudly — re-add the alias only if there's a real reason. */

  /* ── Motion easings · canonical ramp (4 steps) ──
     `--ease` (legacy default) retired 2026-05; consumers migrated to
     `--ease-out` (UI default). */
  --ease-out:       cubic-bezier(0.4, 0, 0.2, 1);
  --ease-soft:      cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-cinematic: cubic-bezier(0.83, 0, 0.17, 1);
  --ease-reveal:    cubic-bezier(0.23, 0.98, 0.34, 0.99);
  /* `--dur-reveal` (800ms) was unified into `--dur-cinema` (1100ms) in
     mayo 2026 — both lived in the "long, intentional" bucket and the
     perceptual delta wasn't worth a separate token. Section .reveal entries
     now play at the same tempo as The Loop. */
  /* Legacy duration tokens (--dur-fast/normal/slow/hover) retired in
     2026-04 coherence pass. Use the canonical ramp below
     (--dur-instant/soft/glide/cinema). */

  /* ── DS · Avatar sizes ── */
  --avatar-sm: 32px;
  --avatar-md: 40px;       /* canonical author-credit avatar */
  --avatar-lg: 48px;

  /* ── DS · Section vertical padding ──
     Catálogo cerrado de 4 escalones. Padding SIEMPRE simétrico arriba/abajo;
     la única excepción documentada es el hero (xl) que admite asimetría
     justificada en su comentario CSS.
       sm  → módulos compactos (reservado)
       md  → editorial default — la mayoría de secciones
       lg  → statement / bandas con peso visual
       xl  → heroes (home, pricing), 100vh/svh */
  /* DENSIDAD VERTICAL EN EL MÓVIL · un solo factor por página (16-10-2026).
     `--ritmo-movil` multiplica el SUELO de cada escalón —lo que se ve en un
     teléfono—; el techo, que es lo que se ve en un escritorio, no se toca.
     Vale 1 en todo el sitio. La home lo sube a 1,25 porque es la página más
     editorial y con el aire de serie se le juntaban las secciones.
     POR QUÉ EXISTE: la home llevaba los dos únicos números escritos a mano
     del sitio (80 y 120) y la revisión los listaba como deuda aceptada. Al
     mirarlos de cerca no eran dos caprichos: eran EXACTAMENTE 1,25 veces el
     suelo de su escalón, los dos. Era un patrón sin declarar. Ahora se
     declara, se puede reutilizar y no hay números sueltos que vigilar. */
  --ritmo-movil: 1;
  --section-pad-y-sm: clamp(calc(48px * var(--ritmo-movil)), 6vw, 80px);
  --section-pad-y-md: clamp(calc(64px * var(--ritmo-movil)), 8vw, 112px);
  --section-pad-y-lg: clamp(calc(96px * var(--ritmo-movil)), 12vw, 180px);
  --section-pad-y-xl: clamp(calc(120px * var(--ritmo-movil)), 22vh, 220px);

  /* `--bg-section-photographic/-loop/-deep/-elevated`, `--bg-final-cta`,
     `--bg-footer` — todos retirados 2026-05. Eran 6 gradients radial+linear
     custom para secciones de la homepage; sustituidos por las 3 bandas
     canónicas `--gradient-subtle/-soft/-strong` + viñetas opcionales vía
     `.has-vignette`. Cero consumers actuales. */

  /* ── DS · Breakpoints (4 canonical, mobile-first cascade) ──
     CSS uses literal values (custom properties don't work inside
     @media queries). These tokens are documentation references only. */
  --bp-xs: 480px;    /* compact phone — type micro-adjust */
  --bp-sm: 720px;    /* phone / portrait — full mobile layout */
  --bp-md: 1024px;   /* tablet — stack diptychs, hamburger nav · + vertical <1200 (QA 1043) */
  --bp-lg: 1200px;   /* small desktop max-width gate */

  /* ── DS · Radius extras
     `--radius-pill` retired 2026-04 → use `--radius-full` (same effect). */
  --radius-circle: 50%;     /* avatar, dot, icon button — geometrically distinct from --radius-full */

  /* ── DS · Shadows (3 elevations, 2026-04 simplification)
     `--shadow-card`, `--shadow-mockup`, `--shadow-popover` consolidated
     into 2 downward shadows (soft + strong). The upward `--shadow-stage`
     was renamed to `--shadow-stage-up` for explicitness. */
  --shadow-soft:     0 8px 24px rgba(0, 0, 0, 0.25),
                     inset 0 1px 0 rgba(255, 255, 255, 0.08);
  --shadow-strong:   0 16px 36px -12px rgba(0, 0, 0, 0.55);
  --shadow-stage-up: 0 -40px 90px -20px rgba(0, 0, 0, 0.85);
  /* Sombra apenas perceptible para tarjetas sobre fondo CLARO (las tres de
     arriba son para fondo oscuro y sobre blanco ensucian). Añadida 15-10-2026
     con la tarjeta blanca; es la que ya usaba Dolomites. */
  --shadow-on-light: 0 1px 4px rgba(0, 0, 0, 0.05);

  /* ── Glow — halo de acción en yellow-5 (2026-07 audit) ──
     Receta compartida entre el Loop de la home y Capabilities de App.
     En HTML se aplica como box-shadow vía estos tokens; en SVG el mismo
     halo va como `filter: drop-shadow(0 0 Npx color-mix(in srgb,
     var(--yellow-5) N%, transparent))` porque drop-shadow no acepta un
     token de box-shadow. Mantener AMBAS recetas con los mismos valores. */
  --glow-action-soft:   0 0 4px color-mix(in srgb, var(--yellow-5) 45%, transparent);
  --glow-action-strong: 0 0 12px color-mix(in srgb, var(--yellow-5) 85%, transparent);
  /* Halo NEUTRO (Irene, 29-10-2026 · QA 1060): una luz blanca muy suave que
     despega un objeto oscuro del fondo negro (el libro de Legacy), donde la
     sombra no se ve. No es de acción: nunca amarillo, nunca en algo pulsable
     para decir «púlsame». Va junto a la sombra del objeto, no en su lugar. */
  --glow-neutral: 0 0 48px color-mix(in srgb, var(--gray-white) 12%, transparent);

  /* ── Glass — receta backdrop-filter unificada (2026-07 audit) ──
     Dos intensidades cierran el catálogo: la canónica del chrome
     (.btn-ghost / .icon-btn) y la fuerte para paneles, barras de filtros
     y tooltips grandes. El blur del nav (20px/180%) queda como receta
     propia del site chrome, documentada en .nav. */
  --glass-filter:        blur(2.5px) saturate(120%);
  --glass-filter-strong: blur(12px) saturate(140%);

  /* ── Glass surfaces (tint) · 2026-08 ──
     El FILTRO ya está tokenizado arriba; esto tokeniza el FONDO translúcido
     del glass. Tres roles, uno por contexto:
       --glass-bg:        tinte claro sobre superficies OSCURAS (chrome:
                               btn-ghost, icon-btn) · va con --glass-filter.
       --glass-bg-panel:  oscuro casi opaco para paneles de contenido
                               (panel--glass) · va con --glass-filter-strong.
       --glass-bg-dark:   oscuro TRANSLÚCIDO sobre FOTO (deja ver la imagen,
                               p.ej. buscador del 404) · va con --glass-filter-strong. */
  --glass-bg:       rgba(255, 255, 255, 0.16);
  /* Los dos ESTADOS del cristal (2026-10). Existían solo como valores
     escritos a mano, repetidos en `.icon-btn`, en `.btn-ghost` y otra vez en
     la guía del sistema para simular el hover. Tres copias del mismo número:
     si una cambiaba, la guía enseñaba un valor que ya no era cierto. */
  --glass-bg-hover:  rgba(255, 255, 255, 0.22);
  --glass-bg-active: rgba(255, 255, 255, 0.28);
  --glass-bg-panel: rgba(10, 13, 18, 0.82);
  --glass-bg-dark:  rgba(10, 13, 18, 0.42);

  /* Aliases — old names kept for transitional compat. */

  /* ── DS · Z-index stack (canonical) ── */
  --z-base:    0;
  --z-overlay: 5;     /* photo credit, paginator overlay */
  --z-chip:    10;    /* chip strips above scrim */
  --z-modal:   50;
  --z-nav:     100;   /* sticky topbar */
  --z-toast:   1000;
  --z-grain:   9999;  /* film grain overlay (always on top) */

  /* ── DS · Motion durations · canonical ramp (4 steps) ──
     `--dur-snap` (200ms) and `--dur-instant` (100ms) unified 2026-05:
     kept the name "instant" at the snap value (200ms). `--dur-reveal`
     (800ms) absorbed by `--dur-cinema` (1100ms). Legacy `--dur-fast/
     normal/slow/hover` retired 2026-04. */
  --dur-instant: 200ms;   /* hover, focus, micro-interactions */
  --dur-soft:    320ms;   /* opacity / colour transitions */
  --dur-glide:   600ms;   /* per-element reveal */
  --dur-cinema: 1100ms;   /* the loop arc, loop crossfade */

  /* ── Spacing ── */
  --sp-tiny:    1px;
  --sp-2xs:     2px;
  --sp-xs:      4px;
  --sp-s:       8px;
  --sp-ds:      12px;
  --sp-default: 16px;
  --sp-m:       24px;
  --sp-l:       32px;
  --sp-xl:      40px;
  /* Aire bajo un título de sección, hasta lo que le sigue (2026-10-16). Era
     un número a mano en el artículo y otro en las páginas de texto; ahora es
     uno: 12 en móvil → 16 en escritorio, sobre la escala. */
  --sp-heading-after: clamp(var(--sp-ds), 1.2vw, var(--sp-default));
  /* Aire interior de una tarjeta GRANDE (cierre de las páginas de texto,
     formulario, Priority list de Missions, panel de calculadora): EL MISMO en
     los cuatro lados, 40 desde 720 y 24 en el teléfono (abajo). Las tarjetas
     pequeñas llevan menos. Sustituye a `--sp-card-inset-x` (24→80 solo a los
     lados), retirado 2026-10-27 · QA 965. */
  --sp-card-inset: var(--sp-xl);
  /* Aire LATERAL de un FORMULARIO a ancho de párrafo (Contact, Workshops,
     Camp): 80 desde 720, 24 en el teléfono (abajo); arriba y abajo lleva el
     de tarjeta grande, 40 (QA 999, 1003). */
  --sp-form-inset: calc(2 * var(--sp-xl));

  /* ── Radius ──
     The system intentionally has 4 radii: small (8) · medium (16) ·
     large (24) · full (9999, capsule). Plus --radius-circle (50%) above
     for true circles regardless of size.
     `--radius-xs` (4px) se retiró en 2026-04 y VUELVE el 15-10-2026 con un
     único uso: la casilla `.checkbox`. A 18px, con 8px de esquina parecía un
     botón de opción redondo; con 4 se lee como casilla (Irene). Segundo uso
     (26-10-2026 · QA 905): las muestras de color de los gráficos, que repiten
     la esquina de 4 de los tramos dibujados. Nada más. */
  --radius-xs:   4px;
  --radius-s:    8px;
  --radius-m:    16px;
  --radius-l:    24px;
  --radius-full: 9999px;

  /* ── Layout ── */
  /* COLUMNA DE LECTURA · 720 → 640 (Irene, 14-10-2026, tras la auditoría de
     legibilidad). A 720 la línea tenía 84 caracteres de media y hasta 89:
     por encima del techo de 80 que fija la accesibilidad AAA y lejos del
     rango cómodo (50-75). A 640 quedan 74 — prácticamente lo mismo que
     Medium (73). Afecta a TODO lo que va en la columna, no solo al texto:
     listas, tablas, avisos, citas y figuras normales. La banda ancha y las
     fotos a sangre no se tocan: ese contraste es deliberado. */
  --max-article: 640px;
  /* FORMULARIO · el ancho de una tarjeta de formulario suelta (Login).
     Más estrecho que la columna de lectura: un formulario no se lee de
     corrido, y a 640 los campos y botones apilados quedaban como bandas.
     Irene, 2026-10-16: se queda a 440, el que ya tenía Login. */
  --max-form:    440px;
  /* CABECERA DE PÁGINA · el bloque título + entradilla (+ su tarjeta, si la
     hay) de los heroes y de los listados. Un solo ancho para todas
     (Irene, 2026-10-16). Antes cada página tenía el suyo escrito a mano
     (640, 655, 720, 741, 960, 1024…). 760 y no 720: a 720 el lema de
     Experiences (741) se partía. Medido también en español: no rompe nada. */
  --max-header:  760px;
  --max-content: 980px;
  --max-wide:    1240px;
  --nav-height:  48px;
  /* ALTO DE PANTALLA · el de todo hero a pantalla completa (Irene, 29-10-2026 ·
     QA 1052-1053). Es lo que SE VE, medido en el aparato (`--alto-visible`, lo
     pone script.js al cargar y al girar); `100svh` solo, lo calcula el
     navegador de antemano y en iPad se pasaba: el crédito de la foto quedaba
     fuera de la primera vista. Nunca `100vh`/`100svh` a mano en un hero. */
  --alto-pantalla: min(100svh, var(--alto-visible, 100svh));
  --nav-bg:      var(--gray-black);   /* negro sólido (2026-07 · antes glass rgba 0.85 que se transparentaba con el scroll) */

  /* Gutter lateral · ÚNICO margen horizontal de sección para todo el sitio.
     Auditoría 2026-08: convivían 4 valores (12/16/24/32) → unificado a un
     token responsive: 16px en móvil (densidad + foto expansiva + convención
     iOS) que escala a 24px en tablet/desktop (aire editorial donde sobra
     espacio). Todas las secciones y `.container` lo usan; los bleeds full-bleed
     escapan por calc(-1 * var(--gutter)) y escalan solos.
     Curva: 16 hasta ~500px · llega a 24 a ~750px · 24 por encima. */
  --gutter: clamp(16px, 3.2vw, 24px);


  /* ── RETIRADOS EL 16-10-2026 · sin un solo consumidor ──────────────────
     Norma del propio proyecto (Irene, mayo 2026): «Removed — no consumers;
     reintroduce only when actually needed».
       · --shadow-card / -mockup / -popover / -stage → el MDS ya los daba por
         sustituidos en abril de 2026 por --shadow-soft / -strong / -stage-up,
         pero nunca se borraron de aquí: llevaban seis meses de adorno.
       · --brand-black / -blue / -red / -white y --content-text-brandbook-label
         → alias de --gray-9, --blue-5, --red-5, --gray-white y --gray-3. Un
         segundo nombre para el mismo color solo sirve para que dos personas
         escriban cosas distintas queriendo decir lo mismo.
       · --bg-tertiary-bold / -strong → hermanos de --bg-tertiary-soft, que ya
         se retiró en mayo por lo mismo. Se quedaron atrás.
       · --content-hint → alias de --gray-3.
       · --max-full (1440) → resto de cuando la nav se capaba y se centraba;
         esa decisión se retiró en agosto (ahora va a todo el ancho).
       · --content-link-disabled / -pressed → restos de la familia
         --color-link-*, retirada en mayo cuando el amarillo se quedó con
         Action y los enlaces. Son los estados «pulsado» y «deshabilitado»
         de un enlace, que no están diseñados en ninguna parte.
       · --signal-default / -hover / -on / -pressed → una familia roja
         entera que no usa nadie y que se pisa con --color-alert-*, que sí
         existe y sí se usa. «signal» parece el nombre viejo de «alert»,
         pero no quedaba constancia en la documentación. (Irene, 16-10:
         «creo que podemos prescindir de ellos».)
     Si mañana hace falta alguno, se vuelve a poner con su consumidor al lado.
     ───────────────────────────────────────────────────────────────────── */

}
@media (max-width: 720px) { :root { --sp-card-inset: var(--sp-m); --sp-form-inset: var(--sp-m); } }

/* Gráfico sobre fondo OSCURO: los mismos papeles de --chart-* con pasos
   más luminosos de las mismas rampas. Regla de fondo: la figura va clara
   en páginas de lectura y bandas claras; oscura sobre bandas oscuras. */
.chart-figure--dark {
  --chart-surface:   var(--gray-10);
  --chart-ink:       var(--gray-white);
  --chart-muted:     var(--gray-3);            /* 8,8:1 */
  --chart-grid:      color-mix(in srgb, var(--gray-white) 10%, transparent);
  --chart-baseline:  color-mix(in srgb, var(--gray-white) 28%, transparent);
  --chart-neutral:   var(--gray-9);
  --chart-cell-off:  var(--chart-neutral);
  --chart-tier-edge: var(--yellow-8);
  --chart-tier-in:   var(--yellow-6);
  --chart-tier-best: var(--yellow-4);
  --chart-sun:       var(--yellow-5);
  --chart-bluehour:  var(--lightblue-5);
  --chart-night:     var(--blue-4);
  --chart-aurora:    var(--green-5);
  --chart-moon:      var(--gray-2);
  --chart-moon-line: var(--gray-5);
  --chart-blood:     var(--red-6);
  --chart-water:     var(--lightblue-8);
  --chart-terrain:   var(--chart-neutral);
  --chart-watermark: var(--red-5);             /* 5,5:1 */
}

.container {
  max-width: var(--max-wide);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.container.narrow {
  max-width: var(--max-content);
}

.section-dark,
.section-darkest,
.section-stars,
.section-glow {
  position: relative;
  isolation: isolate;
}

.section-dark,
.section-darkest {
  color: var(--content-text-default);
}

.section-dark    { background: var(--bg-neutral-default); }
.section-darkest { background: var(--bg-neutral-bold); }

/* Ambient top-glow — very subtle radial spotlight that gives dark sections
   depth without visible texture. Inspired by Apple's product pages: you
   notice the air, not the gradient. */
.section-dark::before,
.section-darkest::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 80% 55% at 50% 0%,
              var(--white-a04) 0%,
              rgba(255, 255, 255, 0) 60%);
  pointer-events: none;
  z-index: -1;
}

/* ── Buttons ──────────────────────────────────────────────────────────
   Sizes:    .btn-xs (32px) · .btn-s (40px) · .btn-m (52px) · .btn-l (62px)
   Variants: .btn-primary (yellow) · .btn-tertiary (glass)
   States:   :hover · :active · :focus-visible · :disabled (all variants)
   ─────────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  font-weight: 600;
  border-radius: var(--radius-full);
  transition: background-color var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out),
              transform var(--dur-instant) var(--ease-out),
              box-shadow var(--dur-instant) var(--ease-out);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  border: 0;
}
/* Focus ring uses layered box-shadows instead of outline so it follows
   the button's border-radius (pill) consistently across all browsers.
   First ring matches the canvas (the offset); second is the yellow ring. */
.btn:focus-visible,
.btn.is-focus {
  outline: none;
  /* Franja = el lienzo (--anillo-hueco), aro = --anillo-foco: amarillo sobre
     oscuro, azul con franja blanca sobre claro. Escribir los colores a mano
     deja sobre blanco una franja negra y un aro amarillo que no se ve. */
  box-shadow: 0 0 0 3px var(--anillo-hueco), 0 0 0 5px var(--anillo-foco);
}
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* `.is-hover` / `.is-active` · la guía del sistema necesita enseñar los
   estados sin que haya un ratón encima, y hasta 2026-10 se los REDIBUJABA
   ella misma con valores copiados a mano. Se desincronizaron: la guía
   enseñaba el hover del terciario como cristal blanco cuando el sistema lo
   hace en gris. Ahora cada variante responde a su clase simulada con la
   MISMA declaración que a `:hover` — una sola fuente, imposible que mienta.
   `.is-focus` (2026-10-18) hace lo mismo con el anillo de foco: la guía lo
   enseña quieto en su columna sin que nadie tabule.
   No usar fuera de la guía. */

/* Sizes — height + horizontal padding + body font size */
/* `.btn-xs` (2026-10) · para CHROME compacto: barras sticky, cabeceras de
   panel, pies de card densos. NO para el CTA principal de una sección ni
   para nada dentro del cuerpo del artículo — ahí el suelo sigue siendo
   `.btn-s`. Su área TÁCTIL se extiende a 44px con un pseudo-elemento
   invisible, así que mide 32 pero se toca como 44 y sigue cumpliendo la
   regla de 40×40 del sistema. */
.btn-xs {
  height: 32px;
  padding: 6px 14px;
  font-size: var(--text-body-xs);
  position: relative;                      /* ancla del área táctil */
}
.btn-xs::after {
  content: "";
  position: absolute;
  inset: -6px 0;                           /* 32 + 6 + 6 = 44px de alto táctil */
}
.btn-s { height: 40px; padding: 10px 18px; font-size: var(--text-body-sm); }
.btn-m { height: 52px; padding: 14px 22px; font-size: var(--text-body-md); }
.btn-l { height: 62px; padding: 18px 24px; font-size: var(--text-body-md); }
/* En móvil y tablet (≤1024) el botón grande toma las medidas del mediano:
   pesaba demasiado frente a los títulos. Norma única del sistema (Irene,
   fila 99: «en móvil los botones bajan un tamaño» · QA 865 · tablet QA 1020). */
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) { .btn-l { height: 52px; padding: 14px 22px; } }

/* `.btn-tertiary` · solid grey (NO glass). Diferenciado de `.btn-ghost`,
   que sí es glass (white 0.16 + blur). El gris opaco simula el efecto
   que el glass deja sobre fondo negro pero sin transparencia — útil
   cuando hay que destacar acción secundaria sobre fondos heterogéneos
   (foto, satélite, gradientes) donde el glass podría no leer bien.
   Diferencia clave entre las tres variantes glass-ish:
   - .btn-ghost    → transparente, blur, alpha (mezcla con el fondo)
   - .btn-tertiary → gris opaco gray-8 (independiente del fondo)
   - .btn-dark     → near-black gray-11 (más sólido, contraste máximo) */
.btn-tertiary {
  background: var(--gray-8);
  color: var(--gray-white);
}
/* :hover solo con ratón: en un teléfono se queda PEGADO tras tocar y el botón
   parece desactivado (QA 561). `.is-hover` es para las demos de la guía. */
.btn-tertiary.is-hover  { background: var(--gray-7); color: var(--gray-white); }
@media (hover: hover) { .btn-tertiary:hover { background: var(--gray-7); color: var(--gray-white); } }
.btn-tertiary:active,
.btn-tertiary.is-active { background: var(--gray-6); }

/* ── Icon button · acción de un solo icono (cerrar, toggle, FAB) ───────
   Sizes: .icon-btn-s (32px · táctil 44) · .icon-btn-m (40px) · .icon-btn-l (44px)

   Apariencia única · glass (white alpha + backdrop blur), mirror exacto
   de `.btn-ghost`. Funciona sobre superficies oscuras del DS y sobre
   fotos por igual (el blur ancla el botón sin necesidad de scrim sólido).

   El catálogo NO incluye variante "primary" porque las acciones
   destacadas usan `.btn-primary` (pill con label). El icon-btn cubre
   solo UI chrome (cerrar, expandir, FAB de menú, scroll-to-top).
   ─────────────────────────────────────────────────────────────────── */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-circle);
  border: 0;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  color: var(--gray-white);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  transition: background-color var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out),
              transform var(--dur-instant) var(--ease-out);
}

/* :hover solo con ratón: en un teléfono se queda PEGADO tras tocar y el botón
   parece desactivado (QA 561). `.is-hover` es para las demos de la guía. */
.icon-btn.is-hover {
  background: var(--glass-bg-hover);
  color: var(--gray-white);
}
@media (hover: hover) { .icon-btn:hover { background: var(--glass-bg-hover); color: var(--gray-white); } }

.icon-btn:active,
.icon-btn.is-active {
  background: var(--glass-bg-active);
}

/* Focus ring · misma técnica que .btn (box-shadow capas) para que el
   anillo siga el radius circle en todos los navegadores. */
.icon-btn:focus-visible,
.icon-btn.is-focus {
  outline: none;
  box-shadow: 0 0 0 2px var(--anillo-hueco), 0 0 0 4px var(--anillo-foco);
}

.icon-btn:disabled,
.icon-btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* Sizes · cuadrado fijo, icono centrado vía flex. SVG hereda currentColor. */
/* La talla S mide 32 pero se TOCA como 44, igual que `.btn-xs`: el mínimo de
   40×40 del sistema se cumple con el área, no con el dibujo (añadido 2026-10
   al incorporar `.btn-xs`; antes la S se quedaba corta allá donde se usara). */
.icon-btn-s { width: 32px; height: 32px; position: relative; }
.icon-btn-s::after { content: ""; position: absolute; inset: -6px; }
.icon-btn-m { width: 40px; height: 40px; }
.icon-btn-l { width: 44px; height: 44px; }

/* Variant · tertiary: solid gray-8 (no glass), espejo de `.btn-tertiary`.
   Para FABs sobre fotos o superficies heterogéneas donde el glass
   puede leer mal. El sólido ancla el botón independientemente del fondo. */
.icon-btn-tertiary {
  background: var(--gray-8);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.icon-btn-tertiary.is-hover  { background: var(--gray-7); color: var(--gray-white); }
@media (hover: hover) { .icon-btn-tertiary:hover { background: var(--gray-7); color: var(--gray-white); } }
.icon-btn-tertiary:active,
.icon-btn-tertiary.is-active { background: var(--gray-6); }

.photo-credit {
  position: absolute;
  bottom: 1rem;
  right: 1.5rem;
  /* Mono caption — shared component used by the hero and any photo
     gallery on the page (e.g. Experiences). */
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--white-a65);
  text-shadow: 0 1px 4px var(--black-a55);
  z-index: var(--z-overlay);
  pointer-events: none;
  margin: 0;
}

/* Top-right variant — used inside the Experiences gallery so the
   credit doesn't compete with the paginator at the bottom. */
.photo-credit--top {
  top: clamp(12px, 1.6vw, 20px);
  right: clamp(14px, 1.8vw, 22px);
  bottom: auto;
}

@keyframes scrollHintFade {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes scrollHintPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* Calm vertical bounce — a 5px wink to invite scrolling without
   distracting. Keeps `translateX(-50%)` from the base centering and
   adds a gentle Y oscillation. ease-in-out + 2.6s feel intentional
   rather than restless. */
@keyframes scrollHintBounce {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(9px); }
}

.app-rating {
  margin-top: 2.25rem;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--gray-white);
  font-size: var(--text-body-sm);
}

.app-rating-score {
  font-weight: 600;
  font-size: var(--text-heading-sm);
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--gray-white);
}

.app-rating-stars {
  display: inline-flex;
  gap: 2px;
  color: var(--brand-yellow);
  align-items: center;
}

.app-rating-stars svg {
  width: 16px;
  height: 16px;
  display: block;
}

.app-rating-count {
  font-family: var(--font-mono);
  font-size: var(--text-body-xs);
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--content-text-subtitle);
  padding-left: 0.65rem;
  border-left: 1px solid var(--white-a20);
  line-height: 1.4;
}
/* Reviews · downloads · en DESKTOP van en una línea, separados por el MISMO
   divider (border-left) que separa las estrellas del contenido. */
.app-rating-count__item + .app-rating-count__item {
  margin-left: 0.65rem;
  padding-left: 0.65rem;
  border-left: 1px solid var(--white-a20);
}
@media (max-width: 720px) {
  /* Móvil · los dos datos APILADOS en 2 líneas, sin divider entre ellos
     (se mantiene el divider estrellas│bloque del .app-rating-count). */
  .app-rating-count { display: inline-flex; flex-direction: column; }
  .app-rating-count__item + .app-rating-count__item {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
  }
}

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}

.section-header {
  text-align: center;
  max-width: var(--max-content);
  margin: 0 auto 2.5rem;
}

/* Match the typographic scale used in product-copy sections (The App,
   Premium) so titles, subtitles and pitch bodies render identically across
   the page — only alignment changes (centered here vs left there). */
.section-header > .section-subtitle {
  font-size: var(--text-editorial-subtitle);
  line-height: 1.125;
  font-weight: 300;
  margin-bottom: 0;
}

.section-header > .section-rule {
  margin: 2rem auto;
}

.section-header > .pitch-body {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body);
  font-weight: 400;
  max-width: 56ch;
  margin: 0 auto;
}

@keyframes baHint {
  0%, 100% { transform: translateX(0); }
  30%      { transform: translateX(-8px); }
  70%      { transform: translateX(8px); }
}

/* (Legacy .loop-dots / .loop-dot rules removed — the live Loop uses
   .loop-stop instead.) */

.section-rule {
  display: block;
  width: 64px;
  height: 2px;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--gray-6);
}

/* ─────────────────────────────────────────────────────────────────────
   Inline text links — yellow + underlined (2026-05)

   Default `a` style is no underline (set in reset.css) so styled
   buttons / nav / icons stay clean. Inline text links inside
   editorial copy get the canonical link treatment automatically;
   anywhere else, opt in with the `.text-link` class.
   ───────────────────────────────────────────────────────────────────── */
.pitch-body a,
.faq__a a,
.includes__group-note a,
p a,
.text-link {
  color: var(--content-link-default);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

.pitch-body a:hover,
.faq__a a:hover,
.includes__group-note a:hover,
p a:hover,
.text-link:hover {
  color: var(--content-link-hover);
}

/* ─────────────────────────────────────────────────────────────────────
   .info-tip — small "i" with hover/focus tooltip bubble (2026-05)

   Inline icon-button used to attach contextual help to a label
   (typically inside fineprint, captions, form fields). The bubble is
   a CSS-only popover — opens on hover, focus and focus-within.

   Markup:
     <span class="info-tip" tabindex="0" role="img"
           aria-label="<the help text>">i<span
           class="info-tip__bubble"><the help text></span></span>

   Notes:
     · `tabindex="0"` makes it keyboard-focusable; `role="img"` keeps the
       SR description simple. The bubble's text is duplicated as
       `aria-label` so screen readers always announce it.
     · The bubble uses --shadow-strong + the canonical hairline + radius-s.
     · Opens via `.info-tip:hover/:focus-visible/:focus-within`.
   ───────────────────────────────────────────────────────────────────── */
.info-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-left: 4px;
  border: 1px solid var(--border-neutral-soft);
  border-radius: var(--radius-circle);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  line-height: 1;
  color: var(--content-text-default-weak);
  cursor: help;
  position: relative;
  vertical-align: middle;
  transition: border-color var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out);
}

.info-tip:hover,
.info-tip:focus-visible {
  border-color: var(--border-secondary-accent);
  color: var(--content-text-default);
  outline: none;
}

.info-tip__bubble {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 260px;
  padding: 10px 14px;
  background: var(--bg-neutral-default);
  border: 1px solid var(--border-neutral-soft);
  border-radius: var(--radius-s);
  box-shadow: var(--shadow-strong);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  letter-spacing: 0;
  font-weight: 400;
  line-height: 1.45;
  color: var(--content-text-default);
  text-align: left;
  text-transform: none;
  cursor: default;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-soft) var(--ease-out),
              visibility 0s linear var(--dur-soft);
  z-index: var(--z-popover, 50);
  pointer-events: none;
}

.info-tip:hover .info-tip__bubble,
.info-tip:focus-visible .info-tip__bubble,
.info-tip:focus-within .info-tip__bubble {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* ─────────────────────────────────────────────────────────────────────
   .faq — accordion canónico de preguntas frecuentes (2026-07 audit)

   Promocionado desde premium-pricing (.faq-item) y experiences
   (.exp-faq), que mantenían dos copias espejo de la misma receta.
   Consumido por: premium-pricing, premium, app, experiences.

   Markup:
     <details class="faq">
       <summary class="faq__q">Question?</summary>
       <div class="faq__a"><p>Answer.</p></div>
     </details>

   Notas:
     · La summary funciona con o sin la clase `.faq__q` (el selector
       `.faq > summary` cubre ambos casos).
     · La respuesta admite wrapper `.faq__a` (permite varios párrafos,
       listas, links) o `<p>` directos hijos del details.
     · El indicador +/− es amarillo (el toggle es el action signifier).
     · Question default: --text-body-lg. Páginas de lectura densa pueden
       bajarla un peldaño a --text-body-md con un override page-scoped
       (ver experiences).
   ───────────────────────────────────────────────────────────────────── */
.faq {
  border-bottom: 1px solid var(--border-neutral-soft);
  /* Token de la escala (2026-10-16 · iban 18px escritos a mano). */
  padding: var(--sp-default) 0;
}
/* Abierta, deja más aire debajo de la respuesta para que no se pegue a la
   pregunta siguiente (18 → 32 · Irene, 2026-10-16). */
.faq[open] { padding-bottom: var(--sp-l); }
/* Al abrir y cerrar, el aire de abajo acompaña a la respuesta, que despliega
   `script.js` (QA 1068); `.faq__cuerpo` guarda dentro sus márgenes. */
.faq { transition: padding-bottom var(--dur-soft) var(--ease-soft); }
.faq.is-cerrando { padding-bottom: var(--sp-default); }
.faq__cuerpo { display: flow-root; }
@media (prefers-reduced-motion: reduce) { .faq { transition: none; } }

.faq > summary {
  position: relative;
  list-style: none;          /* Hide default disclosure triangle */
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-body-lg);
  /* Mismo peso que el cuerpo (400): la jerarquía pregunta↔respuesta ya la
     dan el color (pregunta blanca / respuesta weak) y el tamaño (21 vs 18).
     El 600 sobraba. (Decisión Irene 2026-08 · antes accordion titles = 600.) */
  font-weight: 400;
  color: var(--content-text-default);
  padding-right: 32px;
  line-height: 1.4;
  transition: color var(--dur-instant) var(--ease-out);
}

.faq > summary::-webkit-details-marker { display: none; }
/* Badge de estado en la pregunta (expediciones con sus plazas): centrado con
   el texto, no apoyado en la línea base. */
.faq > summary .mis-badge { vertical-align: middle; margin-left: var(--sp-s); }

/* Custom + / − indicator — yellow (the toggle is the action signifier). */
.faq > summary::after {
  content: '+';
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: var(--text-body-lg); /* decorative '+' marker */
  font-weight: 400;
  line-height: 1;
  color: var(--content-text-highlight);
  transition: transform var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out);
}

.faq[open] > summary::after {
  content: '−';
  color: var(--content-text-highlight);
}

/* Hover solo con ratón: en un teléfono se queda pegado tras tocar (QA 561). */
@media (hover: hover) {
  .faq > summary:hover { color: var(--content-text-highlight); }
}
/* Desplegada, la pregunta se queda en amarillo: marca cuál está abierta
   (Irene, 2026-10-16). */
.faq[open] > summary { color: var(--content-text-highlight); }

/* Respuesta · interlineado de CUERPO del sistema (--lh-body, 1,47), el mismo
   de los artículos. Iba a 1,6 (Irene, 2026-10-16: «¿sigue las normas de
   páginas de contenido?»). */
.faq__a {
  margin-top: 10px;
  /* Respuesta en gris-2 (≈13:1 sobre negro), no en el débil (10:1): en la FAQ
     oscura el débil se leía poco (Irene, 2026-10-19 · QA 526 y 528). La FAQ
     clara sigue con su gris on-light más abajo. */
  color: var(--gray-2);
  font-size: var(--text-body-md);
  line-height: var(--lh-body);
}

.faq__a p {
  margin: 0 0 12px;
  line-height: var(--lh-body);
  font-style: normal;        /* blockquote/em nunca italic (regla DS) */
}

.faq__a p:last-child { margin-bottom: 0; }

/* Variante sin wrapper — párrafos directos dentro del details. */
.faq > p,
.faq__cuerpo > p {
  margin: 10px 0 12px;
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-style: normal;
  line-height: var(--lh-body);
  color: var(--gray-2);          /* mismo gris que .faq__a (QA 526 y 528) */
}

.faq > p:last-of-type,
.faq__cuerpo > p:last-of-type { margin-bottom: 0; }

/* ── .faq--light · la versión CLARA, para lectura larga sobre blanco
   (páginas de texto · 2026-10-16). La oscura sigue acompañando a las landings
   (Pricing, Experiences, Dolomites). Sobre blanco el amarillo NO pasa AA
   (≈1,4:1), así que aquí ni el hover ni la pregunta abierta se ponen amarillos:
   la abierta se marca con PESO (600) y el +/− va en gris on-light. */
.faq--light { border-bottom-color: var(--border-on-light); }
.faq--light > summary { color: var(--content-text-on-light-default); }
.faq--light > summary::after,
.faq--light[open] > summary::after { color: var(--content-text-on-light-weak); }
/* Hover en la clara: la pregunta pasa a 600, como abierta (el amarillo no se
   lee sobre blanco). Solo con ratón (QA 561). */
@media (hover: hover) {
  .faq--light > summary:hover { color: var(--content-text-on-light-default); font-weight: 600; }
}
.faq--light[open] > summary { color: var(--content-text-on-light-default); font-weight: 600; }
.faq--light .faq__a,
.faq--light > p,
.faq--light .faq__cuerpo > p { color: var(--content-text-on-light-weak); }


/* ─────────────────────────────────────────────────────────────────────
   .faq-section — el BLOQUE de preguntas frecuentes de una landing
   (Pricing, Premium, App, Experiences · QA 861, 2026-10-23).
   Banda + título + columna de lectura con `.faq` dentro. El texto cambia
   por página; título, ancho, aire y piezas son los mismos en todas.
     · __list    → las preguntas, con filete arriba que cierra el bloque.
     · __group   → opcional: varios temas, cada uno con su __label (mono).
     · __more    → opcional: línea de cierre con un enlace.
   El fondo por defecto es `--gradient-strong`; una página puede cambiarlo
   para su cascada tonal (App va sobre negro), nunca las medidas.
   ───────────────────────────────────────────────────────────────────── */
.faq-section {
  background: var(--gradient-strong);
  padding: var(--section-pad-y-md) var(--gutter);
}
.faq-section__inner {
  max-width: var(--max-article);   /* 640 · el ancho del texto base (QA 545) */
  margin: 0 auto;
}
.faq-section__title {
  margin: 0 0 var(--sp-xl);
  font-family: var(--font-sans);
  font-size: var(--text-heading-md);
  font-weight: 600;
  line-height: var(--lh-heading);
  text-align: center;
  text-wrap: balance;
  color: var(--content-text-default);
}
.faq-section__list { border-top: 1px solid var(--border-neutral-soft); }
.faq-section__group + .faq-section__group { margin-top: var(--sp-xl); }
.faq-section__group > .faq:first-of-type { border-top: 1px solid var(--border-neutral-soft); }
.faq-section__label {
  margin: 0 0 var(--sp-default);
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--content-text-default-weak);
}
.faq-section__more {
  margin: var(--sp-l) 0 0;
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-weight: 400;
  line-height: var(--lh-body);
  text-align: center;
  text-wrap: pretty;
  color: var(--content-text-default-weak);
}


/* ─────────────────────────────────────────────────────────────────────
   .acc-toggle — pill de toggle para accordions (2026-07)

   Promocionado desde Dolomites v8 (.exp-hero-shot__plan-summary-icon),
   donde vivía sin documentar. Pill mono uppercase con label dinámico
   (data-attributes) + chevron que rota al abrir. Vive DENTRO del
   <summary> de un <details> (aria-hidden: el summary ya es el control).

   Markup:
     <details>
       <summary> …contenido del summary…
         <span class="acc-toggle" data-label-closed="See details"
               data-label-open="Hide details" aria-hidden="true"></span>
       </summary>
       …
     </details>

   Modo de superficie:
     · Default → on-dark (hairline --border-neutral-soft, texto weak).
     · `.acc-toggle--on-light` → superficies claras (--border-on-light).
     · Páginas con secciones que alternan dark/light pueden re-mapear
       las custom props (--acc-toggle-*) a sus tokens semánticos de
       sección (así lo hace Dolomites con --exp-*).
   ───────────────────────────────────────────────────────────────────── */
.acc-toggle {
  --acc-toggle-color:        var(--content-text-default-weak);
  /* Stroke a white 40% (no el hairline 16% decorativo): es el borde
     de un CONTROL interactivo y debe pasar el contraste no-textual
     de WCAG 1.4.11 (≥3:1 sobre el fondo). White 40% sobre las bandas
     oscuras del catálogo ≈ 3.7:1 (2026-07, feedback usuaria). */
  --acc-toggle-border:       var(--border-neutral-strong);
  --acc-toggle-color-hover:  var(--content-text-default);
  --acc-toggle-border-hover: color-mix(in srgb, var(--gray-white) 55%, transparent);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  background: transparent;
  border: 1px solid var(--acc-toggle-border);
  border-radius: var(--radius-full);
  color: var(--acc-toggle-color);
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
  transition: color var(--dur-instant) var(--ease-out),
              border-color var(--dur-instant) var(--ease-out);
}

/* Label dinámico · lee data-attributes; el details[open] ancestro
   conmuta closed → open sin CSS específico por accordion. */
.acc-toggle::before {
  content: attr(data-label-closed);
}

details[open] > summary .acc-toggle::before {
  content: attr(data-label-open);
}

/* Chevron · rotación animada al abrir. */
.acc-toggle::after {
  content: '';
  width: 7px;
  height: 7px;
  margin-top: -2px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform var(--dur-soft) var(--ease-out),
              margin-top var(--dur-soft) var(--ease-out);
}

details[open] > summary .acc-toggle::after {
  margin-top: 2px;
  transform: rotate(225deg);
}

@media (prefers-reduced-motion: reduce) {
  .acc-toggle::after { transition: none; }
}

details > summary:hover .acc-toggle {
  color: var(--acc-toggle-color-hover);
  border-color: var(--acc-toggle-border-hover);
}

/* Superficies claras (light mode del componente). */
.acc-toggle--on-light {
  --acc-toggle-color:        var(--content-text-on-light-weak);
  /* Mismo criterio WCAG 1.4.11: black 45% sobre off-white ≈ 3.4:1. */
  --acc-toggle-border:       rgba(10, 10, 10, 0.45);
  --acc-toggle-color-hover:  var(--content-text-on-light-default);
  --acc-toggle-border-hover: rgba(10, 10, 10, 0.6);
}

/* ─────────────────────────────────────────────────────────────────────
   .has-vignette — atmospheric overlay utility (2026-04 DS-Web)

   Single radial vignette in the canonical blue-gray cinematic tint.
   Add `.has-vignette` to any section that needs atmospheric character.

   Intensity (default = soft):
     .has-vignette--strong

   Position is NOT part of the documented API — it's set ad-hoc per
   section via the CSS custom properties `--vignette-x` and
   `--vignette-y` (default 50% / 50%). Each page positions the vignette
   behind a key visual element of that section (a logo, a hero word,
   a device mockup) by overriding the variables in page-specific CSS.

   Example (in a page's style.css):
     .product-section--app { --vignette-x: 70%; --vignette-y: 50%; }
   ───────────────────────────────────────────────────────────────────── */
.has-vignette {
  position: relative;
  isolation: isolate;
  --vignette-color: var(--vignette-soft);
  --vignette-x: 50%;
  --vignette-y: 50%;
}

.has-vignette::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
    ellipse 70% 60% at var(--vignette-x) var(--vignette-y),
    var(--vignette-color),
    transparent 75%
  );
}

/* Direct children stack above the vignette · `.photo-credit` y
   `.hero-scroll-hint` se excluyen porque son `position: absolute`
   canónicos con anclaje propio (bottom-right y bottom-center
   respectivamente). El selector general los forzaba a `relative`
   → perdían su posicionamiento.
   IMPORTANTE: los `:not(...)` van envueltos en `:where()` para que
   no aporten especificidad. Si no, la regla pasaría de (0,1,0) a
   (0,3,0) y machacaría declaraciones page-scoped como `position:
   absolute` en `.exp-hero__media` / `.exp-movement__media`,
   descuadrando los hero full-bleed. */
.has-vignette > *:where(:not(.photo-credit):not(.hero-scroll-hint)) {
  position: relative;
  z-index: 1;
}

/* Intensity (the only documented modifier) */
.has-vignette--strong { --vignette-color: var(--vignette-strong); }

/* ─────────────────────────────────────────────────────────────────
   .badge — unified contextual label (2026-04 DS-Web rename)
   Replaces .eyebrow-tag (mono pill), .plan-tier (sans pill in author
   credits), and .beta-badge (SVG inline with section title).
   ───────────────────────────────────────────────────────────────── */

/* Default badge — yellow outline pill, mono uppercase.
   Used standalone or inline with body text. Mono is the canonical
   typography for badges (firma técnica) — 2026-05. */
.badge {
  display: inline-block;
  padding: 3px 12px;
  background: transparent;
  border: 1px solid var(--yellow-5);
  border-radius: var(--radius-full);
  color: var(--yellow-5);
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.2;
  white-space: nowrap;
  vertical-align: middle;
}

/* `.badge--mono` retired 2026-05 — mono is now the default.
   Selector kept as a no-op alias so older markup still validates. */
.badge--mono { /* intentionally empty — same as .badge default */ }

/* Inline-with-title — sized RELATIVE to the parent heading so it
   shrinks/grows with the title. Combine with .badge--mono for the
   classic kicker-on-title pattern (e.g. "Captured Plans · BETA"). */
.badge--inline-title {
  font-size: 0.28em;
  padding: 0.35em 0.9em;
  letter-spacing: 2px;
  margin-left: 0.4em;
  position: relative;
  top: -0.4em;
}

/* Sobre fondo CLARO (2026-10-20 · QA 624): el amarillo sobre blanco no se lee
   (~1,3:1). Texto casi negro y filete en el gris débil sobre claro (7:1). Se
   pide con el modificador, como `.faq--light` o `.card--light`. */
.badge--light {
  color: var(--content-text-on-light-default);
  border-color: var(--content-text-on-light-weak);
}

/* `.badge--brand` y `.badge--huge` retirados 2026-05 — el SVG-Beta-mark
   se sustituyó por badge de texto mono (`<span class="badge badge--inline-title">Beta</span>`)
   para mantener la coherencia tipográfica. Si en el futuro vuelve a hacer
   falta un badge tipo SVG/imagen, reintroducir el modifier. */

/* ── Legacy aliases — keep until all pages migrate to .badge.
   Both selectors behave identically to .badge for now. */
.eyebrow-tag {
  display: inline-block;
  padding: 4px 14px;
  background: transparent;
  border: 1px solid var(--yellow-5);
  border-radius: var(--radius-full);
  color: var(--yellow-5);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.2;
  white-space: nowrap;
  vertical-align: middle;
}

/* `.section-title .badge--brand` y `.section-title .beta-badge` retirados
   2026-05 junto con la migración SVG → texto mono (`.badge--inline-title`).
   Si vuelve a hacer falta un badge-imagen, restaurar estas reglas. */

/* ===========================================================================
   CARD · contenedor de contenido canónico (promovido al DS · 2026-08 ·
   refundado sobre auditoría transversal de las 12 hojas · docs/DS-COMPONENT-AUDIT.md)
   Consolida la docena de cards que vivían sueltas por página (.mis-card,
   .review-card, .showcase-card, .award-card, .feature-tile, .sr-card…):
   todas resolvían lo mismo (foto opcional + tipo + título + texto
   en una caja) con recetas ligeramente distintas. Base tomada de `.mis-card`
   (la más completa del sitio); superficie unificada a `--gradient-subtle`
   (decisión Irene 2026-08 · el gradient "card on dark" del catálogo).
   Esta es la única card del sistema; las demás se migran poco a poco.
   Excepción a propósito: `.plan-card` (precio y compra, no contenido · QA 861).
   Superficie = `--gradient-subtle` + filete `--border-neutral-soft` +
   `--radius-l`. Hover (solo en --link): lift 2px + filete más claro +
   shadow-soft + zoom 1.04 de la foto (patrón .mis-card). Media opcional.
   Reutiliza otros componentes por dentro — NO duplicar: tags → `.badge`,
   autor → `.author-credit`, rating → `.app-rating`.
   Anatomía:
     .card               contenedor (columna: media opcional + body)
     .card--link         toda la card es <a> · lift + zoom de foto + foco al hover
     .card--compact      grids densos (resultados, listados): padding y tipo
                         un paso más pequeños; mismo radius para coherencia
     .card--light        superficie CLARA (blanca) · para bandas on-light
                         (guías, artículo, mapas Dolomites)
     .card--row          horizontal densa: miniatura + texto (recomendaciones)
     .card--row-l        horizontal de listado: la tipografía de escritorio
                         en tablet y escritorio (ficha de persona · QA 892)
     .card__media        franja de imagen/vídeo arriba · ratio vía --card-ratio
     .card__kind         afordancia de play (overlay) para media de vídeo
     .card__icon         icono (alternativa al media · feature-tile)
     .card__body         padding + columna + gap
     .card__eyebrow      metadato mono (tipo/categoría) · yellow uppercase
     .card__meta-row     fila eyebrow + meta (space-between)
     .card__meta         píldora mono de dato (duración, tiempo de lectura)
     .card__title        sans 600 (título)
     .card__text         sans weak (descripción)
     .card__footer       meta mono separada por filete (fecha, fuente…)
     .card__actions      fila de botones/CTA al pie (ver regla de interacción)
   Extensiones de dominio (NO en la base) que viven en su página: los datos de
   misión (.card--mission → facts/availability/sold-out) y los de precio
   (chip/amount/per). Ver el documento de auditoría.
   =========================================================================== */
.card {
  display: flex;
  flex-direction: column;
  background: var(--gradient-subtle);
  /* El fondo NO pasa por debajo del borde: el filete (blanco 16 %) se pinta sobre
     la página y sale de un solo tono, el mismo que el de la insignia. Si el fondo
     llega al borde, el filete se mezcla con el degradado y cambia de tono. */
  background-clip: padding-box;
  border: 1px solid var(--border-neutral-soft);
  border-radius: var(--radius-l);
  overflow: hidden;
  color: var(--content-text-default);
}

/* Card enlazada — toda la tarjeta es un <a>. Sin subrayado + el hover
   canónico de .mis-card: lift 2px + filete más claro + shadow-soft. */
a.card,
.card--link {
  text-decoration: none;
  transition: border-color var(--dur-instant) var(--ease-out),
              transform var(--dur-instant) var(--ease-out),
              box-shadow var(--dur-instant) var(--ease-out);
}
a.card:hover,
.card--link:hover {
  border-color: color-mix(in srgb, var(--gray-white) 24%, transparent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-soft);
}
a.card:focus-visible,
.card--link:focus-visible {
  outline: 2px solid var(--anillo-foco);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  a.card:hover,
  .card--link:hover { transform: none; }
}

/* Media — imagen o vídeo arriba. Ratio configurable por instancia con
   --card-ratio (default 16/10 · usa 4/3, 1/1… según el listado). */
.card__media {
  position: relative;
  margin: 0;
  aspect-ratio: var(--card-ratio, 16 / 10);
  overflow: hidden;
  background: var(--gradient-subtle);
}
/* Con <picture> (una imagen por formato: apaisada en escritorio, cuadrada en la
   miniatura del móvil) el envoltorio no cuenta: la imagen se coloca como si fuera
   hija directa de la media. */
.card__media > picture { display: contents; }
.card__media img,
.card__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--dur-glide) var(--ease-out);
}
/* Zoom sutil de la foto al hover de una card enlazada (patrón .mis-card). */
a.card:hover .card__media img,
a.card:hover .card__media video,
.card--link:hover .card__media img,
.card--link:hover .card__media video { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  a.card:hover .card__media img,
  a.card:hover .card__media video,
  .card--link:hover .card__media img,
  .card--link:hover .card__media video { transform: none; }
}

/* Afordancia de play — para cards de vídeo (icono SVG dentro, sin literal). */
/* INSIGNIA DE TIPO · disco sobre la foto que dice de qué es la tarjeta.
   Se llamaba `.card__kind` porque nació para el vídeo, pero el papel no es
   «reproducir»: es «esto es un vídeo» — y eso vale igual para una calculadora
   o una guía (Irene, 2026-10-14). El icono lo pone el marcado; el componente
   solo pone el disco. */
/* POSICIÓN: la insignia de tipo va SIEMPRE abajo a la izquierda, también el
   play del vídeo y también en móvil — sin excepciones (Irene, 2026-10-21).
   FILETE: el del sistema (blanco 16 %) dibuja el disco cuando la media es
   casi negra (portadas de calculadora); sin él solo flota el icono. */
.card__kind {
  position: absolute;
  inset: auto auto 10px 10px;
  margin: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-circle);
  background: var(--black-a55);
  box-shadow: inset 0 0 0 1px var(--border-neutral-soft);
  color: var(--gray-white);
  display: flex;
  align-items: center;
  justify-content: center;
}
.card__kind svg { width: 18px; height: 18px; display: block; }
/* `.card__kind--play` ya no cambia ni sitio ni tamaño (2026-10-21): se queda en
   el marcado solo como nombre del tipo «vídeo». */

/* Body */
.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-s);
  padding: clamp(18px, 1.8vw, 24px);
}

/* Icono — alternativa al media para cards sin foto (feature-tile: icono +
   nombre + desc). Va arriba del body. Color de acento por defecto. */
.card__icon { width: 32px; height: 32px; color: var(--content-text-highlight); }
.card__icon svg { width: 100%; height: 100%; display: block; }
/* Icono desde un asset SVG vía mask-image, tintado con el acento (reutiliza el
   SVG sin tocarlo · feature-tiles). La URL va por inline `style="--icon:url()"`. */
.card__icon--mask {
  background-color: var(--content-text-highlight);
  -webkit-mask: var(--icon) center / contain no-repeat;
          mask: var(--icon) center / contain no-repeat;
}

/* Eyebrow / tipo — metadato mono pequeño (categoría, tipo de contenido).
   Mono es correcto aquí: es metadato, no acción ni título. */
.card__eyebrow {
  font-family: var(--font-mono);
  /* 14px (2026-08 · decisión de Irene): el eyebrow es lo que ORDENA la card —
     dice de qué tipo es — y a 12 se leía como una nota al pie. Sube un peldaño
     del ramp mono. La píldora `.card__meta` NO le acompaña: se queda en 12
     porque es dato secundario, y esa diferencia de tamaño es jerarquía
     deliberada dentro de `.card__meta-row`, no un descuadre. */
  font-size: var(--text-mono-md);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--content-text-highlight);
  margin: 0;
}

/* Meta-row — antetítulo + dato mono a la derecha, en la MISMA fila. Es la
   cabecera de metadatos de una card de listado (Videos, Guides, Search). */
.card__meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* Meta — píldora mono compacta con el dato del item ("28 min", "14 min read").
   Promovida al DS 2026-08 desde `.vid-duration` (Videos) + `.gd-readtime`
   (Guides), que eran idénticas; Search es el tercer uso. Distinta de `.chip`
   (píldora sans, más grande, es etiqueta/filtro clicable): ésta es metadato
   pasivo dentro de una card. */
.card__meta {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  color: var(--content-text-default);
  background: color-mix(in srgb, var(--gray-white) 10%, transparent);
  border-radius: var(--radius-full);
  padding: 2px 8px;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
/* Icono delante del dato (Irene, 2026-10-14): en un módulo que mezcla tipos,
   «12 min» no dice si se lee o se ve. Libro = tiempo de lectura · play =
   duración. El icono es una máscara: toma el color del texto de la píldora. */
.card__meta--read,
.card__meta--watch {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
}
.card__meta--read::before,
.card__meta--watch::before {
  content: "";
  flex: none;
  width: 1em;
  height: 1em;
  background-color: currentColor;
  -webkit-mask: var(--_meta-icon) center / contain no-repeat;
          mask: var(--_meta-icon) center / contain no-repeat;
}
.card__meta--read {
  --_meta-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 4h6a4 4 0 0 1 4 4v13a3 3 0 0 0-3-3H2z'/%3E%3Cpath d='M22 4h-6a4 4 0 0 0-4 4v13a3 3 0 0 1 3-3h7z'/%3E%3C/svg%3E");
}
.card__meta--watch {
  --_meta-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M7 4.5v15l12.5-7.5z'/%3E%3C/svg%3E");
}
.card--light .card__meta {
  background: color-mix(in srgb, var(--gray-black) 8%, transparent);
  color: var(--content-text-on-light-default);
}

/* Título — sans 600 (regla universal de títulos del DS). */
.card__title {
  font-family: var(--font-sans);
  font-size: var(--text-body-lg);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--content-text-default);
  margin: 0;
}

/* Texto / descripción */
/* El texto de la card es un párrafo: texto base en todos los tamaños (QA 544/547).
   Las variantes densas (--compact, --row) conservan su tamaño. */
.card__text {
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  line-height: var(--lh-body);
  font-weight: 400;
  color: var(--content-text-default-weak);
  margin: 0;
}

/* Footer — meta mono separada por filete (autor, fecha, fuente, rating…). */
.card__footer {
  margin-top: var(--sp-xs);
  padding-top: var(--sp-default);
  border-top: 1px solid var(--border-neutral-soft);
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  letter-spacing: 0.02em;
  color: var(--content-text-default-weak);
}
.card__footer strong { color: var(--content-text-default); font-weight: 500; }

/* Actions — fila de botones/CTA al pie del body (masters "Conócele", pricing,
   misiones con 2 CTAs). Reutiliza `.btn` / `.cta-row` por dentro.
   ⚠️ REGLA DE INTERACCIÓN: una card con `.card__actions` (botones reales) NO
   puede ser `<a class="card">` / `.card--link` — un <a>/<button> dentro de un
   <a> es HTML inválido. Dos modos:
     · Card-enlace  → `<a class="card card--link">`, sin botones dentro (el CTA
       es una afordancia visual: chevron/texto). Click en toda la card.
     · Card-acciones → `<article class="card">` con `.card__actions`; el título
       puede ser un <a>. Para cards con uno o varios botones. */
.card__actions { display: flex; flex-wrap: wrap; gap: var(--sp-s); margin-top: var(--sp-s); }

/* Compact — grids densos (resultados de búsqueda, listados largos): padding y
   tipografía un peldaño más pequeños. Mismo radius/superficie → coherente. */
.card--compact .card__body { gap: var(--sp-xs); padding: 14px 16px 16px; }
.card--compact .card__title { font-size: var(--text-body-sm); }
.card--compact .card__text  { font-size: var(--text-body-xs); line-height: 1.45; }

/* Row — card HORIZONTAL: miniatura + texto en una sola fila. Para listados y
   resultados (búsqueda). El título se lee siempre (envuelve); el texto de debajo
   se recorta a 2 líneas. Media cuadrada de ancho fijo a la izquierda. */
.card--row { flex-direction: row; align-items: stretch; border-radius: var(--radius-m); }
.card--row .card__media { flex: none; width: clamp(88px, 26vw, 128px); aspect-ratio: 1 / 1; }
/* LA MINIATURA ES CUADRADA, no del alto de su card (auditoría, 14-10-2026).
   La fila estira a sus hijos, así que la foto crecía hasta llenar el alto —y
   como la primera card tiene el título más largo, su miniatura salía 26px más
   alta que las otras dos. La lista se veía despareja sin que se supiera por
   qué. `flex-start` deja que la proporción mande. El alto de las cards no
   cambia. */
.card--row .card__media { align-self: flex-start; }
.card--row .card__body { flex: 1 1 auto; min-width: 0; justify-content: center; gap: var(--sp-xs); padding: 12px 16px; }
.card--row .card__title { font-size: var(--text-body-sm); }
.card--row .card__text {
  font-size: var(--text-body-xs);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Row L — la card EN FILA como unidad de un listado de una columna (la ficha
   de persona · QA 892). En tablet y escritorio, la tipografía y el aire de la
   card de los listados: título 22, texto base 18 a 3 líneas, relleno 24, hueco
   8, miniatura de 192. En móvil, la fila de los listados. Foto A SANGRE en todo el
   alto: cuadrada, deja un hueco debajo en cuanto la card crece. */
.card--row-l .card__media { position: relative; align-self: stretch; aspect-ratio: auto; min-height: clamp(88px, 26vw, 128px); }
.card--row-l .card__media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
@media (min-width: 721px) {
  .card--row-l .card__media { width: calc(var(--sp-l) * 6); }
  .card--row-l .card__body { gap: var(--sp-s); padding: var(--sp-m); }
  .card--row-l .card__title { font-size: var(--text-body-lg); }
  .card--row-l .card__text { font-size: var(--text-body-md); line-height: var(--lh-body); -webkit-line-clamp: 3; }
}
/* En móvil, la FILA DE LOS LISTADOS (Guides, Blog · QA 893), no la densa:
   título 22, cuerpo apretado, miniatura de 96 a 120 y el icono de tipo
   abajo-izquierda a 36. Igual que las cards genéricas de móvil (QA 896): si
   el título se alarga, la página quita lo accesorio, no cambia la pieza. */
@media (max-width: 720px) {
  .card--row-l .card__media { width: clamp(96px, 28vw, 120px); min-height: clamp(96px, 28vw, 120px); }
  .card--row-l .card__body { gap: 3px; padding: 12px 14px; }
  .card--row-l .card__title { font-size: var(--text-body-lg); }
  .card--row-l .card__kind { inset: auto auto 6px 6px; width: 36px; height: 36px; }
}
/* Etiqueta «WIP» (enlace a la web antigua): en una card entera enlazada va
   arriba a la derecha, en la línea de la etiqueta de tipo (corta, deja el
   sitio libre). No en línea (sería una columna más de la fila), ni junto al
   título (le roba ancho y lo parte en más líneas · QA 893), ni en la línea de
   la firma (sin firma se monta sobre el título · QA 896). */
a.card--row-l[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"]) { position: relative; }
a.card--row-l[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after { position: absolute; right: var(--sp-m); top: var(--sp-m); margin: 0; }
@media (max-width: 720px) {
  a.card--row-l[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after { right: 14px; top: 12px; }
}

/* Light — la TARJETA BLANCA. Para bandas claras (guías, artículo, las
   tarjetas de What to Expect y Levels de Dolomites). Blanco puro + filete
   on-light + sombra apenas perceptible: se levanta sobre una banda gris sin
   hacer ruido. Consolida .exp-wte-card / .v8-level--icon / .exp-map-card /
   .read-next__card. Hasta el 15-10-2026 era gris azulado (#F5F7FA) y sin
   sombra; ninguna página la usaba con caja, y sobre una banda gris se fundía
   (Irene: «¿no tenemos un fondo blanco para tarjetas?»).
   Sobre blanco el amarillo no pasa AA → el metadato y el icono van en gris
   on-light (--content-text-on-light-weak). */
.card--light {
  background: var(--gradient-light-subtle);
  border-color: var(--border-on-light);
  box-shadow: var(--shadow-on-light);
  color: var(--content-text-on-light-default);
}
.card--light .card__media { background: var(--gray-1); }
.card--light .card__icon { color: var(--content-text-on-light-weak); }
.card--light .card__eyebrow { color: var(--content-text-on-light-weak); }
.card--light .card__title { color: var(--content-text-on-light-default); }
.card--light .card__text { color: var(--content-text-on-light-weak); }
.card--light .card__footer { border-top-color: var(--border-on-light); color: var(--content-text-on-light-weak); }
.card--light .card__footer strong { color: var(--content-text-on-light-default); }
a.card--light:hover,
.card--light.card--link:hover {
  border-color: color-mix(in srgb, var(--gray-black) 22%, transparent);
}

/* Bare — card SIN caja (sin fondo/borde/sombra): solo media + texto. Para
   galerías/sugerencias donde manda la foto y no hay recuadro (read-next del
   artículo, showcase de premium). El media conserva su radius; el hover NO
   eleva (no hay caja) — solo hace zoom de la foto. Combínala con --light en
   superficies claras. Va después de --light para ganar en bg/borde. */
.card--bare {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;   /* sin caja tampoco hay sombra (la de .card--light, 15-10-2026) */
  overflow: visible;
}
.card--bare .card__media { border-radius: var(--radius-m); }
.card--bare .card__body { padding: 0.9rem 0 0; }
a.card--bare:hover,
.card--bare.card--link:hover { transform: none; box-shadow: none; border-color: transparent; }

/* ===========================================================================
   FORM CONTROLS · campo + input (promovidos al DS · 2026-08 · desde login)
   El DS no tenía campo de texto (solo el toggle .cookie-switch); lo reinventaban
   login/search/missions/calculadoras. `.field` es el wrapper (label + input +
   hint/error); `.input` el control. Superficie oscura; los inputs son lo ÚNICO
   cuadrado del sistema (--radius-s) — todo lo demás (botones, chips) es pill.
   =========================================================================== */
.field { display: flex; flex-direction: column; gap: 8px; }
.field__row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
/* Etiqueta de campo · NORMA (Irene, 2026-10-19): toda etiqueta que precede a
   un control (campo de texto, desplegable…) va en SANS, escrita como se
   escribe, a 14 (`--text-body-xs`), peso 500, gris débil. El mismo día se
   probó en mono y se descartó («finalmente, también en Sans»). En todo
   formulario, y en las calculadoras, va flotante (`.field--float`): dentro
   del campo y sube a la línea del borde · QA 1025-1026. */
.field label,
.field__label {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  font-weight: 500;
  color: var(--content-text-default-weak);
}
.field__hint {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  color: var(--content-link-default);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.field__hint:hover { color: var(--content-link-hover); }
.field__error { font-family: var(--font-sans); font-size: var(--text-body-xs); color: var(--red-4); }

.input,
textarea.input {
  width: 100%;
  height: 50px;
  padding: 0 15px;
  background: var(--white-a04);
  /* Borde del input más visible que el filete de 16% (decisión Irene 2026-08):
     los campos necesitan más affordance que una card. Blanco al 55%. */
  border: 1px solid color-mix(in srgb, var(--gray-white) 55%, transparent);
  border-radius: var(--radius-s);
  color: var(--content-text-default);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  transition: border-color var(--dur-instant) var(--ease-out),
              background var(--dur-instant) var(--ease-out);
}
textarea.input { height: auto; min-height: 96px; padding: 12px 15px; resize: vertical; }
/* Placeholder blanco 50% → 5.3:1 sobre el input (pasa AA; el 42% previo daba
   4.05, por debajo del 4.5). Sigue más apagado que el texto escrito (blanco). */
.input::placeholder { color: var(--white-a50); }
.input:hover { border-color: color-mix(in srgb, var(--gray-white) 75%, transparent); }
.input:focus-visible {
  outline: 2px solid var(--anillo-foco);
  outline-offset: 2px;
  border-color: transparent;
  background: var(--white-a04);
}
.input--error { border-color: var(--red-5); }

/* ── .input--light · el MISMO campo sobre fondo claro (2026-10-22 · QA 764) ──
   Un solo interruptor, en el control: la etiqueta, la ayuda, el error y los
   adornos de `.input-wrap` lo siguen solos vía `:has()`. Fondo y borde con
   un toque de gris azulado (tokens --*-field-on-light). Contrastes: borde
   pizarra ≥3:1 (control interactivo) · texto escrito 18:1 · placeholder
   --gray-6 5,5:1 · etiqueta 7:1 · error --red-6 5:1 (el --red-4 del oscuro
   da 2,6). Anillo azul: el amarillo no se ve sobre blanco. */
.input.input--light {
  --anillo-foco: var(--blue-5);
  background: var(--bg-field-on-light);
  border-color: var(--border-field-on-light);
  color: var(--content-text-on-light-default);
}
.input.input--light::placeholder { color: var(--gray-6); }
.input.input--light:hover { border-color: var(--border-field-on-light-hover); }
.input.input--light:focus-visible { background: var(--bg-field-on-light); border-color: transparent; }
.input.input--light.input--error { border-color: var(--red-5); }
.field:has(.input--light) label,
.field:has(.input--light) .field__label { color: var(--content-text-on-light-weak); }
.field:has(.input--light) .field__hint {
  --anillo-foco: var(--blue-5);
  color: var(--content-text-on-light-default);
  text-decoration-color: var(--yellow-5);
}
.field:has(.input--light) .field__error { color: var(--red-6); }
.input-wrap:has(.input--light) :is(.input-wrap__icon, .input-wrap__clear) { color: var(--content-text-on-light-weak); }
.input-wrap:has(.input--light) .input-wrap__clear { --anillo-foco: var(--blue-5); }

/* ── .field--float · título ENCIMA de la caja (Irene, 10-11-2026) ─────────
   Sustituye a la etiqueta flotante sobre la línea (QA 967-970, 1025-1026):
   el título va fuera, encima, a 4 del contorno, que se cierra entero. Nunca
   desaparece ni se mueve. Colores de la prueba de la app: título en
   `--content-text-subtitle` (encabeza), pista `.field__help` en
   `--content-text-label-weak`. El nombre de la clase se queda: la llevan
   todos los formularios y calculadoras y la vigila la revisión (comp. 23). */
.field--float { gap: var(--sp-xs); }
.field--float :is(label, .field__label):not(.sr-only) {
  color: var(--content-text-subtitle);
  font-size: var(--text-body-xs);
  font-weight: 500;
  line-height: 1.2;
  overflow-wrap: anywhere;
}
.field--float:has(.input--light) :is(label, .field__label):not(.sr-only) { color: var(--content-text-on-light-default); }
.field--float:has(.input--light) { --anillo-foco: var(--blue-5); }
/* Pista · texto de ayuda bajo la caja (no es un enlace: el enlace es `.field__hint`). */
.field__help {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  line-height: 1.35;
  color: var(--content-text-label-weak);
}
.field:has(.input--light) .field__help { color: var(--content-text-on-light-weak); }
/* Título con botón de información (i): sigue pulsable. */
.field--float :is(label, .field__label) .info-tip { pointer-events: auto; }
.input-wrap > .field__caja { flex: 1; min-width: 0; }

/* ── .field-check · casilla de formulario (consentimiento) · 2026-10-23 ──────
   El `<input type="checkbox">` real, oculto (`.sr-only`), y el dibujo
   `.checkbox` del sistema al lado, que sigue su estado. La fila entera se
   pulsa y mide 40 de alto como mínimo (zona táctil). La casilla va en la
   PRIMERA línea de la frase. Oscura: marcada en amarillo, como en los menús.
   `--light` (sobre blanco): borde pizarra de los campos claros y marcada en
   negro, porque el amarillo no se ve sobre blanco. */
.field-check {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-s);
  min-height: var(--sp-xl);
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  line-height: var(--lh-body);
  color: var(--content-text-default);
}
.field-check .checkbox { margin-top: calc((var(--lh-body) * 1em - 18px) / 2); }
.field-check__input:checked + .checkbox { background: var(--yellow-5); border-color: var(--yellow-5); }
.field-check__input:checked + .checkbox svg { opacity: 1; }
.field-check__input:focus-visible + .checkbox { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
@media (hover: hover) { .field-check:hover .checkbox { border-color: var(--content-text-default); } }
.field-check--light { --anillo-foco: var(--blue-5); color: var(--content-text-on-light-default); }
.field-check--light .checkbox { border-color: var(--border-field-on-light); background: var(--bg-field-on-light); }
.field-check--light .field-check__input:checked + .checkbox { background: var(--gray-11); border-color: var(--gray-11); color: var(--gray-white); }
@media (hover: hover) { .field-check--light:hover .checkbox { border-color: var(--border-field-on-light-hover); } }

/* Input con adornos — icono guía a la izquierda y/o botón de limpiar (×) a la
   derecha. Para buscadores (search-box) y campos con acción. El wrapper posiciona
   los adornos; el input recibe padding para no solaparse. */
.input-wrap { position: relative; display: flex; align-items: center; }
.input-wrap__icon { position: absolute; left: 15px; display: inline-flex; color: var(--content-text-default-weak); pointer-events: none; }
.input-wrap__icon svg { display: block; }
.input-wrap:has(.input-wrap__icon) .input { padding-left: 44px; }
.input-wrap__clear {
  all: unset;
  position: absolute;
  right: 8px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-circle);
  color: var(--content-text-default-weak);
}
.input-wrap__clear:hover { color: var(--content-text-default); }
.input-wrap__clear:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
.input-wrap:has(.input-wrap__clear) .input { padding-right: 44px; }

/* ===========================================================================
   BTN · slot de icono + full-width + variante social (promovidos 2026-08)
   `.btn__icon` es el slot de icono guía (escala con el texto vía em); cuando
   una .btn lo lleva, el gap sube a 8px. `.btn--block` = full-width.
   `.btn-social` = botón de login con proveedor (Google/Apple): superficie
   suave y calmada para APILAR varias opciones sin competir entre sí (distinta
   del ghost/outline, más presentes). Pill como todos los .btn.
   =========================================================================== */
.btn__icon { display: inline-flex; align-items: center; justify-content: center; width: 1.25em; height: 1.25em; flex-shrink: 0; }
.btn__icon svg { width: 100%; height: 100%; display: block; }
.btn:has(.btn__icon) { gap: var(--sp-s); }
.btn--block { width: 100%; }

/* `.btn__caret` (2026-10-18) · la flecha de un botón que ABRE un desplegable
   (filtros, menús de la guía). Amarilla —es la parte que dice «aquí hay
   acción»— y gira 180° cuando el desplegable está abierto
   (`aria-expanded="true"` en el botón). Sobre el primario, que ya es
   amarillo, va del color del texto. Promovida desde los filtros de Missions
   (`.mis-filter-trigger__caret`), que la tenían del color del texto y sin
   sistema (Irene: «el aspa debería ser amarilla y cambiar de orientación
   cuando aparece el desplegable»).
   Markup: <button class="btn btn-tertiary btn-s" aria-haspopup="true"
   aria-expanded="false">Format <svg class="btn__caret" viewBox="0 0 10 10"
   aria-hidden="true"><path d="M1.5 3.5 L5 7 L8.5 3.5" fill="none"
   stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
   stroke-linejoin="round"/></svg></button> */
.btn__caret {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  color: var(--color-action-default);
  transition: transform var(--dur-instant) var(--ease-out);
}
.btn:has(.btn__caret) { gap: var(--sp-s); }
.btn[aria-expanded="true"] .btn__caret { transform: rotate(180deg); }
/* Donde el fondo del botón se vuelve amarillo o blanco, la flecha amarilla
   desaparecería: ahí va del color del texto. Primario siempre; Dark y Outline
   en hover y pulsado (2026-10-18: se comprobó variante por variante). */
.btn-primary .btn__caret,
.btn-dark:is(:hover, .is-hover, :active, .is-active) .btn__caret,
.btn-outline:is(:hover, .is-hover, :active, .is-active) .btn__caret { color: currentColor; }
@media (prefers-reduced-motion: reduce) {
  .btn__caret { transition: none; }
}

/* ===========================================================================
   CHECK-LIST · lista de sí / no (promovida 2026-10-18)
   Cada punto lleva delante su signo: ✓ en verde (incluido, sí, hazlo) o ✗ en
   rojo (no incluido, no, no lo hagas). Receta de Dolomites, donde vivía dos
   veces con nombres distintos (`.exp-list--check/--cross` y
   `.v8-price__included-list--check/--cross`, esta última con «×» en vez de
   «✗»). El verde y el rojo aquí son SEMÁNTICOS: es la única pieza del sistema
   donde el rojo no es un error, sino un «no».
   Markup: <ul class="check-list check-list--check"><li>…</li></ul>
           <ul class="check-list check-list--cross"><li>…</li></ul>
   =========================================================================== */
.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-s);
}
.check-list > li {
  position: relative;
  padding-left: var(--sp-l);
}
.check-list > li::before {
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-weight: 500;
  line-height: var(--lh-heading);
}
.check-list--check > li::before { content: '✓'; color: var(--green-5); }
.check-list--cross > li::before { content: '✗'; color: var(--red-5); }
.btn-social {
  background: var(--white-a04);
  color: var(--gray-white);
  box-shadow: inset 0 0 0 1px var(--border-neutral-soft);
}
@media (hover: hover) { .btn-social:hover { background: var(--white-a04); box-shadow: inset 0 0 0 1px var(--gray-5); } }   /* solo con ratón (QA 561) */

/* ===========================================================================
   DIVIDER-OR · separador «o» entre dos maneras de hacer lo mismo
   (promovido 2026-10-16 desde Login: entre «Continuar con email» y los botones
   sociales). Dos filetes del sistema a los lados de una palabra corta en gris.
   No separa secciones (entre secciones no hay líneas): solo opciones
   alternativas dentro de una misma tarjeta o formulario.
   =========================================================================== */
.divider-or {
  display: flex;
  align-items: center;
  gap: var(--sp-ds);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  text-transform: lowercase;
  color: var(--content-text-default-weak);
}
.divider-or::before,
.divider-or::after { content: ''; flex: 1; height: 1px; background: var(--border-neutral-soft); }

/* ===========================================================================
   CALLOUT · caja de aviso / tips (promovida 2026-08 · desde article .tips)
   Título mono + lista con checks. Variantes de superficie oscura (default) y
   clara. En claro el check va en gris (el amarillo no pasa contraste).
   =========================================================================== */
.callout {
  border-radius: var(--radius-m);
  padding: clamp(20px, 2.5vw, 28px);
  background: color-mix(in srgb, var(--gray-white) 5%, transparent);
  border: 1px solid var(--border-neutral-soft);
}
.callout__title {
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--content-text-highlight);
  margin: 0 0 14px;
}
.callout ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
/* EL ✓ CUELGA FUERA DEL TEXTO, el texto corre seguido. Con la fila flexible,
   una negrita dentro del punto se volvía una columna aparte (QA 898). El
   carril del ✓ es su ancho en Public Sans 600 (0,875em) + 10px: el texto
   empieza donde empezaba; si cambia la letra del ✓, se vuelve a medir. */
.callout li { display: block; list-style: none; position: relative; padding-left: calc(0.875em + 10px); font-size: var(--text-body-sm); line-height: 1.45; color: var(--content-text-default-weak); }
.callout li::marker { content: none; }
.callout li::before { content: '✓'; position: absolute; left: 0; top: 0; width: 0.875em; color: var(--yellow-5); font-weight: 600; }
.callout--light {
  /* UN PASO MÁS OSCURO (Irene, 15-10-2026: «apenas se diferencia del fondo»).
     Iba al mismo gris que la banda de «Read next» (#F5F7FA), que funciona ahí
     porque ocupa el ancho entero; en una caja pequeña sobre papel blanco se
     perdía. `--bg-light-sunk` la despega sin convertirla en un bloque. */
  background: var(--bg-light-sunk);
  border-color: transparent;
  color: var(--content-text-on-light-default);
}
.callout--light .callout__title { color: var(--content-text-on-light-weak); }
.callout--light li { color: var(--content-text-on-light-default); }
.callout--light li::before { color: var(--content-text-on-light-weak); }

/* ===========================================================================
   PANEL · superficie de contenido/CTA sin media (promovido 2026-08)
   El hermano "texto" de la card. `.panel--glass` = superficie oscura
   translúcida + blur + shadow-strong (receta de login-card / nav glass).
   =========================================================================== */
.panel {
  padding: clamp(28px, 3.5vw, 44px);
  border-radius: var(--radius-l);
  background: color-mix(in srgb, var(--gray-white) 4%, transparent);
  border: 1px solid var(--border-neutral-soft);
}
/* ── .panel--cierre · banner de cierre, la llamada final (QA 858) ──────
   Fondo OPACO del catálogo (`--gradient-subtle`, el de las cards): el panel
   base es translúcido y cambiaba de tono según dónde cayera del degradado de
   la página. Botones uno encima de otro, el principal arriba, cada uno a su
   ancho y a la izquierda, como su texto. `--cierre-ancho`: en escritorio,
   texto a la izquierda y botones a la derecha, apoyados abajo. */
.panel--cierre { background: var(--gradient-subtle); }
.panel.panel--cierre > .cta-row {
  flex-direction: column;
  align-items: flex-start;
  width: auto;
  margin: var(--sp-m) 0 0;
}
/* AIRE INTERIOR · el mismo en todos los banners, es un componente (Irene,
   2026-10-23): título `.shelf-head__title` sin margen; 16 entre el título y
   lo que le sigue y entre bloques de texto; 24 hasta los botones o el
   formulario. Las páginas NO ponen aire propio aquí. */
.panel--cierre .shelf-head__title { margin: 0; }
.panel--cierre .shelf-head__title ~ :not(.cta-row, form) { margin-top: var(--sp-default); margin-bottom: 0; }
.panel.panel--cierre > form { margin-top: var(--sp-m); margin-bottom: 0; }
/* Teléfono estrecho: un botón más largo que el panel parte su texto en dos
   líneas en vez de empujarlo fuera de la pantalla. */
@media (max-width: 480px) {
  .panel--cierre .btn { max-width: 100%; white-space: normal; text-align: left; line-height: 1.2; }
  .panel--cierre .btn-m { height: auto; min-height: 52px; }   /* el alto de .btn-m */
}
/* TEXTO · texto base en el gris débil, igual en todos los banners (Irene,
   2026-10-26). Las páginas no le ponen tamaño, peso ni color propios. */
.panel--cierre .shelf-head__title ~ p {
  max-width: var(--max-article);
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-weight: 400;
  line-height: var(--lh-body);
  color: var(--content-text-default-weak);
  text-wrap: pretty;
}
@media (min-width: 1200px), (min-width: 1024px) and (orientation: landscape) {
  .panel--cierre-ancho {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
    gap: var(--sp-xl);
  }
  .panel.panel--cierre-ancho > .cta-row { margin-top: 0; align-items: flex-end; }
}
.panel--glass {
  background: var(--glass-bg-panel);
  -webkit-backdrop-filter: var(--glass-filter-strong);
  backdrop-filter: var(--glass-filter-strong);
  box-shadow: var(--shadow-strong);
}

/* ===========================================================================
   CHIP · píldora base (etiqueta o enlace). `<a class="chip">` es un chip
   clicable. Variante `--tag` (amarilla · etiquetas de contenido, tipo tags de
   blog/guía · validada 2026-08). `.chip-dismiss` (chip eliminable con ×) cuelga
   de la misma base. Distinto de `.badge` (metadato mono) y `.tablist__item`.
   =========================================================================== */
.chip,
.chip-dismiss {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* ALTO FIJADO (2026-10) · el chip no tenía altura: la daba el texto, así que
     salía a 31px —por debajo del mínimo táctil de 40 que pide el propio
     sistema— y una fila de chips podía quedar despareja. El relleno vertical
     ya no decide el alto; lo decide esto. */
  min-height: 40px;
  padding: 6px 14px;
  /* Una etiqueta NUNCA parte en dos líneas (Irene, 13-10): si lo hace, ese
     chip crece de alto y descuadra toda la fila. Si no cabe, la franja se
     desplaza en horizontal — que es lo que ya hace en móvil. Misma regla que
     `.tablist`, corregida ahí varias veces. */
  white-space: nowrap;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--gray-white) 8%, transparent);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  color: var(--content-text-default);
}
/* Chip-enlace: sin subrayado (es tag/nav, no link inline de prosa) + hover. */
a.chip { text-decoration: none; transition: background var(--dur-instant) var(--ease-out), color var(--dur-instant) var(--ease-out); }
a.chip:hover { background: color-mix(in srgb, var(--gray-white) 15%, transparent); color: var(--content-text-default); }
a.chip:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
/* Tag · píldora amarilla para etiquetas de contenido. El hover ACLARA
   (yellow-2 → yellow-1), nunca oscurece. Texto oscuro (lee sobre el amarillo). */
.chip--tag { background: var(--yellow-2); color: var(--content-text-on-light-default); }
a.chip--tag:hover { background: var(--yellow-1); color: var(--content-text-on-light-default); }
/* El × necesita hueco a la derecha. */
.chip-dismiss { padding: 6px 10px 6px 14px; }
.chip-dismiss__remove {
  all: unset;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-circle);
  color: var(--content-text-default-weak);
  transition: color var(--dur-instant) var(--ease-out), background var(--dur-instant) var(--ease-out);
}
.chip-dismiss__remove:hover { color: var(--content-text-default); background: var(--white-a08); }
.chip-dismiss__remove:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }

/* ===========================================================================
   FILTRO · las piezas comunes de toda barra de filtros (Missions, Search).
   Un filtro del sistema se comporta igual en todas partes: el botón dice
   cuántos filtros hay puestos (`.filter-count`), «Clear all» en gris va a su
   lado (`.filter-clear`) y cada filtro puesto sale como `.chip-dismiss` dentro
   de `.filter-chips`, para quitarlo de un toque. Una página que filtre de
   otra manera rompe la norma (QA 609).
   =========================================================================== */
.filter-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--radius-full);
  background: var(--yellow-5);
  color: var(--gray-9);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  font-weight: 600;
}
.filter-count[hidden] { display: none; }
/* ENTRADA Y SALIDA SUAVES (Irene, QA 1078) · al marcar el primer filtro el
   botón crece y el número aparece abriéndose desde 0 (ancho, hueco, opacidad
   y escala); al quitarlo, al revés. El margen negativo absorbe el hueco del
   botón con flecha (`--sp-s`) para que nada salte. Necesita `@starting-style`
   y `allow-discrete`; donde no los hay, aparece de golpe como antes. */
@media (prefers-reduced-motion: no-preference) {
  /* Entra: primero se abre el hueco y, un instante después, el número. */
  .filter-count:not(.filter-count--corner) {
    max-width: 4em;
    overflow: hidden;
    transition: max-width var(--dur-glide) var(--ease-soft),
                min-width var(--dur-glide) var(--ease-soft),
                padding var(--dur-glide) var(--ease-soft),
                margin var(--dur-glide) var(--ease-soft),
                opacity var(--dur-glide) var(--ease-soft) 120ms,
                transform var(--dur-glide) var(--ease-soft) 120ms,
                display var(--dur-glide) allow-discrete;
  }
  /* Sale: al revés, el número se va y después se cierra el hueco. */
  .filter-count:not(.filter-count--corner)[hidden] {
    max-width: 0; min-width: 0; padding-inline: 0;
    margin-left: calc(-1 * var(--sp-s));
    opacity: 0; transform: scale(0.4);
    transition: opacity var(--dur-soft) var(--ease-soft),
                transform var(--dur-soft) var(--ease-soft),
                max-width var(--dur-glide) var(--ease-soft) 120ms,
                min-width var(--dur-glide) var(--ease-soft) 120ms,
                padding var(--dur-glide) var(--ease-soft) 120ms,
                margin var(--dur-glide) var(--ease-soft) 120ms,
                display calc(var(--dur-glide) + 120ms) allow-discrete;
  }
  @starting-style {
    .filter-count:not(.filter-count--corner):not([hidden]) {
      max-width: 0; min-width: 0; padding-inline: 0;
      margin-left: calc(-1 * var(--sp-s));
      opacity: 0; transform: scale(0.4);
    }
  }
  /* En la esquina de un `.icon-btn` no hay ancho que abrir: solo aparece. */
  .filter-count--corner {
    transition: opacity var(--dur-soft) var(--ease-soft),
                transform var(--dur-soft) var(--ease-soft),
                display var(--dur-soft) allow-discrete;
  }
  .filter-count--corner[hidden] { opacity: 0; transform: scale(0.4); }
  @starting-style {
    .filter-count--corner:not([hidden]) { opacity: 0; transform: scale(0.4); }
  }
}
/* Sobre un `.icon-btn` (móvil): en la esquina, con un aro del color del fondo
   que lo separa del icono. */
.icon-btn:has(> .filter-count--corner) { position: relative; }
.filter-count--corner {
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border: 2px solid var(--bg-bold);
}
/* «Clear all» · texto gris subrayado, sin caja: quita todos los filtros. Solo
   existe con algún filtro puesto (`hidden` si no). 40 de alto = zona táctil. */
.filter-clear {
  flex: none;
  min-height: 40px;
  padding: var(--sp-s) var(--sp-ds);
  border: 0;
  background: transparent;
  color: var(--content-text-default-weak);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color var(--dur-instant) var(--ease-out);
}
.filter-clear[hidden] { display: none; }
@media (hover: hover) {
  .filter-clear:hover { color: var(--content-text-default); }
}
/* PULSADO Y SALIDA (Irene, QA 1080) · al pulsar se aclara y se encoge un
   poco, como si se hundiera; al desaparecer (ya sin filtros) se desvanece y
   se encoge desde ahí, y al volver aparece al revés. `@starting-style` +
   `allow-discrete`; sin soporte, aparece y se va de golpe como antes. */
.filter-clear:active { color: var(--content-text-default); }
@media (prefers-reduced-motion: no-preference) {
  .filter-clear {
    transform-origin: left center;
    transition: color var(--dur-instant) var(--ease-out),
                transform var(--dur-instant) var(--ease-out),
                opacity var(--dur-soft) var(--ease-soft),
                display var(--dur-soft) allow-discrete;
  }
  .filter-clear:active { transform: scale(0.94); }
  .filter-clear[hidden] {
    opacity: 0;
    transform: scale(0.85);
    transition: opacity var(--dur-soft) var(--ease-soft),
                transform var(--dur-soft) var(--ease-soft),
                display var(--dur-soft) allow-discrete;
  }
  @starting-style {
    .filter-clear:not([hidden]) { opacity: 0; transform: scale(0.85); }
  }
}
/* Fila de filtros puestos · una `.chip-dismiss` por filtro. */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-s);
}
.filter-chips:empty { display: none; }

/* Chip SELECCIONABLE (filtro por tema · sugerencias) — la píldora canónica usada
   como toggle de filtro dentro de un listado de contenido (vídeo, guía, expedición).
   DISTINTO del filtro por TIPO de contenido de Search, que va en un desplegable
   con casillas (`.popover` + `.checkbox` + las piezas de FILTRO de arriba); el
   chip es una SUGERENCIA de tema, más blanda.

   Reposo = CONTORNO blanco al 16 %, transparente; seleccionado = relleno gris 6
   con texto blanco y aspa — NUNCA amarillo (relleno amarillo = acción). Va
   sobre <button>. (10-11-2026 · QA 1135: sustituye al relleno gray-8 de 2026-08.) */
button.chip {
  border: 0;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  /* reposo · CONTORNO, transparente (Irene, 10-11-2026 · QA 1135: elegido en la
     prueba de la hoja del Planner frente al relleno gray-8). Contorno con
     box-shadow por dentro: no cambia la medida del chip. */
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--border-neutral-soft);
  color: var(--gray-white);
  transition: background var(--dur-instant) var(--ease-out),
    box-shadow var(--dur-instant) var(--ease-out),
    color var(--dur-instant) var(--ease-out);
}
@media (hover: hover) { button.chip:hover { box-shadow: inset 0 0 0 1px var(--border-neutral-strong); } }
button.chip:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
button.chip.is-active,
button.chip[aria-pressed="true"] {
  background: var(--gray-6);           /* seleccionado · relleno gris → destaca + aspa */
  box-shadow: none;
  color: var(--gray-white);
  font-weight: 600;
}
button.chip.is-active:hover,
button.chip[aria-pressed="true"]:hover { background: var(--gray-5); }
/* Multi-select tipo Missions: al seleccionar aparece un aspa (×) que quita ese
   filtro. El × va en el markup (oculto) y se revela en activo. Como el chip es
   un <button> toggle, el clic en cualquier parte del chip activo lo deselecciona;
   el × es la afordancia visual de "quitar". */
.chip .chip__x { display: none; }
.chip .chip__x svg { display: block; width: 10px; height: 10px; }
.chip.is-active .chip__x,
.chip[aria-pressed="true"] .chip__x { display: inline-flex; opacity: 0.85; }

/* ===========================================================================
   ABECEDARIO · `.alpha` (Irene, 2026-10-16 · nace con el Glossary)
   «Tendríamos que tener todo el abecedario a un solo golpe de vista y poder
   elegir una letra» — las pestañas no servían: el glosario es largo y una
   fila que se desliza esconde la mitad de las letras.
   · Las 26 letras SIEMPRE, en casillas redondas de 40 (mínimo táctil): 13 por
     fila en escritorio (A–M / N–Z, al ancho de lectura) y 7 en móvil.
   · Cada letra es un ENLACE a su página (/glossary/b/…): las URLs de hoy se
     mantienen. La actual lleva `aria-current="page"` y va como la pestaña
     activa del sistema (amarillo + negro: «estás aquí»).
   · Una letra sin términos se ve pero no se pulsa (`span.alpha__item.is-empty`):
     el abecedario está entero y se sabe que ahí no hay nada.
   · Vive en la banda negra de la cabecera (propuesta A, elegida por Irene).
   · PEGADO ARRIBA al bajar (Irene: «el abecedario debería quedarse sticky
     por arriba para que pueda seleccionar otras»). `script.js` clona la
     lista en `.alpha-sticky`, una barra fija negra bajo la topbar que
     aparece cuando el abecedario original sale de la pantalla. Es FIJA, no
     sticky: no ocupa sitio en la página, así que al aparecer no mueve nada
     (el «baile» de la barra de listados, QA de 2026-10). En la barra: una
     fila de 26 en escritorio ancho, dos de 13 hasta 1200, y en el teléfono
     una fila que se desliza con la letra actual a la vista.
   =========================================================================== */
.alpha__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(13, minmax(40px, 1fr));
  gap: var(--sp-xs);
  max-width: var(--max-article);
}
@media (max-width: 720px) {
  .alpha__list { grid-template-columns: repeat(7, minmax(40px, 1fr)); }
}
.alpha__list > li { margin: 0; }
/* En la cabecera de contenidos, separado del título. */
.article-hero .alpha { margin-top: var(--sp-l); }
.alpha__item {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  aspect-ratio: 1;
  border-radius: var(--radius-circle);
  /* Relleno sólido gris, el mismo del chip de filtro en reposo (Irene,
     2026-10-19: con el blanco al 8 % apenas se distinguía del negro). */
  background: var(--gray-8);
  color: var(--content-text-default);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background var(--dur-instant) var(--ease-out), color var(--dur-instant) var(--ease-out);
}
a.alpha__item:hover { background: var(--gray-7); color: var(--content-text-default); }
a.alpha__item:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
.alpha__item.is-empty { background: none; color: var(--white-a32); }
/* La actual: como la pestaña activa. El hover no la cambia (nunca oscurecer
   el amarillo — parecería deshabilitada). */
.alpha__item[aria-current="page"],
a.alpha__item[aria-current="page"]:hover { background: var(--color-action-default); color: var(--bg-bold); }

/* Barra pegada · cristal oscuro a todo el ancho bajo la topbar. Sube con la
   topbar cuando ésta se esconde al bajar (misma receta que `.listing__bar`). */
.alpha-sticky {
  position: fixed;
  left: 0;
  right: 0;
  top: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
  z-index: 40;
  padding: var(--sp-s) var(--gutter);
  /* Fondo NEGRO, no cristal (Irene, 2026-10-19): es la continuación de la
     banda negra donde vive el abecedario. */
  background: var(--bg-bold);
  box-shadow: 0 1px 0 var(--border-neutral-soft);
  opacity: 0;
  visibility: hidden;
  /* Se ESCONDE de golpe (sin fundido): si el original vuelve a entrar en
     pantalla, los dos abecedarios no deben coincidir ni un instante. Al
     aparecer sí funde (regla de abajo). */
  transition: transform var(--dur-soft) var(--ease-out);
}
.alpha-sticky.is-visible {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-instant) var(--ease-out), transform var(--dur-soft) var(--ease-out);
}
body:has(.nav.hidden) .alpha-sticky {
  transform: translateY(calc(-1 * var(--nav-height)));   /* sube lo que mide la barra y se queda en el borde del área segura: más arriba, Chrome en iPad lo tapa con su barra encogida (QA 1046) */
}
/* Ninguna letra cortada contra el borde (Irene, 14-10, regla de los chips):
   en escritorio ancho caben las 26 en una fila; hasta 1200, dos filas de 13
   como en la cabecera; solo en el teléfono, una fila que se desliza con el
   dedo (el gesto natural ahí · regla de `.tablist`). */
.alpha-sticky .alpha__list {
  grid-template-columns: repeat(26, 40px);
  justify-content: center;
  max-width: none;
}
@media (max-width: 1200px) {
  .alpha-sticky .alpha__list { grid-template-columns: repeat(13, 40px); }
}
@media (max-width: 720px) {
  /* A SANGRE (Irene, 2026-10-19: «hay márgenes a los laterales que cortan
     los botones»). La barra no lleva margen lateral; el margen pasa DENTRO de
     la fila, así en reposo las letras arrancan en el margen de la página y al
     deslizar llegan hasta el borde de la pantalla. */
  .alpha-sticky { padding-inline: 0; }
  .alpha-sticky .alpha__list {
    display: flex;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    padding-inline: var(--gutter);
    scroll-padding-inline: var(--gutter);
  }
  .alpha-sticky .alpha__list::-webkit-scrollbar { display: none; }
  .alpha-sticky .alpha__list > li { flex: 0 0 40px; }
}
@media (prefers-reduced-motion: reduce) {
  .alpha-sticky, .alpha-sticky.is-visible { transition: none; }
}

/* ===========================================================================
   POPOVER + CHECKBOX · menú anclado + checkbox custom (promovidos 2026-08)
   `.popover` es superficie de menú (misma receta de card). `.checkbox` es el
   checkbox custom con tick amarillo, usable dentro del popover o suelto
   (con `.is-checked`). `.popover__list` agrupa las opciones: una columna, y
   dos a partir de 11 opciones.
   =========================================================================== */
.popover {
  background: var(--gradient-subtle);
  border: 1px solid var(--border-neutral-soft);
  box-shadow: var(--shadow-soft);
  border-radius: var(--radius-l);
  /* Aire interior: 8 (--sp-s). Irene, 2026-10-16: «menos distancia en general,
     que se hacen muy largos los desplegables» — la misma distancia que se
     validó en la prueba de la calculadora (antes 16). Con 8 de aire, la
     esquina de 24 del menú y la de 16 de las filas son concéntricas
     (24 − 8 = 16): las curvas van paralelas. */
  padding: var(--sp-s);
}
.popover__option {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  /* Fila de 40 (el mínimo táctil), 8 arriba y abajo y 12 a los lados. */
  min-height: var(--sp-xl);
  padding: var(--sp-s) var(--sp-ds);
  /* Esquina de la fila = la de Missions. La opción ELEGIDA no se pinta de
     amarillo: el tick amarillo ya la marca (un solo acento). */
  border-radius: var(--radius-m);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  cursor: pointer;
  /* Funciona igual siendo <button> (2026-10-16): sin el gris ni el borde que
     pone el navegador. */
  border: 0;
  background: transparent;
  color: inherit;
  text-align: left;
  transition: background var(--dur-instant) var(--ease-out);
}
.popover__option:hover { background: var(--white-a04); }
/* Lista de opciones · SIEMPRE una columna hacia abajo: se lee de un vistazo.
   A partir de 11 opciones (y con sitio, ≥720) pasa a 2 columnas para que el
   desplegable no se haga eterno. La cuenta la hace el CSS solo (`:has`), no
   hay que marcar nada por página (Irene, 15-10-2026). */
.popover__list {
  display: grid;
  grid-template-columns: 1fr;
  /* Filas pegadas (antes 2 de hueco); entre columnas, 2. */
  gap: 0 var(--sp-2xs);
  min-width: 200px;
}
@media (min-width: 720px) {
  .popover__list:has(> :nth-child(11)) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
}
/* Casilla · 18px, esquina --radius-xs (se lee como casilla, no como botón de
   opción) y filete FUERTE: un control necesita ≥3:1 contra el fondo y el
   suave no llegaba (norma D). Receta de Missions, adoptada por el sistema el
   15-10-2026 (Irene). */
.checkbox {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border: 1.5px solid var(--border-neutral-strong);
  border-radius: var(--radius-xs);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--gray-9);
  transition:
    background var(--dur-instant) var(--ease-out),
    border-color var(--dur-instant) var(--ease-out);
}
.checkbox svg {
  width: 10px;
  height: 10px;
  opacity: 0;
  transition: opacity var(--dur-instant) var(--ease-out);
}
.checkbox.is-checked,
.popover__option.is-selected .checkbox { background: var(--yellow-5); border-color: var(--yellow-5); }
.checkbox.is-checked svg,
.popover__option.is-selected .checkbox svg { opacity: 1; }
/* CASILLA o TICK · la norma (Irene, 2026-10-16): la casilla `.checkbox` SOLO
   cuando se pueden marcar varias opciones a la vez (filtros de Missions,
   interruptores de la guía). Si solo se elige UNA (filtro de Search en móvil,
   desplegables), la opción no lleva caja: lleva el tick A LA IZQUIERDA, en el
   sitio de la casilla, y solo se ve en la elegida (las demás guardan el hueco
   vacío para que los textos sigan alineados). A la izquierda porque la lista
   se lee desde ahí y así casilla y tick ocupan el mismo sitio: la única
   diferencia entre elegir varias o una es la caja (Irene, 2026-10-16).
   UN SOLO TICK en todo el sistema: el dibujo de Missions (10×10, puntas
   redondeadas, 1,8 de trazo). Aquí se ve a 18 —el tamaño de la casilla— y el
   trazo NO engorda al crecer (`vector-effect`): mide lo mismo que en la casilla. */
.popover__tick {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--yellow-5);
  opacity: 0;
  transition: opacity var(--dur-instant) var(--ease-out);
}
.popover__tick svg { width: 18px; height: 18px; display: block; }
.popover__tick svg path { vector-effect: non-scaling-stroke; }
.popover__option.is-selected .popover__tick { opacity: 1; }

/* ===========================================================================
   CALCULADORA · piezas de formulario y resultado (promovidas 2026-10-16
   desde la prueba de Calculator detail, validadas una a una por Irene).
   · `.combo`            el menú desplegable (bloque MENÚ DESPLEGABLE, abajo):
                         aquí, con su disparador campo
   · `.input--s`         campo pequeño (40 en escritorio, el de serie en móvil)
   · `.input-group`      campo + selector de unidad pegados
   · `.input-wrap__unit` unidad única dentro del campo (sin control)
   · `.input-wrap__stepper` − y + propios (fuera las flechas del navegador)
   · `.result`           la cifra que importa, destacada, + copiar
   · `.calculator`       el panel que las reúne (título + campos | resultados)
   El motor (desplegable, − y +, copiar) vive en script.js.
   =========================================================================== */

/* Campo pequeño · 40 de alto (el mínimo táctil) desde 720; en el móvil se
   queda con el alto de serie, que se teclea mejor con el dedo. Para campos
   secundarios dentro de otra pieza (el buscador de un desplegable). */
@media (min-width: 720px) { .input--s { height: var(--sp-xl); } }

/* Números · fuera las flechas del navegador (cada uno pinta las suyas, y no
   llegan al mínimo táctil). Si hacen falta, `.input-wrap__stepper`. */
.input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.input[type="number"]::-webkit-inner-spin-button,
.input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

/* − y + · el botón redondo del sistema (`.icon-btn-s`: 32 a la vista, 44 de
   zona táctil) dentro del campo, a la derecha. */
.input-wrap { --stepper-w: calc(2 * var(--sp-l) + var(--sp-xs)); }
.input-wrap__stepper { position: absolute; right: var(--sp-s); display: flex; gap: var(--sp-xs); }
.input-wrap__stepper svg { width: 14px; height: 14px; display: block; }
.input-wrap:has(.input-wrap__stepper) .input { padding-right: calc(var(--stepper-w) + var(--sp-s) + var(--sp-ds)); }

/* Unidad única (mm) · dentro del campo, en mono gris (etiqueta técnica). No
   es un control: no va en un segmento aparte, que parecería que se toca. */
.input-wrap__unit {
  position: absolute;
  right: var(--sp-default);
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  color: var(--content-text-default-weak);
  pointer-events: none;
}
.input-wrap:has(.input-wrap__unit) .input { padding-right: calc(var(--sp-default) + var(--sp-l)); }
.input-wrap:has(.input-wrap__stepper) .input-wrap__unit { right: calc(var(--stepper-w) + var(--sp-s) + var(--sp-ds)); }
.input-wrap:has(.input-wrap__stepper):has(.input-wrap__unit) .input { padding-right: calc(var(--stepper-w) + var(--sp-s) + var(--sp-ds) + var(--sp-l)); }

/* Campo + selector de unidad · un solo control partido en dos. */
.input-group { display: flex; }
.input-group > .input-wrap:first-child { flex: 1; min-width: 0; }
.input-group > .input-wrap:first-child .input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.input-group > .combo { flex: 0 0 auto; min-width: calc(3 * var(--sp-xl)); }
.input-group > .combo .combo__trigger { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left-width: 0; }
/* Su menú: al ancho de su contenido (mínimo el del segmento), a la derecha. */
.input-group .combo .popover { left: auto; right: 0; min-width: 100%; width: max-content; }

/* MENÚ DESPLEGABLE · `.combo` — UNA pieza con interruptores (Irene,
   20-10-2026 · QA 613) y un solo motor en script.js.
   · Disparador: campo (`button.input.combo__trigger`) o botón (`.btn` +
     `.btn__caret`; con un `.icon-btn` para móvil dentro del mismo `.combo`).
     Lo abre todo hijo directo con `aria-haspopup`.
   · Selección: una (`.popover__tick`) o varias (`data-combo-multi` +
     `.checkbox`, sin opción «All»: nada marcado = todos).
   · Buscador: `.input-wrap` + `[data-combo-search]` dentro del menú.
   La FORMA del menú va con el disparador: con campo, la de formulario;
   con botón, la del `.popover` del sistema. No se elige aparte. */
.combo { position: relative; }
/* El menú cuelga de SU disparador, debajo y a la izquierda. A la derecha con
   `data-combo-align="end"`, o si se saldría de la pantalla (`.is-end`, lo
   pone el motor al abrir; y `.is-start` devuelve a la izquierda el de
   «end» que se saldría por ese lado). Ancho: 240 como mínimo y crece con su contenido
   (Irene, 20-10-2026). Sombra fuerte en todos: la de menús del sistema. */
.combo > .popover {
  position: absolute;
  top: calc(100% + var(--sp-s));
  left: 0;
  z-index: var(--z-modal);
  width: max-content;
  min-width: calc(6 * var(--sp-xl));
  max-width: min(520px, calc(100vw - 2 * var(--gutter)));
  box-shadow: var(--shadow-strong);
}
.combo > .popover[hidden] { display: none; }
.combo[data-combo-align="end"] > .popover,
.combo > .popover.is-end { left: auto; right: 0; }
.combo > .popover.is-start { left: 0; right: auto; }
/* Aro amarillo por dentro (amarillo = acción) en el botón con su menú
   abierto y en el que tiene filtros puestos (`.is-filtered`, lo pone el motor
   en los de varias opciones). El aro de foco va encima, no en su lugar. */
.combo.is-filtered > :is(.btn, .icon-btn),
.combo > :is(.btn, .icon-btn)[aria-expanded="true"] { box-shadow: inset 0 0 0 1.5px var(--border-secondary-accent); }
.combo.is-filtered > .btn:focus-visible,
.combo > .btn[aria-expanded="true"]:focus-visible { box-shadow: inset 0 0 0 1.5px var(--border-secondary-accent), 0 0 0 3px var(--gray-black), 0 0 0 5px var(--yellow-5); }
.combo.is-filtered > .icon-btn:focus-visible,
.combo > .icon-btn[aria-expanded="true"]:focus-visible { box-shadow: inset 0 0 0 1.5px var(--border-secondary-accent), 0 0 0 2px var(--gray-black), 0 0 0 4px var(--yellow-5); }

/* Disparador CAMPO · se lee como un campo más del formulario. */
.combo__trigger { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-s); text-align: left; cursor: pointer; }
.combo__trigger > :first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Flecha clicable = amarilla (norma, Irene 2026-10-19): es la parte que dice
   «aquí se abre algo», como `.btn__caret`. */
.combo__chevron { display: inline-flex; flex-shrink: 0; color: var(--color-action-default); }
.combo__chevron svg { display: block; width: 16px; height: 16px; transition: transform var(--dur-instant) var(--ease-out); }
.combo__trigger[aria-expanded="true"] .combo__chevron svg { transform: rotate(180deg); }
/* Menú de FORMULARIO (con disparador campo): al ancho del campo, esquina 16
   y filas de 8 (Irene, 16-10, opción A). Con 8 de aire, 16 − 8 = 8 = la
   esquina del buscador y de las filas: curvas paralelas. Una columna
   siempre: la lista se busca y se desplaza, no se lee de un vistazo. El de
   botón se queda con el `.popover` del sistema (24/16, dos columnas desde 11). */
.combo__trigger ~ .popover {
  right: 0;
  width: auto;
  min-width: 0;
  max-width: none;
  max-height: calc(8 * var(--sp-xl));
  overflow-y: auto;
  border-radius: var(--radius-m);
}
.combo .popover > .input-wrap { margin-bottom: var(--sp-s); flex-shrink: 0; }
/* Con buscador, el campo se queda arriba y solo se desplaza la lista: si
   bajara con ella, se cortaría por el borde del menú (QA 1095). El relleno
   de 4 de la lista deja sitio al aro de foco de las filas. */
.combo__trigger ~ .popover:has(> .input-wrap) { max-height: none; overflow: visible; }
.combo__trigger ~ .popover:has(> .input-wrap) > .popover__list {
  max-height: calc(6 * var(--sp-xl));
  overflow-y: auto;
  padding: var(--sp-xs);
  margin: calc(-1 * var(--sp-xs));
}
/* La × del buscador es la del sistema (la pone el motor): fuera la del navegador. */
.input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
/* minmax(0, 1fr): sin él la columna crecía al ancho de la opción más larga y
   el texto llegaba al borde del menú, sin el aire de la derecha (QA 1098). */
.combo__trigger ~ .popover .popover__list { grid-template-columns: minmax(0, 1fr); min-width: 0; }
.combo__trigger ~ .popover .popover__option { border-radius: var(--radius-s); }
/* Un nombre sin espacios («N/X/X2/…/X9i») puede partirse: si no, ensancha la fila
   y el texto vuelve a tocar el borde del menú (QA 1098). */
.combo__trigger ~ .popover .popover__option > span:last-child { min-width: 0; overflow-wrap: anywhere; }
.combo__empty { padding: var(--sp-ds); font-size: var(--text-body-sm); color: var(--content-text-default-weak); margin: 0; }
@media (prefers-reduced-motion: reduce) { .combo__chevron svg { transition: none; } }

/* Resultado destacado · la cifra que importa en grande (40, peso 600, como
   las cifras destacadas del sistema) con la unidad en mono; el resto va en
   `.spec-list`. Copiar = botón con texto DEBAJO de los resultados
   (`.btn.btn-ghost.btn-xs.result__copy` + `.btn__icon` + `.result__copy-label`;
   a la derecha de la cifra y alineado con ella por abajo, en `.result__row`
   (Irene 19-10: primero arriba a la derecha, luego «alineado por abajo con la
   cifra»);
   ghost y no contorno, Irene 16-10).
   Irene, 16-10: un icono suelto en una esquina no se ve ni dice qué copia;
   con texto, y un tamaño pequeño para que sea discreto. Al copiar, el icono
   pasa a ser el tick del sistema y el texto dice «Copied» durante 2 s. */
.result { display: flex; flex-direction: column; gap: var(--sp-xs); padding-bottom: var(--sp-m); margin-bottom: var(--sp-m); border-bottom: 1px solid var(--border-neutral-soft); }
/* Etiqueta del resultado a mono xl (18): hace de título del panel, que no
   lleva otro (Irene, 2026-10-19). */
.result__label { font-family: var(--font-mono); font-size: var(--text-mono-xl); font-weight: 500; text-transform: uppercase; letter-spacing: 0.08em; color: var(--content-text-default-weak); }
.result__value { font-family: var(--font-sans); font-size: var(--text-heading-lg); font-weight: 600; line-height: var(--lh-heading); letter-spacing: var(--tracking-heading); color: var(--content-text-default); font-variant-numeric: tabular-nums; }
.result__unit { font-family: var(--font-mono); font-size: var(--text-mono-xl); font-weight: 400; color: var(--content-text-default-weak); margin-left: var(--sp-xs); }
.result__row { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-s); }
/* Grupos de resultado con rótulo (Diffraction: «100% Crop» y «Print», como la
   calculadora real). Sin este aire los rótulos se pegan a la fila de encima. */
.result__group { margin-top: var(--sp-m); }
.result__group > .result__label { display: block; margin: 0 0 var(--sp-s); }
.result__copy svg { width: 100%; height: 100%; display: block; }
.result__copy .is-done { display: none; }
.result__copy.is-copied .is-idle { display: none; }
.result__copy.is-copied .is-done { display: block; color: var(--yellow-5); }
.result__copy .is-done path { vector-effect: non-scaling-stroke; }
/* El icono cambia con un fundido, al marcar y al volver (QA 1087). */
.result__copy.is-copied .is-done,
.result__copy.was-copied .is-idle { animation: icono-entra var(--dur-soft) var(--ease-soft); }
@keyframes icono-entra { from { opacity: 0; transform: scale(0.8); } }
@media (prefers-reduced-motion: reduce) {
  .result__copy.is-copied .is-done,
  .result__copy.was-copied .is-idle { animation: none; }
}

/* El panel de la calculadora · `.panel` del sistema + este interior.
   Título OPCIONAL, 24→32 (el de la tarjeta Priority list de Missions): solo
   si la página no dice ya qué calcula. En Calculator detail NO va: el título
   de la página ya lo dice (Irene, 16-10: «es repetitivo»). Campos a la
   izquierda y resultado a la derecha desde 1024; apilados por debajo. */
/* Fondo: el mismo que los desplegables (`.popover`), el degradado de las
   tarjetas (Irene, 19-10; antes soft, strong y el blanco al 4 % del panel). */
/* Aire del panel: el de las tarjetas grandes, EL MISMO en los cuatro lados
   (`--sp-card-inset` · QA 630, 965). */
.calculator { padding: var(--sp-card-inset); background: var(--gradient-subtle); }
.calculator__title {
  font-family: var(--font-sans);
  font-size: clamp(var(--text-heading-sm), 2.4vw, var(--text-heading-md));
  font-weight: 600;
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-heading);
  color: var(--content-text-default);
  margin: 0 0 var(--sp-m);
}
.calculator__grid { display: grid; gap: var(--sp-xl); }
.calculator__grid > * { min-width: 0; }
/* Campos 3/5 · resultado 2/5: la columna del resultado nunca llegaba a llenar
   la mitad (Irene, 19-10). */
@media (min-width: 1200px), (min-width: 1024px) and (orientation: landscape) { .calculator__grid { grid-template-columns: 3fr 2fr; } }
/* NORMA DEL PANEL (Irene, 20-10 · QA 636). Objetivo: el panel, lo más BAJO
   posible. Se baja siempre la columna más alta, y se decide UNA vez por
   calculadora a ancho de escritorio (se escribe en su marcado):
   · izquierda más alta → campos vecinos en pareja (`.field--half`), en su
     orden; primero los que hablan de lo mismo (Aperture + su Sí/No). Cabe si
     la etiqueta no pasa de dos líneas y el número o el valor no se cortan (un
     nombre largo que se corta con «…», como la cámara, cuenta como que cabe).
     Un campo de varias casillas (Max. Print Dimension) nunca se parte.
   · derecha más alta → resultados a 2 columnas (`.calculator--results-2col`);
     a 3 si van en grupos (`--results-3col`, Diffraction). EXCEPCIÓN: si las
     etiquetas de resultado son frases largas (Spot Stars: «With rule of 500
     (the declination…)»), la lista se queda en una columna, una debajo de otra.
   Sustituye al resultado ancho (`--results-wide`, retirado). */
/* Campos que comparten fila (desde 720, apilados por debajo). Rejilla de 6:
   cada campo ocupa la fila entera salvo que diga cuánto (`.field--half` 3/6,
   `--wide` 4/6, `--narrow` 2/6). Para acortar la calculadora sin estrechar los
   campos que llevan dentro unidad o − y + (Irene, 19-10: focal + diafragma y
   distancia + teleconversor en la misma línea). */
.calculator__fields { display: grid; gap: var(--sp-l) var(--sp-m); align-content: start; }   /* filas 32 · columnas 24: el título flotante pide aire · QA 1027 */
/* La columna NO crece por lo que lleva dentro: un nombre largo (cámara) se
   corta con «…» y un selector que no cabe se desliza. Sin el `minmax(0, …)`,
   ensanchaban la columna y se salían del panel (QA 643). */
.calculator__fields { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 720px) {
  /* `end`: si dos campos de una fila llevan etiquetas de distinta altura,
     los campos quedan alineados igual. */
  .calculator__fields { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: end; }
  .calculator__fields > * { grid-column: span 6; }
  .calculator__fields > .field--half   { grid-column: span 3; }
  .calculator__fields > .field--wide   { grid-column: span 4; }
  .calculator__fields > .field--narrow { grid-column: span 2; }
}
.calculator .spec-list dd { font-variant-numeric: tabular-nums; }
/* Resultados · la etiqueta ENCIMA del dato (Irene, 20-10 · QA 636): Public
   Sans 14 gris, sin mayúsculas (la de las etiquetas de campo), y el dato a 18
   debajo. Se lee mejor que la lista a dos columnas en mono. Los rótulos de
   grupo (`.result__group > .result__label`) siguen en mono. */
.calculator .spec-list { display: block; }
.calculator .spec-list dt {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  text-transform: none;
  letter-spacing: 0;
  margin: 0 0 var(--sp-xs);
}
.calculator .spec-list dd { margin: 0 0 var(--sp-default); }
@media (min-width: 720px) {
  .calculator--results-2col .spec-list { column-count: 2; column-gap: var(--sp-m); }
  .calculator--results-3col .spec-list { column-count: 3; column-gap: var(--sp-default); }
  /* La fila de resultados SIN rótulo (la primera, Magnification + Effective
     aperture) va a 2 columnas; los grupos con rótulo, a 3 (Irene, 20-10 ·
     QA 639: así se ve dónde empieza cada grupo). */
  .calculator--results-3col .result__group:not(:has(> .result__label)) .spec-list { column-count: 2; column-gap: var(--sp-m); }
  /* Etiqueta y dato nunca se separan de columna. */
  :is(.calculator--results-2col, .calculator--results-3col) .spec-list dt { break-after: avoid-column; }
  :is(.calculator--results-2col, .calculator--results-3col) .spec-list dd { break-before: avoid-column; break-inside: avoid; }
}
/* Dentro de la cabecera negra de contenidos: al ancho de contenido
   (`--max-content`, el carril ancho de la plantilla) y separada de la
   entradilla por el aire de zona (`--section-pad-y-sm`, 48→80; Irene, 16-10:
   «más distancia entre entradilla y panel»; antes 40). */
.article-hero > .calculator { max-width: var(--max-content); margin: var(--section-pad-y-sm) auto 0; }

/* ===========================================================================
   SPEC-LIST · lista de pares etiqueta/valor dt/dd (promovido 2026-08)
   Metadatos técnicos (datos de misión, "what to expect", ficha del hero shot).
   =========================================================================== */
/* Etiqueta (mono 14) y valor (sans 18) reposan en la MISMA línea de texto:
   la etiqueta va en la línea base de la PRIMERA línea del valor (Irene,
   2026-10-18 y 19: «con la base de la primera línea de texto, no con todo el
   párrafo»). Ni pegada arriba de la caja (el mono «bailaba» por encima) ni con
   la última línea (`last baseline`, probado el 19-10 y descartado). */
.spec-list { display: grid; grid-template-columns: max-content 1fr; gap: 8px 20px; align-items: baseline; }
/* Etiqueta · mono uppercase weak. Alineada 2026-08 con la convención de pares
   etiqueta/valor que ya usaba Dolomites (`.exp-overview__label`): 14px, weight
   500 y tracking 0.08em — el mismo tracking que `.card__eyebrow`, que es LA
   norma para etiquetas mono en mayúsculas. Al promover el componente se había
   quedado un peldaño por debajo en los tres valores. */
.spec-list dt {
  font-family: var(--font-mono);
  /* 16 (un peldaño sobre el 14 de antes · Irene, 2026-10-19: «el término
     también un tamaño más… todo el componente listado debe heredar estos
     cambios»). En todos los tamaños y en todas las variantes. */
  font-size: var(--text-mono-lg);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--content-text-default-weak);
}
/* Valor · sans 18px (cuerpo por defecto). NO copiamos el weight 500 del valor
   de Dolomites: allí es una cuadrícula compacta de datos, y aquí el valor puede
   ser una definición de glosario — o sea prosa, que va en regular. */
.spec-list dd { margin: 0; font-family: var(--font-sans); font-size: var(--text-body-md); color: var(--content-text-default); }
/* Clara · sobre blanco (páginas de texto · 2026-10-16). */
.spec-list--light dt { color: var(--content-text-on-light-weak); }
.spec-list--light dd { color: var(--content-text-on-light-default); }
/* DEFINICIONES · `.spec-list--defs` (Irene, 2026-10-19). Cuando el valor es
   prosa (glosario, «What each result means», páginas de texto): término 30 %,
   definición 70 %, que es lo que se lee. El término parte línea si no cabe.
   32 de aire entre pares; en móvil se apilan (término encima). Para pares
   cortos (datos de un resultado, ficha técnica) se queda la `.spec-list` sola. */
.spec-list--defs { grid-template-columns: minmax(0, 3fr) minmax(0, 7fr); row-gap: var(--sp-l); }
.spec-list--defs dd { line-height: var(--lh-body); }
@media (max-width: 720px) {
  .spec-list--defs { grid-template-columns: 1fr; gap: var(--sp-xs); }
  .spec-list--defs dd { margin-bottom: var(--sp-m); }
}

/* ===========================================================================
   BREADCRUMB · migas de navegación (promovido 2026-08)
   Chrome de navegación jerárquica (Home / Academy / Videos). Registro SANS
   discreto (no prosa, no acento): enlaces en gris que ACLARAN al hover (nunca
   amarillo/subrayado, que es solo para links de prosa), separador "/" decorativo
   vía ::before, y la página actual sin enlace (aria-current="page").
   Semántica: <nav aria-label="Breadcrumb"> > <ol class="breadcrumb__list">.
   El dato estructurado BreadcrumbList (JSON-LD) va SIEMPRE en la página aunque
   el breadcrumb visible se omita (SEO/GEO · ver nota en el MDS).
   =========================================================================== */
.breadcrumb { font-family: var(--font-sans); font-size: var(--text-body-xs); line-height: 1.4; }
.breadcrumb__list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  margin: 0;
  padding: 0;
}
.breadcrumb__item { display: inline-flex; align-items: center; gap: 4px 8px; color: var(--content-text-default-weak); }
/* Separador decorativo entre migas (no en la primera). */
.breadcrumb__item + .breadcrumb__item::before {
  content: "/";
  color: var(--content-text-default-weak);
  opacity: 0.55;
}
.breadcrumb a {
  color: var(--content-text-default-weak);
  text-decoration: none;
  padding: 2px 0;
  transition: color var(--dur-instant) var(--ease-out);
}
.breadcrumb a:hover { color: var(--content-text-default); }
.breadcrumb a:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; border-radius: 2px; }
/* Sin la página actual: las migas terminan en la SECCIÓN de la que cuelga
   la página, porque su título va justo debajo (QA 791). `.breadcrumb__current`
   retirada el 2026-10-23: no la reintroduzcas. */

/* ===========================================================================
   PHOTO-TILE · tile de foto protagonista + etiqueta (promovido 2026-08)
   Distinto de la card de contenido: aquí manda la foto. Ratio por instancia
   con --tile-ratio (default 4/5). Scrim del catálogo (--scrim-photo-soft).
   Consolida .prep-tile (home) + .exp-pursue__chip (experiences).
   =========================================================================== */
.photo-tile {
  position: relative;
  display: block;
  aspect-ratio: var(--tile-ratio, 4 / 5);
  border-radius: var(--radius-m);
  overflow: hidden;
  background: var(--gradient-subtle);
  text-decoration: none;
}
.photo-tile img,
.photo-tile video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--dur-glide) var(--ease-out);
}
.photo-tile:hover img,
.photo-tile:hover video { transform: scale(1.04); }
.photo-tile:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
.photo-tile figcaption,
.photo-tile__caption { position: absolute; inset: auto 0 0 0; padding: 18px 16px 16px; background: var(--scrim-photo-soft); }
/* Degradado MÁS ALTO (Irene, 2026-10-15: «ampliar el gradient entre texto e
   imagen para hacer más legibles los textos»). Medía lo que el texto (≈60px)
   y acababa de golpe: sobre fotos claras (un sol, un cielo) el título se
   perdía. Ahora el oscurecido arranca bastante más arriba (80px por encima
   del texto) y la transición es gradual. Mismo scrim del catálogo. */
.photo-tile figcaption,
.photo-tile__caption { padding-top: calc(var(--sp-xl) * 2); }
.photo-tile__title { font-family: var(--font-sans); font-size: var(--text-body-lg); font-weight: 600; color: var(--gray-white); margin: 0; }
.photo-tile__by { font-family: var(--font-mono); font-size: var(--text-mono-sm); color: var(--white-a78); margin: 4px 0 0; }

/* ===========================================================================
   OVERLAY-CARD · retrato con caption dentro de la foto (promovido 2026-08)
   Ratio por instancia con --overlay-ratio (default 3/4). Scrim fuerte del
   catálogo. Consolida .exp-way-card--overlay + .exp-master-card.
   =========================================================================== */
.overlay-card {
  position: relative;
  display: block;
  aspect-ratio: var(--overlay-ratio, 3 / 4);
  border-radius: var(--radius-m);
  overflow: hidden;
  background: var(--gradient-subtle);
  text-decoration: none;
}
/* Con <picture> (una imagen por formato de tarjeta), el envoltorio ocupa la
   tarjeta entera; si no, la imagen no tiene alto del que colgarse. */
.overlay-card picture { display: block; width: 100%; height: 100%; }
.overlay-card img,
.overlay-card video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--dur-glide) var(--ease-out);
}
.overlay-card:hover img,
.overlay-card:hover video { transform: scale(1.04); }
/* Con texto que se revela, la foto además SE OSCURECE al pasar el ratón
   (2026-10-15): era el comportamiento de `.exp-way-card--overlay` de
   Experiences y no llegó al consolidarlo aquí. Así el párrafo que aparece
   se lee sobre cualquier foto. */
.overlay-card:has(.overlay-card__reveal) img,
.overlay-card:has(.overlay-card__reveal) video {
  transition: transform var(--dur-glide) var(--ease-soft), filter var(--dur-glide) var(--ease-soft);
}
/* El foco de teclado SIEMPRE oscurece: ahí no hay nada pegado.
   El ratón, solo donde hay ratón (misma razón que el resto). */
.overlay-card:has(.overlay-card__reveal):focus-within img,
.overlay-card:has(.overlay-card__reveal):focus-within video { filter: brightness(0.55); }
@media (hover: hover) {
  .overlay-card:has(.overlay-card__reveal):hover img,
  .overlay-card:has(.overlay-card__reveal):hover video { filter: brightness(0.55); }
}
.overlay-card:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
.overlay-card__caption { position: absolute; inset: auto 0 0 0; padding: 18px 16px 16px; background: var(--scrim-photo-strong); }
/* Oscurecido más alto, como `.photo-tile` (2026-10-15): arranca 80px por
   encima del texto para que el nombre se lea sobre fotos claras. */
.overlay-card__caption { padding-top: calc(var(--sp-xl) * 2); background: var(--scrim-photo-soft); }
.overlay-card__name { font-family: var(--font-sans); font-size: var(--text-body-md); font-weight: 500; color: var(--gray-white); margin: 0; }
.overlay-card__meta { font-family: var(--font-mono); font-size: var(--text-mono-sm); color: var(--white-a78); margin: 3px 0 0; }
/* Insignia de tipo (opcional · Irene, 2026-10-20) · la de las cards, abajo a la
   izquierda JUSTO ENCIMA del nombre: va dentro del pie, en su flujo, no
   flotando sobre la foto. Mismo disco que en los listados. */
.overlay-card__caption .card__kind { position: static; margin: 0 0 var(--sp-s); }
@media (max-width: 720px) {
  .overlay-card__caption .card__kind { width: 36px; height: 36px; }
}
/* Texto que se revela al hover/focus (bio de masters, copy de way-card--overlay).
   Colapsado por defecto; se expande al interactuar con la card. */
.overlay-card__reveal {
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition: max-height var(--dur-glide) var(--ease-soft), opacity var(--dur-glide) var(--ease-soft);
}
.overlay-card:hover .overlay-card__reveal,
.overlay-card:focus-within .overlay-card__reveal { max-height: 12em; opacity: 1; }
.overlay-card__reveal p { font-family: var(--font-sans); font-size: var(--text-body-sm); color: var(--white-a90); line-height: 1.45; margin: 8px 0 0; }

@media (prefers-reduced-motion: reduce) {
  .photo-tile:hover img,
  .photo-tile:hover video,
  .overlay-card:hover img,
  .overlay-card:hover video { transform: none; }
  .overlay-card__reveal { transition: none; }
}

/* ===========================================================================
   PHOTO-SCATTER + POLAROID · fotos premiadas «lanzadas sobre la mesa»
   alrededor de un bloque central (promovido 2026-10-21 desde App · QA 716).
   `.photo-scatter` = la sección (relativa, recorta lo que asoma);
   `__inner` = la columna central de 600 (con `>` para ganar a
   `.editorial-stack`, que suele ir en el mismo elemento y pondría 720); `__cards` = la capa decorativa
   (aria-hidden). `.polaroid` = foto + pie (avatar, nombre, `__detail` mono:
   @instagram o mes). 16 posiciones `--1…--16`: 3-2-3 por lado en horizontal,
   dos bandas (arriba/abajo) en vertical, 3+3 en móvil. El fondo lo pone la
   página.
   =========================================================================== */
.photo-scatter {
  position: relative;
  overflow: hidden;
  padding: var(--section-pad-y-md) var(--gutter);
}
/* En horizontal las polaroids van en tres filas colocadas en % del alto: con
   menos de 900 se amontonan y el borde las corta. Si el texto central es
   corto, la sección no baja de ahí. */
@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  .photo-scatter { min-height: 900px; }
}

.photo-scatter > .photo-scatter__inner {
  position: relative;
  z-index: 1;
  max-width: 600px;
  margin: 0 auto;
}

/* Contenedor full-section · las cards se anclan al centro vía
   `calc(50% - Xpx)` para mantener distancias fijas al texto central
   (~30px gap del inner card al borde de la columna de 600px) y poder
   extenderse hacia los laterales en viewports anchos. En pantallas
   estrechas las cards "outer" quedan recortadas por el `overflow: hidden`
   del section — comportamiento aceptado. */
.photo-scatter__cards {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}

/* Polaroid en oscuro · figcaption integrada (no overlay sobre la foto).
   La rotación se aplica via CSS custom property `--rot` (definida en cada
   modifier .polaroid--N), así el transform base se compone con scale
   en hover sin perder el ángulo. `pointer-events: auto` sobreescribe el
   none del contenedor para que el hover funcione card a card. */
.polaroid {
  --rot: 0deg;
  position: absolute;
  width: 220px;
  margin: 0;
  background: var(--gradient-subtle);
  border-radius: var(--radius-m);
  border: 1px solid var(--border-neutral-soft);
  box-shadow: var(--shadow-soft);
  overflow: hidden;
  pointer-events: auto;
  transform: rotate(var(--rot));
  transition:
    transform var(--dur-soft) var(--ease-out),
    box-shadow var(--dur-soft) var(--ease-out);
  will-change: transform;
}

/* Hover · sube un poco, escala discreta, sombra más fuerte y rotación
   neutralizada (la card se "endereza" al pasar el ratón). z-index muy
   alto para que la card hovered siempre pase por encima de las vecinas
   (el stack base por fila ya llega hasta z:3). */
@media (hover: hover) {
  .polaroid:hover {
    transform: rotate(0deg) translateY(-6px) scale(1.06);
    box-shadow: var(--shadow-strong);
    z-index: 100;
  }
}

@media (prefers-reduced-motion: reduce) {
  .polaroid,
  .polaroid:hover {
    transition: none;
    transform: rotate(var(--rot));
  }
}

.polaroid__photo {
  aspect-ratio: 9 / 8;
  overflow: hidden;
}

.polaroid__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.polaroid__credit {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
}

.polaroid__avatar {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-circle);
  object-fit: cover;
  flex-shrink: 0;
}

.polaroid__meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.polaroid__name {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  font-weight: 500;
  color: var(--content-text-default);
  line-height: 1.2;
}

.polaroid__detail {
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  color: var(--content-text-default-weak);
  text-decoration: none;
  line-height: 1.3;
}

@media (hover: hover) {
  .polaroid__detail:hover {
    color: var(--content-text-highlight);
  }
}

/* Posiciones individuales · 16 cards repartidas en 3 columnas por lado
   con patrón 3-2-3 (alternancia "intercalada"):
     · Inner col (1-3 izq, 9-11 der): 3 cards alineadas en y=2%, 35%, 68%
     · Middle col (4-5 izq, 12-13 der): 2 cards offset a y=18%, 51%
       (entre los huecos de las columnas vecinas → ritmo masonry)
     · Outer col (6-8 izq, 14-16 der): 3 cards alineadas en y=4%, 37%, 70%
       (micro-offset respecto a inner para evitar grid percibido)

   Anclas horizontales para cards de 220px:
     · Inner:  calc(50% - 580px) · gap ~60px al borde del texto
       central (600px wide → 300px desde el centro)
     · Middle: calc(50% - 830px) · 250px más afuera (220 card + 30 gap)
     · Outer:  calc(50% - 1080px) · otros 250px más afuera

   Cards 220px wide con aspect 9/8 casi-square (1.125) → alto ~246px.
   Las 3 filas se concentran en el centro vertical del section
   (5-7% / 32-34% / 59-61%) para liberar ~40px de aire por arriba y
   ~30px por abajo. Eso produce ~55-60px de overlap entre filas
   consecutivas — aceptado, refuerza la sensación "polaroids
   apiladas". Las captions con nombre+handle siempre quedan dentro
   del area visible del section.

   En viewports estrechos las cols outer y middle se recortan por
   `overflow: hidden` del section. Col outer pide ~2100px+ para
   asomar — "hasta 3 columnas en pantallas grandes". */

/* Col 1 · INNER izq · 3 cards */
.polaroid--1 { top:  5%; left: calc(50% - 580px); --rot: -3deg; }
.polaroid--2 { top: 32%; left: calc(50% - 580px); --rot:  4deg; }
.polaroid--3 { top: 59%; left: calc(50% - 580px); --rot: -2deg; }

/* Col 2 · MIDDLE izq · 2 cards offset */
.polaroid--4 { top: 19%; left: calc(50% - 830px); --rot:  2deg; }
.polaroid--5 { top: 46%; left: calc(50% - 830px); --rot: -4deg; }

/* Col 3 · OUTER izq · 3 cards */
.polaroid--6 { top:  7%; left: calc(50% - 1080px); --rot:  3deg; }
.polaroid--7 { top: 34%; left: calc(50% - 1080px); --rot: -3deg; }
.polaroid--8 { top: 61%; left: calc(50% - 1080px); --rot:  4deg; }

/* Col 4 · INNER der · 3 cards (mirror de 1-3) */
.polaroid--9  { top:  5%; right: calc(50% - 580px); --rot:  3deg; }
.polaroid--10 { top: 33%; right: calc(50% - 580px); --rot: -4deg; }
.polaroid--11 { top: 60%; right: calc(50% - 580px); --rot:  2deg; }

/* Col 5 · MIDDLE der · 2 cards offset */
.polaroid--12 { top: 20%; right: calc(50% - 830px); --rot: -2deg; }
.polaroid--13 { top: 47%; right: calc(50% - 830px); --rot:  4deg; }

/* Col 6 · OUTER der · 3 cards */
.polaroid--14 { top:  7%; right: calc(50% - 1080px); --rot: -3deg; }
.polaroid--15 { top: 34%; right: calc(50% - 1080px); --rot:  3deg; }
.polaroid--16 { top: 61%; right: calc(50% - 1080px); --rot: -4deg; }

/* Stack z-index inverso al DOM · las cards de filas superiores tapan
   a las inferiores (no al revés). Así, cuando las fotos se solapan
   verticalmente, la caption con el nombre del fotógrafo siempre queda
   visible: la card de fila 1 cubre la card de fila 2 por su parte
   superior (la foto) pero NO la inferior (la caption), porque la
   card 2 estaba debajo en el stack.
   - Fila 1 (top) y middle upper → z 3
   - Fila 2 y middle lower       → z 2
   - Fila 3 (bottom)             → z 1 (default) */
.polaroid--1,
.polaroid--4,
.polaroid--6,
.polaroid--9,
.polaroid--12,
.polaroid--14 { z-index: 3; }

.polaroid--2,
.polaroid--5,
.polaroid--7,
.polaroid--10,
.polaroid--13,
.polaroid--15 { z-index: 2; }

/* ── Awards en VERTICAL (tablet V + móvil) ──────────────────────────
   En horizontal las polaroids enmarcan el texto por los LADOS. En
   vertical no hay franjas laterales, así que el mismo "lanzado sobre
   la mesa" se traslada a DOS BANDAS: una ARRIBA del texto (cards 1–8)
   y otra ABAJO (cards 9–16). Se anclan al top/bottom del section, se
   reparten en horizontal centradas en 50% con jitter + rotación, y el
   borde del section (overflow:hidden) recorta lo que no cabe — igual
   que en desktop recorta las columnas exteriores. La columna editorial
   deja aire arriba/abajo (padding-block) para no pisarse con las bandas. */
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  .photo-scatter { padding-block: 0; }
  .photo-scatter__cards { display: block; }

  .polaroid { width: clamp(150px, 40vw, 200px); }

  .photo-scatter > .photo-scatter__inner { padding-block: clamp(336px, 60vw, 376px); }

  /* BANDA SUPERIOR · cards 1–8 · ancladas arriba */
  .polaroid--1,
  .polaroid--2,
  .polaroid--3,
  .polaroid--4,
  .polaroid--5,
  .polaroid--6,
  .polaroid--7,
  .polaroid--8 { bottom: auto; right: auto; }

  .polaroid--1 { top: 64px; left: calc(50% - 744px); --rot: -4deg; }
  .polaroid--2 { top: 80px; left: calc(50% - 560px); --rot:  3deg; }
  .polaroid--3 { top: 64px; left: calc(50% - 376px); --rot: -2deg; }
  .polaroid--4 { top: 80px; left: calc(50% - 192px); --rot:  4deg; }
  .polaroid--5 { top: 64px; left: calc(50% - 8px); --rot: -3deg; }
  .polaroid--6 { top: 80px; left: calc(50% + 176px); --rot:  2deg; }
  .polaroid--7 { top: 64px; left: calc(50% + 360px); --rot: -4deg; }
  .polaroid--8 { top: 80px; left: calc(50% + 544px); --rot:  3deg; }

  /* BANDA INFERIOR · cards 9–16 · ancladas abajo */
  .polaroid--9,
  .polaroid--10,
  .polaroid--11,
  .polaroid--12,
  .polaroid--13,
  .polaroid--14,
  .polaroid--15,
  .polaroid--16 { top: auto; right: auto; }

  .polaroid--9  { bottom: 64px; left: calc(50% - 744px); --rot:  3deg; }
  .polaroid--10 { bottom: 80px; left: calc(50% - 560px); --rot: -3deg; }
  .polaroid--11 { bottom: 64px; left: calc(50% - 376px); --rot:  4deg; }
  .polaroid--12 { bottom: 80px; left: calc(50% - 192px); --rot: -2deg; }
  .polaroid--13 { bottom: 64px; left: calc(50% - 8px); --rot:  3deg; }
  .polaroid--14 { bottom: 80px; left: calc(50% + 176px); --rot: -4deg; }
  .polaroid--15 { bottom: 64px; left: calc(50% + 360px); --rot:  2deg; }
  .polaroid--16 { bottom: 80px; left: calc(50% + 544px); --rot: -3deg; }
}

/* ── Awards · MÓVIL (≤480) ──────────────────────────────────────────
   En móvil basta con 3 polaroids por banda (más grandes) para dar el
   marco. Ocultamos el resto y recentramos las 3 de cada banda alrededor
   del eje: la central entera + las dos laterales asomando (peek). Más
   padding-block que en tablet → más aire entre las cards y el texto. */
@media (max-width: 480px) {
  .polaroid--4,
  .polaroid--5,
  .polaroid--6,
  .polaroid--7,
  .polaroid--8,
  .polaroid--12,
  .polaroid--13,
  .polaroid--14,
  .polaroid--15,
  .polaroid--16 { display: none; }

  .polaroid { width: clamp(160px, 46vw, 200px); }

  .photo-scatter > .photo-scatter__inner { padding-block: clamp(348px, 88vw, 384px); }

  /* 3 arriba · centradas (peek lateral) */
  .polaroid--1 { left: calc(50% - 216px); }
  .polaroid--2 { left: calc(50% -  86px); }
  .polaroid--3 { left: calc(50% +  44px); }

  /* 3 abajo · mismo reparto */
  .polaroid--9  { left: calc(50% - 216px); }
  .polaroid--10 { left: calc(50% -  86px); }
  .polaroid--11 { left: calc(50% +  44px); }
}

/* ===========================================================================
   SCROLL-RAIL · raíl vertical que se pinta al hacer scroll (promovido
   2026-10-21 desde el timeline de App · QA 716). Un raíl tenue de punto a
   punto y otro brillante que crece con `--rail-progress`; un punto en cada
   `.scroll-rail__step`, a la altura de su `[data-rail-node]` (el título).
   La geometría (`--rail-top`, `--rail-total`, `--node-y`) y el progreso los
   pone el motor de script.js. `--movil`: solo en vertical (≤1024).
   `--curva`: la línea pasa por puntos colocados en cualquier sitio (S).
   =========================================================================== */
.scroll-rail {
  position: relative;
  --rail-gutter: 36px;
  --rail-progress: 0;
  padding-left: var(--rail-gutter);
}
/* Raíl completo · amarillo tenue, siempre visible: marca el recorrido. */
.scroll-rail::after {
  content: '';
  position: absolute;
  left: calc(var(--rail-gutter) / 2 - 1px);
  top: var(--rail-top, var(--node-top, 0px));
  height: var(--rail-total, 0px);
  width: 2px;
  background: color-mix(in srgb, var(--yellow-5) 22%, transparent);
  z-index: 0;
  pointer-events: none;
}
/* Relleno · crece con el scroll, con la inercia de `--dur-glide`. */
.scroll-rail::before {
  content: '';
  position: absolute;
  left: calc(var(--rail-gutter) / 2 - 1px);
  top: var(--rail-top, var(--node-top, 0px));
  height: calc(var(--rail-total, 0px) * var(--rail-progress));
  width: 2px;
  background: var(--yellow-5);
  box-shadow: 0 0 6px color-mix(in srgb, var(--yellow-5) 35%, transparent);
  z-index: 0;
  pointer-events: none;
  transition: height var(--dur-glide) var(--ease-soft);
}
.scroll-rail__step { position: relative; }
/* Punto · centrado sobre el raíl a la altura del título de su paso. */
.scroll-rail__step::before {
  content: '';
  position: absolute;
  left: calc(var(--rail-gutter) / -2);
  transform: translate(-50%, -50%);
  top: var(--node-y, var(--node-top, 0px));
  width: 12px;
  height: 12px;
  background: var(--yellow-5);
  border-radius: var(--radius-circle);
  box-shadow: var(--glow-action-soft);
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  .scroll-rail::before { transition: none; }
}
/* `--curva`: la línea pasa por los puntos estén donde estén (izquierda,
   centro, derecha) y dibuja una S. La traza el motor en un SVG detrás de
   los pasos; los puntos son elementos (`.scroll-rail__node`), no ::before. */
.scroll-rail--curva { padding-left: 0; }
.scroll-rail--curva::before,
.scroll-rail--curva::after,
.scroll-rail--curva .scroll-rail__step::before { content: none; }
.scroll-rail--curva > .scroll-rail__step { z-index: 1; }
.scroll-rail__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}
.scroll-rail__track,
.scroll-rail__fill { fill: none; stroke-width: 2; stroke-linecap: round; }
.scroll-rail__track { stroke: color-mix(in srgb, var(--yellow-5) 22%, transparent); }
.scroll-rail__fill {
  stroke: var(--yellow-5);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--yellow-5) 35%, transparent));
  transition: stroke-dashoffset var(--dur-glide) var(--ease-soft);
}
.scroll-rail__node {
  display: inline-block;
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-circle);
  background: var(--yellow-5);
  box-shadow: var(--glow-action-soft);
}
@media (prefers-reduced-motion: reduce) {
  .scroll-rail__fill { transition: none; }
}
@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  .scroll-rail--movil { padding-left: 0; }
  .scroll-rail--movil::before,
  .scroll-rail--movil::after,
  .scroll-rail--movil .scroll-rail__step::before { content: none; }
}

/* ===========================================================================
   DISPLAY-PRIZE · cifra grande en amarillo con su etiqueta mono (promovida
   2026-10-21 desde App, «Up to $6,600» · QA 732). `__amount` (display 64↔96,
   600) entre `__label`s. Con `data-count` en `__amount`, el motor de
   script.js la hace contar desde 0 al entrar en pantalla (el valor final ya
   está escrito en el marcado: sin JS o con movimiento reducido se ve tal cual).
   =========================================================================== */
.display-prize {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}
.display-prize__amount {
  font-family: var(--font-sans);
  font-size: clamp(var(--text-display-md), 8vw, var(--text-display-xl));
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--content-text-highlight);
}
/* Mientras cuenta, cifras de ancho fijo: si no, el número «baila». */
.display-prize__amount[data-count] { font-variant-numeric: tabular-nums; }
.display-prize__label {
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--content-text-default);
}

/* ===========================================================================
   LISTING · plantilla de la familia de LISTADOS ESPECÍFICOS (promovida al DS ·
   2026-08 · desde Videos/Guides/Blog). FUENTE ÚNICA del "chrome" de un listado
   de un solo tipo: cabecera (breadcrumb + título + subtítulo + recuento), barra
   sticky (buscador + filtros por TEMA con .chip) con fondo de cristal que
   aparece al pegarse + pliegue suave de los filtros, y grid de .card CON fondo
   en 3 columnas con su píldora `.card__meta`.
   La card es la del DS (misma unidad de resultado en toda la web · ver "Patrón
   de listado" en el MDS). El JS (barra sticky + filtro + paginación) vive en
   script.js. La Search GENÉRICA usa las mismas piezas (barra `--inline`,
   rejilla `--generic`, vacío, «cargar más»); lo único suyo es el filtro por
   tipo, que filtra su propio script (`data-motor-propio` en la rejilla).
   =========================================================================== */

/* Lienzo del listado (el <main>). El body toma el negro ancla para el overscroll. */
body:has(.listing) { background: var(--bg-bold); }
.listing {
  background: var(--gradient-strong);
  /* Suma el área segura del notch, como hace la propia nav. Sin ella, en un
     iPhone la miga y el título arrancaban DEBAJO de la topbar: la barra mide
     `--nav-height + env(...)` y aquí solo se contaba la primera parte
     (2026-10-14). */
  padding-top: calc(var(--nav-height) + env(safe-area-inset-top, 0px) + clamp(32px, 5vw, 56px));
  padding-bottom: clamp(56px, 8vw, 100px);
  min-height: 70svh;
  /* Al filtrar, lo que hay en pantalla desaparece: el navegador no debe
     recolocar el scroll para seguirlo, o la barra pegada se despega. */
  overflow-anchor: none;
  color: var(--content-text-default);
}

/* ── Cabecera ─────────────────────────────────────────────────────────── */
.listing__head { margin-bottom: clamp(24px, 3.5vw, 36px); }
.listing__head .breadcrumb { margin-bottom: clamp(12px, 2vw, 16px); }
.listing__title {
  font-family: var(--font-sans);
  /* El título de PÁGINA de un listado o hub nunca mide lo mismo que sus títulos
     de sección (32): en tablet no baja de 40 y en móvil usa el token de los
     títulos de hero (~45 a 375). Escritorio, 56 (Irene, 2026-10-19 · QA 604). */
  font-size: clamp(var(--text-heading-lg), 4.5vw, var(--text-heading-xl));
  font-weight: 600;
  letter-spacing: var(--tracking-display);
  line-height: var(--lh-heading);
  /* Ancho de cabecera del sistema (2026-10-16), el MISMO que el subtítulo para
     que título y texto rompan en el mismo borde. */
  max-width: var(--max-header);
  margin: 0;
}
.listing__subtitle {
  font-family: var(--font-sans);
  font-size: var(--text-body-lg);
  font-weight: 300;
  line-height: 1.3;
  color: var(--content-text-default-weak);
  max-width: var(--max-header);
  margin: 12px 0 0;
}
.listing__count {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  color: var(--content-text-default-weak);
  margin: 4px 0 clamp(16px, 3.5vw, 22px);
}
.listing__count strong { color: var(--content-text-default); font-weight: 600; }

/* ── .shelf-head · cabecera de SECCIÓN de un hub o listado (2026-10-22) ────
   Título a la izquierda + botón opcional («Browse all») en su misma línea.
   Norma de jerarquía (QA 604): siempre un peldaño por debajo del título de
   página (`.listing__title`, 56 · ~45 móvil) → 32 ↔ 40. Si sube a 56, la
   sección compite con la página. `.shelf-head__title` sirve también suelto
   como título dentro de un `.panel` de cierre. Usan: Resources, Awards
   Legends, Team. */
.shelf-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-default);
  flex-wrap: wrap;
  margin-bottom: clamp(var(--sp-m), 3vw, var(--sp-xl));
}
.shelf-head__title {
  margin: 0;
  font-family: var(--font-sans);
  font-size: clamp(var(--text-heading-md), 4vw, var(--text-heading-lg));
  font-weight: 600;
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-heading);
  text-wrap: balance;
  color: var(--content-text-default);
}
.shelf-head .btn { flex: none; }

/* ── Barra sticky (buscador + filtros) ────────────────────────────────────
   Se pega bajo la nav; sube con ella cuando se oculta. El fondo de cristal lo
   pinta ::before a 100vw y solo aparece al pegarse (.is-stuck · lo activa el JS
   de script.js). Padding simétrico arriba/abajo cuando se pega. */
.listing__bar {
  display: flex;
  flex-direction: column;
  position: sticky;
  top: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
  z-index: 40;
  --listing-bar-gap: clamp(24px, 3.5vw, 36px);
  margin: 0 0 var(--listing-bar-gap);
  padding: 10px 0;
  transform: translateY(0);
  transition: transform var(--dur-soft) var(--ease-out);
}
/* ⚠️ LA BARRA NO PUEDE MOVER LA PÁGINA (Irene, 2026-10 · «baila en algunos
   puntos del scroll»).
   Al pegarse, la barra se COMPACTA: los chips se pliegan (móvil) o se mudan a
   la derecha del buscador (escritorio). En los dos casos el alto baja ~50px.
   Y la barra está EN EL FLUJO: encoger algo que ya ha pasado por arriba del
   viewport acorta el documento, el navegador reajusta el scroll para compensar
   (medido: la página saltaba de y=998 a y=948 sola), ese reajuste dispara otro
   evento de scroll, que vuelve a evaluar el umbral… y entra en bucle. Eso es
   el baile: la barra entrando y saliendo de «pegada» varias veces seguidas.
   No se arregla afinando la animación — la animación no era la causa.
   Se arregla devolviendo al documento exactamente los píxeles que la barra
   deja de ocupar, como margen inferior. Total constante → el documento no
   cambia de alto → no hay reajuste → no hay bucle. El margen vive por encima
   del viewport cuando la barra está pegada, así que no se ve.
   `--listing-bar-lost` lo mide el motor de `script.js` (no se puede saber en
   CSS: depende del alto real de los chips y del buscador). */
.listing__bar.is-stuck {
  margin-bottom: calc(var(--listing-bar-gap) + var(--listing-bar-lost, 0px));
}
/* Durante la medición se congela todo lo animable: si no, se mide un fotograma
   intermedio y la reserva sale mal. Va con 2-4 clases para ganarle por
   especificidad a las reglas de ≥1024 que están más abajo. */
.listing__bar.listing__bar--midiendo,
.listing__bar.listing__bar--midiendo .listing__collapse { transition: none; }
.listing__bar.listing__bar--midiendo.is-stuck .listing__filters { transition: none; animation: none; }
.listing__bar::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  /* Fondo NEGRO, no cristal (Irene, 2026-10-19): es la continuación de la
     banda negra donde vive el abecedario. */
  background: var(--bg-bold);
  box-shadow: 0 1px 0 var(--border-neutral-soft);
  opacity: 0;
  transition: opacity var(--dur-instant) var(--ease-out);
  z-index: -1;
  pointer-events: none;
}
.listing__bar.is-stuck::before { opacity: 1; }
/* Sube con la nav SOLO si está pegada: suelta, va en el flujo y no debe
   moverse cuando la nav se esconde (saltaba arriba y abajo al hacer scroll,
   QA 750). */
body:has(.nav.hidden) .listing__bar.is-stuck {
  transform: translateY(calc(-1 * var(--nav-height)));   /* sube lo que mide la barra y se queda en el borde del área segura: más arriba, Chrome en iPad lo tapa con su barra encogida (QA 1046) */
}
.listing__search { max-width: 640px; }
.listing__search .input::-webkit-search-cancel-button { display: none; }
/* Fila del buscador con el interruptor Grid / Map (`.listing__fila`, opcional ·
   QA 1076): el interruptor va a la DERECHA del buscador y se pega con él, como
   en Missions. En el móvil, solo iconos (el nombre queda para lectores), para
   no estrechar el buscador; cada opción conserva sus 40 de alto. */
.listing__fila { display: flex; align-items: center; gap: var(--sp-default); }
.listing__fila .listing__search { flex: 1 1 auto; min-width: 0; }
.listing__fila > .tablist { flex: none; }
@media (max-width: 720px) {
  .listing__fila { gap: var(--sp-s); }
  .listing__fila .tablist__item { min-width: 40px; justify-content: center; }
  .listing__fila .tablist__label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}

/* Filtros colapsables · al pegarse la barra, la franja de filtros se pliega
   suavemente (altura real vía grid-rows 1fr→0fr) dejando solo el buscador. La
   separación (14px) vive como MARGEN del contenedor (se anima a 0), para que no
   deje residuo al colapsar. */
.listing__collapse {
  display: grid;
  grid-template-rows: 1fr;
  margin-top: 14px;
  transition: grid-template-rows var(--dur-soft) var(--ease-soft),
              margin-top var(--dur-soft) var(--ease-soft);
}
.listing__bar.is-stuck .listing__collapse { grid-template-rows: 0fr; margin-top: 0; }
.listing__filters { display: flex; flex-wrap: wrap; gap: 8px; min-height: 0; overflow: hidden; }
.listing__bar.is-stuck .listing__filters {
  opacity: 0;
  transition: opacity var(--dur-instant) var(--ease-out);
}
/* Modo búsqueda en pantalla táctil (`.listing--buscando`, lo pone el motor al
   tocar el buscador): la cabecera se recoge para que la barra quede arriba
   por su sitio y los resultados quepan sobre el teclado. Vuelve al salir con
   el campo vacío. La barra va JUSTO bajo la topbar, sin hueco: con fondo
   (pegada) un hueco deja ver una franja de página (QA 1081). */
.listing--buscando .listing__head { display: none; }
.listing.listing--buscando { padding-top: calc(var(--nav-height) + env(safe-area-inset-top, 0px)); }
/* Buscando en el móvil (`.is-buscando`, lo pone el motor): los filtros no se
   pliegan, así que la barra no pierde alto ni necesita margen de
   compensación — el contador queda a su hueco normal bajo la barra. */
.listing__bar.is-stuck.is-buscando { margin-bottom: var(--listing-bar-gap); }
.listing__bar.is-stuck.is-buscando .listing__collapse { grid-template-rows: 1fr; margin-top: 14px; }
.listing__bar.is-stuck.is-buscando .listing__filters { opacity: 1; }
@media (max-width: 720px) {
  /* En móvil la franja de chips va en UNA línea con scroll horizontal y SANGRA a
     los bordes de la pantalla. El bleed va en `.listing__collapse` (flex item →
     crece con margen negativo a ambos lados), NO en `.listing__filters` (grid
     item → no sangra a la derecha). */
  .listing__collapse { margin-left: calc(-1 * var(--gutter)); margin-right: calc(-1 * var(--gutter)); }
  .listing__filters {
    flex-wrap: nowrap;
    max-width: none;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: 0 var(--gutter);
  }
  .listing__filters::-webkit-scrollbar { display: none; }
  .listing__filters .chip,
  .listing__filters .tablist__item { flex: 0 0 auto; white-space: nowrap; }
  .listing__bar .listing__search { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .listing__collapse { transition: none; }
  .listing__bar.is-stuck .listing__filters { transition: none; }
}

/* Al pegarse en ESCRITORIO (≥1024): en vez de ocultar los chips, la barra se
   compacta a UNA fila — buscador a la izquierda, chips a la derecha. En reposo
   los chips siguen DEBAJO; solo cambian de sitio al quedarse pegada. (En
   móvil/tablet no caben en fila → se ocultan, como arriba.) */
@media (min-width: 1200px), (min-width: 1024px) and (orientation: landscape) {
  .listing__bar:not(.listing__bar--inline).is-stuck { flex-direction: row; align-items: center; gap: clamp(16px, 3vw, 32px); }
  /* El buscador CONSERVA su ancho de la página (640) — nunca se encoge. */
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__search { flex: 0 0 640px; max-width: 640px; }
  .listing__bar:not(.listing__bar--inline).is-stuck > .listing__fila { flex: none; }
  /* Los chips ocupan lo que sobre a la derecha, en UNA línea; los que no quepan
     se recortan (no se muestran). Prioridad: el ancho correcto del buscador. */
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__collapse { grid-template-rows: 1fr; margin-top: 0; flex: 1 1 auto; min-width: 0; overflow: hidden; }
  /* Los chips ocupan lo que sobre a la derecha, en UNA línea. Los que no quepan
     NO se esconden: la franja se DESPLAZA en horizontal, igual que en móvil
     (decisión de Irene, 2026-10 — antes se ocultaban y desaparecía un filtro
     entero al scrollear). Barra de desplazamiento oculta: el gesto basta. */
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__filters {
    opacity: 1;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__filters::-webkit-scrollbar { display: none; }
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__filters .chip,
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__filters .tablist__item { flex: 0 0 auto; white-space: nowrap; }
  /* El chip elegido SE QUEDA EN SU SITIO (Irene, 29-10 · QA 1063): antes se
     adelantaba al principio de la fila para no esconderse al pegarse la
     barra. Desde que un chip es una búsqueda (QA 1044), su nombre ya se ve en
     el buscador, y moverlo despistaba. No volver a poner `order: -1`. */
  /* SUAVIZADO del cambio de sitio (Irene, 2026-10). Los chips pasan de DEBAJO
     del buscador a su DERECHA, y eso es un cambio de `flex-direction`: una
     propiedad discreta, que el navegador NO puede interpolar. Sin ayuda, los
     chips TELETRANSPORTAN — aparecen de golpe en el sitio nuevo.
     No se arregla con `transition`: cuando la clase entra, el reflujo ya ha
     ocurrido y la transición llegaría tarde (los verías saltar y LUEGO fundirse).
     Se arregla con una animación de ENTRADA: arrancan invisibles y un pelo más
     abajo YA en su posición nueva, así el salto cae dentro del fotograma en que
     no se ven. El ojo lee una disolvencia. */
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__filters {
    animation: listing-chips-entran var(--dur-soft) var(--ease-soft) both;
  }
}
@keyframes listing-chips-entran {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Va DESPUÉS de la regla de ≥1024 a propósito: el bloque general de
   prefers-reduced-motion de este componente está más arriba en el archivo y la
   regla de ≥1024 lo pisaría por orden y por especificidad. */
@media (prefers-reduced-motion: reduce) {
  .listing__bar:not(.listing__bar--inline).is-stuck .listing__filters { animation: none; }
}

/* Barra en fila (variante genérica ≥1200 · Search): buscador a la izquierda +
   filtros a la derecha en una sola fila → ya es compacta, NO se pliega. */
@media (min-width: 1200px) {
  .listing__bar--inline { flex-direction: row; align-items: center; gap: clamp(16px, 3vw, 32px); }
  .listing__bar--inline .listing__search { flex: 0 1 640px; }
  .listing__bar--inline .listing__collapse { margin-top: 0; margin-left: auto; align-self: center; }
  .listing__bar--inline.is-stuck .listing__collapse { grid-template-rows: 1fr; }
  .listing__bar--inline.is-stuck .listing__filters { opacity: 1; }
}

/* ── Grid · cards CON fondo (.card del DS), 3 columnas ────────────────────
   La card es la unidad de resultado del DS: media 16/9, `.card__meta-row` +
   `.card__meta` para el antetítulo + píldora (todo ya definido en `.card`).
   Aquí solo el layout del grid y el play como etiqueta abajo-izquierda. */
.listing__grid {
  --card-ratio: 16 / 9;
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
}
@media (min-width: 720px)  { .listing__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px), (min-width: 1024px) and (orientation: landscape) { .listing__grid { grid-template-columns: repeat(3, 1fr); } }
/* Variante GENÉRICA (Search): la MISMA card, solo más densidad → 4 columnas en
   desktop (misma card 16/9, título 22, gap 24 · lo único que cambia es la
   rejilla). La card sin píldora es decisión de MARKUP (no incluye `.card__meta`),
   ver "Patrón de listado" en el MDS. Lo usan Search y Calculators. */
@media (min-width: 1200px), (min-width: 1024px) and (orientation: landscape) { .listing__grid--generic { grid-template-columns: repeat(4, 1fr); } }
/* En MÓVIL las cards del listado son HORIZONTALES (miniatura + texto, SIN
   snippet) — patrón validado de read-next / resultados de Search: más compactas
   y escaneables que la vertical a ancho completo (decisión Irene 2026-08).
   `.card-rows-mobile` (2026-10-15) · la MISMA receta para cualquier rejilla de
   contenidos fuera de `.listing` (Search, destacados de Resources): «ese es el
   diseño que deberíamos mostrar por defecto en mobile sobre contenidos»
   (Irene). Se pone en el contenedor de las cards. */
@media (max-width: 720px) {
  :is(.listing__grid, .card-rows-mobile) .card { flex-direction: row; align-items: stretch; border-radius: var(--radius-m); }
  /* Foto A SANGRE, todo el alto de la card (Irene, 2026-10-19 · QA 589-590):
     cuadrada y arriba dejaba una franja debajo que parecía un corte. Como
     mínimo es cuadrada; si el título crece, la foto crece y se RECORTA.
     La imagen va absoluta para que su proporción no empuje la card (una
     captura vertical la llevaba a 224 px). Sustituye a «siempre cuadrada,
     arriba y fija» del 15-10. */
  :is(.listing__grid, .card-rows-mobile) .card__media { position: relative; flex: none; align-self: stretch; width: clamp(96px, 28vw, 120px); aspect-ratio: auto; min-height: clamp(96px, 28vw, 120px); }
  :is(.listing__grid, .card-rows-mobile) .card__media > img,
  :is(.listing__grid, .card-rows-mobile) .card__media > picture > img,
  :is(.listing__grid, .card-rows-mobile) .card__media > video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
  /* Cuerpo apretado: poco aire entre antetítulo, título y autor. */
  :is(.listing__grid, .card-rows-mobile) .card__body { flex: 1 1 auto; min-width: 0; justify-content: center; gap: 3px; padding: 12px 14px; }
  :is(.listing__grid, .card-rows-mobile) .card__text { display: none; }
  /* En la fila estrecha manda el título; se retira la píldora de meta (duración/
     lectura) para no apretar el antetítulo. */
  :is(.listing__grid, .card-rows-mobile) .card__meta { display: none; }
  /* En la miniatura de móvil, todas las insignias (también el play) abajo-izquierda,
     a 36: también en las recomendaciones (`.read-next`), que usan la misma miniatura. */
  :is(.listing__grid, .card-rows-mobile, .read-next__grid) .card__kind { inset: auto auto 6px 6px; width: 36px; height: 36px; }
  /* 18px · a 15 el icono se perdía dentro del disco (Irene, 2026-10-14). */
  :is(.listing__grid, .card-rows-mobile, .read-next__grid) .card__kind svg { width: 18px; height: 18px; }
}
/* INTERRUPTOR · `.listing__grid--texto`: la descripción se ve también en las filas
   del móvil (Calculators: el título solo no dice qué calcula cada una). Tope de 3
   líneas en todos los tamaños: el texto se escribe para caber, el corte es la red. */
.listing__grid--texto .card__text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
@media (max-width: 720px) {
  .listing__grid--texto .card__text { font-size: var(--text-body-xs); line-height: 1.45; }
}
/* Insignia en el listado de escritorio: abajo-izquierda, disco de 40. */
@media (min-width: 721px) {
  .listing__grid .card__kind { inset: auto auto 10px 10px; margin: 0; width: 40px; height: 40px; }
  /* 20px · a 16 se perdía. El disco mide 40, así que hay sitio de sobra. */
  .listing__grid .card__kind svg { width: 20px; height: 20px; }
}

/* ── Estados · vacío + paginación "cargar más" ────────────────────────────── */
.listing__empty { text-align: center; padding: clamp(48px, 8vw, 96px) 0; }
.listing__empty[hidden] { display: none; }
.listing__empty__icon { color: var(--content-text-default-weak); margin-bottom: 16px; }
/* Con el teclado abierto y poco hueco, la lupa (decorativa) se retira para que
   quepa el texto · el motor centra el bloque en el hueco que deja el teclado. */
.listing__empty.is-compacto .listing__empty__icon { display: none; }
.listing__empty__title { font-family: var(--font-sans); font-size: var(--text-heading-sm); font-weight: 600; margin: 0 0 8px; }
.listing__empty__title strong { color: var(--content-text-highlight); font-weight: 600; }
.listing__empty__text { font-family: var(--font-sans); font-size: var(--text-body-sm); color: var(--content-text-default-weak); margin: 0 0 18px; }
/* Búsquedas populares del vacío: las de la Search genérica y, en un listado,
   sus chips (las pone el motor · QA 1044). */
.listing__empty__suggest { display: flex; flex-wrap: wrap; gap: var(--sp-ds); justify-content: center; }
.listing__empty__suggest:empty { display: none; }
.listing__empty .btn { text-decoration: none; }
.listing__more { display: flex; justify-content: center; margin-top: clamp(28px, 4vw, 44px); }

@media (max-width: 720px) {
  .listing__title { font-size: var(--text-hero-title-sm); }
}

/* ===========================================================================
   STATUS BADGE (chip de estado) · glass + dot · familia semántica de
   disponibilidad · usado en las cards de Missions y en el CTA de plazas de
   las Expeditions. Base glass neutra + `.mis-badge--pastel` que añade el
   color de estado (bg del paso -7/-8 al 82% + texto blanco) y un punto
   (`.mis-badge__dot`) del paso -5 delante del label.
   Estados:
     · --pastel-available · NEUTRO (bg-bold 82%) · punto BLANCO · "disponible / N spots"
     · --pastel-few       · amarillo (yellow-8)  · punto yellow-5 · "quedan pocas"
     · --pastel-soldout   · rojo (red-7)         · punto red-5    · "agotado"
     · --pastel-coming    · azul (blue-7)        · punto blue-5   · "próximamente"
   Excepción AA: yellow usa -8 (en -7 el blanco daba 3.0-3.8:1). Contraste
   peor caso (alpha .82 sobre foto clara): blue 5.4 · yellow 4.9 · red 5.5 ·
   neutral > 8 — todos pasan 4.5:1. Nota: en Missions el estado "open" (con
   plazas de sobra) NO lleva chip; la variante --pastel-available es para
   cuando el estado disponible SÍ debe mostrarse (p. ej. el CTA de plazas de
   Expeditions). Clase legacy `mis-` por su origen en Missions; componente
   compartido del DS. */
.mis-badge {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 10px;
  border-radius: var(--radius-full);
  background: var(--black-a55);
  backdrop-filter: var(--glass-filter-strong);
  -webkit-backdrop-filter: var(--glass-filter-strong);
  color: var(--content-text-default);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.mis-badge--pastel {
  backdrop-filter: var(--glass-filter-strong);
  -webkit-backdrop-filter: var(--glass-filter-strong);
  font-weight: 600;
  gap: 8px;
  padding-left: 9px;
}
/* Available · fondo neutro oscuro · lleva un filete claro muy sutil (hairline
   canónico del DS, blanco 16%) para no perderse sobre fondos negros. Las demás
   variantes tienen tinte de color y no lo necesitan. */
.mis-badge--pastel-available { background: color-mix(in srgb, var(--bg-bold) 82%, transparent); color: var(--gray-white); border: 1px solid var(--border-neutral-soft); }
.mis-badge--pastel-coming    { background: color-mix(in srgb, var(--blue-7) 82%, transparent);   color: var(--gray-white); }
.mis-badge--pastel-few       { background: color-mix(in srgb, var(--yellow-8) 82%, transparent); color: var(--gray-white); }
.mis-badge--pastel-soldout   { background: color-mix(in srgb, var(--red-7) 82%, transparent);    color: var(--gray-white); }

/* Dot · punto de 6px del color saturado `-5` (sin alpha) delante del texto. */
.mis-badge__dot {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-circle);
  display: inline-block;
  flex-shrink: 0;
  /* Centrado con las MAYÚSCULAS, no con la caja del texto: la caja reserva
     sitio abajo para los rabos (p, g) que en mayúsculas no hay, y el punto
     parecía apoyado abajo. 0,06em = lo que sube el centro de las mayúsculas
     en la mono a 10. */
  translate: 0 -0.06em;
}
.mis-badge--pastel-available .mis-badge__dot { background: var(--gray-white); }
.mis-badge--pastel-coming    .mis-badge__dot { background: var(--blue-5);   }
.mis-badge--pastel-few       .mis-badge__dot { background: var(--yellow-5); }
.mis-badge--pastel-soldout   .mis-badge__dot { background: var(--red-5);    }

/* `--light` · el mismo badge sobre fondo claro (páginas de texto, cards
   claras). Sin cristal: fondo `-3` de su familia, punto `-7` y texto `-9` de
   la misma familia (gris en Available). Con el `-5` el amarillo se confundía
   con los enlaces subrayados; con el `-1`, con su fondo resaltado. */
.mis-badge--light {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-color: transparent;
}
.mis-badge--light.mis-badge--pastel-available { background: var(--gray-3);   color: var(--gray-9);   }
.mis-badge--light.mis-badge--pastel-coming    { background: var(--blue-3);   color: var(--blue-9);   }
.mis-badge--light.mis-badge--pastel-few       { background: var(--yellow-3); color: var(--yellow-9); }
.mis-badge--light.mis-badge--pastel-soldout   { background: var(--red-3);    color: var(--red-9);    }
.mis-badge--light.mis-badge--pastel-available .mis-badge__dot { background: var(--gray-7);   }
.mis-badge--light.mis-badge--pastel-coming    .mis-badge__dot { background: var(--blue-7);   }
.mis-badge--light.mis-badge--pastel-few       .mis-badge__dot { background: var(--yellow-7); }
.mis-badge--light.mis-badge--pastel-soldout   .mis-badge__dot { background: var(--red-7);    }

.prep-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  max-width: 1100px;
  margin: 1.5rem auto 2rem;
}

.prep-chip {
  display: inline-flex;
  align-items: center;
  padding: 9px 16px;
  background: var(--white-a04);
  border: 1px solid var(--white-a12);
  border-radius: var(--radius-full);
  color: var(--white-a90);
  font-family: inherit;
  /* Sits one notch above fs-body-sm (14) for legibility, without
     jumping all the way to fs-body (17). Mirrors .exp-story-chip-name. */
  font-size: var(--text-body-sm);
  font-weight: 400;
  letter-spacing: 0;
  cursor: pointer;
  transition: background var(--dur-soft) ease,
    border-color var(--dur-soft) ease,
    color var(--dur-soft) ease,
    transform var(--dur-soft) ease;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.prep-chip:hover {
  background: var(--white-a08);
  border-color: var(--white-a20);
  color: var(--gray-white);
}

.prep-chip:focus-visible {
  outline: 2px solid var(--bg-primary-default);
  outline-offset: 3px;
}

@keyframes finalCtaHeadline {
  from { opacity: 0; transform: scale(0.96); filter: blur(8px); }
  to   { opacity: 1; transform: scale(1);    filter: blur(0); }
}

@keyframes finalCtaFade {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}

.section-glow .reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

@keyframes slowZoom {
  /* Gentle Ken-Burns drift — bumped from 1.05 to 1.10 so the zoom is a
     touch more cinematic without ever calling attention to itself. */
  from { transform: scale(1); }
  to   { transform: scale(1.10); }
}

/* ── Gallery zoom · utilidad de slideshow ────────────────────────────────
   Añade `gallery-zoom` al contenedor de slides de cualquier galería (el que
   tiene overflow:hidden). La foto de la slide activa (`.is-active img`) hace
   un zoom-in lento con el `slowZoom` canónico — mismo gesto que los heroes.
   Es el efecto compartido entre TODAS las galerías de slideshow del sitio.
   La animación se reinicia al cambiar de slide (la clase .is-active se mueve). */
.gallery-zoom .is-active img {
  animation: slowZoom 12s var(--ease-out) forwards;
}
@media (prefers-reduced-motion: reduce) {
  .gallery-zoom .is-active img { animation: none; transform: none; }
}

.btn-primary {
  background: var(--bg-primary-default);
  color: var(--gray-9);
}
/* :hover solo con ratón: en un teléfono se queda PEGADO tras tocar y el botón
   parece desactivado (QA 561). `.is-hover` es para las demos de la guía. */
.btn-primary.is-hover  { background: var(--yellow-6);             color: var(--gray-9); }
@media (hover: hover) { .btn-primary:hover { background: var(--yellow-6); color: var(--gray-9); } }
.btn-primary:active,
.btn-primary.is-active { background: var(--bg-primary-strong); color: var(--gray-9); }

.field-ready {
  color: var(--gray-9);
}

.btn-secondary:not(.get-app-trigger) {
  background: transparent !important;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: none !important;
  padding: 0 !important;
  height: auto !important;
  font-size: var(--text-body-md);
  font-weight: 500;
  color: var(--gray-white);
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.btn-secondary:not(.get-app-trigger)::after {
  content: "\203A";
  display: inline-block;
  margin-left: 6px;
  font-size: 1.35em;
  font-weight: 300;
  line-height: 1;
  transition: transform var(--dur-instant) var(--ease-out);
}

/* Dentro de la puerta de ratón: en un teléfono el `:hover` se queda PEGADO
   tras tocar, y este efecto dejaría la pieza apagada o movida para siempre.
   (Revisión a fondo del 16-10-2026, del mismo tipo que el bug del
   photoslider que vio Irene: «la foto no crece, sino más bien lo contrario».) */
@media (hover: hover) {
  .btn-secondary:not(.get-app-trigger):hover {
    background: transparent !important;
    color: var(--gray-white);
    opacity: 0.85;
  }
}

.btn-secondary:not(.get-app-trigger):hover::after {
  transform: translateX(3px);
}

.section-subtitle {
  font-size: clamp(var(--text-body-lg), 2vw, var(--text-lead));
  line-height: 1.3;
  font-weight: 300;
  letter-spacing: -0.01em;
  color: var(--gray-white);
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
  margin-bottom: 2.5rem;
  text-wrap: pretty;
}

/* ── Word-joining SOLO en títulos (2026-08, decisión de la usuaria) ────
   La norma de "juntar palabras" / equilibrar líneas (`text-wrap: balance`)
   se aplica ÚNICAMENTE a titulares y subtítulos. Los PÁRRAFOS y LISTAS
   envuelven de forma NORMAL (greedy) — antes llevaban `text-wrap: pretty`,
   que reordenaba líneas para evitar huérfanos y provocaba cortes que no
   tenían sentido (sobre todo en los checks cortos y en párrafos anchos). */
p, li, dd, dt, figcaption, blockquote {
  text-wrap: wrap;
}

h1, h2, h3, h4,
.section-subtitle,
.statement-line {
  text-wrap: balance;
}

/* Cinturón para la norma anti-viudas donde el motor no llega (p.ej.
   statements con las palabras en spans coreografiados, o Safari sin
   `pretty`): envolver las DOS últimas palabras en `.keep-together`. */
.keep-together {
  white-space: nowrap;
}

/* Eyebrow line — mono yellow uppercase text that introduces a page or
   section title. The "title above the title" pattern. Used on the page
   intros (Premium, Design System styleguide). Pair with margin-bottom
   in the parent layout to space it from the headline below. */
.eyebrow-line {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-mono-lg);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--yellow-5);
  margin: 0;
}

/* (.section-eyebrow retired in 2026-04 coherence pass — replaced by
   .eyebrow-line above. The previous gray-sans pattern had zero uses
   in the homepage and was not picked up by any new page.) */

/* Brand accent — a single keyword highlighted in yellow within a larger
   statement (e.g. "Preparation is not."). Site-wide pattern. */
.statement-accent-word {
  color: var(--brand-yellow);
}

/* Ghost pill — glass-style secondary CTA. Comparte el MISMO glass que
   `.btn-tertiary` (blur 2.5px / saturate 120% / white 0.16). Si cambias el
   valor del glass, cámbialo en AMBOS para no divergir (ver nota en
   .btn-tertiary). Use when a hero/CTA row needs two or three button-shaped
   actions (e.g. Experiences hero). */
.btn-ghost {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  color: var(--gray-white);
}

.btn-ghost.is-hover {
  background: var(--glass-bg-hover);
  color: var(--gray-white);
}
@media (hover: hover) { .btn-ghost:hover { background: var(--glass-bg-hover); color: var(--gray-white); } }   /* solo con ratón (QA 561) */

.btn-ghost:active,
.btn-ghost.is-active {
  background: var(--glass-bg-active);
}

/* Ghost / Dark / Outline variants no longer override `.btn:focus-visible`.
   The base box-shadow ring (yellow, follows pill border-radius) applies to
   every variant — one ring colour for the whole .btn family. */

.btn-tertiary.get-app-trigger {
  color: var(--gray-white);
}

.btn-tertiary.get-app-trigger:hover {
  color: var(--gray-white);
}

.btn-dark {
  background: var(--gray-11);
  color: var(--gray-white);
}

/* Hover y pulsado en AMARILLO (Irene, 2026-10-16: «para fondo claro, el hover
   puede ser en amarillo»). Antes se volvía blanco y sobre una página blanca el
   botón perdía su borde. Amarillo vivo, nunca oscurecido (mostaza = parece
   deshabilitado). */
.btn-dark.is-hover,
.btn-dark:active,
.btn-dark.is-active {
  background: var(--color-action-default);
  color: var(--gray-11);
}
@media (hover: hover) { .btn-dark:hover { background: var(--color-action-default); color: var(--gray-11); } }   /* solo con ratón (QA 561) */

/* `.btn-dark--panel` · el secundario oscuro DENTRO de un panel gris claro
   (Contents). Hover y pulsado en BLANCO con su borde fino pintado (Irene,
   2026-10-19): el amarillo no pegaba con el panel. Sobre una página blanca NO
   se usa: el botón blanco se fundiría con ella (por eso `.btn-dark` va en
   amarillo). Con foco de teclado conserva el anillo, sumado al borde. */
.btn-dark--panel.is-hover,
.btn-dark--panel:active,
.btn-dark--panel.is-active {
  background: var(--gray-white);
  color: var(--gray-11);
  box-shadow: inset 0 0 0 1px var(--gray-11);
}
@media (hover: hover) {
  .btn-dark--panel:hover { background: var(--gray-white); color: var(--gray-11); box-shadow: inset 0 0 0 1px var(--gray-11); }
  .btn-dark--panel:hover:focus-visible { box-shadow: inset 0 0 0 1px var(--gray-11), 0 0 0 3px var(--gray-black), 0 0 0 5px var(--yellow-5); }
}
.btn-dark--panel:active:focus-visible { box-shadow: inset 0 0 0 1px var(--gray-11), 0 0 0 3px var(--gray-black), 0 0 0 5px var(--yellow-5); }

/* (Inherits .btn:focus-visible — see comment above btn-ghost.) */

.btn-outline {
  background: transparent;
  color: var(--gray-white);
  box-shadow: inset 0 0 0 1px var(--gray-white);
}

.btn-outline.is-hover {
  background: var(--gray-white);
  color: var(--gray-9);
}
@media (hover: hover) { .btn-outline:hover { background: var(--gray-white); color: var(--gray-9); } }   /* solo con ratón (QA 561) */

.btn-outline:active,
.btn-outline.is-active {
  background: var(--gray-1);
  color: var(--gray-9);
}

/* `.btn-outline--light` · el contorno sobre fondo CLARO (2026-10-22 · QA 792):
   secundario junto a `.btn-dark` (cierres de las páginas de texto). Borde y
   texto casi negros; hover y pulsado con el gris azulado del catálogo claro,
   el mismo tono de los campos. El foco repite el borde: el aro de `.btn`
   pisa la sombra que lo dibuja y el botón se quedaría sin contorno. */
.btn-outline.btn-outline--light {
  color: var(--gray-11);
  box-shadow: inset 0 0 0 1px var(--gray-11);
}
.btn-outline.btn-outline--light.is-hover,
.btn-outline.btn-outline--light:active,
.btn-outline.btn-outline--light.is-active { background: var(--bg-light-sunk); color: var(--gray-11); }
@media (hover: hover) { .btn-outline.btn-outline--light:hover { background: var(--bg-light-sunk); color: var(--gray-11); } }   /* solo con ratón (QA 561) */
.btn-outline.btn-outline--light:focus-visible,
.btn-outline.btn-outline--light.is-focus { box-shadow: inset 0 0 0 1px var(--gray-11), 0 0 0 3px var(--anillo-hueco), 0 0 0 5px var(--anillo-foco); }

/* (Inherits .btn:focus-visible — see comment above btn-ghost.) */

/* Section title — sans 600 SemiBold by default. Editorial/marketing
   sections (.product-copy, .final-cta, .statement-*) all
   inherit this weight without overrides. The previous default of 400 was
   inconsistent with the home, where every section title rendered 600 via
   a catch-all override in style.css. Source of truth aligned 2026-05. */
.section-title {
  font-family: var(--font-sans);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
  line-height: var(--lh-heading);
  font-feature-settings: 'ss01' on;
  /* Jerarquía (norma, QA 832/792): un título de sección nunca mide lo mismo
     que el del hero (--text-hero-title, 78): tope un peldaño por debajo. */
  font-size: clamp(var(--text-heading-lg), 5vw, var(--text-display-md));
  margin-bottom: 1.5rem;
  text-wrap: balance;
}

.section-title + .section-subtitle {
  margin-top: -0.25rem;
}

/* Product hero title — COMPONENTE COMPARTIDO (Pricing + Premium usan el mismo
   H1 "PhotoPills Premium Beta"). Vivía duplicado en cada page CSS; centralizado
   aquí para que actualizar uno actualice ambos (decisión Irene 2026-08).
   Desktop: suelo display-md (64) → display-lg (78), una línea. Móvil: suelo
   heading-xl (56), repartido. El badge (.badge--inline-title) escala con él.
   NOTA: nombre legacy `pricing-intro__title-v2` (heredado del markup de las 3
   páginas) — App NO lo usa (se le retiró la clase para no tocar su desktop). */
.pricing-intro__title-v2,
.page-hero__title.pricing-intro__title-v2 {
  /* Tamaño, peso e interlineado: los pone `.page-hero__title` (2026-10-16).
     Aquí solo lo propio: «PhotoPills Premium» + Beta en UNA línea (con la
     doble clase para ganar al `text-wrap: balance` del componente). */
  white-space: nowrap;
  text-wrap: nowrap;
}

@media (max-width: 720px) {
  .pricing-intro__title-v2,
  .page-hero__title.pricing-intro__title-v2 {
    white-space: normal;
    text-wrap: balance;
  }
}

/* `.section` y `.section-lg` retiradas (mayo 2026): eran utilidades
   placebo — todas las secciones que las usaban hacían override. El padding
   vertical de cada sección se aplica directamente con un token
   `--section-pad-y-{sm,md,lg,xl}` en su CSS específico. */

.reveal {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity var(--dur-cinema) var(--ease-reveal),
    transform var(--dur-cinema) var(--ease-reveal);
  will-change: opacity, transform;
}

/* Active chip: subtle yellow accent — a soft yellow tint on the fill +
   a low-opacity yellow border. The chrome reads as selected without
   shouting. */
.prep-chip.is-active {
  background: color-mix(in srgb, var(--yellow-5) 10%, transparent);
  border-color: color-mix(in srgb, var(--yellow-5) 55%, transparent);
  color: var(--gray-white);
  font-weight: 500;
}
.prep-chip.is-active:hover {
  background: color-mix(in srgb, var(--yellow-5) 16%, transparent);
  border-color: color-mix(in srgb, var(--yellow-5) 75%, transparent);
  color: var(--gray-white);
}

/* ════════════════════════════════════════════════════════════════════════════
   SITE CHROME — nav, footer, global focus
   Moved from style.css so all landings share the same chrome.
   ════════════════════════════════════════════════════════════════════════════ */

/* style.css — page-specific styles (nav, hero, product sections, footer, etc.) */

/* Global focus ring — for any focusable element that doesn't have its own
   override. Outline (not box-shadow) here so we don't break the layout of
   container-y elements that use box-shadow for other reasons.
   IMPORTANT: no `border-radius` declaration — the outline follows the
   element's own radius natively in modern browsers. Setting one here would
   mutate pill buttons into rounded squares on focus. */
/* EL ARO CAMBIA DE COLOR SEGÚN EL FONDO (Irene, 15-10-2026).
   El amarillo funciona sobre el negro del sitio (15:1) pero sobre el blanco
   del artículo da **1,38:1**: quien navega con el teclado no ve dónde está.
   No se había notado porque las páginas de contenidos son la PRIMERA familia
   del sitio cuya superficie principal es clara.
   El color va en una variable: amarillo por defecto, azulado oscuro donde el
   fondo es claro. */
:root { --anillo-foco: var(--content-link-default); --anillo-hueco: var(--gray-black); }

:focus-visible {
  outline: 2px solid var(--anillo-foco);
  outline-offset: 2px;
}

/* Superficies claras · el aro se vuelve azulado oscuro: 14,5:1 sobre blanco y
   12,6:1 sobre el gris de las cajas. Es el azulado que ya usan los degradados
   del sistema, no un color nuevo. */
/* FONDO CLARO = AZUL DE LA MARCA (Irene, 15-10-2026). Empezó siendo un gris
   azulado oscuro (#1f2a3a) y ella lo descartó: «es prácticamente negro», no se
   lee como un color sino como un borde. El azul de PhotoPills da 3,25:1 sobre
   el lienzo claro —por encima del 3:1 que pide la norma para algo que no es
   texto— y se reconoce como intención. El amarillo aquí daría 1,29:1: está
   pintado y no se ve. Es la única excepción a «azul = solo logo». */
.article-body,
.callout--light { --anillo-foco: var(--blue-5); --anillo-hueco: var(--gray-white); }
/* OJO con el alcance (corregido tras verlo Irene): ponerlo en el `body` de la
   página teñía TAMBIÉN la topbar y el pie, que son oscuros — y allí el
   azulado se lee como un gris apagado. Y «Read next» dejó de ser una banda
   clara hace unas horas: su fondo es ahora el degradado oscuro, así que su
   aro vuelve al amarillo como el resto de lo oscuro. */
.read-next,
.ebook-cta { --anillo-foco: var(--content-link-default); --anillo-hueco: var(--gray-black); }   /* bandas oscuras dentro del artículo */

/* EXCEPCIÓN · los enlaces DENTRO DEL TEXTO se quedan en amarillo (decisión de
   Irene): ahí el amarillo es la marca del enlace y el aro lo refuerza en vez
   de contradecirlo. Son además los únicos que ya van subrayados, así que el
   aro no es su única pista. */
.article-body > p a:focus-visible,
.article-body > ul li a:focus-visible,
.article-body > ol li a:focus-visible,
.article-body .text-link:focus-visible { --anillo-foco: var(--content-link-default); }
/* TRAMPA · el panel de contenidos NO es prosa, es navegación: su aro va en
   azul, no en amarillo. Sin este matiz lo alcanza la regla de arriba, porque
   sus enlaces también viven en un `li`. */
/* EL PANEL DE CONTENIDOS, EN EL AZUL DE PHOTOPILLS (Irene, 15-10-2026: «el
   foco en introduction es prácticamente negro»). El gris azulado del cuerpo
   es tan oscuro que, sobre la caja clara del panel, no se lee como un color:
   se lee como un borde negro. El azul de la marca sí se reconoce (3:1 sobre
   la caja, el mínimo que pide la norma para un elemento que no es texto). */
.article-toc a:focus-visible { --anillo-foco: var(--blue-5); }   /* = el de fondo claro */

/* LAS ETIQUETAS, TAMBIÉN EN AZUL (Irene, 15-10-2026). Primero las dejamos en
   amarillo, pero sobre el fondo claro el aro amarillo da 1,29:1: está, y no se
   ve. Solo las del cuerpo del artículo —las que viven en claro—; las de los
   listados siguen sobre fondo oscuro y conservan el amarillo de serie. */
.article-body .chip:focus-visible { --anillo-foco: var(--blue-5); }

/* EL ARO SIGUE LA FORMA DE LO QUE SEÑALA (Irene: «el radio debe ser el mismo
   que el elemento que señala»). El aro adopta solo el radio del ELEMENTO, así
   que el problema estaba en los envoltorios sin radio propio que contienen
   algo redondeado: la card de «Read next» (su foto va a 16 y el enlace que la
   envuelve iba a 0) y la miniatura ampliable (foto a 16, enlace a 2). El aro
   salía cuadrado alrededor de algo redondo. Se le da al envoltorio el radio de
   lo que enseña: no se ve nunca, salvo justo cuando hace falta. */
.article-body a.card { border-radius: var(--radius-m); }
/* Excepto la card de recomendados: dibuja su propio aro (abajo) y lleva un
   filete inferior de separación, que con el radio se CURVABA en las esquinas
   (Irene, 2026-10-19: «antes eran una línea recta, ahora se curvan»). */
.article-body a.card.read-next__card { border-radius: 0; }
.article-body p > a.fancybox { border-radius: var(--radius-m); }

/* ...Y NO APRIETA EL TEXTO (Irene, 15-10-2026: «se pega con los textos por los
   lados y oscurece toda la card»). La card de «Read next» va desnuda —sin
   fondo ni borde—, así que su caja coincide EXACTAMENTE con la del texto y el
   aro le pasaba rozando: parecía un recuadro alrededor de la frase, no la
   marca de dónde estás. Se dibuja aparte, con aire propio a los cuatro lados:
   12px por los costados (donde no estorba a nadie) y 6 arriba y abajo (menos
   de los 14 de separación entre cards, así que nunca toca a la vecina). */
.read-next__card { position: relative; --aro-aire: 6px; }
/* TRAMPA · esta anulación necesita más peso que `a.card:focus-visible` del
   componente. Con menos peso salen los DOS aros a la vez. */
.read-next .read-next__card:focus-visible { outline: none; }
.read-next .read-next__card:focus-visible::after {
  content: '';
  position: absolute;
  inset: calc(-1 * var(--aro-aire));   /* MISMO aire por los cuatro lados */
  border: 2px solid var(--anillo-foco);
  border-radius: var(--radius-m);
  pointer-events: none;
}

/* ════════════════════════════════════════════════════════════════════════════
   NAVIGATION
   ════════════════════════════════════════════════════════════════════════════ */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Altura total = nav-height + safe area (status bar notch/dynamic
     island). El fondo y blur cubren hasta arriba del todo del display
     · el padding-top desplaza el contenido (logo, links) por debajo
     del status bar. Sin esto, el contenido se "comería" la status
     bar en iOS Safari con viewport-fit=cover. */
  height: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
  padding-top: env(safe-area-inset-top, 0px);
  background: var(--nav-bg);
  /* Fondo NEGRO SÓLIDO (Apple-style: barra fija, nada del fondo se ve a
     través, en ningún lado). Antes tenía `backdrop-filter: blur` (glass),
     pero en iOS Safari con `viewport-fit=cover` el backdrop-filter impedía
     que el fondo OPACO pintara → se transparentaba el hero por TODA la barra.
     Lo sustituimos por `transform: translateZ(0)`: crea el MISMO bloque
     contenedor que el drawer móvil necesita (lo que antes daba el
     backdrop-filter) SIN romper el pintado del fondo opaco. */
  transform: translateZ(0);
  z-index: var(--z-nav);
  transition: transform var(--dur-soft) var(--ease-out);
}

/* iOS Safari · con `viewport-fit=cover` + `backdrop-filter`, el fondo de la
   nav a veces NO pinta la franja del notch (env(safe-area-inset-top)) y se
   transparenta el contenido de detrás (p.ej. el hero). Este pseudo pinta esa
   franja superior en negro sólido, por encima, SIN tocar el backdrop-filter
   (que debe seguir creando el bloque contenedor del drawer móvil). El
   contenido de la nav va por debajo (padding-top), así que no se solapa. */
.nav::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top, 0px);
  background: var(--nav-bg);
}

.nav.hidden {
  transform: translateY(-100%);
}

/* (La barra de progreso de lectura vive entera en «PÁGINA DE CONTENIDOS», al final
   de este archivo · 2026-10. Antes estaba partida: la base en la hoja de cada
   página y solo el acople a la nav aquí.) */

.nav-inner {
  /* Contenido de la topbar a FULL WIDTH en TODO el sitio (decisión Irene
     2026-08: unificar). Antes capaba a --max-full (1440) centrado y solo
     experiences/dolomites/article lo overrideaban → incoherencia. */
  max-width: none;
  height: 100%;
  margin: 0 auto;
  padding: 0 1.5rem;
  /* Grid 3-col (1fr auto 1fr) · logo izq · links CENTRADOS de verdad
     en el viewport (columna central equidistante) · acciones der.
     Antes era flex con `margin-right/left: auto`, que centraba los
     links en el hueco entre logo y acciones — como el bloque de
     acciones es más ancho que el logo, los links quedaban desplazados
     a la izquierda del centro real. El grid los centra sin importar
     el ancho de logo/acciones. En móvil (≤1024) vuelve a flex (ver
     media query). */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 2rem;
}

.nav-logo {
  display: flex;
  align-items: center;
  justify-self: start;
}

.nav-logo img {
  /* 20px · reducido desde 24 (feedback usuaria jul 2026) · uniforme en
     todos los breakpoints (antes 24 desktop / 20 phone). A 20px el logo
     mide ~119px de ancho — sobre el mínimo de 80px del MDS (QA #46). */
  height: 20px;
}

.nav-links {
  display: flex;
  gap: 0;
  align-items: center;
  justify-self: center;
}

.nav-link {
  color: var(--white-a90);
  font-size: var(--text-body-sm);
  font-weight: 400;
  padding: 0 0.85rem;
  line-height: var(--nav-height);
  transition: color var(--dur-instant) var(--ease-out);
  position: relative;
  white-space: nowrap;
}

.nav-link:hover {
  color: var(--gray-white);
}

.nav-link.active {
  color: var(--gray-white);
}

.nav-link.active::after {
  content: '';
  position: absolute;
  bottom: 8px;
  left: 0.75rem;
  right: 0.75rem;
  height: 2px;
  background: var(--content-link-default);
}

.nav-link.has-dropdown::after {
  content: none;
}
/* Página actual dentro de un desplegable: el padre también lleva el
   subrayado (lo pone `utils.js` con `.active`), y en el submenú el título de
   la página va en amarillo. */
.nav-link.has-dropdown.active::after { content: ''; }
.nav-sub-link[aria-current] .nav-sub-title { color: var(--color-action-default); }

.nav-caret {
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-left: 6px;
  /* Amarilla y entera (norma 2026-10-19: toda flecha clicable va en amarillo).
     Antes iba al 60 %: un amarillo apagado se lee como desactivado. */
  color: var(--color-action-default);
  transition: transform var(--dur-instant) var(--ease-out);
}

.nav-item {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.nav-item-has-sub .nav-link {
  display: inline-flex;
  align-items: center;
}

.nav-sub-link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border-radius: var(--radius-s);
  color: var(--gray-white);
  text-decoration: none;
  line-height: 1.3;
  transition: background var(--dur-instant) ease;
  min-height: 44px;
  justify-content: center;
}

/* Hover y foco de teclado · fondo al 8 % (al 4 % no se veía sobre el negro
   de la topbar), título en el amarillo de acción, descripción al 90 % y una
   flecha que se dibuja tras el título (el recorte se abre de izquierda a
   derecha): invita a pulsar sin ruido. El hover, solo con ratón (QA 846). */
a.nav-sub-link .nav-sub-title::after {
  content: "";
  display: inline-block;
  width: 0.9em;
  height: 0.9em;
  margin-left: var(--sp-xs);
  vertical-align: -0.1em;
  background-color: currentColor;
  -webkit-mask: var(--_nav-flecha) center / contain no-repeat;
          mask: var(--_nav-flecha) center / contain no-repeat;
  --_nav-flecha: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E");
  opacity: 0;
  clip-path: inset(0 100% 0 0);
  transform: translateX(-4px);
  transition: clip-path var(--dur-soft) var(--ease-out),
              transform var(--dur-soft) var(--ease-out),
              opacity var(--dur-instant) var(--ease-out);
}
.nav-sub-link .nav-sub-title { transition: color var(--dur-instant) var(--ease-out); }
a.nav-sub-link:focus-visible .nav-sub-title { color: var(--color-action-default); }
a.nav-sub-link:focus-visible .nav-sub-title::after { opacity: 1; clip-path: inset(0 0 0 0); transform: none; }
@media (hover: hover) {
  .nav-sub-link:hover { background: var(--white-a08); }
  .nav-sub-link:hover .nav-sub-desc { color: var(--white-a90); }
  a.nav-sub-link:hover .nav-sub-title { color: var(--color-action-default); }
  a.nav-sub-link:hover .nav-sub-title::after { opacity: 1; clip-path: inset(0 0 0 0); transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  a.nav-sub-link .nav-sub-title::after { transition: none; transform: none; }
}

.nav-sub-link:focus-visible {
  background: var(--white-a04);
  outline: 2px solid var(--content-link-default);
  outline-offset: -2px;
}

.nav-sub-title {
  font-size: var(--text-body-sm);
  font-weight: 600;
  color: var(--gray-white);
}

.nav-sub-desc {
  font-size: var(--text-body-xs);
  color: var(--white-a78);
}

/* Item de menú cuya página aún no existe: inerte (no clicable) + atenuado +
   etiqueta "Soon". Se activa cambiando el <span> por <a href> cuando la página
   esté validada. */
/* ATENUAR SIN MULTIPLICAR (oct 2026) · esto bajaba la opacidad del bloque
   ENTERO, así que se multiplicaba con la transparencia que el texto ya traía:
   la descripción del mega-menú (blanco 55%) acababa a 2,48:1 — prácticamente
   ilegible. Ahora cada pieza lleva su propio tono y ninguno baja de 7:1. */
.nav-sub-link--soon {
  pointer-events: none;
  cursor: default;
}
.nav-sub-link--soon .nav-sub-title {
  /* El título es lo que se apaga: en un item disponible es blanco puro (21:1),
     aquí baja a 9,9:1. Suficiente para leerse, claramente «aún no». La
     descripción se queda con su tono normal — informa igual en ambos casos. */
  color: color-mix(in srgb, var(--gray-white) 70%, transparent);
}
/* «WIP» · etiqueta AUTOMÁTICA de todo enlace que aún lleva a la web antigua
   (photopills.com). Hermana de «Soon»: mismo dibujo, pero el enlace sí se
   pulsa. No se escribe en el marcado: al cambiar el enlace por la página
   nueva, desaparece sola. Las fotos (`/sites/default/files/`) no la llevan.
   Al lanzar no debe quedar ninguna (Irene, 2026-10-23 · QA 799). */
a[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after {
  content: "WIP";
  content: "WIP" / "";
  margin-left: 6px;
}
.nav-soon,
a[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border-neutral-soft);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  font-weight: 500;
  font-style: normal;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--content-text-default-weak);
  vertical-align: middle;
}
/* DENTRO DE UN BOTÓN la etiqueta toma el color del botón. El gris de serie
   está pensado para el menú y el pie, que van sobre negro; sobre un botón
   amarillo o sobre el cristal de un ghost encima de una foto no se leía.
   (16-10-2026 · Irene pidió que estos CTA conserven su diseño y solo lleven
   la etiqueta, así que la etiqueta es la que se adapta.) */
.btn .nav-soon,
a.btn[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after,
a.chip[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after {
  color: currentColor;
  border-color: currentColor;
  opacity: 0.72;
}
/* Sobre claro (cuerpo del artículo, páginas de texto, cards claras), el gris
   de serie no se lee: el de texto débil sobre claro, como `.badge--light`. */
:is(.article-body, .pg-zone, .card--light, .callout--light) a[href*="//www.photopills.com/"]:not([href*="/sites/default/files/"])::after {
  color: var(--content-text-on-light-weak);
  border-color: var(--content-text-on-light-weak);
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  justify-self: end;
}

.nav-search {
  width: 20px;
  height: 20px;
  color: var(--white-a78);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color var(--dur-instant);
}

.nav-login {
  display: inline-flex;
  align-items: center;
  position: relative;        /* ancla del área táctil de 40px (::after) */
  height: 32px;
  padding: 0 14px;
  border: 1px solid var(--white-a20);
  border-radius: var(--radius-full);
  color: var(--white-a90);
  font-size: var(--text-body-sm);
  font-weight: 500;
  letter-spacing: -0.005em;
  text-decoration: none;
  white-space: nowrap;       /* never wraps to two lines */
  flex-shrink: 0;            /* don't squeeze when nav-actions runs short */
  transition: background-color var(--dur-instant) var(--ease-out),
              border-color var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
@media (max-width: 480px) {
  .nav-login {
    height: 28px;
    padding: 0 11px;
    font-size: var(--text-mono-sm);   /* 12px — still readable */
  }
}
/* Área táctil mínima del DS (40×40) SIN cambiar el aspecto del botón.
   El pill mide 32px de alto (28 en ≤480), por debajo del mínimo de la regla 9.
   El ::after extiende la zona de toque por arriba y por abajo hasta 40px;
   no crece a lo ancho (width:100%), así que no le roba el toque a sus vecinos
   de .nav-actions. Decisión de Irene 2026-10: ampliar el área, no el botón. */
.nav-login::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  min-width: 40px;
  height: 40px;
}

.nav-login:hover {
  background: var(--white-a04);
  border-color: var(--white-a32);
  color: var(--gray-white);
}
.nav-login:focus-visible {
  outline: 2px solid var(--anillo-foco);
  outline-offset: 2px;
}

.nav-search:hover {
  color: white;
}

.nav-lang {
  color: var(--white-a78);
  font-size: var(--text-body-xs);
  letter-spacing: 0.5px;
  display: flex;
  gap: 4px;
}

.nav-lang .active {
  color: white;
  font-weight: 600;
}

.nav-lang .sep {
  color: var(--white-a50);
}

.nav-lang a {
  color: var(--white-a78);
  transition: color var(--dur-instant);
}

.nav-lang a:hover {
  color: var(--gray-white);
}
/* ESP deshabilitado (mockup solo en inglés · reactivable con ESP_ENABLED en i18n.js). */
.nav-lang .is-disabled {
  opacity: 0.4;
  pointer-events: none;
  cursor: not-allowed;
}

.nav-toggle {
  display: none;
  width: 40px;
  height: 40px;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  -webkit-tap-highlight-color: transparent;
  border-radius: var(--radius-s);
  transition: background var(--dur-instant) var(--ease-out);
}

/* Botón hamburguesa LIMPIO · sin fondo (como el resto de iconos del topbar).
   En hover/active las barras se aclaran levemente; el aro amarillo solo con
   navegación por teclado (:focus-visible), sin relleno. */
.nav-toggle:hover .nav-toggle-bar {
  background: var(--gray-white);
}

.nav-toggle:focus-visible {
  outline: 2px solid var(--content-link-default);
  outline-offset: 2px;
}

.nav-toggle-bar {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--gray-white);
  border-radius: 1px;
  transition: transform var(--dur-soft) var(--ease-out), opacity var(--dur-soft) var(--ease-out);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  .nav-item-has-sub:hover .nav-caret,
  .nav-item-has-sub:focus-within .nav-caret,
  .nav-item-has-sub.is-open .nav-caret {
    transform: rotate(-180deg);
    opacity: 1;
  }
}

@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  .nav-sub {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%) translateY(6px);
    min-width: 280px;
    padding: 12px;
    background: var(--bg-neutral-default);
    border: 1px solid var(--white-a08);
    border-radius: var(--radius-m);
    box-shadow:
      0 2px 4px var(--black-a20),
      0 16px 36px var(--black-a40),
      inset 0 1px 0 var(--white-a04);
    display: flex;
    flex-direction: column;
    gap: 2px;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-instant) var(--ease-out), transform var(--dur-instant) var(--ease-out);
    z-index: var(--z-chip);
  }
}

@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  .nav-item-has-sub:hover .nav-sub,
  .nav-item-has-sub:focus-within .nav-sub,
  .nav-item-has-sub.is-open .nav-sub {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    pointer-events: auto;
  }

  /* Invisible bridge so hover doesn't drop when moving parent → submenu */
  .nav-item-has-sub::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 10px;
    pointer-events: none;
  }

  .nav-item-has-sub:hover::after {
    pointer-events: auto;
  }
}

/* =============================================================================
   MEGA-MENÚ DE NAVEGACIÓN (estilo Apple) · desplegable a TODO EL ANCHO.
   Chrome canónico (decisión Irene + jefes · ago 2026). Se activa con la clase
   `.nav--mega` en el <nav> + un <div class="nav-scrim"> hermano. El panel cae
   desde la barra a ancho completo, la página se atenúa (scrim) y los enlaces
   quedan aireados. Solo desktop (≥1025); en móvil el nav usa el drawer canónico.
   ========================================================================== */
@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  /* El item no crea contenedor: el panel se ancla a la barra (la .nav con
     transform es el bloque contenedor del position:fixed → ancho completo). */
  .nav--mega .nav-item-has-sub { position: static; }
  .nav--mega .nav-item-has-sub::after { content: none; }  /* sin bridge: el panel toca la barra */

  .nav--mega .nav-sub {
    position: fixed;
    top: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
    left: 0;
    right: 0;
    width: 100%;
    min-width: 0;
    margin: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 4px 28px;
    /* padding lateral = margen que centra el contenido a --max-wide (como Apple). */
    padding: 28px max(24px, calc((100vw - var(--max-wide)) / 2)) 40px;
    background: var(--nav-bg);
    border: 0;
    border-bottom: 1px solid var(--white-a08);
    border-radius: 0;
    box-shadow: 0 28px 56px var(--black-a55);
    opacity: 0;
    pointer-events: none;
    transform: translateY(-10px);
    transition: opacity var(--dur-soft) var(--ease-out),
                transform var(--dur-soft) var(--ease-out);
    z-index: var(--z-chip);
  }
  .nav--mega .nav-item-has-sub:hover .nav-sub,
  .nav--mega .nav-item-has-sub:focus-within .nav-sub,
  .nav--mega .nav-item-has-sub.is-open .nav-sub {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }

  /* Enlaces más aireados y tipografía más tranquila. */
  /* Contenido arriba: en una fila de la rejilla con una opción más alta, las
     demás no se centran respecto a ella (escalones · QA 843). */
  .nav--mega .nav-sub-link { padding: 12px 10px; justify-content: flex-start; }
  .nav--mega .nav-sub-title { font-size: var(--text-body-md); font-weight: 500; }
    /* 55% daba 6,25:1 · sube a 70% (9,9:1) para cumplir el mínimo de texto
     secundario del sistema sin dejar de ser claramente secundaria. */
  .nav--mega .nav-sub-desc { font-size: var(--text-body-xs); color: color-mix(in srgb, var(--gray-white) 70%, transparent); }

  /* Scrim · atenúa la página con un panel abierto (va bajo la barra z-nav 100;
     los paneles viven dentro de la nav → quedan por encima del scrim). */
  .nav-scrim {
    position: fixed;
    inset: 0;
    top: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
    background: var(--black-a55);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-soft) var(--ease-out);
    z-index: 40;
  }
  body:has(.nav--mega .nav-item-has-sub:hover) .nav-scrim,
  body:has(.nav--mega .nav-item-has-sub:focus-within) .nav-scrim,
  body:has(.nav--mega .nav-item-has-sub.is-open) .nav-scrim {
    opacity: 1;
  }
}
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  .nav-scrim { display: none; }   /* en móvil manda el drawer canónico */
}
@media (prefers-reduced-motion: reduce) {
  .nav--mega .nav-sub, .nav-scrim { transition: none; }
}

/* ── Buscador DESPLEGABLE de la topbar (estilo Apple) ──────────────────────
   Baja desde la barra al pulsar la lupa (`body.search-open`). No navega: el
   usuario escribe aquí; Enter / un quick link llevan a /search/?q=…
   Global (vive en el partial de la nav) → funciona en todas las páginas. */
.nav-search-panel {
  position: fixed;
  top: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
  left: 0;
  right: 0;
  z-index: 90;                     /* bajo la barra (z-nav 100), sobre el scrim */
  background: var(--bg-bold);
  border-bottom: 1px solid var(--border-neutral-soft);
  box-shadow: var(--shadow-strong);
  transform: translateY(-10px);
  opacity: 0;
  visibility: hidden;
  transition:
    transform var(--dur-soft) var(--ease-out),
    opacity var(--dur-soft) var(--ease-out),
    visibility 0s linear var(--dur-soft);
}
body.search-open .nav-search-panel {
  transform: translateY(0);
  opacity: 1;
  visibility: visible;
  transition:
    transform var(--dur-soft) var(--ease-out),
    opacity var(--dur-soft) var(--ease-out);
}
.nav-search-panel__inner {
  padding-top: clamp(20px, 3vw, 32px);
  padding-bottom: clamp(20px, 3vw, 32px);
}
.nav-search-form {
  display: flex;
  align-items: center;
  gap: 12px;
  border-bottom: 1px solid var(--border-neutral-soft);
  padding-bottom: 14px;
}
.nav-search-form__icon { display: flex; color: var(--content-text-default-weak); flex: none; }
.nav-search-form__input {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--content-text-default);
  font-family: var(--font-sans);
  font-size: clamp(var(--text-heading-sm), 3vw, var(--text-heading-md));   /* grande, tipo Apple */
  font-weight: 300;
  padding: 6px 0;
}
.nav-search-form__input::placeholder { color: var(--content-text-default-weak); }
.nav-search-form__input:focus { outline: none; }
.nav-search-form__input::-webkit-search-cancel-button { display: none; }
.nav-search-form__close {
  appearance: none;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--content-text-default-weak);
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 40px;
  border-radius: var(--radius-full);
  flex: none;
  transition: color var(--dur-instant) var(--ease-out);
}
.nav-search-form__close:hover { color: var(--content-text-default); }
.nav-search-links { margin-top: clamp(16px, 2.5vw, 22px); }
.nav-search-links__label {
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--content-text-default-weak);
  margin: 0 0 12px;
}
.nav-search-links ul { list-style: none; display: flex; flex-wrap: wrap; gap: 10px; }
.nav-search-links a {
  display: inline-block;
  padding: 8px 14px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border-neutral-soft);
  color: var(--content-text-default);
  font-size: var(--text-body-sm);
  text-decoration: none;
  transition: background var(--dur-instant) var(--ease-out), border-color var(--dur-instant) var(--ease-out);
}
.nav-search-links a:hover { background: var(--white-a04); border-color: var(--white-a32); }
.nav-search-scrim {
  position: fixed;
  inset: 0;
  top: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
  background: var(--black-a55);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  z-index: 80;
  transition: opacity var(--dur-soft) var(--ease-out), visibility 0s linear var(--dur-soft);
}
body.search-open .nav-search-scrim {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--dur-soft) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .nav-search-panel, .nav-search-scrim { transition: opacity var(--dur-instant) linear; }
  .nav-search-panel { transform: none; }
}

/* Variante MINIMAL del topbar · pantallas de foco (login, checkout…): se queda
   solo el logo + el selector de idioma, sin links, buscador, "Log in" ni
   hamburguesa. Sigue siendo el MISMO componente `.nav` (mismo fondo, alto,
   hide-on-scroll) — solo oculta el ruido. */
.nav--minimal .nav-links,
.nav--minimal .nav-search,
.nav--minimal .nav-login,
.nav--minimal .nav-toggle { display: none; }
/* Sin los `.nav-links`, el grid 3-col dejaría las acciones en el centro:
   con flex + space-between el logo va a la izquierda y las acciones a la derecha. */
.nav--minimal .nav-inner { display: flex; justify-content: space-between; align-items: center; }

/* =============================================================================
   CONTACTO DE TOPBAR · WhatsApp + teléfono + horario (`.v8-contact`)
   Componente del chrome que SOLO aparece en las páginas de Expeditions
   (decisión Irene + jefes · ago 2026). Va dentro de `.nav-actions`, antes del
   buscador. Desde oct 2026 vive en el MENÚ CENTRAL y está apagado: solo asoma
   donde la página lo pide con `data-contacto` en el <body>. Antes se controlaba
   incluyéndolo o no en el markup, y por eso Dolomites llevaba su propia copia
   del menú — una copia que se quedó atrás y ofrecía páginas que no existen. Icono monocromo blanco (sin verde de marca).
   El teléfono es PLACEHOLDER. Se oculta en móvil (≤720). Promovido desde
   dolomites v10/v11 a common.css para reutilizarlo. */
.v8-contact {
  position: relative;                            /* ancla del tooltip de horario */
  display: none;                                 /* lo enciende `data-contacto` */
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  white-space: nowrap;
  color: var(--content-text-default);
  text-decoration: none;
}
.v8-contact__icon { display: block; width: 15px; height: 15px; flex-shrink: 0; }
.v8-contact__phone {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
}
.v8-contact__hours {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(-4px);
  padding: 5px 10px;
  background: var(--bg-neutral-default);
  border: 1px solid var(--white-a08);
  border-radius: var(--radius-s);
  color: var(--gray-white);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  box-shadow: 0 4px 12px var(--black-a40);
  z-index: var(--z-overlay);
  transition: opacity var(--dur-instant) var(--ease-out),
    transform var(--dur-instant) var(--ease-out),
    visibility 0s linear var(--dur-instant);
}
.v8-contact:hover .v8-contact__hours,
.v8-contact:focus-visible .v8-contact__hours {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  transition: opacity var(--dur-instant) var(--ease-out),
    transform var(--dur-instant) var(--ease-out);
}
/* Encendido por página. En modo drawer (≤1024) el contacto del topbar NO
   asoma: pasa DENTRO del menú hamburguesa (ver .nav-drawer-contact). */
@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  body[data-contacto] .v8-contact {
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }
}

/* Contacto DENTRO del drawer · solo Expeditions · solo modo hamburguesa (≤1024).
   Va lo PRIMERO del menú (que se vea antes que nada), separado por un filete
   abajo, con icono + teléfono + horario (el horario en línea: aquí hay sitio). */
.nav-drawer-contact { display: none; }
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  body[data-contacto] .nav-drawer-contact {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
    padding: 4px 8px 16px;
    border-bottom: 1px solid var(--white-a08);
    color: var(--content-text-default);
    text-decoration: none;
  }
  .nav-drawer-contact__icon { width: 22px; height: 22px; flex-shrink: 0; }
  .nav-drawer-contact__text { display: flex; flex-direction: column; gap: 3px; }
  .nav-drawer-contact__phone {
    font-family: var(--font-sans);
    font-size: var(--text-body-sm);
    font-weight: 500;
    line-height: 1.2;
  }
  .nav-drawer-contact__hours {
    font-family: var(--font-mono);
    font-size: var(--text-mono-xs);
    color: var(--content-text-default-weak);
    line-height: 1;
  }
}

@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  .nav-toggle {
    display: inline-flex;
    /* Las barras (22px) van centradas en el botón de 40px (touch target),
       lo que dejaba el icono a ~33px del borde mientras el logo está a 24px
       (asimetría · "mucho espacio a la derecha"). Con margin-right -9px el
       botón se acerca al borde y las barras quedan a 24px = mismo inset que
       el logo. El touch target de 40px se conserva (solapa el padding). */
    margin-right: -9px;
  }

  /* Móvil · vuelve a FLEX · los `.nav-links` pasan a `position: fixed`
     (panel desplegable, fuera de flujo), así que el grid 3-col del
     desktop no aplica · con flex + auto-margins el logo queda a la
     izquierda y las acciones + hamburguesa a la derecha. */
  .nav-inner {
    display: flex;
  }
  .nav-logo {
    margin-right: auto;
  }
  .nav-actions {
    margin-left: auto;
  }

  .nav-links {
    /* DRAWER LATERAL DERECHO · entra deslizando desde el borde derecho
       (no un desplegable centrado/a lo ancho). Ocupa desde debajo de la
       barra de nav hasta el fondo, con un ancho de panel cómodo dejando
       ver un poco de la página detrás. */
    position: fixed;
    /* OJO · el .nav tiene `transform: translateZ(0)` (antes era backdrop-filter),
       y eso lo convierte en el BLOQUE CONTENEDOR de este position:fixed (no el
       viewport). Por eso el panel se posiciona relativo a la BARRA NAV, no a la
       pantalla:
         · top: 100% → el borde inferior de la nav (soporta safe-area).
         · la altura la fijamos con dvh del viewport (no con bottom:0, que
           sería el fondo de la nav de 48px). Sin esto el panel salía de 48px.
       (Este era el bug de las "franjas"/panel pequeño: top+bottom+height se
       resolvían contra la nav de 48px, no contra la pantalla.) */
    top: 100%;
    right: 0;
    bottom: auto;
    left: auto;
    height: calc(100vh - var(--nav-height));
    height: calc(100svh - var(--nav-height));
    height: calc(100dvh - var(--nav-height));
    width: min(80vw, 320px);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 1.5rem;
    background: var(--bg-neutral-bold);
    border-left: 1px solid var(--white-a08);
    box-shadow: -12px 0 32px var(--black-a40);
    overflow-y: auto;
    opacity: 0;
    pointer-events: none;
    /* CERRADO = INEXISTENTE (a11y, oct 2026) · con solo `opacity: 0` el panel
       seguía estando ahí para el teclado: tabulando desde el logo había que
       atravesar 16 enlaces invisibles, fuera de pantalla, antes de llegar al
       contenido. `visibility: hidden` lo saca del tabulador Y del lector de
       pantalla. El retardo de 0s al final hace que no desaparezca de golpe
       mientras el panel se desliza hacia fuera. */
    visibility: hidden;
    transform: translateX(100%);
    transition: transform var(--dur-soft) var(--ease-out), opacity var(--dur-instant) var(--ease-out), visibility 0s linear var(--dur-soft);
  }

  .nav.is-open .nav-links {
    /* Force override — la regla base de .nav-links (desktop) gana la
       cascada en algunos Chrome pese a menor especificidad · !important
       lo mantiene fiable. */
    transform: translateX(0) !important;
    opacity: 1 !important;
    pointer-events: auto !important;
    visibility: visible !important;
    transition-delay: 0s;              /* al abrir aparece ya, sin esperar */
  }

  .nav-links .nav-link {
    padding: 14px 8px;
    font-size: var(--text-body-md);
    line-height: 1.4;
  }

  .nav-links .nav-link.active::after {
    display: none;  /* underline doesn't make sense stacked */
  }
  /* En el menú móvil, sin subrayado, la página actual va en amarillo. */
  .nav-links .nav-link.active { color: var(--color-action-default); }

  /* Mobile: the submenu is ALWAYS expanded — no toggle, no confusion
     between "go to Academy" vs "open Academy". Every sub-item is a
     first-class link. The parent link still works ("Academy" goes to
     the index page, "Guides" goes directly to Guides). */
  .nav-item,
  .nav-item-has-sub {
    display: block;
    position: static;
  }

  .nav-item-has-sub > .nav-link {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  /* Chevron de colapso (QA #69) · en móvil el grupo (Experiences,
     Academy) es un accordion: el chevron indica el estado y rota como
     en Dolomites. Abierto → apunta ARRIBA (180°); plegado (el estado
     de entrada, salvo los grupos que el usuario dejó abiertos: script.js
     los recuerda) → apunta abajo (0°, "hay más, toca para abrir").
     GATED tras `.js-nav-collapse` (clase que añade script.js): sin el
     JS del toggle el chevron sería una flecha muerta — p.ej. la página
     de Dolomites tiene los grupos pero carga map.js, no script.js, así
     que ahí el caret sigue oculto (comportamiento previo). */
  .nav-item-has-sub .nav-caret {
    display: none;
  }
  .js-nav-collapse .nav-item-has-sub .nav-caret {
    display: block;
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    color: var(--content-text-default-weak);
    transform: rotate(180deg);
    transition: transform var(--dur-instant) var(--ease-out);
  }
  .js-nav-collapse .nav-item-has-sub.is-collapsed .nav-caret {
    transform: rotate(0deg);
  }

  .nav-sub {
    position: static;
    transform: none;
    opacity: 1;
    pointer-events: auto;
    min-width: 0;
    /* Más sangría · las subsecciones cuelgan claramente del padre (antes 20px).
       El texto del sub-item queda a ~40px del borde del panel vs ~8px del
       padre → jerarquía inequívoca sin necesidad de líneas divisorias. */
    padding: 0 0 8px 32px;
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    gap: 0;
    margin-top: -4px;
    display: flex;
    flex-direction: column;
    /* Colapsable (QA #69) · plegado de entrada (lo pone script.js); is-collapsed lo cierra.
       max-height generoso (cabe holgado el grupo más largo) → 0. */
    max-height: 320px;
    overflow: hidden;
    transition: max-height var(--dur-soft) var(--ease-out),
                opacity var(--dur-soft) var(--ease-out);
  }
  .nav-item-has-sub.is-collapsed .nav-sub {
    max-height: 0;
    opacity: 0;
    margin-top: 0;
    padding-bottom: 0;
    /* Un grupo PLEGADO no se tabula (a11y, oct 2026): sin esto, al tabular
       dentro del panel el foco entraba en enlaces plegados e invisibles.
       Aquí sí podemos ocultarlo del todo porque el grupo se abre con su
       flecha, no al recibir foco (en escritorio SÍ se abre al recibir foco,
       por eso allí no se oculta — se quedaría inalcanzable con el teclado). */
    visibility: hidden;
    transition: max-height var(--dur-soft) var(--ease-out),
                opacity var(--dur-soft) var(--ease-out),
                visibility 0s linear var(--dur-soft);
  }

  .nav-sub-link {
    padding: 10px 8px;
  }

  .nav-sub-title {
    /* Smaller than top-level nav-link (17px) — stays clearly subordinate. */
    font-size: var(--text-body-sm);
    font-weight: 400;
    color: var(--white-a78);
  }

  /* Description is desktop-only — mobile hides it to avoid making
     sub-items look more important than top-level parents. */
  .nav-sub-desc {
    display: none;
  }

  /* Keep nav-lang + search inline in the bar, just hide the big links.
     Position nav-actions right, hamburger at the very end. */
  .nav-actions {
    gap: 1rem;
  }
}

@media (max-width: 480px) {
  .nav-lang {
    display: none;  /* save horizontal room — available inside mobile panel if needed */
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav-links,
  .nav-toggle-bar {
    transition: none !important;
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   FOOTER
   ════════════════════════════════════════════════════════════════════════════ */

.footer-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding: 2rem 0 2.5rem;
  border-bottom: 1px solid var(--white-a08);
  margin-bottom: 2.5rem;
  flex-wrap: wrap;
}

.footer-socials-block {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.footer-socials-label {
  margin: 0;
  font-size: var(--text-body-xs);
  font-weight: 600;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--white-a78);
}

.footer-socials {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: -10px;
}

.footer-social {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-s);
  color: var(--white-a65);
  transition: background var(--dur-instant) var(--ease-out),
    color var(--dur-instant) var(--ease-out),
    transform var(--dur-instant) var(--ease-out);
}

.footer-social[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 5px 10px;
  background: var(--bg-neutral-default);
  border: 1px solid var(--white-a08);
  border-radius: var(--radius-s);
  color: var(--gray-white);
  font-size: var(--text-body-xs);
  font-weight: 500;
  letter-spacing: 0.2px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-instant) var(--ease-out), transform var(--dur-instant) var(--ease-out);
  box-shadow: 0 4px 12px var(--black-a40);
  z-index: var(--z-overlay);
}

.footer-social:hover::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.footer-social:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.footer-social svg {
  width: 20px;
  height: 20px;
  display: block;
}

.footer-social:hover {
  background: var(--white-a08);
  color: var(--gray-white);
}

.footer-social:focus-visible {
  background: var(--white-a08);
  color: var(--gray-white);
  outline: 2px solid var(--content-link-default);
  outline-offset: 2px;
}

.footer {
  /* Deepest band of the page — solid `--bg-bold` (= --g-black) anchors
     the bottom edge against the slightly-lifted final CTA gradient. */
  background: var(--bg-bold);
  color: var(--white-a78);
  padding: 3.5rem 1.5rem 0;
}

.footer-inner {
  max-width: var(--max-wide);
  margin: 0 auto;
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 2rem;
  margin-bottom: 2.5rem;
}

.footer-col h5 {
  font-size: var(--text-body-xs);
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--content-text-default);
  margin-bottom: 1.1rem;
}

.footer-col ul {
  list-style: none;
}

.footer-col li {
  margin-bottom: 0.55rem;
}

.footer-col a {
  /* `inline-block` para que el aro de foco salga en UNA caja. Como enlace
     corriente (en línea), un texto que ocupa dos renglones se marca con una
     figura escalonada —Irene, 15-10-2026: «un cuadrado con muchas esquinas»—
     que además se pega a las letras. En bloque en línea el texto sigue
     partiéndose igual, pero la figura es un rectángulo limpio. */
  display: inline-block;
  border-radius: var(--radius-s);
  color: var(--white-a78);
  font-size: var(--text-body-sm);
  font-weight: 400;
  transition: color var(--dur-instant);
}
/* Y con aire: a 2px el aro tocaba las letras de al lado. */
.footer-col a:focus-visible { outline-offset: 4px; }

.footer-col a:hover {
  color: var(--content-link-default);   /* amarillo · acento interactivo del DS */
}

.footer-col a:active {
  color: var(--yellow-6);               /* clic/pressed · amarillo más profundo (= pressed de los botones) */
}

/* Item de footer cuya página aún no existe: inerte + atenuado + badge "Soon"
   (mismo criterio que la nav). Es un <span>, no un <a> → nunca es enlace muerto. */
.footer-col .footer-soon {
  display: inline-flex;
  align-items: baseline;
  /* LEGIBILIDAD (oct 2026) · estaba al 40% = 3,66:1 sobre el negro, por debajo
     del mínimo que el propio sistema se exige para texto secundario (7:1).
     Al 60% da 7,37:1 y sigue leyéndose claramente más apagado que los enlaces
     de al lado (78%), que es lo que tiene que comunicar: «esto aún no está». */
  color: color-mix(in srgb, var(--gray-white) 60%, transparent);
  font-size: var(--text-body-sm);
  font-weight: 400;
  pointer-events: none;
  cursor: default;
}

.footer-bottom {
  border-top: 1px solid var(--white-a08);
  padding: 1.5rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  background: var(--bg-bold);   /* MISMO fondo que el footer (antes --bg-neutral-bold → franja más clara) */
  margin: 0 -1.5rem;
}

.footer-bottom span {
  font-size: var(--text-body-xs);
  color: var(--white-a78);
}

.footer-bottom a {
  font-size: var(--text-body-xs);
  color: var(--white-a78);
}

@media (prefers-reduced-motion: reduce) {
  .footer-social[data-tooltip]::after { transition: opacity var(--dur-instant); }
}

@media (max-width: 720px) {
  .footer-top {
    justify-content: center;
    text-align: center;
    flex-direction: column;
    gap: 2rem;
  }
  .footer-socials-block { align-items: center; }
  .footer-socials { justify-content: center; margin-left: 0; }
}

/* ════════════════════════════════════════════════════════════════════════════
   HERO HELPERS — shared across all landing heroes
   ════════════════════════════════════════════════════════════════════════════ */

/* `.hero-scroll-hint` — small "scroll for more" affordance
   Used at the bottom of any hero or first-viewport zone (home hero,
   premium-pricing __hero, future landings). Component lives entirely
   here so every consumer renders identical typography + animation. */
.hero-scroll-hint {
  position: absolute;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  color: var(--white-a65);
  text-transform: uppercase;
  animation:
    scrollHintFade 1.5s var(--ease-out) var(--dur-glide) both,
    scrollHintPulse 2.4s var(--ease-out) 2.4s infinite,
    scrollHintBounce 2.6s var(--ease-soft) 2.4s infinite;
  pointer-events: none;
}

/* The label inside — mono firma técnica (no editorial), small.
   These overrides used to live in `mockup/style.css` (home-specific);
   promoted to the component 2026-05 so every page gets the same font. */
.hero-scroll-hint span {
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);     /* 10px */
  font-weight: 400;
  letter-spacing: 0.3em;
  font-feature-settings: 'ss01' off;
}

.hero-scroll-hint svg {
  width: 20px;
  height: 20px;
}

/* Mobile · subir el hint para que su rebote no pise el photo-credit
   (que se ancla abajo). */
@media (max-width: 720px) {
  .hero-scroll-hint { bottom: 3.75rem; }
}

.hero-scroll-hint.hidden {
  opacity: 0;
  transition: opacity var(--dur-soft) var(--ease-out);
  animation: none;
}

/* ════════════════════════════════════════════════════════════════════════════
   CAPTURED PLANS PATTERN — shared before/after carousel used by home's
   Captured Plans section and Experiences' True Stories.
   ════════════════════════════════════════════════════════════════════════════ */

/* ════════════════════════════════════════════════════════════════════════════
   BEFORE/AFTER SLIDER
   ════════════════════════════════════════════════════════════════════════════ */
.ba-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.ba-layer-shot {
  z-index: 1;
}

.ba-layer-plan {
  z-index: 2;
  clip-path: inset(0 50% 0 0);
  transition: clip-path 0s;
}

.ba-layer img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  position: absolute;
  inset: 0;
}

.ba-layer-plan::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, color-mix(in srgb, var(--yellow-5) 8%, transparent), rgba(14, 21, 35, 0.95) 50%),
    linear-gradient(45deg, #0d1117, #161b22, #1f2a3a);
  background-size: cover;
}

.ba-layer-plan .ba-content {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* Placeholder hasta que llegue la foto real del case — el 30% de alpha
     NO pasa AA como texto; no llevar a producción tal cual. */
  color: color-mix(in srgb, var(--yellow-5) 30%, transparent);
  font-size: var(--text-body-sm);
  letter-spacing: 3px;
  text-transform: uppercase;
  font-weight: 600;
  z-index: 1;
}

.ba-layer-shot::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, #1a1a2e, #16213e, #0f3460);
}

.ba-layer-shot .ba-content {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: rgba(254, 217, 37, 0.3);
  font-size: var(--text-body-sm);
  letter-spacing: 3px;
  text-transform: uppercase;
  font-weight: 600;
}

.ba-label {
  position: absolute;
  top: 16px;
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  font-weight: 600;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: white;
  background: var(--black-a55);
  backdrop-filter: var(--glass-filter-strong);
  -webkit-backdrop-filter: var(--glass-filter-strong);
  padding: 6px 12px;
  border-radius: var(--radius-full);
  z-index: 4;
  pointer-events: none;
}

.ba-label-plan {
  left: 16px;
  color: var(--white-a90);
  border: 1px solid var(--white-a12);
}

.ba-label-shot {
  right: 16px;
  color: var(--white-a90);
  border: 1px solid var(--white-a12);
}

.ba-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 48px;
  margin-left: -24px;
  z-index: 3;
  cursor: ew-resize;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.ba-handle::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background: var(--gray-white);
  box-shadow: 0 0 1px var(--black-a40);
}

.ba-handle-knob {
  width: 28px;
  height: 44px;
  border-radius: var(--radius-full);
  background: var(--gray-white);
  display: grid;
  place-items: center;
  box-shadow: 0 1px 2px var(--black-a20),
    0 6px 18px var(--black-a40);
  pointer-events: auto;
  cursor: ew-resize;
  /* El knob reclama el gesto táctil (drag horizontal) · sin esto el
     navegador se lleva el touch para hacer scroll y el slider no se
     puede arrastrar en mobile. Solo afecta al knob (target pequeño),
     así que el swipe del carrusel en el resto del slider sigue libre. */
  touch-action: none;
  transition: transform var(--dur-instant) var(--ease-soft),
    background var(--dur-instant) ease;
  position: relative;
  z-index: 1;
  border: 0;
}

.ba-slider:hover .ba-handle-knob {
  background: var(--gray-white);
  transform: scale(1.06);
}

.ba-slider.dragging .ba-handle-knob {
  transform: scale(1.1);
  box-shadow: 0 2px 4px var(--black-a20),
    0 10px 24px var(--black-a40);
}

.ba-handle-knob:focus-visible {
  outline: 2px solid var(--white-a90);
  outline-offset: 3px;
}

.ba-handle-knob svg {
  width: 14px;
  height: 14px;
  color: #1a1a1c;
  opacity: 0.85;
}

/* (CSS knob-bounce removed — the JS nudgeHint now drives both the
   handle position AND the clip-path together so the line moves
   with the knob from the very first hint.) */

.ba-slider {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  /* UNA FOTO, NO UNA BURBUJA (Irene, 16-10-2026). Estaba a `--radius-s` (8),
     que en el sistema es el radio de las piezas pequeñas: las burbujas de
     ayuda, el horario del WhatsApp, los bloques de código. Esto es una foto
     grande, y las fotos del sistema van a 16 (`.photo-tile`, `.overlay-card`)
     o a 24 (`.card`). A 16, que es donde están sus hermanas. */
  border-radius: var(--radius-m);
  overflow: hidden;
  user-select: none;
  touch-action: pan-y;
  background: linear-gradient(160deg, #0d1117, #161b22, #1f2a3a);
  cursor: ew-resize;
  /* LA ELEVACIÓN, DEL CATÁLOGO. Llevaba `--shadow-md`, un token que existía
     en `:root` pero NO en el catálogo documentado (tres elevaciones: soft,
     strong, stage-up) y que usaba UNA sola pieza en todo el proyecto: esta.
     Una elevación con un único inquilino no es una elevación, es un número
     suelto con nombre. Pasa a `--shadow-soft`, la de las cards. */
  box-shadow: var(--shadow-soft), inset 0 1px 0 var(--white-a08);
}
/* Mobile · pequeño margen lateral (12px) para que el slider no vaya a
   sangre contra los bordes del viewport. Componente base → lo heredan
   todas las instancias del before/after. */
@media (max-width: 720px) {
  .ba-slider {
    width: calc(100% - 24px);
    margin-inline: 12px;
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   CAPTURED PLANS CAROUSEL
   ════════════════════════════════════════════════════════════════════════════ */

.plan-case-title {
  font-size: var(--text-heading-md);
  line-height: var(--lh-heading);
  font-weight: 500;
  margin-bottom: 0.5rem;
}

.plan-case-meta {
  font-size: var(--text-body-sm);
  color: var(--content-text-default-weak);
  margin-bottom: 1.5rem;
  letter-spacing: 1px;
}

/* (.plan-author absorbed into .author-credit__name in common.css) */

.plans-carousel {
  max-width: var(--max-wide);
  margin: 0 auto;
  position: relative;
}

.plans-viewport {
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.plans-track-wrapper {
  overflow: hidden;
  border-radius: var(--radius-m);
  flex: 1;
  min-width: 0;
}

@media (min-width: 720px) {
  .plan-quote { font-size: var(--text-body-lg); line-height: var(--lh-body); }
}

@media (max-width: 720px) {
  .plans-viewport {
    gap: 0.5rem;
  }
  
  
}

/* ════════════════════════════════════════════════════════════════════════════
   CAPTURED PLANS (STORY SLIDES)
   ════════════════════════════════════════════════════════════════════════════ */

.captured-plans .reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

.captured-plans .plans-track {
  display: flex;
  transition: transform var(--dur-soft) var(--ease-out);
  will-change: transform;
}

.captured-plans .plan-slide {
  /* Each slide is exactly the track-wrapper width so transform
     translateX(-N * 100%) lands cleanly without leaking the next
     case on the right at wide breakpoints. The whole carousel is
     already constrained by .plans-carousel { max-width }. */
  flex: 0 0 100%;
  min-width: 0;
  padding: 0 1rem;
  display: grid;
  grid-template-rows: auto auto;
  gap: 2.25rem;
  margin: 0 auto;
}

/* Columna del caso · UNA sola anchura compartida por foto e historia
   (regla 2026-07): el slider cede ancho (y por aspect-ratio, alto) en
   viewports bajos para que foto + texto quepan en pantalla con ~16px
   de aire arriba y abajo. Presupuesto: 100svh − ~300px (16×2 de aire
   + gap 36 + story más alta ~225px). Suelo de 560px para que la foto
   nunca quede tímida; techo 100% (móvil y pantallas altas, como antes).
   El texto usa la MISMA variable — sus columnas no se separan más que
   el ancho de la foto. */
.captured-plans .plan-slide {
  --case-col: min(100%, max(560px, calc((100vh - 300px) * 16 / 9)));
  --case-col: min(100%, max(560px, calc((100svh - 300px) * 16 / 9)));
}

.captured-plans .plan-slide .ba-slider {
  width: var(--case-col);
  max-width: none;
  margin-inline: auto;
  /* Frame the slider: a thin outer stroke + a strong inset corner vignette
     so the photo reads as its own object. */
  border: 1px solid var(--white-a08);
  box-shadow:
    inset 0 0 0 1px var(--white-a08),
    inset 0 0 140px 20px var(--black-a55),
    0 0 0 1px var(--black-a40);
}

/* Story block: 2-col grid — col 1 stacks the head (eyebrow+title+meta) and
   the attribution (anchored bottom); col 2 holds the pull quote spanning
   both rows so it visually balances the column on the left. This avoids
   the previous overlap where the attribution sat in row 1 col 2 and the
   quote in row 2 col 2 (the layout broke when titles wrapped). */
/* Plan story = 2-col grid: left col is .plan-story-head (a flex column
   that holds eyebrow + title + meta + attribution as siblings — true
   "auto layout" with one consistent gap between every line); right
   col is the pull quote. */
.captured-plans .plan-story {
  /* Mismo carril que la foto (--case-col) · las dos columnas del
     relato quedan alineadas con los bordes del slider, no con el
     contenedor de sección (2026-07). */
  width: var(--case-col, 100%);
  margin: 0 auto;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: 3rem;
  /* Both columns share the same vertical centre — when the quote is
     longer or shorter than the head, neither column is anchored to
     the top of the row. */
  align-items: center;
  text-align: left;
}

.captured-plans .plan-story .plan-story-head {
  grid-column: 1;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: flex-start;
}

.captured-plans .plan-story .plan-case-eyebrow {
  margin-bottom: 0;
  font-size: var(--text-body-md);
}

.captured-plans .plan-story .plan-case-title {
  font-size: clamp(var(--text-heading-sm), 3vw, var(--text-heading-md));
  line-height: 1.2;
  margin-bottom: 0;
}

.captured-plans .plan-story .plan-case-meta {
  margin-top: 0;
  margin-bottom: 0;
  font-size: var(--text-body-sm);
}

/* The author-credit sits in the same flex column as the head meta
   lines and inherits its 8px gap. Extra 8px on top so the total gap
   between the date and the author block is 16px — a small breath
   that separates "the moment" (case + title + date) from "the maker"
   (avatar + name + tier). */
.captured-plans .plan-story .author-credit {
  margin: 0.5rem 0 0;
}

.captured-plans .plan-story .plan-quote {
  margin: 0;
  padding: 0;
  border-left: 0;
  max-width: none;
  text-align: left;
  grid-column: 2;
  align-self: center;
  /* Cita: peso 300 (pieza de display), pero con el interlineado de
     lectura (--lh-body): a 1,2 se leía demasiado apretada (QA 559). */
  font-size: var(--text-editorial-body);
  line-height: var(--lh-body);
  font-weight: 300;
  color: var(--content-text-subtitle);
}

/* Quote-mark SVG component — drop-in glyph that any quote/blockquote
   can use. The viewBox is tightened to 7 7 13 9 (matches the visible
   path bounds) so the SVG bounding box has zero extra padding around
   the glyph: the left edge aligns flush with the quote paragraph. */
.quote-mark {
  display: block;
  width: 28px;
  /* 13:9 aspect → height = 28 * 9 / 13 ≈ 19.4px */
  height: 19.4px;
  color: var(--yellow-5);
  margin-bottom: 0.75rem;
}

/* Position-only override — visuals come from the shared .pager
   component in common.css. */
.captured-plans .plans-pager {
  margin-top: clamp(20px, 3vw, 36px);
}

.plan-case-eyebrow {
  color: var(--content-text-subtitle);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-body-sm);
  letter-spacing: 2px;
  text-transform: uppercase;
  margin-bottom: 1rem;
  display: block;
}

/* (.plan-tier was a duplicate of the unified .badge above — its second
   definition was retired 2026-04. The legacy alias near .badge keeps
   existing markup working until the HTML migrates to .badge.) */

.plan-quote {
  font-size: var(--text-body-md);
  line-height: var(--lh-body);
  font-weight: 300;
  font-style: normal;
  color: var(--content-text-subtitle);
  margin-bottom: 1.5rem;
  padding-left: 1.25rem;
  border-left: 3px solid var(--content-link-default);
}

/* (.plan-attribution / .plan-attribution-text / .plan-avatar absorbed
   into .author-credit / .author-credit__body / .author-credit__avatar.) */

.captured-plans {
  padding: 100px 0;
}

/* El relato se apila a 1-col SOLO en móvil (≤720). En tablet (721-1024, p.ej.
   iPad en vertical) se mantienen las 2 columnas (case-head | quote) como en
   landscape/desktop — la foto va arriba y el texto a 2-col debajo. */
@media (max-width: 720px) {
  .captured-plans .plan-story {
    grid-template-columns: 1fr;
    row-gap: 1rem;
    text-align: left;
  }
  .captured-plans .plan-story .plan-story-head {
    grid-column: 1;
  }
  .captured-plans .plan-story .plan-quote {
    grid-column: 1;
    margin-top: 0.25rem;
  }
}

/* ── Mobile: native horizontal scroll-snap, no arrows ───────────────────
   The transform-based slider is replaced by native overflow-scroll. The
   arrows would be cramped at this size and add noise — we hide them and
   let the dots act as both indicator and quick jump. JS detects this
   breakpoint and switches modes. */
@media (max-width: 720px) {
  .captured-plans .plans-viewport {
    gap: 0;
    align-items: stretch;
  }
  
  .captured-plans .plans-track-wrapper {
    border-radius: 0;
    overflow-x: auto;
    overflow-y: visible;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Escape el padding lateral del .container (var(--gutter)) para que las
       IMÁGENES (.ba-slider) puedan ir full-bleed. El TEXTO de cada slide
       (.plan-story) recupera el gutter con su propio padding. */
    margin: 0 calc(-1 * var(--gutter));
    padding: 0;
  }
  .captured-plans .plans-track-wrapper::-webkit-scrollbar {
    display: none;
  }
  .captured-plans .plans-track {
    transition: none;
    transform: none !important;
    will-change: auto;
  }
  .captured-plans .plan-slide {
    scroll-snap-align: center;
    scroll-snap-stop: always;
    /* Sin padding horizontal aquí · la IMAGEN va edge-to-edge.
       El TEXTO recupera 32px abajo con su propio padding. */
    padding: 0;
    /* Mobile: tighter gap; dots return to normal flow below the story. */
    gap: 1.75rem;
  }
  /* Slider · margen lateral = gutter (antes iba full-bleed / 12px) · esta
     regla, más específica (0,3,0), pisa a `.captured-plans .plan-slide .ba-slider`
     que fijaba width:var(--case-col) + margin-inline:auto (= 100% en móvil).
     Sin esto el margen del componente base no llega a la home. */
  .captured-plans .plan-slide .ba-slider {
    width: calc(100% - 2 * var(--gutter));
    margin-inline: var(--gutter);
    /* Móvil · de 16/9 (muy apaisado, se veía pequeño en vertical) a CUADRADO ·
       aprovecha mejor la pantalla del teléfono. Para "más vertical" aún:
       4/5 o 3/4. */
    aspect-ratio: 1 / 1;
  }
  /* Captured Plans · iteración móvil (2026-07):
     · alineado a la IZQUIERDA (coincide con desktop · centrado se veía
       flotante y costaba leer un bloque caso/título/fecha/quote/autor).
     · orden caso → título → fecha → QUOTE → AUTOR · se logra con
       display:contents en el head (promueve eyebrow/título/meta/autor a
       hermanos de la quote) + `order` · SOLO móvil, DOM intacto → desktop
       no cambia.
     · quote DESACOPLADA del token editorial y un paso más pequeña (había
       crecido al subir --text-editorial-body en móvil; una cita no es un
       párrafo editorial).
     · nombre de autor a SANS (el mono topa en 16px) para que tenga presencia. */
  .captured-plans .plan-slide .plan-story {
    /* El texto ocupa EXACTAMENTE el ancho de la imagen del slider: la foto
       (.ba-slider) va al gutter (margin-inline: var(--gutter)), así que el
       texto usa el mismo padding y sus bordes coinciden con los de la foto. */
    padding-left: var(--gutter);
    padding-right: var(--gutter);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
    gap: 0.5rem;
  }
  .captured-plans .plan-slide .plan-story .plan-story-head {
    display: contents;
  }
  .captured-plans .plan-slide .plan-story .plan-case-eyebrow { order: 1; }
  .captured-plans .plan-slide .plan-story .plan-case-title   { order: 2; }
  .captured-plans .plan-slide .plan-story .plan-case-meta    { order: 3; }
  .captured-plans .plan-slide .plan-story .plan-quote {
    order: 4;
    text-align: left;
    margin-top: 0.75rem;
    font-size: var(--text-body-md);
  }
  .captured-plans .plan-slide .plan-story .quote-mark {
    margin-left: 0;
    margin-right: 0;
  }
  .captured-plans .plan-slide .plan-story .author-credit {
    order: 5;
    justify-content: flex-start;
    margin-top: 0.75rem;
  }
  
}

/* ════════════════════════════════════════════════════════════════════════════
   SR-ONLY — visually-hidden utility for screen-reader-only labels/text.
   Accessibility staple (form labels, aria headings). Promovido al DS
   2026-07: antes solo vivía en missions/style.css; ahora cualquier página
   con `<label class="sr-only">` (Coming Soon, Missions) lo hereda de aquí.
   ════════════════════════════════════════════════════════════════════════════ */
.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;
}

/* ════════════════════════════════════════════════════════════════════════════
   FORM INPUTS — autofill reset (dark surfaces)
   Chrome/Safari repintan el campo con fondo claro + texto oscuro al
   autocompletar (sugerencia de email, etc.), rompiendo los inputs oscuros del
   sistema. Mantiene el campo TRANSPARENTE (delay ~infinito del background de
   autofill) y el texto/caret en blanco. Fix a nivel DS · si se crea una nueva
   clase de input oscuro, añadirla a esta lista — NO re-parchear por página.
   ════════════════════════════════════════════════════════════════════════════ */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus,
.input:-webkit-autofill:active,
.mis-input:-webkit-autofill,
.mis-input:-webkit-autofill:hover,
.mis-input:-webkit-autofill:focus,
.mis-input:-webkit-autofill:active {
  -webkit-text-fill-color: var(--content-text-default);
  caret-color: var(--content-text-default);
  transition: background-color 9999s ease-in-out 0s;
  /* Tipografía del valor ya CONFIRMADO (al elegir la sugerencia). Nota: el
     PREVIEW del autofill (al hacer hover sobre una sugerencia, antes de
     elegirla) lo pinta Chrome en una capa propia con su fuente por defecto y
     NO es alcanzable por CSS — ese "salto" de fuente en el hover es una
     limitación del navegador, no del DS. Inputs oscuros: sans · body-sm · 300. */
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 300;
}
/* El campo claro (.input--light) conserva su texto oscuro al autocompletar. */
.input--light:-webkit-autofill,
.input--light:-webkit-autofill:hover,
.input--light:-webkit-autofill:focus,
.input--light:-webkit-autofill:active {
  -webkit-text-fill-color: var(--content-text-on-light-default);
  caret-color: var(--content-text-on-light-default);
}

/* ════════════════════════════════════════════════════════════════════════════
   SKIP LINK — keyboard-only "Skip to content" jumping past the nav.
   Visually hidden until focused.
   ════════════════════════════════════════════════════════════════════════════ */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: var(--z-toast);
  padding: 0.75rem 1.25rem;
  background: var(--yellow-5);
  color: var(--gray-9);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 600;
  text-decoration: none;
  border-radius: 0 0 var(--radius-s) 0;
  transform: translateY(-110%);
  /* INVISIBLE, no solo subido (Irene, 2026-10-19: «a veces aparece en la parte
     superior izquierda»). Subido por encima del borde, el rebote de iOS al
     tirar de la página hacia abajo lo dejaba ver. Solo aparece con el foco del
     teclado, que es para quien existe. */
  opacity: 0;
  pointer-events: none;
  transition: transform var(--dur-instant) var(--ease-out), opacity var(--dur-instant) var(--ease-out);
}
.skip-link:focus-visible {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
  outline: none;
}

/* ════════════════════════════════════════════════════════════════════════════
   CTA ROW — shared horizontal row of CTAs (wraps on mobile)
   Used by hero, App / Premium pitch, The Loop, Experiences, final CTA.
   ════════════════════════════════════════════════════════════════════════════ */
.cta-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.cta-row--start { justify-content: flex-start; }

/* Móvil (≤720): una PAREJA de botones va siempre apilada, uno encima del
   otro y centrada; cada botón en «hug», al ancho de su texto (QA 567-573).
   No se igualan ni se estiran: a todo el ancho quedaban demasiado grandes. */
@media (max-width: 720px) {
  .cta-row:has(> :nth-child(2)) {
    flex-direction: column;
    align-items: center;
    width: fit-content;
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
  }
  /* `.product-copy .pitch-cta` fija su margen: sin esto la pareja de la
     home (App, Premium) quedaba pegada a la izquierda. */
  .product-copy .pitch-cta:has(> :nth-child(2)) { margin-left: auto; margin-right: auto; }
}

/* ════════════════════════════════════════════════════════════════════════════
   AUTHOR CREDIT — shared "avatar + name [+ tier badge]" component
   Used by Captured Plans (with .plan-tier badge) and Experiences
   (just the name). 40px grayscale avatar, mono 16px caption, 1rem gap.
   ════════════════════════════════════════════════════════════════════════════ */
.author-credit {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  /* Avatar height drives line-height so the text vertically matches
     the avatar mass when on a single line. */
  --_avatar-h: var(--avatar-md);
  line-height: var(--_avatar-h);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xl);
  font-weight: 400;
  color: var(--gray-white);
}
.author-credit__avatar {
  width: var(--avatar-md);
  height: var(--avatar-md);
  border-radius: var(--radius-circle);
  object-fit: cover;
  filter: grayscale(1) contrast(1.05) brightness(0.9);
  border: 1px solid var(--white-a20);
  flex-shrink: 0;
}
/* Body wraps name + optional tier badge so a long badge can drop to a
   second line, anchored flush-left under the name. */
.author-credit__body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem 0.85rem;
  line-height: inherit;
  min-height: var(--_avatar-h);
}
.author-credit__name { color: var(--gray-white); }
/* Firma enlazada a la ficha de persona (/team/{persona}/) · `<a class="author-credit">`:
   la firma entera es el enlace (el avatar da los 40 de alto del objetivo táctil).
   El nombre va en blanco y pasa a amarillo con ratón, sin subrayado.
   Nunca dentro de una card que ya es enlace. */
a.author-credit { color: var(--gray-white); text-decoration: none; }
a.author-credit .author-credit__name { transition: color var(--dur-instant) var(--ease-out); }
a.author-credit:focus-visible { border-radius: var(--radius-s); }
@media (hover: hover) {
  a.author-credit:hover { color: var(--gray-white); }
  a.author-credit:hover .author-credit__name { color: var(--color-action-default); }
}

/* ════════════════════════════════════════════════════════════════════════════
   APP ICON — iOS-style squircle + glass highlight
   Reusable "app icon" chrome: rounded-square mask (radius-l ≈ iOS
   squircle at this scale), strong shadow (home-screen elevation), and
   an elliptical specular highlight (::after) that simulates the glass
   reflection of classic iOS icons.

   Usage:
     <span class="app-icon" style="width: clamp(64px, 6vw, 84px)">
       <img src="..." alt="">
     </span>

   Size and margins are set by the instance (page CSS or inline). This
   class only provides the chrome. Used in: app hero, app pricing card,
   premium apps-compare. Promoted to DS in 2026-05 (was duplicated in
   3 page CSS files: `.app-hero__app-icon`, `.app-pricing__app-icon`,
   `.apps-compare__icon-wrap`).
   ════════════════════════════════════════════════════════════════════════════ */
.app-icon {
  position: relative;
  display: inline-block;
  border-radius: var(--radius-l);
  overflow: hidden;
  box-shadow: var(--shadow-strong);
}

.app-icon::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 130% 65% at 50% -8%,
    var(--white-a50) 0%,
    var(--white-a08) 45%,
    rgba(255, 255, 255, 0) 70%
  );
  pointer-events: none;
  border-radius: inherit;
}

.app-icon > img {
  display: block;
  width: 100%;
  height: auto;
}

/* ════════════════════════════════════════════════════════════════════════════
   PLAN-CARD — la tarjeta de precio (QA 861, 2026-10-23 · antes vivía en
   premium-pricing y App tenía su copia `.app-pricing__card`).
   Una pieza, dos variantes por contenido, mismas medidas:
     · Suscripción (Pricing, Premium) → `__cta` (un botón a todo el ancho).
     · App → `__icon` (`.app-icon`) arriba y `__stores` con los `.btn-store`.
   Piezas opcionales: `__head` (nombre + `__tagline`, que lleva `.pitch-body`),
   `__price-note`, `__fineprint`. `--featured` = filete amarillo (plan
   recomendado). Los estados de plazas (abierto/lleno/reabierto) son de la
   página de Pricing y viven en su hoja.
   ════════════════════════════════════════════════════════════════════════════ */
.plan-card {
  position: relative;
  display: flex;
  flex-direction: column;
  max-width: var(--max-form);      /* 440 · el mismo ancho en todas las páginas */
  margin: 0 auto;
  padding: clamp(var(--sp-m), 4vw, var(--sp-l));
  background: var(--gradient-subtle);
  border: 1px solid var(--border-neutral-soft);
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-soft);
  text-align: center;
  transition: border-color var(--dur-instant) var(--ease-out);
}
.plan-card--featured { border-color: var(--border-secondary-accent); }
@media (hover: hover) {
  .plan-card:hover { border-color: var(--white-a20); }
  .plan-card--featured:hover { border-color: var(--color-action-soft); }
}

.plan-card__icon {
  align-self: center;
  width: clamp(64px, 6vw, 84px);
  margin-bottom: var(--sp-m);
}
.plan-card__head { margin-bottom: var(--sp-m); }
.plan-card__name {
  margin: 0 0 var(--sp-xs);
  font-family: var(--font-sans);
  font-size: var(--text-heading-md);
  font-weight: 600;
  line-height: var(--lh-heading);
  color: var(--content-text-default);
}
/* La frase bajo el nombre es un `.pitch-body`: tamaño, peso y color salen
   de ahí (QA 551). */
.plan-card__tagline { margin: 0; }

.plan-card__price {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
  margin-bottom: var(--sp-xs);
}
.plan-card__amount {
  font-family: var(--font-sans);
  font-size: var(--text-heading-lg);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--content-text-default);
}
.plan-card__per,
.plan-card__price-note,
.plan-card__fineprint {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  color: var(--content-text-default-weak);
}
.plan-card__price-note { margin: 0 0 var(--sp-m); }

.plan-card__cta { width: 100%; margin-bottom: var(--sp-default); }
.plan-card__stores {
  justify-content: center;
  flex-wrap: wrap;
  margin: 0 0 var(--sp-default);
}
.plan-card__fineprint { margin: 0; line-height: 1.5; }

/* `.btn-store` · botón de tienda (App Store / Google Play): un `.btn-ghost`
   con el logo de la tienda delante del texto. */
.btn-store { gap: var(--sp-s); }
.btn-store__icon { width: 20px; height: 20px; flex-shrink: 0; }

/* ════════════════════════════════════════════════════════════════════════════
   PAGER — shared paginator (prev arrow · counter · next arrow)
   Used by Plan Every Shot, Captured Plans and Experiences. Two size
   modifiers: --lg (56px arrows, default) and --md (44px arrows, used
   when overlaid on a photo).
   ════════════════════════════════════════════════════════════════════════════ */
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(16px, 2.5vw, 28px);
}

.pager__arrow {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-circle);
  background: var(--black-a55);
  border: 1px solid var(--white-a12);
  /* Flecha clicable = amarilla (norma, Irene 2026-10-19). */
  color: var(--color-action-default);
  display: grid;
  place-items: center;
  cursor: pointer;
  margin: 0;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: background-color var(--dur-instant) var(--ease-out),
              border-color var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
.pager__arrow:hover:not(:disabled) {
  background: var(--black-a75);
  border-color: var(--white-a20);
}
.pager__arrow:disabled {
  opacity: 0.3;
  cursor: not-allowed;
  pointer-events: none;
}
.pager__arrow svg { width: 24px; height: 24px; display: block; }

.pager__counter {
  font-family: var(--font-mono);
  font-size: var(--text-mono-lg);
  letter-spacing: 0.08em;
  color: var(--gray-white);
  display: inline-flex;
  align-items: baseline;
  gap: 0.4ch;
  min-width: 6ch;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}
.pager__sep   { color: var(--white-a32); }
.pager__total { color: var(--white-a50); }

/* --md modifier: 44px arrows for overlays inside a photo. */
.pager--md { gap: clamp(12px, 1.6vw, 20px); }
.pager--md .pager__arrow { width: 44px; height: 44px; }
.pager--md .pager__arrow svg { width: 20px; height: 20px; }
.pager--md .pager__counter { text-shadow: 0 1px 4px var(--black-a75); }

@media (max-width: 720px) {
  .pager__arrow { width: 44px; height: 44px; }
  .pager__arrow svg { width: 20px; height: 20px; }
}

/* ════════════════════════════════════════════════════════════════════════════
   PRODUCT SECTIONS — shared 2-col grid for App / Premium / The Tool
   Used by home (App, Premium) and Experiences (The Tool).
   ════════════════════════════════════════════════════════════════════════════ */

.store-price {
  text-align: center;
  font-size: var(--text-body-sm);
  font-weight: 500;
  line-height: var(--lh-heading);
  color: var(--content-text-subtitle);
}

/* ════════════════════════════════════════════════════════════════════════════
   PRODUCT SECTIONS — THE APP + PREMIUM
   ════════════════════════════════════════════════════════════════════════════ */

.product-section {
  padding: clamp(64px, 7vw, 96px) 0;
  overflow: hidden;
}

.product-grid--reverse .product-copy {
  order: 2;
}

/* ════════════════════════════════════════════════════════════════════════════
   .editorial-stack  ·  patrón canónico de cabecera editorial
   Title → Subtitle → Rule → Pitch-body. La receta tipográfica que se repite
   en hero de premium-pricing, hero de premium, sección Premium de la home,
   The Loop, statement slides… cualquier sitio donde introducimos un bloque
   con el lenguaje editorial de la marca.

   Centrado por defecto (el uso más común). Modificador `--start` para
   layouts de columna (grids 2-col). El título conserva su tamaño global
   .section-title — si una página necesita un display más grande (heroes
   tipo PhotoPills Premium · Beta), bumpea el font-size en su CSS local.

   Tamaños y pesos:
     .section-title    → globl (.section-title), weight 600
     .section-subtitle → clamp(heading-sm, 3vw, heading-md) · weight 300 · lh 1.125
     .section-rule     → 64×2px gray-6 centrado
     .pitch-body       → body-lg (22) · weight 400 · --lh-body · ≤ 640
   ════════════════════════════════════════════════════════════════════════════ */
.editorial-stack {
  max-width: 720px;
  margin: 0 auto;
  text-align: center;
}

.editorial-stack .section-title {
  margin: 0 0 1.25rem;
}

.editorial-stack .section-subtitle {
  font-size: var(--text-editorial-subtitle);
  line-height: 1.125;
  font-weight: 300;
  text-align: center;
  max-width: none;
  margin: 0 auto 2rem;
}

.editorial-stack .section-rule {
  margin: 0 auto 2rem;
}

.editorial-stack .pitch-body {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body);
  font-weight: 400;
  max-width: var(--max-article);
  text-align: center;
  margin: 0 auto;
}

/* Modificador --start: alinea a la izquierda. Para columnas de product
   grids (`.product-section--app`, `.product-section--premium`) y bloques
   editoriales con visual al lado.

   Coexiste con `.product-copy` (legacy de la home), que aplica los mismos
   overrides + un max-width de 48ch para encajar en el grid 2-col. Si vas
   a crear un bloque editorial en columna nuevo, usa `.editorial-stack
   .editorial-stack--start` para no inventar otra vez. */
.editorial-stack--start {
  text-align: left;
}
.editorial-stack--start .section-subtitle,
.editorial-stack--start .section-rule,
.editorial-stack--start .pitch-body {
  text-align: left;
  margin-left: 0;
  margin-right: 0;
}
/* TABLET (721-1024) · UN SOLO ANCHO DE TEXTO (Irene, 28-10 · QA 1022): con
   las columnas apiladas, todo bloque de texto va a la columna de lectura
   (640), centrada en la pantalla. Sin esto, las columnas con tope de 48ch
   (~470) y las de 640 se alternan y la página cambia de ancho al bajar.
   Las cabeceras de página (`.page-hero`, 760) quedan fuera. */
@media (min-width: 721px) and (max-width: 1024px), (orientation: portrait) and (min-width: 721px) and (max-width: 1199px) {
  .editorial-stack--start:not(.page-hero) {
    max-width: var(--max-article);
    margin-left: auto;
    margin-right: auto;
  }
  .photo-scatter > .photo-scatter__inner { max-width: var(--max-article); }
}

/* ════════════════════════════════════════════════════════════════════════════
   .page-hero  ·  LA CABECERA DE HERO, UNA SOLA PARA TODO EL SITIO (2026-10-16)
   Reúne lo que vivía repartido en cinco clases de título distintas
   (.hero-tagline, .mis-hero__title, .error-hero__title, .pricing-intro__title-v2,
   .section-title de App) con cuatro espaciados de letra, dos interlineados y
   tres tamaños en móvil (Irene: «si el título tiene el mismo tamaño, sería
   añadir o quitar entradilla, botones… Esto daría coherencia»).

   (Las páginas de texto NO lo usan: llevan la cabecera de contenidos sin foto,
   `.article-hero--plain`, porque sus títulos son frases largas. 2026-10-16.)
   Se monta SOBRE `.editorial-stack` (la receta obligatoria título → entradilla
   → filete → texto): `class="editorial-stack page-hero"`. El componente añade
   el ancho de cabecera, el título de hero y los huecos para lo demás. La foto,
   el fondo y las animaciones de entrada siguen siendo de cada sección.

   Piezas (todas opcionales salvo el título), en este orden:
     .eyebrow-line            etiqueta pequeña encima          (404/500)
     .page-hero__title        título · 78 escritorio, ~45 móvil (QA #33)
       + .page-hero__title--lema   dos versos con <br> (Home, Experiences)
     .section-subtitle        entradilla grande                (Missions, Premium)
     .section-rule + .pitch-body   filete y texto              (Pricing, App, 404)
     .cta-row                 botones
     form / .input-wrap       buscador                         (404)
     .page-hero__card         tarjeta · al ancho de párrafo    (Missions)
   Modificador:
     .page-hero--split        columna junto a una imagen (App): centrado en
                              su columna, con el tope de 56ch.
   ════════════════════════════════════════════════════════════════════════════ */
.page-hero {
  width: 100%;
  max-width: var(--max-header);
  margin-left: auto;
  margin-right: auto;
}
/* Columna junto a una imagen (App): el bloque se queda centrado DENTRO de su
   columna, con el tope de lectura de 56ch en vez del ancho de cabecera. */
.page-hero--split { max-width: 56ch; }
/* Título · tamaño, peso, interlineado y espaciado de DISPLAY del sistema. */
.page-hero__title {
  font-family: var(--font-sans);
  font-size: var(--text-hero-title);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
  color: var(--content-text-default);
  text-wrap: balance;
  margin: 0 0 1.25rem;
}
.page-hero__title:last-child { margin-bottom: 0; }
/* Móvil · el «punto intermedio» de hero (QA #33): ~45px a 375 y empalma con
   56 en tablet sin salto. Antes convivían 40, 45 y 56. */
@media (max-width: 720px) {
  .page-hero__title { font-size: var(--text-hero-title-sm); }
}
/* Etiqueta encima del título. */
.page-hero > .eyebrow-line { margin: 0 0 0.75rem; }
/* Del texto a lo siguiente (botones, buscador, tarjeta): un mismo aire.
   Título y entradilla ya traen su margen de la receta editorial. */
.page-hero > .pitch-body { margin-bottom: 0; max-width: 56ch; margin-left: auto; margin-right: auto; }
.page-hero > .section-subtitle:last-child { margin-bottom: 0; }
/* Botones, buscador y tarjeta dejan SIEMPRE 32 con lo que tengan encima. Los
   márgenes verticales entre hermanos se solapan (gana el mayor), así que tras
   la entradilla (que ya trae 32) no se duplica. */
.page-hero > * + .cta-row,
.page-hero > * + form,
.page-hero > * + .page-hero__card { margin-top: var(--sp-l); }
/* Lema seguido de botones: mismo aire, puesto en el título. */
.page-hero__title--lema { margin-bottom: 2rem; }
.page-hero > .cta-row { justify-content: center; }
.page-hero--split > .cta-row { justify-content: center; }
/* Tarjeta · al ancho de párrafo, no al de la cabecera (Irene, 2026-10-16). */
.page-hero__card {
  width: 100%;
  max-width: var(--max-article);
  margin-left: auto;
  margin-right: auto;
}

/* ── .photo-hero · la banda de hero con FOTO A SANGRE (2026-10-28 · QA 975) ──
   Foto o vídeo a pantalla completa + velo + `.page-hero` centrado encima.
   Mide la pantalla entera: empieza bajo la topbar fija y por eso arriba suma
   su alto (asimetría del escalón Hero, a propósito). Velo `--scrim-photo-
   strong`, el de texto centrado: `--scrim-hero` es para texto abajo y aquí no
   da AA. Piezas: `__media` (img o video) · `__scrim` · `__inner` ·
   `.photo-credit` · `__crumbs` (opcional, migas arriba). */
.photo-hero {
  position: relative;
  min-height: var(--alto-pantalla);   /* el alto que se ve, medido en el aparato (QA 1052-1053) */
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  background: var(--bg-bold);
  color: var(--content-text-default);
  padding: calc(var(--nav-height, 48px) + var(--section-pad-y-xl)) 0 var(--section-pad-y-xl);
}
.photo-hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Autoplay bloqueado (iOS en ahorro de batería): fuera el vídeo para que no
   salga el botón de play nativo; queda el fondo de la sección. */
.photo-hero__media.is-paused { visibility: hidden; }
.photo-hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--scrim-photo-strong);
}
.photo-hero__inner {
  position: relative;
  z-index: 2;
  width: 100%;
}
.photo-hero .photo-credit { right: var(--gutter); bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); z-index: 2; }   /* encima del indicador de inicio del iPad */
/* Migas · con el aire de las de un listado (`.listing`): topbar + 32→56, en
   la columna de contenido, por encima de la foto. */
.photo-hero__crumbs {
  position: absolute;
  top: calc(var(--nav-height) + env(safe-area-inset-top, 0px) + clamp(var(--sp-l), 5vw, calc(var(--sp-xl) + var(--sp-default))));
  left: 0;
  right: 0;
  z-index: 2;
}


/* ════════════════════════════════════════════════════════════════════════════
   .expedition-hero  ·  HERO DE EXPEDICIÓN (promovido 2026-10-16 desde Dolomites)
   El hero a pantalla completa de una ficha de expedición: vídeo (o foto) a
   sangre + velo oscuro + bloque centrado (edición en mono, título, fecha,
   CTA) + fila de datos (precio, duración, grupo, dificultad) pegada al
   bloque. Vivía solo en la hoja de Dolomites con el nombre `.exp-hero`,
   que además chocaba con el hero de Experiences (otro diseño): por eso
   sube con nombre propio (Irene: «el hero de expedición debería ser un
   componente y mostrarse en la guía»). La próxima expedición lo reutiliza.

   Anatomía:
     .expedition-hero.expedition-hero--video   sección 100svh
       video.expedition-hero__video            fondo (póster de respaldo)
       .expedition-hero__scrim                 velo para leer sobre el vídeo
       .expedition-hero__overlay               bloque centrado
         [.mis-badge …]                        opcional · plazas
         h1.expedition-hero__title
           .expedition-hero__title-edition     «Expedition 068» · mono
           .expedition-hero__title-main        nombre de la expedición
         p.expedition-hero__dates              fechas
         .cta-row.expedition-hero__cta-row     CTA
       dl.expedition-hero__facts               fila de datos
         .expedition-hero__fact > dt.__fact-label + dd.__fact-value
           a.expedition-hero__fact-info        opcional · enlace (i)
   El vídeo móvil ligero y el arranque en iOS los gestiona el script de la
   página (ver Dolomites).
   ════════════════════════════════════════════════════════════════════════════ */
.expedition-hero {
  background: var(--bg-bold);
}

/* CTA · aire bajo la fila de botones. */
.expedition-hero .cta-row {
  margin-bottom: clamp(8px, 1.2vw, 14px);
}

/* Hero video · full-bleed · ocupa 100svh · oculta el hero clásico
   de 2 cols si estuviera (no debería existir cuando aplicamos
   exp-hero--video). */
.expedition-hero--video {
  position: relative;
  width: 100%;
  height: var(--alto-pantalla);
  min-height: 600px;
  overflow: hidden;
  background: var(--bg-bold);
  /* Layout · el hero centra como UN GRUPO el bloque de texto (overlay)
     + los facts, con un gap controlado entre ambos. Así, al crecer la
     pantalla, los facts quedan pegados al texto y el aire sobrante se
     reparte arriba/abajo — ya no se descuelgan al fondo. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: clamp(28px, 5vh, 72px);
  /* z-index 70 · POR ENCIMA del .page-toc (z-60). El TOC siempre está
     desplegado, pero mientras el hero está en pantalla queda tapado
     por la imagen del video. Cuando el lector scrolea más allá del
     hero, el TOC emerge naturalmente porque ya no hay nada que lo
     cubra (el hero está fuera del viewport). */
  z-index: 70;
}

/* Video background · cubre todo el hero · object-fit: cover
   centra y recorta para que ningún espacio quede vacío. */
.expedition-hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* Autoplay bloqueado (p.ej. iOS Modo Poca Batería) · ocultamos el vídeo
   para que NO salga el botón de play nativo; el póster del fondo de la
   sección queda como still del hero. */
.expedition-hero__video.is-paused {
  display: none;
}

/* Scrim · el velo del catálogo para un hero con el texto centrado
   (`--scrim-photo-strong`, el de Missions). `--scrim-hero` no da AA ahí. */
.expedition-hero__scrim {
  position: absolute;
  inset: 0;
  background: var(--scrim-photo-strong);
  pointer-events: none;
}

/* Overlay · contenido del hero · centrado horizontal y vertical.
   Gap uniforme entre title → dates → CTA para garantizar que las
   distancias entre los 3 elementos sean IGUALES. */
.expedition-hero__overlay {
  position: relative;
  z-index: 2;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: clamp(24px, 3vw, 40px);
  padding:
    calc(var(--nav-height) + 32px)
    clamp(24px, 4vw, 48px)
    clamp(40px, 6vw, 80px);
  box-sizing: border-box;
}

/* Reset de márgenes de los children · el gap del overlay controla las
   distancias. */
.expedition-hero__overlay > .expedition-hero__title,
.expedition-hero__overlay > .expedition-hero__dates,
.expedition-hero__overlay > .expedition-hero__cta-row {
  margin: 0;
}

/* Título compuesto · stack vertical · edition en mono uppercase
   weak arriba + título principal sans 300 grande debajo. Todo
   en blanco sobre el video. */
.expedition-hero__title {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(12px, 1.5vw, 20px);
  margin: 0 0 clamp(32px, 4vw, 48px);
  color: var(--content-text-default);
}

/* Edition stamp · mono uppercase · color token semántico
   `--content-text-default-weak` (gray-3 sobre dark, ratio AAA). */
.expedition-hero__title-edition {
  font-family: var(--font-mono);
  font-size: var(--text-mono-lg);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--content-text-default-weak);
  line-height: 1;
}

/* Título principal · sans cinematográfico grande · weight 600
   SemiBold (regla universal del DS · CLAUDE.md §1 · todos los
   títulos y subtítulos a 600 desde 2026-06).
   Rampa heading-xl → display-lg (56 → 78px) · mismo cap que The
   Mission y The Dolomites in October pero con 6vw (vs 5vw de las
   secciones) para que el hero llegue al máximo antes y domine a
   partir de ~1024px. En pantallas anchas iguala a las secciones
   pero NO queda por debajo · "muchas palabras" no se descontrolan. */
.expedition-hero__title-main {
  font-family: var(--font-sans);
  font-size: clamp(var(--text-heading-xl), 6vw, var(--text-display-lg));
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.02em;
  max-width: 18ch;
  text-wrap: balance;
}

/* Fecha del hero · vive entre el título y el CTA · sans heading-md
   (32px), weight editorial 400, color BLANCO puro vía token
   `--content-text-default`. La sombra de texto es ad-hoc local
   (el DS no canoniza text-shadows · aceptable como recurso de
   legibilidad sobre video variable). Margin lo controla el gap
   del .expedition-hero__overlay. */
.expedition-hero__dates {
  font-family: var(--font-sans);
  font-size: var(--text-heading-md);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0;
  color: var(--content-text-default);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
  text-align: center;
}

/* CTA row · centrado · sin override · usa el .btn-primary btn-l
   del DS (62px height yellow). */
.expedition-hero__cta-row {
  margin: 0;
}

@media (max-width: 720px) {
  .expedition-hero--video {
      height: var(--alto-pantalla);
      min-height: 520px;
      /* Baja el grupo centrado (badge→CTA→facts) un poco para que no quede
         pegado a los botones sticky superiores (Contents / Book a spot)
         (Irene, ago 2026). Con justify-content:center, un padding-top extra
         desplaza el centro hacia abajo ~la mitad. Solo móvil. */
      padding-top: clamp(48px, 12vw, 88px);
      /* El póster de respaldo (solo móvil) lo pone cada página: es su
         imagen y su ruta (ver Dolomites). */
    }
}

@media (max-width: 720px) {
  /* Iteración mobile (QA #21 → #33): las fechas bajan un peldaño
       (heading-md 32 → heading-sm 24). El título baja UN peldaño de la
       categoría heading (Irene, ago 2026): antes lg↔xl con 12vw (~45-51px
       en iPhone); ahora md↔xl con 10vw → ~37px @375, ~39 @390, ~43 @430
       (≈8px menos = un escalón). El techo sigue en heading-xl y 10vw llega
       a 56px a 560px de viewport, así que empalma con el 56 de tablet sin
       salto igual que antes. */
    .expedition-hero__title-main {
      max-width: 14ch;
      font-size: clamp(var(--text-heading-md), 10vw, var(--text-heading-xl));
    }
}

@media (max-width: 720px) {
  .expedition-hero__dates {
      font-size: var(--text-heading-sm);
    }
}

@media (max-width: 720px) {
  /* CTA del hero · un peldaño menos que el step-down global de botones
       (Irene, ago 2026): btn-m (52px) → btn-s (40px / padding 10-18 /
       body-sm). Solo el hero; los CTAs de precio y cierre se quedan
       grandes (son la conversión). */
    .expedition-hero__cta-row .btn-l {
      height: 40px;
      padding: 10px 18px;
      font-size: var(--text-body-sm);
    }
}

/* ── Hero facts (v5) · grid 4-col en bottom del hero ────────────
   Location · Dates · Masters · Group. Cada item: label mono
   uppercase weak + value sans 600 white. Scrim del hero ya tiene
   gradient más fuerte en bottom para garantizar contraste sobre
   el video. */
.expedition-hero__facts {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 0;
  padding: clamp(20px, 2.5vw, 32px) clamp(24px, 4vw, 48px);
  margin: 0;
  /* Grid 4 cols con cap 880px (mismo ancho que el contenedor de texto
     editorial · cap canónico del DS). El strip absolute mantiene
     left:0/right:0; el max-width + margin auto centran el grid en
     el viewport. El scrim full-width se aplica vía ::before. */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  max-width: 880px;
  margin-left: auto;
  margin-right: auto;
  gap: clamp(20px, 2.5vw, 40px);
  color: var(--content-text-default);
  background: transparent;
}

/* Scrim full-width detrás del strip · garantiza contraste sobre
   cualquier frame del video sin reducirse al cap de 880px. */
.expedition-hero__facts::before {
  content: '';
  position: absolute;
  left: 50%;
  right: auto;
  bottom: 0;
  width: 100vw;
  height: 100%;
  transform: translateX(-50%);
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0) 0%,
    rgba(0, 0, 0, 0.45) 100%
  );
  z-index: -1;
  pointer-events: none;
}

.expedition-hero__fact {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.expedition-hero__fact-label {
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* Token semántico `--content-text-default-weak` (gray-3 sobre dark
     · AAA contrast). Sustituye al rgba blanco 65% hardcoded. */
  color: var(--content-text-default-weak);
  line-height: 1.2;
  margin: 0;
}

.expedition-hero__fact-value {
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-weight: 400;
  /* line-height 1.4 (era 1.3) para que "12 photographers / 2 masters"
     en dos líneas tenga respiro vertical legible. */
  line-height: 1.4;
  color: var(--content-text-default);
  margin: 0;
}

/* Info link inline · acompaña al valor "Light – Moderate" del fact
   Difficulty level. Círculo sutil con "i" que enlaza a #levels
   (sección Levels & Physical Demand). */
.expedition-hero__fact-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 6px;
  vertical-align: -2px;
  color: var(--yellow-5);     /* amarillo · idle */
  text-decoration: none;
  transition: transform var(--dur-instant) var(--ease-out),
              filter var(--dur-instant) var(--ease-out);
}

.expedition-hero__fact-info:hover {
  transform: scale(1.15);
  filter: brightness(1.1);
}

.expedition-hero__fact-info:focus-visible {
  outline: 2px solid var(--yellow-5);
  outline-offset: 2px;
  border-radius: var(--radius-circle);
}

/* Overlay del hero · padding-top = padding-bottom · SIMÉTRICOS para
   que el contenido (eyebrow + título + CTA) quede EXACTAMENTE
   centrado en la altura del viewport. Los facts viven en un strip
   absolute en bottom — están fuera del flow del overlay, así que no
   afectan al centrado óptico. El padding interior nav-height en
   ambos lados es solo guardarraíl en viewports muy bajos. */
.expedition-hero--video .expedition-hero__overlay {
  /* height:auto (era 100%) · el overlay ya NO llena el hero; ahora es el
     flex del hero quien centra el grupo (overlay + facts). */
  height: auto;
  flex: 0 0 auto;
  padding-top: var(--nav-height);   /* holgura bajo la nav fija */
  padding-bottom: 0;                /* la distancia a los facts la da el gap del hero */
}

/* Facts · dentro del grupo centrado (ya no `absolute; bottom:0`). Mantienen
   el grid 4-col + cap 880 + centrado horizontal de la regla base. */
.expedition-hero--video .expedition-hero__facts {
  position: relative;
  left: auto;
  right: auto;
  bottom: auto;
  padding-top: 0;
  padding-bottom: 0;
  /* Legibilidad sobre el vídeo · sustituye a la banda scrim inferior
     (ya no aplica al vivir los facts a media pantalla). */
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
}

.expedition-hero--video .expedition-hero__facts::before {
  display: none;
}

@media (max-width: 720px) {
  .expedition-hero__facts {
      grid-template-columns: 1fr 1fr;
      gap: 16px 24px;
      padding: 20px 24px;
    }
}

@media (max-width: 720px) {
  .expedition-hero__fact {
      text-align: center;
    }
}

/* Scroll hint reposicionado · vive ARRIBA del strip de facts del hero
   (que ocupa el bottom del hero). El componente del DS por defecto
   se ancla en `bottom: 2rem` · aquí lo subimos para no pisar los
   datos destacados. */
.expedition-hero--video .hero-scroll-hint {
  bottom: clamp(140px, 14vw, 200px);
}


/* ════════════════════════════════════════════════════════════════════════════
   .tablist  ·  segmented control reusable
   Pill-shape track con varios items dentro · el activo destaca con fondo
   amarillo (--color-action-default). Hover: oscurece el track sin
   reemplazar el activo. Tipografía Public Sans (sans), igual que .btn —
   no usamos mono aquí porque actúa como control interactivo, no como
   metadata.

   Markup:
     <div class="tablist" role="tablist">
       <button class="tablist__item is-active" role="tab" aria-selected="true">A</button>
       <button class="tablist__item" role="tab" aria-selected="false">B</button>
     </div>

   El comportamiento (swap del contenido al pulsar, auto-advance) lo
   gestiona JS en cada página.
   ════════════════════════════════════════════════════════════════════════════ */
.tablist {
  display: inline-flex;
  /* SIEMPRE ajustado al contenido («hug», nunca «fill»), esté donde esté:
     dentro de una columna flex (la barra de Search) el navegador lo estiraba
     a todo el ancho (Irene, 2026-10-16: «siempre lo pones en modo fill»).
     `max-width: 100%` deja que, si no cabe, haga scroll en una línea. */
  width: fit-content;
  max-width: 100%;
  /* …y lo hace de verdad: las opciones NUNCA parten en dos líneas; si no caben,
     la fila se desliza (QA 643; el comentario lo prometía y el código no). El
     relleno de 4 deja sitio al anillo de foco (2 + 2) para que no se recorte. */
  overflow-x: auto;
  scrollbar-width: none;
  padding: 4px;
  background: var(--gradient-subtle);
  border: 1px solid var(--border-neutral-soft);
  border-radius: var(--radius-full);
  gap: 2px;
}

.tablist__item {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--content-text-default-weak);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 500;
  letter-spacing: 0;
  padding: 10px 18px;
  border-radius: var(--radius-full);
  white-space: nowrap;
  flex: none;
  cursor: pointer;
  transition:
    color var(--dur-instant) var(--ease-out),
    background var(--dur-instant) var(--ease-out);
}

/* A TODO EL ANCHO · `.tablist--fill` SOLO en la hoja izquierda del Planner y en
   la app móvil (Irene, 10-11-2026 · QA 1134); en cualquier otro sitio, «hug».
   Cada pestaña crece desde su contenido; si no caben, la fila se desliza. */
.tablist--fill { display: flex; width: 100%; }
.tablist--fill > .tablist__item { flex: 1 0 auto; text-align: center; justify-content: center; }

.tablist::-webkit-scrollbar { display: none; }

/* `.tablist-scroll` · cuando el selector no cabe: en vez de cortar las
   opciones dentro de la píldora, la píldora entera se desplaza y sangra
   hasta el borde de la pantalla — se ve que sigue (Irene, 2026-10-21 ·
   QA 751). Envolver el `.tablist`; el borde lo pone `--gutter`. */
.tablist-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  margin-left: calc(-1 * var(--gutter));
  margin-right: calc(-1 * var(--gutter));
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}
.tablist-scroll::-webkit-scrollbar { display: none; }
.tablist-scroll > .tablist { max-width: none; overflow: visible; }

.tablist__item:hover {
  color: var(--content-text-default);
  background: var(--white-a04);
}

.tablist__item.is-active,
.tablist__item[aria-selected="true"] {
  color: var(--bg-bold);
  background: var(--color-action-default);
}

/* La tab ACTIVA se queda en el amarillo vivo también al hover. NO oscurecer con
   `--color-action-bold`: sobre amarillo, "bold" (#988216) vira a mostaza oliva y
   la tab parece deshabilitada (bug recurrente de tabs — oct 2026). Una tab ya
   seleccionada no necesita feedback de hover. */
.tablist__item.is-active:hover,
.tablist__item[aria-selected="true"]:hover {
  background: var(--color-action-default);
}

.product-grid--reverse .laptop-showcase {
  order: 1;
}

.product-copy {
  max-width: 48ch;
}

.product-copy .section-title {
  text-align: left;
  margin-bottom: 1.25rem;
}

.product-copy .pitch-cta {
  /* Visuals from .cta-row + .cta-row--start; only the section-specific
     bottom margin lives here. */
  margin: 0 0 1rem;
}

.product-copy .store-price {
  text-align: left;
  margin: 0;
}

.product-copy > .section-rule {
  margin: 0 0 2rem;
}

.pitch-cta {
  /* Visuals from .cta-row in common.css; just the top spacing here. */
  margin-top: 2.5rem;
}

.product-copy .section-subtitle {
  font-size: var(--text-editorial-subtitle);
  line-height: 1.125;
  font-weight: 300;
  text-align: left;
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  margin-bottom: 2rem;
}

.product-section--app .product-copy .section-subtitle {
  font-weight: 300;
}

/* El párrafo editorial: un peldaño por encima del texto base (body-lg, 22),
   con su peso (400), su interlineado y su ancho (640) · QA 547/550.
   Antes 18-22 · 300 · 1,2: se leía peor. */
.pitch-body {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body);
  font-weight: 400;
  max-width: var(--max-article);
  color: var(--content-text-subtitle);
}

.product-copy .pitch-body {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body);
  font-weight: 400;
  text-align: left;
  margin: 0 0 2rem;
}

/* Móvil (≤720): el pitch-body baja un peldaño, a body-md (18) · QA 556. */
@media (max-width: 720px) {
  .pitch-body,
  .section-header > .pitch-body,
  .editorial-stack .pitch-body,
  .product-copy .pitch-body { font-size: var(--text-body-md); }
}

.product-grid {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  align-items: center;
  gap: clamp(48px, 6vw, 96px);
}

.product-grid--reverse {
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
}

@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  .product-grid {
    /* minmax(0, 1fr) — NO `1fr` a secas (QA #72): un `1fr` crece hasta el
       min-content de su contenido, y el showcase de móviles de The App
       (~354px, dos teléfonos) forzaba la columna a 354 en un área de
       311 → arrastraba la columna de copy y el párrafo se CORTABA por la
       derecha ("...imagined" / "Exact" recortados). minmax(0,...) fija la
       pista al área disponible (igual que el grid de escritorio). */
    grid-template-columns: minmax(0, 1fr);
    gap: 3rem;
    min-height: auto;
  }
  .product-grid--reverse .product-copy { order: 1; }
  .product-grid--reverse .laptop-showcase { order: 2; }
  .product-copy { max-width: 100%; text-align: center; }
  .product-copy .eyebrow,
  .product-copy .section-title,
  .product-copy .section-subtitle,
  .product-copy .pitch-body,
  .product-copy .store-price { text-align: center; }
  /* Apilado y centrado, el párrafo (tope 640) se centra como bloque, no
     solo su texto: si no, en tablet queda pegado a la izquierda · QA 1020. */
  .product-copy .pitch-body { margin-left: auto; margin-right: auto; }
  .product-copy .pitch-cta { justify-content: center; }
  .product-copy > .section-rule { margin-left: auto; margin-right: auto; }
  .app-showcase { justify-content: center; gap: 12px; }
  .laptop-showcase { justify-content: center; }
  .laptop-mockup {
    transform: none;
    /* Capeamos el mockup al 88% del column track para que la
       .laptop-base (que es 112% del mockup con margin-left -6%)
       NO sobresalga del track ni del .container. Sin esto, el body
       overflow-x: hidden recorta el base por la derecha en viewports
       estrechos. 88% × 1.12 = 98.56% del track → base cabe entera. */
    width: 88%;
    max-width: 480px;
    margin-left: 0;
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   STATEMENT SLIDE
   ════════════════════════════════════════════════════════════════════════════ */

.statement-slide {
  position: relative;
  background: var(--bg-neutral-bold, #010B18);
  min-height: 100svh;
  padding: clamp(64px, 7vw, 96px) var(--gutter);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  text-align: center;
}

.statement-inner {
  position: relative;
  z-index: 2;
  max-width: 1100px;
  margin: 0 auto;
  width: 100%;
}

.statement-logo {
  display: inline-block;
  margin-bottom: 2.5rem;
  opacity: 0;
  transform: translateY(16px) scale(0.95);
  filter: blur(8px);
  transition: opacity var(--dur-glide) var(--ease-soft),
    transform var(--dur-glide) var(--ease-soft),
    filter var(--dur-glide) var(--ease-soft);
}

.statement-slide.is-visible .statement-logo {
  opacity: 1;
  transform: translateY(0) scale(1);
  filter: blur(0);
}

/* Shared hero tagline — used by home and landing heroes. Top of the
   weight hierarchy (500 > section-title 400 > statement-headline 300).
   Overridable per page if a softer tone is needed (experiences: 300). */
.hero-tagline {
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: var(--tracking-display);
  line-height: var(--lh-display);
  font-feature-settings: 'ss01' on;
  font-size: clamp(var(--text-heading-xl), 8vw, var(--text-display-lg));
  color: var(--gray-white);
  margin: 0 0 2rem;
}

.statement-headline {
  font-family: var(--font-sans);
  font-weight: 300;
  letter-spacing: -0.03em;
  line-height: 1.05;
  font-feature-settings: 'ss01' on;
  /* Abre sección: al nivel de .section-title (norma de jerarquía, QA 838). */
  font-size: clamp(var(--text-heading-lg), 5vw, var(--text-display-md));
  color: var(--gray-white);
  margin: 0;
}

.statement-line {
  display: block;
}

.statement-line-2 {
  margin-top: 0.5em;
}

.statement-headline .word {
  display: inline-block;
  opacity: 0;
  transform: translateY(18px);
  filter: blur(10px);
  transition: opacity var(--dur-glide) var(--ease-soft),
    transform var(--dur-glide) var(--ease-soft),
    filter var(--dur-glide) var(--ease-soft);
}

.statement-slide.is-visible .word {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

/* Per-word transition-delay is assigned by script.js at runtime so the
   stagger works for any language / word count. See: "Statement headline
   dynamic delays" in script.js. */

@media (prefers-reduced-motion: reduce) {
  .statement-logo,
  .statement-headline .word {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
}

/* Bridge variant — a standalone transition statement between two sections,
   set against pure black so it reads as the dramatic pause-point of the page. */
.statement-slide--bridge {
  background: var(--gray-black);
}

.statement-slide--bridge .statement-inner {
  max-width: var(--max-wide);
}

/* =============================================================================
   LEAFLET · CONTROLES UNIFICADOS DEL DS

   Patrón compartido para CUALQUIER mapa Leaflet de la web (Missions, plan
   maps de expedición, futuros mapas editoriales). Antes vivía duplicado
   en `missions/style.css` y `dolomites-068-oct-2027/style.css` — al
   unificarlo aquí garantizamos que todos los mapas hablen el mismo idioma
   visual (zoom como tab capsule `.btn-dark`, atribución mono discreta,
   contenedor con bg-bold).

   Si una página necesita variaciones (e.g. atribución full-width con
   text-shadow en Missions, o tunning específico de tiles), las hace
   localmente sobre estos defaults. NO redefinir el control de zoom por
   página — debe ser idéntico en toda la web.
   ============================================================================= */

/* Contenedor base · fondo bold del DS + fuente sans para overlays. */
.leaflet-container {
  background: var(--bg-bold);
  font-family: var(--font-sans);
}

/* Controles de zoom (+/−) · diseño TAB CAPSULE estilo `.btn-dark`
   (gray-11 + hover flip a blanco). Iconos SVG inyectados por JS
   (los caracteres tipográficos `+` y `−` no se centran consistentemente
   entre fuentes — el SVG centra por viewBox).

   ATENCIÓN: Leaflet aplica border-radius asimétrico a :first-child y
   :last-child con especificidad (0,2,2). Para ganarle usamos selectores
   con la clase específica de cada botón (.leaflet-control-zoom-in /
   .leaflet-control-zoom-out → 0,2,1 → empata pero gana por orden de
   carga, ya que `common.css` siempre va DESPUÉS de `leaflet.css`). */
.leaflet-control-zoom {
  border: 0;
  box-shadow: var(--shadow-strong);
  background: transparent;
  margin: clamp(12px, 1.6vw, 18px);
  border-radius: var(--radius-full);
  overflow: hidden;
}
.leaflet-bar a.leaflet-control-zoom-in,
.leaflet-bar a.leaflet-control-zoom-out {
  background: var(--gray-11);
  color: var(--gray-white);
  border: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  box-sizing: border-box;
  border-radius: 0;
  transition: background var(--dur-instant) var(--ease-out),
              color var(--dur-instant) var(--ease-out),
              opacity var(--dur-instant) var(--ease-out);
}
.leaflet-bar a.leaflet-control-zoom-in {
  border-bottom: 1px solid var(--border-neutral-soft);
}
.leaflet-bar a.leaflet-control-zoom-in:hover,
.leaflet-bar a.leaflet-control-zoom-out:hover {
  background: var(--gray-white);
  color: var(--gray-9);
}
.leaflet-bar a.leaflet-control-zoom-in:active,
.leaflet-bar a.leaflet-control-zoom-out:active {
  background: var(--gray-1);
  color: var(--gray-9);
}
/* Disabled · atenua al llegar al zoom mínimo/máximo. NO se oculta — el
   affordance siempre visible permite al usuario entender los límites. */
.leaflet-bar a.leaflet-control-zoom-in.leaflet-disabled,
.leaflet-bar a.leaflet-control-zoom-out.leaflet-disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
.leaflet-bar a.leaflet-control-zoom-in.leaflet-disabled:hover,
.leaflet-bar a.leaflet-control-zoom-out.leaflet-disabled:hover {
  background: var(--gray-11);
  color: var(--gray-white);
}

/* Atribución · estilo discreto mono del DS. Por defecto Leaflet pone
   bg blanco con texto oscuro — lo invertimos. Variaciones (e.g. Missions
   con text-shadow + full-width) se hacen localmente sobre este default. */
.leaflet-control-attribution {
  background: var(--black-a40);
  color: var(--white-a65);
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  letter-spacing: 0.04em;
  padding: 4px 10px;
}
.leaflet-control-attribution a {
  color: var(--white-a78);
}
.leaflet-control-attribution a:hover {
  color: var(--content-text-highlight);
}

/* =============================================================================
   MAP-VIEW · vista de mapa de un listado (2026-10-22 · QA 773)
   Sube desde Missions para usarla también en Travel Guides (y en los hubs con
   mapa del plan). Mapa satélite + una chincheta por resultado (`.map-marker`)
   + la card del resultado al pulsarla (`__detail`). El motor es `PP.mapa(el)`
   en script.js: la página le pasa los resultados filtrados, con su lat/lng y
   el HTML de su card. En un `.listing`, el interruptor Grid / Map
   (`.tablist[data-listing-view]`) y las chinchetas los pone el motor solo,
   leyendo `data-lat`/`data-lng` de cada card.
   El aislamiento (`isolation`) NO es opcional: los paneles de Leaflet llevan
   z-index de 200 a 700 y, sin él, se montan sobre la barra pegada y la nav.
   ============================================================================= */
.map-view {
  position: relative;
  z-index: 0;
  isolation: isolate;
  border-radius: var(--radius-l);
  overflow: hidden;
  border: 1px solid var(--border-neutral-soft);
}
.map-view[hidden] { display: none; }
/* Alto: barra pegada + mapa caben enteros en una pantalla; suelo para
   portátiles bajos, techo para que no parezca desmesurado. */
.map-view__canvas {
  height: clamp(360px, calc(100svh - 300px), 600px);
  width: 100%;
}
/* Fondo mientras cargan las teselas · con dos clases para ganar al
   `.leaflet-container` (negro del sistema y gris de Leaflet). */
.map-view .map-view__canvas { background: var(--gradient-subtle); }
/* Satélite con menos saturación y brillo: cinematográfico, no Google Maps. */
.map-view .leaflet-tile { filter: saturate(0.7) brightness(0.85) contrast(1.05); }
/* Teselas en cuadrado SIEMPRE: dentro de un artículo las imágenes llevan
   radio, y cada esquina redondeada dejaba un hueco en forma de estrella
   donde se juntan cuatro teselas (QA 1101). */
.map-view .leaflet-container img { border-radius: 0; margin: 0; box-shadow: none; }
/* Atribución a todo el ancho, sin fondo y con sombra para leerse sobre
   cualquier zona del satélite (TOS de Esri). */
.map-view .leaflet-bottom.leaflet-right { left: 0; right: 0; }
/* Con `.leaflet-container` en medio: si la hoja de Leaflet se carga después
   (Missions), su fondo blanco al 80 % ganaba y salía una franja gris bajo el
   mapa (QA 1122). */
.map-view .leaflet-container .leaflet-control-attribution {
  width: 100%;
  background: transparent;
  color: var(--white-a65);
  text-shadow: 0 1px 3px var(--black-a75);
  padding: 6px 16px;
  border-radius: 0;
  margin: 0;
  text-align: right;
}
.map-view .leaflet-control-attribution a { color: var(--white-a90); text-decoration: none; }

/* Card del resultado, flotando arriba a la derecha (abajo, a lo ancho, en
   móvil). El mapa sigue vivo debajo. La × es la `.icon-btn` del sistema. */
.map-view__detail {
  position: absolute;
  top: clamp(16px, 2vw, 24px);
  right: clamp(16px, 2vw, 24px);
  bottom: clamp(16px, 2vw, 24px);
  width: clamp(320px, 26vw, 400px);
  z-index: 1000;
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-strong);
  overflow: hidden;
  animation: map-view-detalle var(--dur-soft) var(--ease-out);
}
.map-view__detail[hidden] { display: none; }
@keyframes map-view-detalle {
  from { transform: scale(0.96) translateY(8px); opacity: 0; }
  to   { transform: scale(1) translateY(0); opacity: 1; }
}
.map-view__close { position: absolute; top: 12px; right: 12px; z-index: 3; }
/* La card llena el panel: foto arriba, cuerpo con scroll si no cabe. */
.map-view__body { height: 100%; }
.map-view__body .card { height: 100%; display: flex; flex-direction: column; }
.map-view__body .card:hover { transform: none; }
.map-view__body .card__media { flex-shrink: 0; }
.map-view__body .card__body { flex: 1; overflow-y: auto; min-height: 0; }
/* Card HORIZONTAL en la ficha (la del propio sitio en una ficha de spot,
   `.card--row-l`): la ficha se ajusta a su alto en vez de ocupar el mapa
   entero, y el texto deja sitio a la × (QA 1114). */
.map-view__detail:has(.card--row) { height: auto; }
@media (min-width: 721px) { .map-view__detail:has(.card--row) { bottom: auto; } }
.map-view__body .card--row { flex-direction: row; height: auto; }
.map-view__body .card--row .card__body { overflow: visible; }
/* El hueco de la × solo en la primera línea: el resto del texto usa todo el ancho. */
.map-view__body .card--row .card__body > :first-child { padding-right: calc(var(--sp-xl) + var(--sp-s)); }
/* En la horizontal del mapa, sin la píldora de tiempo de lectura: iría bajo la × (QA 1118). */
.map-view__body .card--row .card__meta { display: none; }
/* Card de hub sobre el mapa (lleva «Read more», QA 1119): en la horizontal
   del móvil el botón ocupa el sitio de la entradilla. */
.map-view__body .card--row:has(.card__actions) .card__text { display: none; }
.map-view__body .card--row .card__actions { margin-top: var(--sp-s); }   /* un peldaño más de aire bajo el título (QA 1120) */
/* La card del propio sitio (ficha de spot): la descripción entera, crece lo
   que haga falta, en todos los tamaños (QA 1119). */
.map-view__body [data-fila-movil] .card__text { display: block; -webkit-line-clamp: unset; overflow: visible; }
/* Dentro de una página de lectura (fondo claro) el mapa sigue siendo
   oscuro: la card no hereda los colores del texto del artículo. */
.map-view__body .card .card__title { color: var(--content-text-default); }
/* …ni su radio: el artículo pone las cards a 16 y el panel recorta a 24, y
   el filete de la card quedaba despegado de la esquina (QA 1105). */
.map-view .map-view__body .card { border-radius: var(--radius-l); }
/* Sobre la foto del mapa el filete claro del sistema hacía de halo gris: la
   card lleva el filete oscuro (negro 40 %), que marca el canto sin aclararlo
   (Irene, 30-10 · QA 1115). Y su foto no lleva el radio de 16 que la página
   de lectura pone a sus imágenes: las esquinas las recorta la card, y donde
   la foto toca el texto va recta (QA 1116). */
.map-view .map-view__body .card { border-color: var(--black-a40); }
.map-view__body .card__media img { border-radius: 0; }
/* AJUSTADA A SU CARD («hug», Irene, 30-10 · QA 1118): en tablet y escritorio
   el panel mide lo que la card, en todos los mapas, con tope en el alto del
   mapa; si no cabe, el texto se desplaza dentro. Antes solo la ruta. */
@media (min-width: 721px) {
  .map-view__detail { bottom: auto; max-height: calc(100% - 2 * clamp(16px, 2vw, 24px)); display: flex; flex-direction: column; }
  .map-view__body { height: auto; min-height: 0; display: flex; flex-direction: column; }
  .map-view__body .card { height: auto; min-height: 0; }
}
@media (max-width: 720px) {
  .map-view__detail {
    top: auto;
    left: clamp(12px, 3vw, 20px);
    right: clamp(12px, 3vw, 20px);
    bottom: clamp(12px, 3vw, 20px);
    width: auto;
    max-height: 70%;
    animation-name: map-view-detalle-movil;
  }
  @keyframes map-view-detalle-movil {
    from { transform: translateY(20px); opacity: 0; }
    to   { transform: translateY(0); opacity: 1; }
  }
}
@media (prefers-reduced-motion: reduce) {
  .map-view__detail { animation: none; }
}

/* Chincheta · punto amarillo con anillo; `--alerta` en rojo (agotado). La
   seleccionada crece y gana un anillo doble para casarla con su card. */
.map-marker {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: var(--radius-circle);
  background: var(--yellow-5);
  border: 2px solid var(--gray-9);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--yellow-5) 32%, transparent);
  transition: transform var(--dur-instant) var(--ease-out),
              box-shadow var(--dur-instant) var(--ease-out);
}
.map-marker--alerta {
  background: color-mix(in srgb, var(--red-5) 92%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--red-5) 25%, transparent);
}
.map-marker.is-selected {
  transform: scale(1.5);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--yellow-5) 50%, transparent),
              0 4px 14px var(--black-a55);
}
.map-marker--alerta.is-selected {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--red-5) 50%, transparent),
              0 4px 14px var(--black-a55);
}
/* Parada de una ruta (`--num`, la pone el motor con `data-paradas`): la
   chincheta lleva su número de orden, en mono, para casarla con la lista. */
.map-marker--num {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  color: var(--gray-11);
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 500;
  line-height: 1;
}

/* RUTA · las paradas en orden (`.ruta-paradas`, guía de ruta 3.3): una
   `.card--row card--light` por parada (la card clara, sobre la página blanca), en el orden en que se caminan; cada una enlaza a
   su ficha de spot. El mapa las lee de aquí (`data-paradas="#…"` en el
   `.map-view`) y las numera en el mismo orden. */
/* Doble clase: gana a la lista numerada del cuerpo de artículo. */
.ruta-paradas.ruta-paradas {
  display: grid;
  gap: var(--sp-s);
  margin: var(--sp-m) 0 0;
  padding: 0;
  list-style: none;
}
.ruta-paradas.ruta-paradas > li { margin: 0; padding: 0; }
.ruta-paradas.ruta-paradas > li::marker { content: none; }

/* Interruptor Grid / Map de un listado: fila contador + interruptor. */
.listing__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-s) var(--sp-default);
  margin: 4px 0 clamp(16px, 3.5vw, 22px);
}
.listing__meta .listing__count { margin: 0; }
/* La fila mide SIEMPRE lo que un chip de filtro (`.chip-dismiss`, 40): si no,
   crece al aparecer el primer chip y encoge al quitar el último, y la rejilla
   de debajo salta 16 (QA 1092). */
.listing__meta { min-height: var(--sp-xl); }
/* Icono delante del texto en una opción de `.tablist` (Grid / Map). */
.tablist__item:has(svg) { display: inline-flex; align-items: center; gap: 8px; }
/* En vista de mapa, fuera la rejilla, el vacío y «cargar más». */
.listing.is-mapa :is(.listing__grid, .listing__empty, .listing__more) { display: none; }

/* =============================================================================
   COOKIE / CMP · banner de consentimiento + panel de preferencias granular
   Chrome global (LSSI 22.2 · "Aceptar" y "Rechazar" al mismo nivel, sin casillas
   no esenciales premarcadas). Se muestra en la 1ª visita (JS · sin consentimiento
   guardado) y se reabre desde "Cookie settings" del footer.
   ========================================================================== */
.cookie-banner {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: var(--z-toast);
  padding: 16px var(--gutter);
  padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  background: var(--bg-bold);   /* negro puro */
  border-top: 1px solid var(--border-neutral-soft);
  box-shadow: 0 -12px 32px var(--black-a40);
}
.cookie-banner[hidden] { display: none; }
/* Entra y sale subiendo desde fuera de pantalla (`PP.funde`, QA 1087). */
.cookie-banner { --funde-y: 100%; }
.cookie-banner__inner {
  max-width: var(--max-wide);
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: clamp(16px, 3vw, 40px);
  flex-wrap: wrap;
}
.cookie-banner__text { flex: 1 1 320px; min-width: 0; }
.cookie-banner__title {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 600;
  color: var(--content-text-default);
  margin: 0 0 4px;
}
.cookie-banner__body {
  font-family: var(--font-sans);
  font-size: var(--text-body-xs);
  line-height: 1.45;
  color: var(--content-text-default-weak);
  margin: 0;
  max-width: 68ch;
}
.cookie-banner__body a { color: var(--content-link-default); text-decoration: underline; text-underline-offset: 2px; }
.cookie-banner__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; flex-shrink: 0; }

/* Panel de preferencias (modal) */
.cookie-prefs { position: fixed; inset: 0; z-index: calc(var(--z-toast) + 1); }
.cookie-prefs[hidden] { display: none; }
.cookie-prefs__scrim { position: absolute; inset: 0; background: var(--black-a55); }
.cookie-prefs__panel {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(92vw, 520px);
  max-height: min(86vh, 640px);
  overflow-y: auto;
  background: var(--bg-neutral-default);
  border: 1px solid var(--border-neutral-soft);
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-strong);
  padding: clamp(20px, 3vw, 32px);
}
.cookie-prefs__title {
  font-family: var(--font-sans);
  font-size: var(--text-heading-sm);
  font-weight: 600;
  color: var(--content-text-default);
  margin: 0 0 8px;
}
.cookie-prefs__intro {
  font-size: var(--text-body-sm);
  color: var(--content-text-default-weak);
  line-height: 1.5;
  margin: 0 0 20px;
}
.cookie-prefs__list { list-style: none; margin: 0 0 20px; padding: 0; }
.cookie-cat { padding: 14px 0; border-top: 1px solid var(--border-neutral-soft); }
.cookie-cat:first-child { border-top: 0; }
.cookie-cat__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.cookie-cat__name { font-family: var(--font-sans); font-size: var(--text-body-sm); font-weight: 600; color: var(--content-text-default); }
.cookie-cat__always { font-family: var(--font-mono); font-size: var(--text-mono-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--content-text-default-weak); }
.cookie-cat__desc { font-size: var(--text-body-xs); color: var(--content-text-default-weak); line-height: 1.45; margin: 6px 0 0; max-width: 52ch; }
.cookie-prefs__actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }

/* Interruptor (switch) */
.cookie-switch { position: relative; display: inline-block; width: 44px; height: 26px; flex-shrink: 0; }
.cookie-switch input { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.cookie-switch__track {
  position: absolute; inset: 0;
  background: var(--gray-7);
  border-radius: var(--radius-full);
  transition: background var(--dur-instant) var(--ease-out);
}
.cookie-switch__track::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--gray-white);
  transition: transform var(--dur-instant) var(--ease-out);
}
.cookie-switch input:checked + .cookie-switch__track { background: var(--yellow-5); }
.cookie-switch input:checked + .cookie-switch__track::after { transform: translateX(18px); }
.cookie-switch input:focus-visible + .cookie-switch__track { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }

@media (max-width: 720px) {
  .cookie-banner__actions { width: 100%; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
  .cookie-switch__track, .cookie-switch__track::after { transition: none; }
}

/* ===========================================================================
   PÁGINA DE CONTENIDOS · la plantilla de Article + Guide
   ---------------------------------------------------------------------------
   Decisión de Irene (2026-10-14): Article y Guide son EL MISMO diseño. Son dos
   categorías distintas, pero la estructura es idéntica — solo cambia el
   contenido. Por tanto no hay «madre e hija»: hay UNA plantilla, y vive aquí.
   El diseño válido es el de Article.

   De aquí beberán TODOS los artículos y guías actuales de photopills.com
   cuando se migren al diseño nuevo. Por eso cada pieza de este bloque tiene
   que estar documentada en los tres sitios (este archivo, el MDS y la guía
   visual) y no puede depender de la hoja de una página concreta.

   Las piezas que conservan el contexto `.article-body` lo hacen a propósito:
   son componentes DE la página de contenidos, y mantener el ámbito deja la
   especificidad exactamente como estaba (si se quitara, las reglas de prosa
   del cuerpo les ganarían y cambiarían de tamaño sin avisar).
   =========================================================================== */

/* ── Barra de progreso de lectura ─────────────────────────────────────────
   Filete fino bajo la topbar que crece según lo leído. Pista transparente:
   sobre fondo claro solo se ve el relleno amarillo. `z-index: 101` para que
   quede POR ENCIMA de la nav (100) — si baja, desaparece bajo su desenfoque.
   Estuvo partida en dos (la base en la hoja de cada página, el acople a la
   nav aquí) hasta 2026-10. */
.scroll-progress {
  position: fixed;
  top: var(--nav-height, 64px);
  left: 0; right: 0;
  height: 4px;
  background: transparent;
  z-index: 101;
  pointer-events: none;
  /* Sube CON la nav cuando se oculta, y se queda pegada al borde superior.
     Decisión de Irene 2026-08: la barra SIEMPRE visible — al esconderse la
     nav, la barra ocupa su hueco, no se esconde con ella. */
  transition: transform var(--dur-soft) var(--ease-out);
}
@supports (top: env(safe-area-inset-top)) {
  .scroll-progress { top: calc(var(--nav-height, 64px) + env(safe-area-inset-top, 0px)); }
}
body:has(.nav.hidden) .scroll-progress {
  transform: translateY(calc(-1 * var(--nav-height)));   /* sube lo que mide la barra y se queda en el borde del área segura: más arriba, Chrome en iPad lo tapa con su barra encogida (QA 1046) */
}
.scroll-progress__fill {
  height: 100%;
  width: 100%;
  background: var(--yellow-5);
  transform-origin: left center;
  transform: scaleX(0);
  /* rAF frame-smoothing · EXCEPCIÓN documentada del DS: NO migrar a --dur-*.
     Son 80ms lineales que suavizan el fotograma; con la rampa del sistema el
     progreso se arrastra y se despega del scroll. */
  transition: transform 80ms linear;
}

/* ── Visor de foto (lightbox) ─────────────────────────────────────────────
   Pantalla completa al pulsar una foto del cuerpo. Scrim casi negro + imagen
   contenida + botón de cerrar (`.icon-btn` del DS, no uno propio).
   `touch-action: none` en el scrim es lo que bloquea el scroll de fondo en
   iOS — `overflow: hidden` NO frena el táctil (gotcha documentado del DS). */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 4vw, 56px);
  background: var(--black-a92);
  touch-action: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-soft) var(--ease-out),
              visibility 0s linear var(--dur-soft);
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-soft) var(--ease-out);
}
.lightbox__img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius-m);
  box-shadow: var(--shadow-strong);
  transform: scale(0.98);
  transition: transform var(--dur-soft) var(--ease-out);
}
.lightbox.is-open .lightbox__img { transform: scale(1); }
.lightbox__close {
  position: absolute;
  top: clamp(12px, 3vw, 24px);
  right: clamp(12px, 3vw, 24px);
}
/* Galería (`[data-lightbox]`): flechas a los lados y pie abajo. Solo existen
   con más de una foto / con `data-caption`; la foto suelta de un artículo no
   las enseña. Flecha clicable = amarilla (norma F). */
.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-action-default);
}
.lightbox__nav--prev { left: var(--gutter); }
.lightbox__nav--next { right: var(--gutter); }
.lightbox__nav[hidden],
.lightbox__caption[hidden] { display: none; }
.lightbox__caption {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: var(--gutter);
  margin: 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  color: var(--white-a78);
  text-shadow: 0 1px 4px var(--black-a55);
}
/* Bloqueo del scroll de fondo con el visor abierto · SOLO overflow-y.
   ⚠️ No tocar `overflow-x: clip` del reset: sostiene el sticky de las tablas
   (si se pone `overflow: hidden` a secas, la cabecera flotante deja de
   pegarse). El scrim con touch-action cubre el táctil en iOS. */
html.lb-open { overflow-y: hidden; }

/* ── Vídeo embebido ───────────────────────────────────────────────────────
   Marco 16:9 que mantiene la proporción sea cual sea el ancho de columna. */
.article-body .video-wrapper {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%;                     /* 16:9 */
  height: 0;
  border-radius: var(--radius-m);
  overflow: hidden;
  background: var(--bg-bold);
}
.article-body .video-wrapper iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ── Notas y créditos al pie de un bloque ─────────────────────────────────
   `.notice` es la caja de aviso sin título (ver `.callout`).
   `.credits` es la LÍNEA DE CRÉDITOS de la pieza: «Photographs by… · Words
   by…», «Summary generated with AI · Reviewed by the team». Mono, gris,
   filete fino encima. Sin cursiva (la versión antigua la tenía · norma DS).

   ⚠️ NO CONFUNDIR CON `.credit` (singular), que es el crédito de UNA foto y
   vive dentro de su pie. Plural = los créditos de la pieza entera; singular =
   de quién es esta foto. Se parecen en el nombre porque se parecen en el
   papel, pero no son intercambiables.

   Se llamaba `.photographer-author` hasta el 15-10-2026. El nombre se quedó
   corto en cuanto empezó a usarse para aclaraciones de autoría que no son de
   un fotógrafo (el resumen hecho con IA del detalle de vídeo). Lo renombró
   Irene. */
.article-body .notice,
.article-body .credits {
  color: var(--content-text-on-light-weak);
  font-style: normal;
}
/* EL FILETE DE LOS CRÉDITOS, CORTO Y CON AIRE (Irene, 15-10-2026; validado
   antes en el detalle de vídeo y ahora promovido al componente).
   A todo el ancho cortaba la página en dos y separaba demasiado: una firma no
   es un cambio de sección. Mide 64px —el mismo que el filete de «About…»— y
   conserva el grosor y el gris suave para seguir leyéndose como una nota.
   Y respira POR ENCIMA: 2,5rem lo despegan del texto que lo precede, que era
   lo que faltaba para que no pareciera pegado al último párrafo. */
.article-body .credits {
  margin-top: 2.5rem;
}
.article-body .credits::before {
  content: "";
  display: block;
  width: 64px;
  border-top: 1px solid var(--border-on-light);
  margin-bottom: 1.1rem;
}
/* `.notice` ES LA MISMA CAJA QUE `.callout`, sin título (Irene, 14-10-2026:
   «vamos a dar ese diseño a todo este tipo de notas»). Antes era un párrafo
   gris con un filete encima: como aparte no se distinguía del texto que la
   rodeaba. Ahora comparte superficie con la caja de aviso del sistema —
   la diferencia entre las dos es solo si la nota lleva título o no.
   Al vivir en el componente, cualquier nota futura lo hereda sola. */
.article-body .notice {
  font-size: var(--text-body-md);
  border-top: 0;
  padding: clamp(20px, 2.5vw, 28px);
  border-radius: var(--radius-m);
  background: var(--bg-light-sunk);
  color: var(--content-text-on-light-default);
}
.article-body .credits {
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  letter-spacing: 0.03em;
}

/* ── Nota técnica BAJO UNA FOTO · `.figure-note` ──────────────────────────
   La tercera voz de la misma familia: el dato de cámara, óptica o trípode
   que acompaña a una figura («Nikon D500 + 200-500mm f/5.6 + Gitzo 4542»).
   Mono como `.credits` —las dos son firma técnica— pero un paso
   más pequeña y SIN filete: no cierra un bloque, cuelga de la foto.
   Vivía solo en la hoja de la guía de eclipses, sin estar en el sistema ni
   en la documentación; subida aquí en octubre de 2026. */
.article-body .figure-note {
  margin-top: 0.5rem;
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  color: var(--content-text-on-light-weak);
  font-style: normal;
}
/* En una foto ancha la nota arranca donde arranca el pie, en la columna de
   lectura, no en el borde de la foto (QA 934; misma regla que el pie, 1-bis). */
.article-body > .u-wide .figure-note,
.article-body > .u-full .figure-note {
  max-width: var(--max-article);
  margin-left: max(0px, calc((100% - var(--max-article)) / 2));
  margin-right: auto;
}

@media (prefers-reduced-motion: reduce) {
  .lightbox { transition: opacity var(--dur-instant) linear, visibility 0s linear var(--dur-instant); }
  .lightbox__img { transform: none; transition: none; }
}

/* ===========================================================================
   ·· EL RESTO DE LA PLANTILLA ··
   ---------------------------------------------------------------------------
   Todo lo que sigue vivía en `mockup/article/style.css` y se trajo aquí entero
   el 2026-10-14, por encargo de Irene: «coge todos los componentes existentes
   en Article y migralos al DS».

   Se mantiene EL MISMO ORDEN que tenía en la hoja de página. Eso importa: esa
   hoja cargaba DESPUÉS de todo este archivo, así que al traerla al final el
   orden en cascada queda idéntico y ninguna regla cambia de ganador.

   Dos reglas se ACOTARON al mudarse, porque eran globales desde una hoja de
   página y al subirlas se habrían comido todo el sitio:
     · la barra de scroll nativa (estaba sobre `html`) → `html:has(.page-article)`
     · el botón un escalón más bajo en móvil           → `.page-article .btn-l`
   Todo lo demás se mudó sin tocar ni un valor.
   =========================================================================== */

.page-article {
  /* Blanco en el <body>, a propósito: Chrome en iPhone en horizontal pinta
     con este color las franjas de los lados de la cámara. Se probó negro
     (topbar «de borde a borde») y enmarcaba el texto; manda el texto (QA 1048). */
  background: var(--gray-white);
  color: var(--content-text-on-light-default);
}

/* Barra de scroll nativa SIEMPRE visible (además de la progressbar de arriba).
   En macOS los scrollbars overlay se autoocultan; al estilar `::-webkit-scrollbar`
   se fuerza una barra clásica persistente. Solo afecta a las páginas de lectura
   (este CSS solo carga en article / lunar-eclipse-guide). */
html:has(.page-article) {
  scrollbar-gutter: stable;
  scrollbar-width: thin;                     /* Firefox */
  scrollbar-color: var(--gray-6) transparent;
}
.page-article::-webkit-scrollbar,
html:has(.page-article)::-webkit-scrollbar { width: 13px; }
.page-article::-webkit-scrollbar-track,
html:has(.page-article)::-webkit-scrollbar-track { background: transparent; }
.page-article::-webkit-scrollbar-thumb,
html:has(.page-article)::-webkit-scrollbar-thumb {
  background: var(--gray-6);
  border-radius: var(--radius-full);
  border: 3px solid var(--gray-white);       /* deja aire alrededor del thumb */
  background-clip: content-box;
}
.page-article::-webkit-scrollbar-thumb:hover,
html:has(.page-article)::-webkit-scrollbar-thumb:hover { background: var(--gray-7); }

/* (Topbar full-width ahora es global en common.css — override retirado.) */

/* ==========================================================================
   1 · HERO PARTIDO
   ========================================================================== */
.article-hero { background: var(--bg-bold); color: var(--content-text-default); }

.article-hero__cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  min-height: var(--alto-pantalla);
}

/* Columna de texto · negra, contenido centrado vertical. El padding-top suma
   el alto de la nav fija al padding editorial para que el bloque no quede
   tapado. */
.article-hero__text {
  --article-hero-pad-x: clamp(40px, 6vw, 88px);
  background: var(--bg-bold);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding:
    calc(var(--nav-height) + clamp(24px, 4vw, 56px))
    var(--article-hero-pad-x)
    clamp(40px, 5vw, 72px);
}
.article-hero__inner {
  max-width: 560px;            /* ~50ch cómodos dentro de la media columna */
  margin: 0 auto;
  width: 100%;
}

/* MIGAS · `.article-hero__crumbs` (Irene, 2026-10-23 · QA 786) · la misma
   altura que en las páginas de contenido (`.article-hero--plain`): topbar +
   escalón compacto (128 a 1440 · 96 a 375). Terminan en la SECCIÓN de la
   que cuelga la página, sin la página: el título va debajo y repetirlo
   (cortado) sobraba (QA 789). Con foto, el bloque va centrado en vertical,
   así que en escritorio las migas se FIJAN arriba de la columna, alineadas
   con el bloque; el relleno de arriba crece lo que miden + 16, para que un
   título largo no las pise. En tablet y móvil, en su sitio. */
.article-hero__crumbs { margin-bottom: var(--sp-default); }
.article-hero__text > .article-hero__crumbs .breadcrumb { max-width: 560px; margin: 0 auto; }
@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  .article-hero__text:has(> .article-hero__crumbs) {
    position: relative;
    padding-top: calc(var(--nav-height) + env(safe-area-inset-top, 0px) + var(--section-pad-y-sm) + 1.5rem + var(--sp-default));
  }
  .article-hero__text > .article-hero__crumbs {
    position: absolute;
    top: calc(var(--nav-height) + env(safe-area-inset-top, 0px) + var(--section-pad-y-sm));
    left: var(--article-hero-pad-x);
    right: var(--article-hero-pad-x);
    margin: 0;
  }
}
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  .article-hero__text:has(> .article-hero__crumbs) { padding-top: calc(var(--nav-height) + env(safe-area-inset-top, 0px) + var(--section-pad-y-sm)); }
}

/* Eyebrow de categoría · componente .eyebrow-line canónico (mono yellow). */
.article-hero__title {
  font-size: clamp(var(--text-heading-xl), 5.5vw, var(--text-display-md));
  font-weight: 600;            /* regla universal: títulos a 600 */
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
  margin: 1rem 0 0;
}
/* Móvil · "hero intermedio" (MDS / QA #33) · igual que app: 56 fijo se veía
   grande en móvil → clamp fluido 40↔56 (~45px @375, empalma con 56 en tablet). */
@media (max-width: 720px) {
  .article-hero__title {
    font-size: clamp(var(--text-heading-lg), 12vw, var(--text-heading-xl));
  }
}
.article-hero__lead {
  /* 22 en TODOS los tamaños (QA 993): en móvil, a 18 como el texto, la
     entradilla no se distinguía. Misma receta que `.article-lead`. */
  font-size: var(--text-body-lg);
  font-weight: 300;
  line-height: 1.35;
  color: var(--content-text-default-weak);
  margin: 1.25rem 0 0;
}
/* Meta (autor + fecha) al pie del bloque de texto. */
.article-hero__meta {
  display: flex;
  align-items: center;
  gap: clamp(12px, 2vw, 20px);
  margin-top: 2rem;
  flex-wrap: wrap;
}
/* La fecha va SIEMPRE en su propia línea, debajo de la firma (firma única en
   todo el sitio). Sin esto solo baja cuando no cabe, y la firma cambia de
   forma según lo largo que sea el texto. */
.article-hero__date {
  flex-basis: 100%;
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  letter-spacing: 0.04em;
  color: var(--content-text-default-weak);
}
/* Fila de etiqueta + dato · `.article-hero__kicker` (QA 795, 797): la
   píldora de las cards (`.card__meta--read` con el tiempo de lectura,
   `--watch` con la duración) va junto a la etiqueta; sin etiqueta, sola en
   su sitio. 12 de hueco, centradas en vertical. */
.article-hero__kicker { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-ds); }
.article-hero__kicker .eyebrow-line { margin: 0; }
/* Compartir · fila de .icon-btn glass del DS (sin label, solo iconos). */
.article-hero__share { display: flex; align-items: center; gap: 10px; margin-top: 1.5rem; flex-wrap: wrap; }
/* Móvil · botones de compartir un poco más grandes (32 → 40px · cumple el
   mínimo táctil de 40×40 del DS). */
@media (max-width: 720px) {
  .article-hero__share .icon-btn { width: 40px; height: 40px; }
  .article-hero__share .icon-btn svg { width: 18px; height: 18px; }
}

/* SIN FOTO · `.article-hero--plain` (2026-10-16) · la misma cabecera de
   contenidos en UNA columna y sin imagen, para las páginas de texto (Careers,
   FAQs, Glossary, Page, Press kit, Awards). Alineada a la izquierda y al
   ancho de la columna de lectura, así el título arranca en el mismo borde que
   el texto de debajo (Irene: «alinea todo a la izquierda, es más legible»).
   Título: el de contenidos (56↔64, móvil ~45), más pequeño que el del hero
   porque estos títulos son frases largas. Piezas, todas opcionales salvo el
   título: migas (en el hueco de la etiqueta) · `.article-hero__lead` ·
   `.article-hero__meta` (firma + fecha) · `.article-hero__share`. */
.article-hero--plain {
  padding:
    calc(var(--nav-height) + env(safe-area-inset-top, 0px) + var(--section-pad-y-sm))
    var(--gutter)
    var(--section-pad-y-sm);
}
.article-hero--plain .article-hero__inner { max-width: var(--max-article); }
.article-hero--plain .breadcrumb + .article-hero__title { margin-top: 0.75rem; }
/* Migas + etiqueta: 16 entre las dos. La etiqueta nunca repite la última
   miga (Calculator detail no lleva etiqueta · QA 793). */
.article-hero--plain .breadcrumb + .eyebrow-line { margin-top: var(--sp-default); }

/* Columna media · imagen cubre toda la columna. */
.article-hero__media {
  position: relative;
  overflow: hidden;
  min-height: 60svh;
}
.article-hero__media img {
  /* LLENA SIEMPRE SU MARCO (Irene, 15-10-2026: «has dejado un espacio en negro
     por debajo de la foto del header»).
     El marco tiene `min-height` pero no altura propia, así que el `height:100%`
     de la foto no se resolvía contra nada y la imagen caía a su proporción
     natural. Si esa proporción daba MENOS alto que el mínimo, el marco seguía
     midiendo lo suyo y debajo asomaba el fondo negro del hero. En un emulador
     de 390×844 no se ve —ahí los dos números coinciden por casualidad— pero en
     cuanto cambia el alto de la pantalla, aparece.
     Sacándola del flujo, el marco manda y la foto lo llena pase lo que pase. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* El crédito de foto (.photo-credit del DS) se posa abajo-derecha sobre la foto. */
.article-hero__media .photo-credit {
  position: absolute;
  right: clamp(12px, 2vw, 20px);
  bottom: clamp(12px, 2vw, 20px);
  z-index: var(--z-overlay);
}

/* Hasta 1024 · una sola columna. Titular primero (prioridad lectura),
   foto después. Si se prefiere foto-primero (como el hero de expediciones),
   añadir `order:-1` a .article-hero__media aquí. */
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {  /* tablet, también el iPad Pro de 13" en vertical (1032): apila como el móvil (QA 1039/1041) */
  .article-hero__cols { grid-template-columns: 1fr; min-height: 0; }
  .article-hero__text {
    /* padding-top suma la nav fija + notch para que el antetítulo (eyebrow)
       no quede tapado por la topbar. A los lados, el margen del sistema: el
       título arranca en el mismo borde que el texto de debajo (QA 857). */
    padding:
      calc(var(--nav-height) + env(safe-area-inset-top, 0px) + clamp(28px, 7vw, 48px))
      var(--gutter)
      clamp(40px, 8vw, 64px);
  }
  .article-hero__media { min-height: clamp(260px, 52svh, 460px); order: 2; }
}
/* Tablet (721-1024) · la cabecera a la columna de lectura, centrada: la misma
   anchura que el texto de debajo (norma de tablet · QA 1031). */
@media (min-width: 721px) and (max-width: 1024px), (orientation: portrait) and (min-width: 721px) and (max-width: 1199px) {
  .article-hero__inner,
  .article-hero__text > .article-hero__crumbs .breadcrumb { max-width: var(--max-article); }
}

/* ==========================================================================
   2 · CUERPO DEL ARTÍCULO · rejilla de 3 anchos + prosa base
   --------------------------------------------------------------------------
   content = columna de lectura (720px) · wide = figuras/tablas · full = bandas
   a sangre completa (notice, cita oscura).
   ========================================================================== */
.article-body {
  display: grid;
  /* El carril lateral usa el GUTTER del sistema (16 en móvil → 24 en
     escritorio), no un literal. Llevaba `1.25rem` escrito a mano = 20px fijos,
     que es justo lo que la regla del gutter prohíbe: «nunca un literal ni un
     valor por página». En móvil eso robaba 8px de columna de lectura
     (Irene preguntó por el margen lateral · 2026-10-14). */
  /* CARRIL ANCHO = `--max-content` SIEMPRE (Irene, 2026-10-15). Los carriles
     laterales llevaban 132px escritos a mano: con la columna de 720 sumaban
     984 (≈ 980, el ancho de contenido del sistema), pero al bajar la columna
     a 640 el carril ancho encogió con ella a 904 — un ancho que no es ningún
     token, y que dejaba el reproductor de Video detail (980) y sus
     recomendaciones (904) con bordes distintos. Ahora cada carril lateral se
     calcula de los dos tokens, y el ancho no se mueve aunque la columna sí. */
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] minmax(0, calc((var(--max-content) - var(--max-article)) / 2))
    [content-start] min(var(--max-article), 100% - 2 * var(--gutter)) [content-end]
    minmax(0, calc((var(--max-content) - var(--max-article)) / 2)) [wide-end]
    minmax(var(--gutter), 1fr) [full-end];
  padding: var(--section-pad-y-md) 0;
}
.article-body > *        { grid-column: content; }
.article-body > .u-wide  { grid-column: wide; }
.article-body > .u-full  { grid-column: full; }

/* Ritmo vertical (lobotomized owl) · márgenes, no row-gap, para poder dar más
   aire por encima de los encabezados. Espaciado GENEROSO (feedback usuaria):
   mucho aire entre bloques para que el artículo respire. */
.article-body > * { margin: 0; }
.article-body > * + *          { margin-top: clamp(2rem, 4vw, 3rem); }
/* DOS PÁRRAFOS SEGUIDOS van MÁS JUNTOS (Irene, 14-10-2026, tras la auditoría
   de legibilidad). Hasta ahora todo lo que iba seguido recibía el mismo aire
   —48px— daba igual que fueran dos párrafos del mismo razonamiento o un
   párrafo y una tabla. Eso tenía dos efectos malos: el texto se leía como una
   lista de párrafos sueltos en vez de como un discurso, y la escalera del
   documento se aplanaba («párrafo nuevo» 48 contra «subapartado nuevo» 77 —
   demasiado cerca para distinguirlos de un vistazo).
   Ahora la escalera es 28 → 48 → 77 → 102: cada escalón casi el doble del
   anterior. Solo se toca este peldaño; el resto se queda como estaba. */
.article-body > p + p          { margin-top: 1.75rem; }
.article-body > h2             { margin-top: clamp(4.5rem, 8vw, 6.5rem); }
.article-body > h3             { margin-top: clamp(3.5rem, 6vw, 5rem); }
.article-body > h4             { margin-top: clamp(2.75rem, 4.5vw, 3.75rem); }
/* Un título que ABRE el cuerpo cae donde caería el primer párrafo (QA 1001). */
.article-body > :is(h2, h3, h4):first-child { margin-top: 0; }
.article-body > .u-full,
.article-body > .u-wide        { margin-top: clamp(3.5rem, 6vw, 5rem); }
/* El elemento que sigue a una figura/tabla/banda también respira más. */
.article-body > .u-full + *,
.article-body > .u-wide + *    { margin-top: clamp(3.5rem, 6vw, 5rem); }

/* EL PANEL DE CONTENIDOS RESPIRA IGUAL POR ARRIBA QUE POR ABAJO (Irene,
   2026-10-16). Debajo casi siempre viene el primer h2, con su aire de sección;
   encima, la entradilla le dejaba solo el de bloque (48 contra 104) y el panel
   quedaba pegado al texto de arriba. Ahora lleva el mismo por los dos lados. */
.article-body > * + .article-toc,
.article-body > .article-toc + * { margin-top: clamp(4.5rem, 8vw, 6.5rem); }

/* Título PEGADO a su texto · el primer elemento tras un encabezado arranca
   cerca (el aire grande va ENCIMA del título, no debajo). */
.article-body > h2 + *,
.article-body > h3 + *,
.article-body > h4 + *,
.article-body > h5 + *         { margin-top: var(--sp-heading-after); }

/* Lista ENGANCHADA a su frase introductoria · 20px (auditoría de listas,
   14-10-2026). Antes eran 12,8: tan pegada que parecía continuar la frase en
   vez de colgar de ella. 20 sigue siendo menos que los 28 de un cambio de
   párrafo, así que el vínculo se mantiene. */
.article-body > p + ul,
.article-body > p + ol         { margin-top: 1.25rem; }

/* --- 19 · Párrafo base --- */
.article-body p {
  font-size: var(--text-body-md);        /* 18px · mínimo legible para prosa */
  line-height: var(--lh-body);           /* 1.47 */
}
/* ENTRADILLA DEL CUERPO · `.article-lead`: el primer párrafo del cuerpo de
   TODA página de artículo, también si la cabecera lleva la suya (Irene,
   2026-10-28 · QA 985: las dos no se ven nunca a la vez, y así la vista sabe
   que el texto empieza ahí). MISMA receta que `.article-hero__lead`
   (22 · 300 · 1.35), así las dos no pueden separarse con el tiempo. Ni
   negrita ni capitular. */
.article-body > p.article-lead {
  font-size: var(--text-body-lg);   /* 22 siempre, como .article-hero__lead (QA 993) */
  font-weight: 300;
  line-height: 1.35;
  color: var(--content-text-on-light-default);
}
/* Más aire DEBAJO de la entradilla (Irene, 2026-10-16, en Legal): seguida de
   otro párrafo recibía el paso de «párrafo seguido» (28) y se leía como un
   párrafo más. Toma el de bloque (48), el siguiente peldaño de la escalera. */
.article-body > p.article-lead + p { margin-top: clamp(2rem, 4vw, 3rem); }
.article-body strong { font-weight: 700; }     /* strong editorial = 700 */
.article-body em     { font-style: normal; font-weight: 600; }  /* sin cursiva */

/* Enlaces inline · texto del cuerpo + SUBRAYADO AMARILLO (QA #132).
   Antes iban en `--blue-5`, lo que incumplía la regla 7 del DS: «cada color
   tiene un único papel — amarillo = acción, azul = SOLO logo». Se había tirado
   de azul porque el amarillo no pasa contraste como TEXTO sobre fondo claro.
   La salida: el amarillo se va al subrayado, donde no tiene que pasar contraste
   de texto, y el enlace se lee en el color del cuerpo. El subrayado es
   permanente y grueso, así que sigue leyéndose como enlace para todo el mundo.
   Al pasar por encima, el amarillo sube a toda la caja: el enlace se "enciende". */
/* Enlaces de PROSA (QA #133 · #141).
   Dos lecciones, las dos aprendidas rompiendo cosas:
   1) La primera versión cogía TODO <a> del cuerpo y dejaba cuadrados los chips
      y el botón del ebook (son píldoras del sistema).
   2) La segunda los excluía con `:not(.btn)…`, pero eso SUBE el peso del
      selector: pasaba a pesar más que los componentes y teñía de amarillo el
      índice entero, cuyos enlaces también viven en un <li>.
   Por eso los paréntesis van en `:where()`, que no suma peso: esta regla pesa
   lo mínimo y CUALQUIER componente la gana sin tener que listarlo aquí. */
/* (`.pg-zone` = el CUERPO de las páginas de texto, que desde 2026-10-16 se
   lee sobre blanco con la norma del artículo: mismo enlace, sin copiarlo.
   Solo las zonas, no toda la página: con `.pg` alcanzaba a las migas de la
   cabecera y «Home» salía resaltado como si estuviera seleccionado.) */
:is(.article-body, .pg-zone) :where(p, li, dd, blockquote, figcaption) a:not(:where(.btn, .chip, .icon-btn, .card)) {
  color: var(--content-text-on-light-default);
  /* El subrayado amarillo SOLO no se veía (QA #139): el amarillo sobre blanco
     da ~1.3:1, así que como filete fino pasa desapercibido. Se le suma un
     lavado amarillo muy claro en reposo — el enlace se lee como «resaltado»
     sin pedirle al amarillo que cargue el contraste del texto. */
  background: var(--yellow-1);
  box-shadow: 0 0 0 2px var(--yellow-1);   /* respira sin desplazar el texto */
  text-decoration: underline;
  text-decoration-color: var(--yellow-5);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
  border-radius: 2px;
  transition:
    background-color var(--dur-instant) var(--ease-out),
    box-shadow var(--dur-instant) var(--ease-out);
}
:is(.article-body, .pg-zone) :where(p, li, dd, blockquote, figcaption) a:not(:where(.btn, .chip, .icon-btn, .card)):hover {
  background: var(--yellow-3);
  box-shadow: 0 0 0 3px var(--yellow-3);
  /* ⚠️ El color HAY QUE repetirlo aquí. Sin él ganaba `p a:hover` del chrome
     global, que pinta el texto de amarillo — y sobre el lavado amarillo del
     hover el enlace se volvía ilegible (Irene, 2026-10-14). Los paréntesis de
     arriba van en `:where()` y no suman peso, así que la regla de reposo no
     alcanzaba al estado hover. */
  color: var(--content-text-on-light-default);
}

/* --- 17 · Encabezados de sección --- */
.article-body h2, .article-body h3,
.article-body h4, .article-body h5 {
  font-weight: 600;                       /* todos los títulos a 600 (regla DS) */
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-heading);
  color: var(--content-text-on-light-default);
  /* CADA LÍNEA LLEGA HASTA EL FINAL (Irene, 15-10-2026: «la norma de salto de
     línea solo afecta al titular principal de la página, no a los elementos de
     contenido»). `balance` reparte las líneas para que queden de largo
     parecido, y eso corta antes de llenar la primera — en «Start with the
     core, not the camera» se notaba. `pretty` llena la línea y solo evita que
     quede una palabra sola abajo, que es lo que sí hace falta aquí. */
  text-wrap: pretty;
}
/* RAMPA CORREGIDA (2026-10-14 · decisión de Irene: «el diseño válido es el de
   Article»). Article marcaba sus secciones con `h3` y sus subsecciones con
   `h4`, saltándose el `h2`: un lector de pantalla oía el documento saltarse un
   capítulo, y el panel de contenidos y el índice lateral —que se construyen
   LEYENDO los encabezados— no podían generarse igual en las dos páginas.
   Se arregla el NIVEL sin tocar el TAMAÑO que Irene validó: la sección pasa a
   ser `h2` y conserva sus 32px. Consecuencia buscada: los títulos de la guía
   bajan a la medida de Article (40→32 y 32→24), que es la válida. */
/* UN PELDAÑO MÁS (Irene, 26-10-2026 · QA 905): el título de tabla y de
   gráfico sube a 22, así que todos los títulos del cuerpo suben uno para
   seguir por encima de él. El título del hero (56-64) sigue mandando. */
.article-body h2 { font-size: var(--text-heading-lg); }   /* 40 · sección */
.article-body h3 { font-size: var(--text-heading-md); }   /* 32 · subsección */
.article-body h4 { font-size: var(--text-heading-sm); }   /* 24 */
.article-body h5 { font-size: var(--text-body-lg); }      /* 22 */

/* SECCIONES NUMERADAS · `.article-body--numerada` (EN PRUEBA, QA 901/903).
   La primera sección es la 01 (la introducción va sin número en el índice
   lateral y el panel declara `--toc-antes: 0`), en la mono gris del índice.
   Sin filete entre secciones (se probó y se quitó, QA 909). Los tres números
   tienen que coincidir. */
.article-body--numerada { counter-reset: seccion 0; }
.article-body--numerada > h2:not([data-sin-numero]) { counter-increment: seccion; }
/* MÁS AIRE AL EMPEZAR CAPÍTULO (QA 912): el escalón Statement de sección,
   no el del título. Tras el panel de contenidos, el de siempre. */
.article-body--numerada:has(> h2:not([data-sin-numero]) ~ h2:not([data-sin-numero])) > h2:not(.article-toc + h2) { margin-top: var(--section-pad-y-lg); }
/* UNA sola sección: no hay nada que contar (sin número) y su título lleva el
   aire de cualquier título, no el de inicio de capítulo (QA 1001). */
.article-body--numerada:not(:has(> h2:not([data-sin-numero]) ~ h2:not([data-sin-numero]))) > h2::before { content: none; }
.article-body--numerada > h2:not([data-sin-numero])::before {
  /* «CHAPTER 01» encima del título, como un antetítulo de revista (Irene,
     2026-10-28 · QA 992, elegida entre ocho en _pruebas/capitulos/). Gris
     débil y no amarillo: el número no se pulsa, y amarillo = acción. */
  content: "Chapter " counter(seccion, decimal-leading-zero);
  display: block;
  margin-bottom: var(--sp-s);
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--content-text-on-light-weak);
}
/* `--seccion` · los textos legales numeran «SECTION 01»: no son capítulos de
   una lectura, son apartados de un documento (Irene, 2026-10-28 · QA 998). */
.article-body--numerada.article-body--seccion > h2:not([data-sin-numero])::before {
  content: "Section " counter(seccion, decimal-leading-zero);
}

/* Móvil · los títulos de sección bajan un paso (norma DS · QA #34): a 40px un
   título largo se rompe en 3+ líneas; a 32px respira y parte menos. */
@media (max-width: 720px) {
  .article-body h2 { font-size: var(--text-heading-md); }   /* 40 → 32 */
  .article-body h3 { font-size: var(--text-heading-sm); }   /* 32 → 24 */
}

/* --- 20 · Listas · marcador NATIVO con sangrado natural (padding-left) --- */
/* UN SOLO CARRIL PARA LOS DOS TIPOS (auditoría de listas, 14-10-2026).
   La numerada arrancaba 15px más adentro que la de viñetas, en el mismo
   artículo: el ojo lo lee como un desalineado. Y los números no colgaban del
   margen, FLOTABAN a 22px de él, dejando un pasillo vacío a la izquierda.
   Medium y Notion usan el mismo carril para los dos y el marcador cuelga
   pegado al borde. Con 1,8rem los números de dos cifras asoman 6px hacia el
   margen — que es justo lo que hace una lista bien compuesta. */
/* EL CARRIL TIENE QUE CABER AL MARCADOR (Irene, 15-10-2026: «el número
   incumple el margen de la página»). Medido: un marcador de UNA cifra con su
   hueco necesita 29px y el carril eran 28,8 — se salía por la izquierda, por
   delante del margen de la página. Con la mono se nota más porque sus cifras
   son más anchas, pero el carril ya venía corto de antes.
   2,6rem (41,6px) deja sitio a una cifra con holgura y también a dos («10.»,
   que pide 38) — importante de cara a migrar artículos con listas largas.
   El mismo carril para los dos tipos: viñetas y números arrancan igual. */
/* (`.pg-prose` = la prosa de las páginas de texto, que desde 2026-10-16 sigue
   la norma del artículo también en las listas. Y la respuesta de la FAQ clara,
   desde el 28-10: sin el carril, la viñeta se cortaba en el móvil · QA 1012.) */
:is(.article-body, .pg-prose) ul,
.faq--light .faq__a ul,
.faq--light .faq__a ol,
:is(.article-body, .pg-prose) ol { padding-left: 2.6rem; }
:is(.article-body, .pg-prose) ul,
.faq--light .faq__a ul { list-style: disc; }
:is(.article-body, .pg-prose) ol,
.faq--light .faq__a ol { list-style: decimal; }
:is(.article-body, .pg-prose) li,
.faq--light .faq__a li {
  font-size: var(--text-body-md);
  line-height: var(--lh-body);
}
/* AIRE ENTRE PUNTOS · 8px era 0,44 veces la letra; Medium usa 0,68. A 12px
   damos 0,67. Importa sobre todo en la guía, donde 2 de cada 3 puntos ocupan
   más de una línea: el salto de un punto al siguiente tiene que verse
   claramente mayor que el salto entre dos líneas del mismo punto. */
.faq--light .faq__a li + li,
:is(.article-body, .pg-prose) li + li { margin-top: 0.75rem; }
/* LOS DOS MARCADORES, IGUAL VESTIDOS. La viñeta iba en gris flojo y el número
   en negro seminegrita: el mismo papel resuelto de dos maneras, y en la misma
   página uno susurraba y el otro gritaba. Medium y Notion ponen los dos en el
   color del texto y en peso normal — el marcador ordena, no compite. */
:is(.article-body, .pg-prose) ul > li::marker,
.faq--light .faq__a ul > li::marker,
.faq--light .faq__a ol > li::marker,
:is(.article-body, .pg-prose) ol > li::marker {
  color: var(--content-text-on-light-default);
  font-weight: 400;
}
/* EL NÚMERO, EN MONO (Irene, 14-10-2026) · no es prosa, es un marcador — y la
   regla 5 del sistema reserva la mono justo para eso: «etiquetas técnicas y
   números de capítulo». Hay precedente dentro de esta misma plantilla: el
   panel de contenidos y el índice lateral ya numeran sus secciones en mono.
   Así el sitio tiene UN solo idioma para «esto va en orden».
   Un peldaño por debajo del texto porque la mono se ve ópticamente mayor a
   igualdad de tamaño. La viñeta NO cambia: una viñeta no es un número y la
   mono ahí no diría nada. */
:is(.article-body, .pg-prose) ol > li::marker {
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
}
/* Sub-bullet con el TRAZO del punto, no otro punto relleno (Irene,
   2026-10-14: «como hace Docs»). Hace falta decirlo porque `list-style: disc`
   está declarado en todos los `ul` y anula el cambio de nivel que el navegador
   haría solo. Tercer nivel: cuadrado hueco. */
.article-body li > ul { list-style: circle; }
.article-body li > ul ul { list-style: square; }
.article-body li > ul, .article-body li > ol { margin-top: 0.75rem; }

/* LA LISTA, ENMARCADA IGUAL POR ARRIBA Y POR ABAJO. Estaba pegada a su frase
   de entrada (12,8px) y abandonada por detrás (48px): un agujero. Ahora 20
   arriba —sigue claramente enganchada a la frase que la presenta, porque 20
   es menos que los 28 de un cambio de párrafo— y 28 por debajo cuando lo que
   viene detrás es otro párrafo. Si detrás viene otra lista, un título o una
   figura, se queda en 48: en la guía hay tres listas seguidas que necesitan
   ese aire para no fundirse. */
.article-body > ul + p,
.article-body > ol + p         { margin-top: 1.75rem; }

/* La lista de una CAJA DE AVISO no lleva el carril del cuerpo: ahí el ✓ se
   dibuja aparte, así que ese carril era un pasillo vacío de 28,8px que
   descuadraba la lista respecto al título de su propia caja. */
/* La caja de aviso es una lista FLEX con su propio hueco: si además lleva
   margen, se suman y los checks acaban al doble de distancia que cualquier
   otra lista (22px contra 12). Se iguala en el hueco, sin margen.
   Y UN PASO DE SANGRÍA (Irene, 15-10-2026): el ✓ no va pegado al borde de su
   caja; la lista entra un poco, como entra una lista dentro de un texto. */
.article-body .callout ul,
.article-body .callout ol {
  padding-left: 0.5rem;
  gap: 0.75rem;
}
/* …y sin el margen de las listas del cuerpo, que se SUMABA al hueco de la
   fila y dejaba los checks al doble de distancia (24px contra 12). */
.article-body .callout li + li { margin-top: 0; }

/* --- 21 / imágenes sueltas + dentro de figuras --- */
.article-body img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-m);
}

/* ==========================================================================
   3 · FIGURAS Y PIES  (elementos 1, 2, 6, 7)
   ========================================================================== */
.article-body figure { margin: 0; }
/* NORMA · ritmo vertical de figuras. El reset `figure{margin:0}` de arriba es
   solo para figuras ANIDADAS (dentro de .two-figures). Toda figura HIJA DIRECTA
   del cuerpo lleva aire por encima para que el texto no quede pegado a la
   imagen. Las `.u-wide` ya reciben su propio margen mayor (regla > .u-wide),
   así que aquí cubrimos las demás (.portrait, .screenshot sueltos). */
.article-body > figure:not(.u-wide) { margin-top: clamp(2.75rem, 5vw, 4rem); }
.article-body figcaption {
  margin-top: 0.65rem;
  /* EL MISMO ANCHO QUE EL CUERPO (Irene, 14-10-2026: «el pie no tiene el
     mismo ancho que el resto del texto, y debería»).
     La auditoría proponía acortarlo a 56ch (548px) porque con letra más
     pequeña caben más caracteres —el pie llegaba a 87 por línea contra los
     74-78 del cuerpo— pero verlo más estrecho que el texto al que acompaña
     pesa más que ese argumento. Decisión suya, tomada mirándolo. */
  max-width: var(--max-article);
  font-size: var(--text-body-sm);          /* 16 */
  line-height: 1.4;
  color: var(--content-text-on-light-weak);
  text-align: left;                        /* 1 · figura simple → pie a la izquierda */
}
/* 1-bis · LA FOTO ES ANCHA; SU PIE, NO (Irene, 14-10-2026) · en una banda
   ancha el pie llegaba a 124 caracteres por línea: los textos más difíciles
   de leer de la página —gris flojo, letra pequeña— eran justo los que tenían
   las líneas más largas. Se acota a la columna de lectura, alineado con el
   borde izquierdo de la foto. La foto sigue respirando a lo ancho. */
.article-body > .u-wide figcaption,
.article-body > .u-full figcaption {
  max-width: var(--max-article);
  /* ARRANCA DONDE ARRANCA EL TEXTO, no en el borde de la foto (Irene,
     14-10-2026). Centrarlo ya no vale: con el pie más estrecho que la columna,
     centrarlo lo desplazaba 46px a la derecha del cuerpo. El margen izquierdo
     es exactamente el que la banda ancha deja a cada lado de la columna. */
  /* `max(0px, …)` porque en el teléfono la figura es MÁS ESTRECHA que la
     columna de lectura: la resta salía negativa y el pie se escapaba 125px
     por la izquierda del margen de la página (Irene, 15-10-2026). */
  margin-left: max(0px, calc((100% - var(--max-article)) / 2));
  margin-right: auto;
}

/* 2 · figura retrato → el pie hereda el ancho de la FOTO, no el de la columna,
   así no sobresale por los lados. Como el ancho de la foto lo marca un cap de
   ALTO (no un max-width fijo), no hay forma CSS pura de igualar el pie sin
   colapsar la imagen → un JS (final del HTML) fija el max-width del pie al
   ancho real de la imagen. Centrado. Aplica igual a fotos y a capturas. */
.article-body figure.portrait figcaption {
  max-width: 100%;            /* el JS lo ajusta al ancho de la foto */
  margin-inline: auto;
  text-align: center;
}

/* Autor / crédito dentro del pie · en su propia línea, ENCIMA del texto del
   pie (salto), y en mono (chrome técnico del DS). */
/* EL CRÉDITO VA DENTRO DE LA FOTO (Irene, 15-10-2026: «debería ir dentro de
   todas las fotos, como en la foto del header»). Antes cerraba la frase del
   pie; ahora cae sobre la esquina de la imagen, que es a quien pertenece: el
   pie cuenta QUÉ se ve, el crédito dice DE QUIÉN es.
   La rejilla se aplica SOLO a las figuras que llevan crédito —`:has()`— para
   no tocar ninguna de las demás: imagen y crédito comparten celda, el pie
   sigue debajo. El aspecto (píldora sobre velo) lo pone `.photo-credit`, el
   mismo componente que usa la foto del hero.
   (Las reglas del crédito en línea dentro del pie se retiran aquí: ya no hay
   ningún `.credit` dentro de un `figcaption` en todo el sitio.) */
.article-body figure:has(> .photo-credit) {
  display: grid;
  grid-template-rows: auto auto;
}
.article-body figure:has(> .photo-credit) > img { grid-area: 1 / 1; }
.article-body figure:has(> .photo-credit) > .photo-credit {
  /* El componente nace flotando (en el hero va anclado a la esquina de la
     foto). Dentro de una figura lo coloca la rejilla, así que vuelve al
     flujo: si no, se ancla al primer ancestro posicionado y se va a otra
     parte de la página. */
  position: static;
  grid-area: 1 / 1;
  align-self: end;
  justify-self: end;
  margin: clamp(10px, 1.6vw, 16px);
  z-index: var(--z-overlay);
}
.article-body figure:has(> .photo-credit) > figcaption { grid-area: 2 / 1; }

/* ==========================================================================
   FOTOS (paisaje) vs CAPTURAS (app) · TAMAÑOS DIFERENCIADOS
   Petición de Irene: una captura vertical NO debe dominar la lectura → cap de
   ALTO. Una foto (paisaje) gana cuanto más grande → sin ese cap: la del par
   LLENA su columna; la suelta usa un cap de alto generoso.
   ========================================================================== */

/* --- FOTOS verticales (figure.portrait) --- */
/* Foto vertical suelta · cap de alto generoso (más grande que antes) pero
   acotado para no ocupar toda la pantalla. */
.article-body figure.portrait img {
  width: auto;
  max-width: 100%;
  max-height: min(78vh, 700px);
  margin-inline: auto;
}
/* 6 · par de fotos verticales · acotado al ancho de lectura (--max-article) y
   centrado → respeta el MISMO margen que el texto, no se va a sangre. Cada foto
   llena su media columna → quedan pegadas (distancia = el gap). */
.article-body .two-figures:not(.screenshot) {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(0.75rem, 2vw, 1.25rem);
  max-width: var(--max-article);
  margin-inline: auto;
}
.article-body .two-figures:not(.screenshot) > figure { margin: 0; }

/* REJILLA DE FOTOS · `.figure-grid` (2026-10-28 · QA 996). Una galería dentro
   del cuerpo: fotos cuadradas del mismo tamaño (`.photo-tile`, recorte
   `cover`), que se abren en el visor y se recorren con flechas
   (`data-lightbox`). 2 columnas en móvil, 3 en escritorio; con 4 fotos, las 4
   en una fila (en 2 + 2 las fotos de la web real se pixelaban · QA 1007). Para 3 o más fotos; con
   2, `.two-figures`; con 1, `figure`. */
.article-body .figure-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-default);
  margin-top: var(--section-pad-y-sm);
}
@media (min-width: 720px) {
  .article-body .figure-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .article-body .figure-grid:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.article-body .figure-grid > .photo-tile { --tile-ratio: 1 / 1; }
/* La foto LLENA el mosaico: la regla de prosa `.article-body img` (alto
   automático) dejaba una franja vacía debajo. */
.article-body .figure-grid > .photo-tile img { height: 100%; border-radius: 0; }

/* BLOQUE PLEGABLE · `details.article-fold` (2026-10-28 · QA 1000). Un bloque
   largo que se pliega dentro de un artículo (las expediciones de un mes): el
   título con su `.acc-toggle--on-light`, y dentro el contenido CON EL ASPECTO
   DEL ARTÍCULO —texto en el color del cuerpo, su aire, la foto ancha—. No es
   `.faq` (preguntas): la respuesta de una FAQ va en gris y en su caja.
   Estructura: summary > h3.article-fold__title (+ badge) + .acc-toggle, y
   el contenido en .article-fold__body. Filete entre bloques seguidos. */
.article-body > details.article-fold { border-top: 1px solid var(--border-on-light); }
.article-body > details.article-fold:last-of-type,
.article-body > details.article-fold:has(+ :not(details.article-fold)) { border-bottom: 1px solid var(--border-on-light); }
.article-body > details.article-fold + details.article-fold { margin-top: 0; }
.article-fold > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-default);
  padding-block: var(--sp-m);
  cursor: pointer;
  list-style: none;
}
.article-fold > summary::-webkit-details-marker { display: none; }
.article-body .article-fold__title { margin: 0; }
/* Título en dos líneas (QA 1002): el nombre arriba y, debajo, el dato
   (fechas, badge) en cuerpo, un peldaño por debajo del título. */
.article-fold__head { display: flex; flex-direction: column; gap: var(--sp-xs); min-width: 0; }
.article-fold__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-s);
  font-size: var(--text-body-lg);
  font-weight: 400;
  color: var(--content-text-on-light-default);
}
@media (max-width: 720px) {
  /* Móvil: el badge siempre en su propia línea, la tercera (QA 1010; la
     prueba con el badge sobre la pastilla, QA 1011, se descartó). */
  .article-fold__meta { flex-direction: column; align-items: flex-start; gap: var(--sp-xs); font-size: var(--text-body-md); }
}
.article-fold > summary .acc-toggle { margin-left: auto; flex: none; }
.article-fold__body { padding-bottom: var(--sp-xl); }
.article-fold__body > * { margin: 0; }
.article-fold__body > * + * { margin-top: var(--sp-l); }
.article-fold__body > p + p,
.article-fold__body > p + ul,
.article-fold__body > p + ol,
.article-fold__body > ul + p { margin-top: var(--sp-m); }
/* La foto sale de la columna de lectura al ancho grande, como `.u-wide`. */
.article-fold__body > .u-wide {
  margin-inline: calc((100% - min(var(--max-content), 100vw - 2 * var(--gutter))) / 2);
}
.article-body .two-figures:not(.screenshot) .portrait img {
  width: 100%;
  max-height: none;              /* la foto manda: llena la columna */
}

/* ==========================================================================
   CAPTURAS DE APP  (elementos 3, 4, 5, 7)
   Sin caja gris. Las VERTICALES se capan por ALTO (540) para que no dominen;
   la HORIZONTAL (.landscape) va a ancho completo.
   ========================================================================== */
.article-body figure.screenshot { text-align: center; }
/* cap de alto para capturas verticales (single, par de dos pies, par de un pie) */
.article-body figure.screenshot:not(.landscape) > img,
.article-body .two-figures.screenshot img,
.article-body figure.screenshot.two .shots img {
  max-height: 540px;
  width: auto;
  max-width: 100%;
  margin-inline: auto;
}
/* 4 · captura horizontal · ancho completo (sin cap de alto) */
.article-body figure.screenshot.landscape > img { width: 100%; max-width: 100%; }
/* 3 · dos capturas, dos pies · acotado al ancho de lectura y centrado (como el
   par de fotos) → respeta el margen del texto, no se va a la banda ancha. Las
   capturas apaisadas encogen para caber en las dos medias columnas. */
.article-body .two-figures.screenshot {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(0.75rem, 2vw, 1.25rem);
  max-width: var(--max-article);
  margin-inline: auto;
}
.article-body .two-figures.screenshot > figure { margin: 0; min-width: 0; flex: 1 1 0; }
/* 5 · dos capturas, un solo pie · mismo cap de ancho + centrado; el pie va
   centrado bajo el par. */
.article-body figure.screenshot.two {
  max-width: var(--max-article);
  margin-inline: auto;
}
.article-body figure.screenshot.two .shots {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(0.75rem, 2vw, 1.25rem);
}
.article-body figure.screenshot.two .shots > * { min-width: 0; flex: 1 1 0; }
.article-body figure.screenshot.two figcaption { text-align: center; }

/* Pares de capturas HORIZONTALES · usan la banda ancha (mismo margen que el
   element 4, la captura apaisada suelta), NO el ancho de lectura. Las verticales
   se quedan en el cap de --max-article de arriba. Se marca con `.landscape`
   porque el CSS no puede deducir la orientación de la imagen. */
.article-body .two-figures.screenshot.landscape,
.article-body figure.screenshot.two.landscape { max-width: none; }
/* pies de captura vertical · centrados bajo la imagen (el JS del final iguala
   su ancho al de la captura). */
.article-body .two-figures.screenshot figcaption,
.article-body figure.screenshot:not(.landscape):not(.two) figcaption {
  text-align: center;
  margin-inline: auto;
}

@media (max-width: 720px) {
  /* par de fotos → apila en una columna */
  .article-body .two-figures:not(.screenshot) { grid-template-columns: 1fr; }
  /* pares de capturas (flex) → apila en columna */
  .article-body .two-figures.screenshot,
  .article-body figure.screenshot.two .shots {
    flex-direction: column;
    align-items: center;
    /* Apiladas, el hueco de 12px dejaba el pie de la primera pegado a la
       segunda foto: parecían una sola pieza (Irene, 15-10-2026). */
    gap: 2rem;
  }
  .article-body .two-figures:not(.screenshot) { gap: 2rem; }
}

/* ==========================================================================
   5 · CITAS  (elementos 8, 16)
   ========================================================================== */
/* 8 · cita con autor · filete izquierdo + sin cursiva (la énfasis va en peso
   300 y tamaño). Rojo legacy retirado: el rojo es SOLO alerta en el DS. */
.article-body blockquote {
  padding-left: 1.25rem;
  border-left: 3px solid var(--content-text-on-light-default);
  font-weight: 300;
  font-size: var(--text-body-lg);           /* 22 */
  line-height: 1.35;
  font-style: normal;
}
.article-body blockquote cite {
  display: block;
  margin-top: 0.7rem;
  font-style: normal;
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);            /* un punto más que antes (sm→md) */
  letter-spacing: 0.04em;
  color: var(--content-text-on-light-weak);
}

/* 16 · cita destacada OSCURA · banda a sangre completa. En vez del rojo legacy
   (rojo = alerta), banda negra cinematográfica con comilla amarilla de acento
   — más de sistema (negro + blanco + amarillo). */
.article-body blockquote.dark {
  background: var(--bg-bold);
  color: var(--content-text-default);
  border: 0;
  padding: clamp(2.5rem, 6vw, 4.5rem) clamp(1.5rem, 6vw, 4rem);
  text-align: center;
  font-size: var(--text-editorial-subtitle);
  font-weight: 300;
}
.article-body blockquote.dark::before {
  content: "\201C";                          /* comilla decorativa */
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-heading-lg);         /* 40 · antes 3rem (48) a mano (QA 558) */
  line-height: 0.6;
  color: var(--color-action-default);        /* amarillo acento */
  margin-bottom: 0.5rem;
}
.article-body blockquote.dark cite {
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  color: var(--content-text-default-weak);
  margin-top: 1.25rem;
}

/* ==========================================================================
   6 · CAJA DE TIPS  (elemento 9)
   ========================================================================== */
/* La caja de tips es el `.callout .callout--light` del DS. El artículo solo
   estila hijos directos (.article-body > *), así que el callout fluye en la
   columna sin que su lista interna sufra interferencia. */

/* 7 · Vídeo embebido → ahora en common.css «PÁGINA DE CONTENIDOS». */

/* ==========================================================================
   8 · PÁRRAFO DE AVISO Y FIRMA  (elementos 11, 12)
   ========================================================================== */
/* 11-12 · Aviso y firma de foto → ahora en common.css «PÁGINA DE CONTENIDOS». */

/* ==========================================================================
   READ NEXT · módulo de cierre (sustituye al paginador)
   Galería de artículos sugeridos (foto + categoría + título) + botón "ver todos".
   Sobre el lienzo CLARO de lectura. Específico de artículo (candidato a DS si
   se reusa en más plantillas de blog).
   ========================================================================== */
/* band gris a sangre completa · usa el gris Apple ya tokenizado
   (--bg-light-default), que se lee sobre la superficie de lectura BLANCA del
   artículo (patrón Apple: contenido blanco + secciones en gris). El aire
   vertical simétrico lo pone el padding base de `.article-body`. */
/* Tags · fila de chips-enlace del DS (`.chip.chip--tag`, promovido al DS). El
   chip vive dentro de `.article-body`, cuyo `a` es azul subrayado → subimos
   especificidad para que el chip mande (píldora, no link inline). */
/* ETIQUETAS DE «MÁS POR PLANIFICAR» (Irene, 15-10-2026) · los chips cerraban
   el artículo sin decir qué eran: podían leerse como metadatos del texto que
   acababas de leer y no como una invitación a seguir. Ahora llevan un rótulo
   —«More to plan»— y un filete por encima que los despega del final del
   artículo, igual que la banda de «Read next» despega lo que viene después. */
.article-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding-top: clamp(1.5rem, 3vw, 2rem);
  border-top: 1px solid var(--border-on-light);
}
/* Con la voz de un título de tabla (Irene, 15-10-2026): sans, 18, semibold y
   en el color del texto. Antes iba en mono, versales y gris, que es voz de
   metadato — y esto no es un dato del artículo, es un encabezado de lo que
   viene después. */
.article-tags__label {
  flex: 1 0 100%;
  margin: 0 0 0.6rem;
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--content-text-on-light-default);
}
.article-body a.chip { text-decoration: none; }
.article-body a.chip--tag,
.article-body a.chip--tag:hover { color: var(--content-text-on-light-default); }

/* RECOMENDACIONES · franja OSCURA (Irene, 2026-10-15). Validada en la prueba
   de Video detail: sobre fondo claro competía con el texto de la página; en
   oscuro, solo tiene protagonismo la lectura.
   DOS VARIANTES, mismo componente:
     · un bloque     → `__head` (título + «Browse all…») + `__grid`
     · con Go deeper → lo anterior + `__rule` + segundo `__head` + `__grid`
   Aire: el de abajo es 16px MAYOR que el de arriba. Rompe a propósito la regla
   de simetría de secciones: la franja pega con el pie y necesita despegarse. */
/* ANCHO 980 (`--max-content`, el carril ancho) · Irene, 2026-10-19: «más
   estrecho, sin llegar a ser el del párrafo; era más legible». Historia: 904
   (heredado) → 980 (#333) → 1240 (#342, 15-10) → 980. Es el mismo ancho que
   el panel de la calculadora y que las fotos anchas. La franja redefine la
   rejilla del cuerpo: columna y carril ancho pasan a ser el mismo. */
.article-body.read-next {
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start content-start] min(var(--max-content), 100% - 2 * var(--gutter)) [content-end wide-end]
    minmax(var(--gutter), 1fr) [full-end];
}
.read-next {
  background: var(--gradient-soft);
  padding-top: clamp(2.25rem, 4.5vw, 3.25rem);      /* mucho menos alta que una sección editorial */
  padding-bottom: calc(clamp(2.25rem, 4.5vw, 3.25rem) + var(--sp-default));
}
/* Sin hueco blanco entre la banda ebook (full-bleed, última del cuerpo) y
   read-next: la banda oscura pega directa con la gris. */
/* NUNCA SE ACTIVABA (auditoría, 14-10-2026) · dejaba 102px de blanco entre la
   banda negra del ebook y la gris de «Read next». La regla pedía que la banda
   del ebook fuera el ÚLTIMO hijo del cuerpo, y no lo es: el motor que mantiene
   pegada la cabecera de una tabla al scrollear deja detrás dos elementos
   invisibles de tamaño cero. La regla miraba el último hijo, encontraba un
   fantasma y se rendía.
   Es el caso de libro de «mirar el alcance»: la regla no estaba rota, la rompió
   algo que vive en otro archivo y que nadie relacionó.
   `:last-of-type` mira el último de SU tipo, así que los fantasmas no estorban. */
.article-body:has(> .ebook-cta):not(:has(> .ebook-cta ~ *:not(.table-sticky-head))) { padding-bottom: 0; }
/* título del módulo · Public Sans grande (como un título de sección), no mono */
.article-body .read-next__eyebrow {
  grid-column: content;
  margin: 0;                                       /* el gap lo controla el margin-top de la galería */
  font-family: var(--font-sans);
  /* 32 → 40 (Irene, 2026-10-14): el título del módulo sube un escalón para
     separarse de verdad de los títulos de las cards. Fluido 32↔40: en el
     móvil se queda en 32 y no pesa más que las propias cards. */
  font-size: clamp(var(--text-heading-md), 4vw, var(--text-heading-lg));
  font-weight: 600;
  line-height: 1.1;
  text-align: left;                                /* a la izquierda (2026-10-15) */
  color: var(--content-text-default);
}
/* Cabecera de bloque · título a la izquierda + «Browse all…» en la misma línea
   (patrón de estante: Nat Geo, Netflix, MasterClass). Va a la columna ancha,
   alineada con el borde de las cards. Sin el margen grande que el cuerpo da a
   lo ancho: aquí el aire lo marcan la franja y el filete. */
.article-body > .read-next__head {
  grid-column: wide;
  margin-top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-default);
  flex-wrap: wrap;
}
.article-body .read-next__head a.btn { flex: none; text-decoration: none; }
/* Filete entre los dos bloques (variante con Go deeper) · el fino canónico, al
   ancho de las cards, con el mismo aire encima y debajo. */
.article-body > .read-next__rule {
  grid-column: wide;
  height: 0;
  border: 0;
  border-top: 1px solid var(--border-neutral-soft);
  margin-block: clamp(2.5rem, 4vw, 3.5rem);
}
/* Read-next cards = componente del DS `.card--row.card--bare.card--light`
   (horizontal · foto + antetítulo + título + autor · on-light). El layout de
   fila, radios, zoom y ratio los pone el propio componente — aquí solo el
   contexto (grid, filete, color del autor on-light) y el paso a vertical en
   desktop. NO forzamos `display` (el DS ya pone flex; antes un `display:block`
   rompía la fila). */
.article-body a.read-next__card { text-decoration: none; color: inherit; }

/* La foto LLENA su marco. La regla de prosa `.article-body img` (alto
   automático) le ganaba a la del componente (`.card__media img`, alto 100%)
   por ir después con la misma fuerza: una foto más apaisada que el marco
   dejaba un hueco vacío debajo (Irene, 2026-10-14, en Video detail). */
.article-body .card__media img { height: 100%; }

/* Móvil (base) · una columna de cards horizontales, separadas por filete. */
.article-body .read-next__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  margin-top: clamp(2rem, 3.2vw, 2.5rem);
}
.read-next__grid .card { padding-block: 14px; }
.read-next__grid .read-next__card:first-child { padding-top: 0; }
/* …y la última no deja colchón debajo: sin esto la franja tenía 36 arriba y
   50 abajo en el móvil (Irene, 2026-10-14: «por arriba debería haber menos»). */
.read-next__grid .read-next__card:last-child { padding-bottom: 0; }
.read-next__grid .read-next__card:not(:last-child) { border-bottom: 1px solid var(--border-neutral-soft); }
.read-next__grid .card__title {
  color: var(--content-text-default);              /* sobre la franja oscura */
  font-size: var(--text-body-lg);
  transition: opacity var(--dur-instant) var(--ease-out);
}
/* Autor · compacto sin avatar y en gris on-light (el DS lo pinta para oscuro). */
.read-next__grid .author-credit { --_avatar-h: auto; line-height: 1.3; gap: 0; margin: 2px 0 0; font-size: var(--text-mono-md); } /* 12→14 · un peldaño de la rampa mono (QA #129) */
/* EL GRIS, UN PASO MÁS OSCURO SOLO AQUÍ (auditoría, 14-10-2026) · el gris que
   elegimos hoy da 7,0:1 clavados SOBRE BLANCO, y esta banda no es blanca: es
   el gris #F5F7FA. Sobre él, el mismo color cae a 6,53:1 y se queda por debajo
   del mínimo del sistema. No es que el valor esté mal: es que se eligió
   mirando un fondo y aquí hay otro. Sobre esta banda, 10,6:1. */
/* (2026-10-15) La franja pasó a oscuro: el antetítulo vuelve al amarillo de
   serie de la card (como en los listados) y el autor al gris débil de oscuro. */
.read-next__grid .author-credit__name { color: var(--content-text-default-weak); }
.article-body .read-next__grid .card__eyebrow { color: var(--content-text-highlight); }
/* Feedback sutil al hover/focus: el título se aclara un pelín. */
/* Atenuar el título es el gesto del RATÓN («esto es clicable»). Con el teclado
   no: ahí el título es justo lo que hay que leer, y apagarlo era lo que hacía
   que la card entera pareciera oscurecerse (Irene, 15-10-2026). */
/* Dentro de la puerta de ratón: en un teléfono el `:hover` se queda PEGADO
   tras tocar, y este efecto dejaría la pieza apagada o movida para siempre.
   (Revisión a fondo del 16-10-2026, del mismo tipo que el bug del
   photoslider que vio Irene: «la foto no crece, sino más bien lo contrario».) */
@media (hover: hover) {
  .read-next__card:hover .card__title { opacity: 0.75; }
}

/* Móvil · aire entre miniatura y texto (Irene, 2026-10-14). `.card--bare`
   quita el relleno lateral del cuerpo —pensado para la card vertical, con la
   foto ENCIMA— y en la fila del móvil el texto quedaba pegado a la foto. */
@media (max-width: 720px) {
  .read-next__grid .card--row .card__body { padding: 0 0 0 var(--sp-default); }
}

/* Desktop · rejilla vertical de 3 columnas (deshacemos la fila del componente). */
@media (min-width: 721px) {
  .article-body .read-next__grid { grid-template-columns: repeat(3, 1fr); gap: clamp(1.25rem, 2.5vw, 2rem); }
  .read-next__grid .card { flex-direction: column; padding-block: 0; }
  /* Apiladas en el móvil, las cards solo tienen 14px de separación y un filete
     en medio: el aro se queda en 6 para no cruzarlo. En columnas ya no hay
     filete ni vecina debajo, así que puede respirar el doble. */
  .read-next__card { --aro-aire: 12px; }
  .read-next__grid .read-next__card:not(:last-child) { border-bottom: 0; }
  .read-next__grid .card__media { width: auto; aspect-ratio: 16 / 9; }   /* 16:9 = formato del vídeo y de los listados (2026-10-15) */
  /* Texto ARRIBA, no centrado (Irene, 2026-10-14): la card en fila centra su
     cuerpo en vertical; en columna, con títulos de distinto largo, eso bajaba
     el antetítulo de cada card a una altura distinta. */
  .read-next__grid .card__body { padding: 0.9rem 0 0; justify-content: flex-start; }
  /* Sin tamaño propio en escritorio (Irene, 2026-10-14): el título de la
     card mide lo mismo que en los listados — el `--text-body-lg` (22) de
     `.read-next__grid .card__title`. Antes subía a 24 y casi igualaba al
     título del módulo. */
}

/* margin-top pequeño · VENCE a `.article-body > .u-wide + *` (56-80px). */
/* `.read-next__all` (botón bajo las cards) RETIRADO 2026-10-15: el «Browse
   all…» sube a la cabecera, en la línea del título (`.read-next__head`). */

/* ==========================================================================
   9 · TABLAS  (elementos 13, 15)
   ========================================================================== */
/* Cajón scrollable para tablas · una tabla más ancha que su banda (muchas
   columnas o celdas largas) NUNCA rompe la página ni se recorta por el
   `overflow-x: clip` global: scrollea dentro de su propio contenedor.
   Técnica "scroll-shadow" (Lea Verou): dos degradados sólidos del color de
   la superficie en los bordes + dos sombras; el `background-attachment`
   mixto (local/scroll) hace que la sombra de un lado SOLO aparezca cuando
   hay más contenido hacia ese lado → pista visual de "desliza para ver más".
   La tabla interior mantiene su ancho natural (celdas de texto siguen
   ajustando línea); solo scrollea cuando ni así cabe. */
/* `.table-scroll` es una fila flex: [botón ‹] [viewport scrollable] [botón ›].
   Los botones viven en sus PROPIOS gutters a los lados → nunca tapan la tabla.
   El scroll real vive en `__viewport`. */
:is(.article-body, .calculator) .table-scroll {
  display: flex;
  align-items: center;
  gap: clamp(6px, 1.2vw, 12px);
}

:is(.article-body, .calculator) .table-scroll__viewport {
  /* Lo que la sombra NO cubre por arriba lo publica el motor en
     `--tabla-sin-cabecera`: es el borde inferior REAL de la cabecera, medido.
     No se suma aquí el título más la fila de títulos, porque esa cuenta se
     deja fuera el margen que hay entre los dos (daba 72 donde el borde real
     estaba en 82). */
  flex: 1 1 auto;
  min-width: 0;                 /* deja que el viewport encoja (si no, no scrollea) */
  /* SIN recorte por defecto (QA #134): la tabla fluye en la página, así que no
     se puede ocultar ninguna fila sin que se note. Un contenedor con overflow
     recortaría y además rompería el sticky de página de los encabezados.
     El cajón scrollable vuelve solo cuando la tabla NO cabe a lo ancho — lo
     marca el JS con `.is-scrollable` (ver regla inmediatamente debajo). */
  overflow: visible;
  -webkit-overflow-scrolling: touch;
  /* LA SOMBRA ES UNA FRANJA, NO UNA ELIPSE (Irene, 15-10-2026 — su intuición,
     confirmada midiendo). Estaba dibujada como un óvalo tumbado del alto de la
     tabla: valía 38% en el centro y **4,5% a la altura de la fila de títulos**,
     que es justo donde el ojo empieza. Y como se desvanecía con el alto,
     **cuanto más larga la tabla, más débil la pista** — al revés de lo que
     hace falta. (La técnica original de la que salió esta era de alto
     completo; en algún momento se dibujó como elipse y perdió la propiedad.)
     Ahora vale lo mismo de arriba abajo y da igual si la tabla tiene 4 filas
     o 24. El color pasa a la rampa del sistema: antes era un azulado escrito
     a mano que no estaba en ninguna.
     Las franjas blancas NO son decoración: son las que apagan la sombra sola
     al llegar al extremo. No se tocan (y miden más que la sombra: 64 > 48).
     MÁS LIMPIA (Irene, 27-10-2026 · QA 930): al 10 %; al 22 % ensuciaba la
     página. MÁS ANCHA (29-10-2026 · QA 1073): 48px, en 32 apenas se veía. */
  background:
    linear-gradient(to right, var(--gray-white) 24%, transparent) left center,
    linear-gradient(to left,  var(--gray-white) 24%, transparent) right center,
    linear-gradient(to right, color-mix(in srgb, var(--gray-11) 10%, transparent), transparent) left center,
    linear-gradient(to left,  color-mix(in srgb, var(--gray-11) 10%, transparent), transparent) right center;
  background-repeat: no-repeat;
  /* LA SOMBRA EMPIEZA BAJO EL TÍTULO (Irene, 15-10-2026: «que solo aplique a
     todas las filas excepto al título, que se corte justo ahí»). El título de
     la tabla es texto de la página, no parte de la rejilla que se desplaza:
     teñirlo confundía dos cosas distintas. La sombra arranca justo donde
     empieza la cabecera de columnas y baja hasta la última fila.
     Tampoco tiñe la fila de títulos de columna: esa fila **no se desplaza en
     vertical**, se queda pegada mientras las demás pasan por debajo, así que
     sombrearla contaría una historia que no es. La sombra empieza en la
     primera fila de datos.
     Dónde acaba la cabecera lo MIDE el motor: depende de la tipografía, del
     ancho y de los márgenes de por medio, así que no se puede calcular. */
  background-size:
    64px calc(100% - var(--tabla-sin-cabecera, 0px)),
    64px calc(100% - var(--tabla-sin-cabecera, 0px)),
    48px calc(100% - var(--tabla-sin-cabecera, 0px)),
    48px calc(100% - var(--tabla-sin-cabecera, 0px));
  background-position:
    left var(--tabla-sin-cabecera, 0px),
    right var(--tabla-sin-cabecera, 0px),
    left var(--tabla-sin-cabecera, 0px),
    right var(--tabla-sin-cabecera, 0px);
  background-attachment: local, local, scroll, scroll;
}
:is(.article-body, .calculator) .table-scroll__viewport > table { margin: 0; }
/* Cajón horizontal · SOLO cuando la tabla no cabe a lo ancho. En ese caso el
   recorte es inevitable (y deseado: es el gesto de «desliza»), y con él se
   pierde el sticky de página — pero ahí el usuario ya está moviendo la tabla. */
:is(.article-body, .calculator) .table-scroll.is-scrollable .table-scroll__viewport {
  overflow-x: auto;
  overflow-y: hidden;
  /* AL LLEGAR AL FINAL, LA TABLA NO SE PEGA AL BORDE (Irene, 15-10-2026).
     Sangra hasta el borde mientras te desplazas —es lo que invita a hacerlo—
     pero cuando acaba el recorrido la última columna quedaba tocando el
     cristal. Este relleno viaja con el contenido, así que solo se ve al
     final: devuelve el margen de la página. */
  /* El relleno va en el CAJÓN, no en la tabla: una tabla con los bordes
     fundidos (`border-collapse: collapse`) ignora su propio relleno, así que
     el primer intento no hizo nada. En el cajón sí cuenta, y como es relleno
     por la DERECHA solo se ve al llegar al final del recorrido. */
  padding-right: var(--gutter);
}
/* SE PUEDE MOVER CON EL TECLADO. Un cajón que se desplaza y no se puede
   enfocar deja fuera a quien no usa ratón ni dedo — y es un incumplimiento de
   nivel A que salta en cualquier auditoría automática. Safari no lo hace solo.
   `tabindex` lo pone el motor en `script.js`; aquí solo se cuida que el anillo
   amarillo del sistema se vea sobre el blanco del artículo. */
:is(.article-body, .calculator) .table-scroll.is-scrollable .table-scroll__viewport:focus-visible {
  outline: 2px solid var(--anillo-foco);
  outline-offset: 2px;
}
/* (El refuerzo que vivía aquí solo para el teléfono se ha retirado: con la
   sombra en franja, la pista vale igual en los dos tamaños.) */

/* ── Tablas largas · el título y la fila de encabezados no se pierden (QA #128)
   Irene: «si hago scroll, pierdo el título y la fila de títulos del resto de
   columnas». No se puede anclar al viewport de la PÁGINA: `__viewport` recorta
   en vertical, y un contenedor de recorte rompe cualquier `position: sticky`
   de página — mismo patrón que el `overflow-x: hidden` que rompía el sticky de
   todo el sitio. Así que el cajón pasa a tener scroll vertical propio (arriba)
   y aquí se fijan el <caption> y el <thead> DENTRO de él.
   El desfase del thead lo mide el JS de la página en `--table-cap-h`: el
   caption es una línea, pero su alto depende de la tipografía y del ancho. */
/* Cabecera fija de tabla · SOLO en tablas más altas que el viewport (QA #138).
   Una tabla de 4-5 filas cabe entera: fijarle la cabecera no aporta nada. El
   JS marca `.is-tall` y lo remide al redimensionar.

   El anclaje SIGUE A LA TOPBAR (QA #142), con el mismo patrón que ya usan la
   barra de listados y la de progreso en este proyecto: `body:has(.nav.hidden)`.
   Antes estaba clavado a 104px, así que al esconderse la barra quedaba una
   franja blanca enorme por arriba — y hacía falta un parche que tapara las
   filas pasando por detrás. Pegándose exacto al borde de la barra, ese hueco
   no existe y el parche sobra: se ha retirado.

   El final lo resuelve el propio CSS: un elemento `sticky` está limitado por su
   contenedor, así que el <thead> se despega solo al llegar al final de la
   tabla. No hay que programarlo — solo no estorbarlo. */
/* El aire sobre el título va en LAS DOS situaciones (pegado nativo en
   escritorio y copia flotante en móvil): si solo lo llevara una, el bloque
   que se queda arriba mediría distinto en cada sitio y el relevo saltaría. */
:is(.article-body, .calculator) .table-scroll.is-tall .stripped-table > caption { padding-top: 12px; }
:is(.article-body, .calculator) .table-scroll.is-tall:not(.is-scrollable) .stripped-table > caption {
  position: sticky;
  /* `--chrome-bottom` lo publica el JS con el borde inferior REAL de la
     topbar en cada momento (0 cuando está fuera de pantalla). Se midió en vez
     de deducirlo de la clase `.nav.hidden` porque en estas páginas esa clase
     no llega a mover la barra — otra regla le gana — y el título habría
     quedado debajo. Medir no depende de adivinar el mecanismo. */
  top: var(--chrome-bottom, var(--nav-height));
  z-index: 3;
  background: var(--gray-white);
  /* Aire sobre el título cuando está pegado (QA #144). Va como RELLENO, no
     bajando el anclaje: si se bajara, quedaría una franja transparente entre
     la topbar y el título por la que se verían pasar las filas. Como relleno,
     el fondo blanco del título cubre ese aire. El desfase del <thead> se
     recalcula solo: el JS mide `--table-cap-h` con `offsetHeight`, que ya
     incluye este relleno. */
  padding-top: 12px;
}
:is(.article-body, .calculator) .table-scroll.is-tall:not(.is-scrollable) .stripped-table thead th {
  position: sticky;
  top: calc(var(--chrome-bottom, var(--nav-height)) + var(--table-cap-h, 0px));
  z-index: 2;
  background: var(--gray-white);
  box-shadow: inset 0 -1px 0 var(--border-on-light);
}
:is(.article-body, .calculator) .table-scroll__viewport > table { scroll-margin-top: var(--table-cap-h, 0px); }
/* SIN barra de desplazamiento (Irene 2026-10): ensucia el cierre de la tabla.
   En escritorio la tabla cabe y la barra no llega a aparecer; si en una
   ventana estrecha llegara a desbordar, la pista es la misma que en el móvil
   — las columnas cortadas en el borde. Va en dos reglas porque un navegador
   que no conozca el pseudo-elemento descartaría el bloque entero. */
:is(.article-body, .calculator) .table-scroll__viewport { scrollbar-width: none; }
:is(.article-body, .calculator) .table-scroll__viewport::-webkit-scrollbar { display: none; }

/* Botones tipo galería · circulares sobre la superficie clara, en su gutter.
   Ocultos hasta que la tabla se desborda (`.is-scrollable` en el wrapper, lo
   pone el JS). En el extremo alcanzado quedan DESHABILITADOS (atenuados) — así
   no cambian de posición ni desaparecen (layout estable). Touch target 40×40. */
/* (Aquí vivían los estilos de dos flechas ‹ › para mover la tabla. **No
   existen en ningún HTML del sitio** —se quitaron en algún momento y el
   estilo se quedó—, así que eran cinco bloques manteniendo viva una pieza
   que no está. Retirados el 15-10-2026, con la auditoría de tablas delante.
   Si algún día vuelven las flechas, vuelven con su marcado y su decisión. */

/* Figura de tabla · agrupa el cajón scrollable + su línea de FUENTE. Misma
   norma que el DS de charts (`.chart-source`, decisión editorial 2026-07):
   toda figura de datos lleva UNA línea de fuente al pie. Aquí sobre la
   superficie clara del artículo (weak = gray-6, AA). */
/* (No reseteamos el margin de .table-figure: así hereda el margin-top de
   ritmo de `.article-body > .u-wide` (~56-80px) y no queda pegada al párrafo
   anterior. El reset base `.article-body > * { margin: 0 }` ya limpia los
   márgenes laterales por defecto del <figure>.) */
:is(.article-body, .calculator) .table-source {
  margin: 10px 0 0;
  font-family: var(--font-sans);
  font-size: var(--text-mono-md);            /* 14px · igual que chart-source */
  font-weight: 400;
  line-height: var(--lh-body);
  text-align: left;
  color: var(--chart-muted);                 /* el mismo gris que la fuente de un gráfico */
}

/* ── Tabla en MÓVIL (≤720) ────────────────────────────────────────────────
   · A SANGRE: el cajón scrollable rompe el gutter de lectura y va de borde a
     borde, así el corte de columnas llega al borde de la pantalla ("hay más,
     desliza"). El caption se re-inseta para alinear con el texto.
   · Botones ‹ › DEBAJO de la tabla (no a los lados, que robaban ancho): el
     viewport ocupa toda la fila y los botones bajan a una segunda fila a la
     derecha. La línea de fuente sigue en la columna de lectura. */
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) {
  /* A SANGRE por los dos lados (decisión de Irene 2026-10; también en tablet · QA 1032): en reposo el
     contenido arranca en el margen de lectura, y al deslizar de lado llega
     hasta el borde de la pantalla. Eso NO se consigue con un margen —un
     margen deja una franja muerta por la izquierda, con su sombra—, sino
     con el cajón a todo ancho y el margen de lectura metido DENTRO como
     relleno: lo que está antes del relleno se desplaza y desaparece. */
  .article-body .table-scroll {   /* solo en artículos: en el panel no va a sangre */
    flex-wrap: wrap;
    justify-content: flex-end;
    margin-inline: calc(50% - 50vw);
  }
  .article-body .table-scroll__viewport {
    flex-basis: 100%;
    order: 0;
    /* = el borde donde estaba la tabla: el gutter en móvil; en tablet el del carril ancho (QA 917 · 1032) */
    padding-inline: max(var(--gutter), calc((100vw - var(--max-content)) / 2));
  }
  /* El título NO se va con el desplazamiento lateral: se queda a la vista
     para no perder de vista qué tabla se está leyendo (Irene 2026-10).
     Se fija el TEXTO, no el `<caption>`: el caption ocupa todo el ancho de la
     tabla, así que no le queda hueco donde desplazarse y no se quedaría. El
     JS envuelve el título en un `.table-title` justo para esto. */
  :is(.article-body, .calculator) .table-scroll.is-scrollable .stripped-table > caption > .table-title {
    position: sticky;
    left: 0;
    display: inline-block;
  }
}

/* 13 · tabla con líneas · ancho completo, filete fino entre filas */
:is(.article-body, .calculator) .stripped-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-body-sm);
}
/* Título de tabla · misma receta que `.chart-title` del DS de charts (toda
   figura de datos titula igual): sans 22px, weight 600, color ink, alineado
   a la izquierda, al pie superior. */
:is(.article-body, .calculator) .stripped-table caption {
  caption-side: top;
  text-align: left;
  margin-bottom: 0.6rem;
  font-family: var(--font-sans);
  /* UN PELDAÑO MENOS (Irene, 15-10-2026) · iba a 22, el mismo tamaño que el
     subtítulo de sección, y los dos juntos confundían la jerarquía: una tabla
     es un bloque DENTRO de una sección, así que su título debe pesar menos
     que el subtítulo que la presenta. */
  font-size: var(--text-body-md);            /* 18px */
  font-weight: 600;
  color: var(--content-text-on-light-default);
}
:is(.article-body, .calculator) .stripped-table th {
  text-align: left;
  font-weight: 600;
  padding: 0.7rem 1.1rem;
  border-bottom: 1px solid var(--border-on-light);
  color: var(--content-text-on-light-default);
}
:is(.article-body, .calculator) .stripped-table td {
  padding: 0.7rem 1.1rem;
  border-bottom: 1px solid var(--border-on-light);
}
/* FIGURA DE DATOS · tabla y gráfico miden igual (Irene, 26-10-2026 · QA
   878/879/884): título 22 · 600 · texto 18 · fuente 14 en el gris de los
   gráficos, y la fuente arranca en el borde de la tabla (la columna de lectura
   es para los pies de FOTO). Va DETRÁS de las reglas base de la tabla: con el
   mismo peso, delante perdía. Solo en el artículo: las tablas de calculadora
   son densas y conservan su 16. */
.article-body .stripped-table { font-size: var(--text-body-md); }
.article-body .stripped-table caption { font-size: var(--text-body-lg); line-height: var(--lh-heading); margin-bottom: var(--sp-default); }
.article-body .table-figure > .table-source { max-width: none; margin: var(--sp-default) 0 0; }
/* HUECOS DE LA FIGURA, TODOS A 16 (Irene, 27-10-2026 · QA 932): título →
   tabla y tabla → fuente, igual que título → subtítulo → leyenda → dibujo →
   fuente en el gráfico. */
/* TÍTULO Y CABECERA AL BORDE, FILAS DE DATOS CON SU MARGEN (Irene, 27-10-2026 ·
   QA 925): el título y la fila de cabecera empiezan donde empieza el filete;
   las filas de datos conservan el relleno de celda a los dos lados. En móvil
   la tabla va a sangre y su margen de lectura es el relleno del cajón
   (`--gutter`), no el de la celda (QA 910 · 917). */
.article-body .stripped-table th:first-child { padding-left: 0; }
.article-body .stripped-table th:last-child { padding-right: 0; }
/* TEXTO DE DATOS (Irene, 27-10-2026 · QA 926-929): las filas de datos en
   `--text-body-data` (17) y `--content-text-on-light-data` (--gray-9), un
   punto por debajo del título y la cabecera, en escritorio y en móvil. La
   leyenda de los gráficos usa el mismo texto (charts.css). */
.article-body .stripped-table td { font-size: var(--text-body-data); color: var(--content-text-on-light-data); }
/* COLUMNAS AL ANCHO DE SU TEXTO EN MÓVIL (QA 923): la de prosa mide lo que
   ocupa su texto en una línea (`--ancho-natural`, lo mide `script.js`) con el
   tope de 38ch; la corta (`.nowrap`) recupera su ancho aunque sea la última.
   Solo en el artículo: las tablas de calculadora no cambian. */
@media (max-width: 720px) {
  .article-body .table-figure .stripped-table :is(th, td).wrap { min-width: min(38ch, var(--ancho-natural, 38ch)); }   /* `.table-figure`: gana a la regla de «última columna» */
  .article-body .table-figure .stripped-table :is(th, td).nowrap { min-width: 0; max-width: none; white-space: nowrap; }
}

/* ── NORMA de partición de columnas ───────────────────────────────────────
   Las columnas de VALOR ATÓMICO (fechas, horas, números, etiquetas cortas)
   van SIEMPRE en una línea → nunca parten a 2 líneas (ilegible). Solo la
   columna de PROSA parte, y solo si hace falta. Como la columna larga casi
   siempre es la ÚLTIMA (ids/fechas/números primero, descripción al final),
   la última vuelve a `normal` con un `min-width` cómodo — automático, sin
   etiquetar celda a celda. Cuando ni así cabe, la tabla scrollea (cajón),
   pero las fechas siguen en una línea.
   Overrides: `.wrap` fuerza que otra columna parta · `.nowrap` evita que la
   última lo haga. */
:is(.article-body, .calculator) .stripped-table th,
:is(.article-body, .calculator) .stripped-table td { white-space: nowrap; }
:is(.article-body, .calculator) .stripped-table th:last-child,
:is(.article-body, .calculator) .stripped-table td:last-child,
:is(.article-body, .calculator) .stripped-table .wrap {
  white-space: normal;
  min-width: 18ch;
}
:is(.article-body, .calculator) .stripped-table .nowrap { white-space: nowrap; min-width: 0; }

/* Hover de fila (solo puntero) · un tinte sutil en TODA la fila ayuda a seguir
   el dato al recorrer columnas anchas. Gated a `(hover: hover)` → en táctil no
   deja estado "pegado" (y ahí no hay hover). Tinte con el recipe claro del DS. */
@media (hover: hover) {
  :is(.article-body, .calculator) .stripped-table tbody tr {
    transition: background-color var(--dur-instant) var(--ease-out);
  }
  :is(.article-body, .calculator) .stripped-table tbody tr:hover {
    background: color-mix(in srgb, var(--gray-11) 5%, transparent);
  }
}
/* En táctil no hay hover: TOCAR una fila la marca con ese mismo tinte, y otro
   toque la desmarca (Irene, 2026-10-19). Solo color: no abre ni filtra nada.
   La clase la pone `script.js` (bloque «Tablas · fila marcada»). */
/* El tinte (también el del hover) es TRANSPARENTE, no gris mezclado con
   blanco: sobre blanco se ve igual, y deja ver por debajo la sombra de «hay
   más, desliza», que es el fondo del cajón (QA 1072). Opaco, la tapaba. */
:is(.article-body, .calculator) .stripped-table tbody tr {
  -webkit-tap-highlight-color: transparent;
}
:is(.article-body, .calculator) .stripped-table tbody tr.is-marcada {
  background: color-mix(in srgb, var(--gray-11) 5%, transparent);
}

/* 15 · tabla de iconos · ancho automático, sin bordes, filas cortas */
/* LA TABLA DE CHECKS SIGUE EL CARRIL DE LAS LISTAS (Irene, 15-10-2026: «los
   checks no siguen las normas de sangría de los listados»). Es una tabla, no
   una lista, así que arrancaba pegada al margen mientras las viñetas de al
   lado entraban 41,6px. Para el lector son la misma cosa — una enumeración
   con su marca delante — y tienen que alinearse igual: el ✓ ocupa el carril
   del marcador y el texto empieza donde empieza el de cualquier lista. */
.article-body .icons-table {
  width: auto;
  border-collapse: collapse;
}
.article-body .icons-table td {
  border: 0;
  /* 10px entre filas y entre check y texto · la misma respiración que la caja
     de tips (`.callout ul { gap: 10px }`). */
  padding: 5px 0;
  vertical-align: middle;
  /* 18px · el MISMO que la lista de la caja de tips. Iba a 16 y los dos
     checks del artículo no medían igual: dentro del cuerpo, `li` sube a
     `--text-body-md` y el td se quedaba en `--text-body-sm`. */
  font-size: var(--text-body-md);
}
.article-body .icons-table td:first-child {
  /* LA COLUMNA DEL ✓ ES EL CARRIL DEL MARCADOR · mismo ancho que el de las
     listas (2,6rem), con el ✓ centrado dentro como lo está una viñeta. Así el
     texto de los checks arranca EXACTAMENTE donde el de cualquier lista, y el
     ✓ ocupa el sitio del marcador en vez de ir pegado al margen. */
  width: 2.6rem;
  min-width: 2.6rem;
  padding-right: 0;
  text-align: center;
  white-space: nowrap;
}
.article-body .icons-table td:first-child + td { padding-left: 0; }
/* Check de checklist · EXACTAMENTE el mismo que el de la caja de tips
   (`.callout--light li::before`): mismo signo, mismo tamaño, mismo color y la
   misma separación de 10px. Antes iba a 1.15em y en tono fuerte, y los dos
   checks del artículo no se parecían entre sí (Irene, 2026-10-14). */
.article-body .icons-table__check {
  display: inline-block;
  font-weight: 600;
  font-size: inherit;
  line-height: 1;
  text-align: center;
  color: var(--content-text-on-light-weak);
}

/* ==========================================================================
   10 · CÓDIGO  (elemento 18)
   ========================================================================== */
.article-body pre {
  /* MISMO gris que la caja de tips (`.callout--light`) · Irene 2026-10-14:
     el 7% de gris encima lo dejaba notablemente más oscuro y pesaba demasiado
     sobre el fondo de lectura. Un bloque de código es un bloque destacado
     más: comparte superficie con los demás. */
  background: var(--bg-light-default);
  border-radius: var(--radius-s);
  padding: 1rem 1.25rem;
  /* ENVUELVE, NO SE DESPLAZA · en los DOS tamaños (Irene, 15-10-2026: «deben
     verse y comportarse igual»). El desplazamiento lateral dentro de un
     bloque no tiene nada que lo anuncie, así que el final de la línea
     simplemente no existe para quien no lo descubra. */
  white-space: pre-wrap;
  overflow-x: visible;
  word-break: break-word;
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  line-height: 1.5;
}
/* LA SANGRÍA VA POR LÍNEA, NO POR BLOQUE. En un bloque preformateado la
   sangría de primera línea afecta SOLO a la primera línea real: puesta en el
   bloque entero, «ISO» se salía por la izquierda y las otras tres se metían.
   Para que la continuación de una línea larga entre por la izquierda, cada
   línea tiene que ser su propio bloque — por eso el marcado las envuelve.
   11ch: en monoespaciado `ch` es EXACTAMENTE un carácter, y la columna del
   valor empieza en el 11 («ISO» + 8 espacios). Así la continuación cae justo
   bajo su propio valor, no en un punto arbitrario. */
.article-body pre .pre-line {
  display: block;
  padding-left: 11ch;
  text-indent: -11ch;
}
/* Entre línea y línea, el MISMO aire que entre los elementos de una lista
   (12 · Irene, 2026-10-19): en el móvil una línea larga parte en dos, y sin
   ese hueco el corte no se distinguía de un ajuste nuevo. */
.article-body pre .pre-line + .pre-line { margin-top: 0.75rem; }
.article-body pre code { font-family: inherit; font-size: inherit; }
.article-body :not(pre) > code {
  background: var(--bg-light-default);   /* mismo gris que el bloque de código */
  border-radius: var(--radius-s);
  padding: 0.12em 0.4em;
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* ==========================================================================
   11 · ENLACE LIGHTBOX  (elemento 14) · solo un hook de comportamiento
   ========================================================================== */
.article-body a.fancybox {
  display: inline-block;
  cursor: zoom-in;
  text-decoration: none;
}
.article-body a.fancybox img { transition: opacity var(--dur-instant) var(--ease-out); }
/* Dentro de la puerta de ratón: en un teléfono el `:hover` se queda PEGADO
   tras tocar, y este efecto dejaría la pieza apagada o movida para siempre.
   (Revisión a fondo del 16-10-2026, del mismo tipo que el bug del
   photoslider que vio Irene: «la foto no crece, sino más bien lo contrario».) */
@media (hover: hover) {
  .article-body a.fancybox:hover img { opacity: 0.9; }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  .article-body a.fancybox img { transition: none; }
}

/* Visor de foto (lightbox) → ahora en common.css «PÁGINA DE CONTENIDOS». */

/* ==========================================================================
   12 · ÍNDICE (TOC) · portado del "index overlay" de expediciones
   --------------------------------------------------------------------------
   FAB "Contents" (top-left) + barra de progreso (bajo la nav) + drawer
   lateral oscuro con scroll-spy. En ≥1024px el drawer NO tapa: empuja y
   estrecha #main + footer (push drawer). El drawer va en oscuro (--bg-bold)
   también en esta página clara — contrasta y mantiene el mismo componente.
   ========================================================================== */

/* Los anclas del índice no deben quedar bajo la nav fija. */
.article-body [id],
#article-top {
  /* EL SALTO CUENTA TAMBIÉN EL ÁREA SEGURA (Irene, 15-10-2026: «no termina de
     coincidir»). La barra mide `--nav-height`, pero en un iPhone con muesca
     se apoya además sobre el área segura, y ese trozo no estaba en la cuenta:
     el título quedaba por debajo del borde de la barra. */
  /* El borde real de la barra lo mide el motor y lo publica como
     `--ancla-offset`; el cálculo de aquí es solo el respaldo por si el
     JavaScript no llegara. */
  scroll-margin-top: calc(
    var(--ancla-offset, calc(var(--nav-height) + env(safe-area-inset-top, 0px))) + 24px
  );
}

/* Override de página · el contenido reclama el espacio (no hay sidebar fija). */
body { --toc-width: 0px; }

/* ── PUSH DRAWER (escritorio >1024) · estrecha la página, no la tapa ──
   Hasta 1024 (iPad Pro vertical incluido) el panel tapa y entra por la derecha (QA 1040).
   En escritorio también va a la DERECHA (Irene, 30-10-2026 · QA 1096): el texto
   se estrecha por la derecha y su borde izquierdo no se mueve. */
@media (min-width: 1200px), (min-width: 1025px) and (orientation: landscape) {
  /* Transición FLUIDA del estrechado · en sync con el deslizamiento del panel
     (--dur-soft/--ease-soft). Aquí SÍ animamos margin/width (a diferencia de la
     página de expediciones, que lo dejaba instantáneo por el mapa Leaflet +
     parallax): este artículo es estático, reflowear su texto durante 320ms es
     barato y se ve suave. */
  /* Base = curva de CIERRE (--ease-soft, arranque rápido · te gusta).
     width: 100% EXPLÍCITO (no `auto`) · clave para que el ancho ANIME en vez de
     saltar: con `auto` el ancho brincaba de golpe (contenido a la izquierda) y
     solo el margin animaba → "izquierda y luego derecha". Con 100%, ancho y
     margen animan sincronizados y el borde derecho queda fijo → va directo a la
     derecha. */
  #main,
  .footer {
    width: 100%;
    transition: width var(--dur-soft) var(--ease-soft);
  }
  .nav-inner {
    transition: padding-right var(--dur-soft) var(--ease-soft);
  }
  /* Estado abierto = curva de APERTURA (--ease-out, arranque suave · evita el
     tirón abrupto al abrir). Un pelín más largo (--dur-glide) para que respire. */
  body.index-pushed #main,
  body.index-pushed .footer {
    width: calc(100% - 300px);
    overflow-x: clip;
    transition: width var(--dur-glide) var(--ease-out);
  }
  body.index-pushed .nav-inner {
    padding-right: calc(1.5rem + 300px);
    transition: padding-right var(--dur-glide) var(--ease-out);
  }
}
@media (prefers-reduced-motion: reduce) {
  #main, .footer, .nav-inner { transition: none; }
}

/* ── FAB "Contents" · placement (apariencia del .btn del DS) ── */
.index-fab {
  position: fixed;
  top: calc(var(--nav-height, 64px) + 16px);
  /* A LA DERECHA en todos los tamaños (QA 1033 · 1096): todo lo que se lee
     empieza por la izquierda (a la izquierda pisaba el título de las tablas)
     y coincide con el menú principal, que también entra por la derecha. */
  right: var(--gutter);
  z-index: 75;
  gap: 8px;
  box-shadow: 0 2px 10px var(--black-a04);   /* halo suave · como dolomites */
  /* ⚠️ LA BASE ES VISIBLE. Antes este bloque nacía oculto porque venía de la
     hoja de Article, donde el botón espera a que pases la firma del autor.
     Al subirlo al sistema (2026-10-14) ese estado se lo comió TODA página con
     índice: en las tres de Dolomites —que no re-declaran la opacidad— el
     botón desapareció. Esperar es la EXCEPCIÓN, no la norma, y además depende
     de que exista una firma de autor, que Dolomites no tiene.
     Quien espera lo pide con `.is-gated`, y lo pone el motor solo cuando
     encuentra esa firma. */
  transition:
    opacity var(--dur-soft) var(--ease-out),
    visibility 0s linear var(--dur-soft),
    transform var(--dur-soft) var(--ease-out);
}
/* Espera opt-in · solo donde hay firma de autor que pasar. */
.index-fab.is-gated {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.index-fab.is-gated.is-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition:
    opacity var(--dur-soft) var(--ease-out),
    visibility 0s,
    transform var(--dur-soft) var(--ease-out);
}
@supports (top: env(safe-area-inset-top)) {
  .index-fab { top: calc(var(--nav-height, 64px) + env(safe-area-inset-top, 0px) + 16px); }
}
/* Acople a la topbar (como dolomites) · el FAB sube con la nav al ocultarse. */
body:has(.nav.hidden) .index-fab {
  transform: translateY(calc(-1 * var(--nav-height)));   /* sube lo que mide la barra y se queda en el borde del área segura: más arriba, Chrome en iPad lo tapa con su barra encogida (QA 1046) */
}
/* Móvil · Contents = solo el icono del libro, botón cuadrado 40×40 (dolomites). */
@media (max-width: 720px) {
  .index-fab {
    gap: 0;
    padding: 0;
    width: 40px;
    height: 40px;
    min-width: 0;
  }
  .index-fab__label { display: none; }

}

/* Barra de progreso de lectura → ahora en common.css «PÁGINA DE CONTENIDOS». */

/* ── Drawer · panel lateral oscuro, no modal ── */
.index-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: transparent;
  pointer-events: none;
}
.index-overlay__inner {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: min(300px, 86vw);
  background: var(--bg-bold);
  border-left: 1px solid var(--border-neutral-soft);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: translateX(100%);
  pointer-events: auto;
  color: var(--content-text-default);
  transition: transform var(--dur-soft) var(--ease-soft);   /* cierre */
}
/* Abierto con el dedo, el foco queda en el panel: sin aro (QA 1042). */
.index-overlay__inner:focus { outline: none; }
/* Apertura · misma curva/duración que el estrechado de la página (arranque
   suave, no abrupto). */
.index-overlay.is-open .index-overlay__inner {
  transform: translateX(0);
  transition: transform var(--dur-glide) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .index-overlay__inner { transition: none; }
}


/* Header */
.index-overlay__header {
  position: relative;
  flex-shrink: 0;
  padding: clamp(18px, 2.6vh, 32px) clamp(24px, 3vw, 32px) clamp(14px, 1.8vh, 22px);
  padding-right: calc(clamp(24px, 3vw, 32px) + 48px);
  border-bottom: 1px solid var(--border-neutral-soft);
}
.index-overlay__eyebrow {
  margin: 0 0 2px;
  font-family: var(--font-mono);
  font-size: var(--text-mono-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--content-text-default-weak);
  line-height: 1.4;
}
.index-overlay__eyebrow--strong {
  margin: 0 0 clamp(8px, 1.4vh, 16px);
  font-size: var(--text-mono-sm);
  color: var(--content-text-highlight);
}
.index-overlay__title {
  margin: clamp(6px, 1.2vh, 16px) 0;
  font-family: var(--font-sans);
  font-size: var(--text-heading-sm);
  font-weight: 600;
  line-height: 1.1;
  color: var(--content-text-default);
}
.index-overlay__close {
  position: absolute;
  top: clamp(20px, 3vh, 28px);
  right: clamp(20px, 2.5vw, 28px);
}

/* Nav (scrollable) */
.index-overlay__nav {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;   /* al llegar al final, la página de detrás no se mueve (QA 1040) */
  padding: 8px 0 16px;
  scrollbar-width: thin;
  scrollbar-color: var(--gray-7) transparent;
}
.index-overlay__nav::-webkit-scrollbar { width: 6px; }
.index-overlay__nav::-webkit-scrollbar-track { background: transparent; }
.index-overlay__nav::-webkit-scrollbar-thumb { background: var(--gray-7); border-radius: var(--radius-full); }
.index-overlay__nav::-webkit-scrollbar-thumb:hover { background: var(--gray-6); }

/* Lista */
.index-overlay__list { list-style: none; margin: 0; padding: 0; }
.index-overlay__list > li { position: relative; padding-left: 20px; padding-right: 20px; }
.index-overlay__list > li + li { border-top: 0; }
.index-overlay__list a {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: clamp(4px, 0.9vh, 12px) 0;
  text-decoration: none;
  color: var(--content-text-default);
  transition: color var(--dur-instant) var(--ease-out);
}
.index-overlay__list > li > a { padding-right: 36px; }
.index-overlay__list a:hover { color: var(--content-text-highlight); }
.index-overlay__list a:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 4px; }

/* Active state · barra amarilla a la izquierda + texto highlight */
.index-overlay__list > li.is-active::before {
  content: '';
  position: absolute;
  left: 0; top: 8px; bottom: 8px;
  width: 4px;
  background: var(--yellow-5);
  border-radius: 1px;
}
.index-overlay__list > li.is-active > a .index-overlay__label,
.index-overlay__list > li.is-active > a .index-overlay__num,
.index-overlay__list > li.is-active > .index-overlay__row > a .index-overlay__label,
.index-overlay__list > li.is-active > .index-overlay__row > a .index-overlay__num {
  color: var(--content-text-highlight);
}

/* Num + label */
.index-overlay__num {
  flex-shrink: 0;
  width: 2em;
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  font-weight: 500;
  color: var(--content-text-default-weak);
  letter-spacing: 0.02em;
}
.index-overlay__label {
  font-family: var(--font-sans);
  font-size: var(--text-body-md);
  font-weight: 500;
  line-height: 1.3;
}

/* Chevron toggle + fila */
.index-overlay__row { display: flex; align-items: center; gap: 4px; }
.index-overlay__row > a { flex: 1; }
.index-overlay__toggle {
  flex-shrink: 0;
  width: 32px; height: 32px;
  margin-right: -9px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: var(--radius-s);
  color: var(--content-text-default-weak);
  cursor: pointer;
  transition: color var(--dur-instant) var(--ease-out), background var(--dur-instant) var(--ease-out);
}
.index-overlay__toggle:hover { background: var(--white-a04); color: var(--content-text-default); }
.index-overlay__toggle:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
.index-overlay__toggle svg { transition: transform var(--dur-instant) var(--ease-out); }
.index-overlay__list > li.is-expanded > .index-overlay__row .index-overlay__toggle svg { transform: rotate(90deg); }

/* Sub-items · collapsible */
.index-overlay__sub {
  list-style: none;
  margin: 0;
  padding: 0 0 0 calc(2em + 16px);
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-height var(--dur-soft) var(--ease-soft),
              opacity var(--dur-instant) var(--ease-out),
              margin var(--dur-soft) var(--ease-soft);
}
/* El tope solo sirve para animar el despliegue; no es un tamaño. Tiene que
   caber la sublista más larga (User Guide · 15 apartados ≈ 470px): con 400
   se cortaban las últimas entradas. */
.index-overlay__list > li.is-expanded > .index-overlay__sub { max-height: 1200px; opacity: 1; margin: 4px 0 8px; }
@media (prefers-reduced-motion: reduce) { .index-overlay__sub { transition: none; } }
.index-overlay__sub a {
  display: block;
  padding: 6px 0;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: 400;
  line-height: 1.3;
  color: var(--content-text-default-weak);
  text-decoration: none;
  transition: color var(--dur-instant) var(--ease-out);
}
.index-overlay__sub a:hover { color: var(--content-text-default); }
.index-overlay__sub a:focus-visible { outline: 2px solid var(--anillo-foco); outline-offset: 2px; }
.index-overlay__sub li.is-active > a { color: var(--content-text-highlight); font-weight: 500; }

/* Footer sticky · CTA siempre visible */
.index-overlay__footer {
  flex-shrink: 0;
  padding: clamp(16px, 2.5vh, 20px) clamp(24px, 3vw, 32px) clamp(20px, 3vh, 28px);
  border-top: 1px solid var(--border-neutral-soft);
  background: var(--bg-bold);
}
.index-overlay__footer .btn { width: 100%; justify-content: center; }

/* (13 · Etiquetas de elemento · andamiaje de revisión RETIRADO el 2026-10-14.
   El propio código pedía quitarlo «cuando la plantilla esté validada».) */


/* =============================================================================
   COMPONENTE · EBOOK DOWNLOAD  (banner inline + portada CSS + barra sticky)
   Promovido desde lunar-eclipse-guide (2026-08-19). Vive en la hoja compartida
   del artículo para que cualquier página sobre la plantilla `article` lo use.
   · `.ebook-cta`   — banda oscura a sangre (`.u-full`) con portada de libro.
   · `.ebook-cover` — la portada (squircle amarillo + mark PhotoPills en SVG).
   · `.ebook-bar`   — barra sticky slim (abajo) que aparece tras el 1er banner.
   TRANSVERSAL: va en cualquier página, no solo dentro de `.article-body`.
   Las piezas internas se prefijan con `.ebook-cta` (0,2,0) para ganar a la
   prosa del artículo (`.article-body p/img` = 0,1,1); no quitar ese prefijo.
   ========================================================================== */

/* 1 · BANNER (banda a sangre) --------------------------------------------- */
.ebook-cta {
  background: var(--bg-bold);
  color: var(--content-text-default);
  padding: var(--section-pad-y-md) var(--gutter);
  overflow: visible;                          /* deja que el libro asome por arriba */
}
.ebook-cta .ebook-cta__inner {
  max-width: var(--max-article);              /* MISMO ancho que el párrafo de lectura */
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(24px, 4vw, 36px);
  align-items: center;
  justify-items: center;                      /* apilado (móvil/tablet): todo centrado */
  text-align: center;
}
@media (min-width: 721px) {
  .ebook-cta { padding-block: clamp(36px, 4vw, 56px); }
  /* 2 columnas · texto (título + descripción + CTA) | libro · todo a la izq.
     También en tablet vertical (QA 1034); solo el teléfono lo apila. */
  .ebook-cta .ebook-cta__inner {
    grid-template-columns: 1fr auto;
    grid-template-areas: "title book" "body book";
    column-gap: clamp(32px, 4vw, 56px);
    row-gap: 14px;
    align-items: center;
    justify-items: start;
    text-align: left;
  }
  .ebook-cta .ebook-cta__title { grid-area: title; align-self: end; text-align: left; }
  .ebook-cta .ebook-cta__body  { grid-area: body;  align-self: start; text-align: left; }
  /* El libro anclado abajo y más alto que la banda → asoma por arriba. */
  .ebook-cta .ebook-cta__cover { grid-area: book; align-self: end; justify-self: end; }
  .ebook-cta .ebook-cta__cover .ebook-cover {
    width: clamp(170px, 15vw, 200px);
    margin-top: calc(-1 * clamp(72px, 8vw, 120px));
    margin-bottom: calc(-1 * clamp(6px, 1vw, 14px));
  }
}
.ebook-cta .ebook-cta__title {
  font-family: var(--font-sans);
  font-size: clamp(var(--text-heading-md), 4vw, var(--text-heading-lg));  /* 32→40 */
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: var(--tracking-display);
  color: var(--content-text-default);
  margin: 0;
  max-width: 14ch;
}
.ebook-cta .ebook-cta__body { max-width: 42ch; }
.ebook-cta .ebook-cta__lead {
  font-family: var(--font-sans);
  font-size: var(--text-body-lg);            /* 22 */
  font-weight: 300;
  line-height: 1.3;
  color: var(--content-text-default-weak);
  margin: 0 0 clamp(20px, 2.4vw, 28px);
}
.ebook-cta .ebook-cta__cta { flex-wrap: wrap; margin: 0; justify-content: center; }
.ebook-cta .ebook-cta__icon { width: 18px; height: 18px; flex-shrink: 0; }

/* <a class="btn"> dentro del cuerpo: no heredar el enlace editorial (azul+subrayado). */
.article-body a.btn { text-decoration: none; }
.article-body a.btn-primary,
.article-body a.btn-primary:hover,
.article-body a.btn-primary:active { color: var(--gray-9); }
.article-body a.btn-tertiary { color: var(--content-text-default); }

/* 2 · PORTADA DEL LIBRO (CSS puro) · squircle amarillo con el mark PhotoPills
   (SVG en línea) arriba-izq, cinta negra arriba-der y el título abajo. ----- */
/* Apilado (móvil/tablet <1024): el libro va PRIMERO → libro · título · texto ·
   CTA. En desktop el grid-template-areas lo recoloca a la derecha (el `order`
   no afecta a los items colocados por área). */
.ebook-cta .ebook-cta__cover { justify-self: center; order: -1; }
.ebook-cta .ebook-cover {
  position: relative;
  width: clamp(150px, 42vw, 210px);
  aspect-ratio: 3 / 4;
  background: var(--yellow-5);
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-strong);
  padding: clamp(16px, 2.2vw, 22px);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}
.ebook-cta .ebook-cover__logo {
  width: clamp(30px, 4vw, 38px);
  height: auto;
  display: block;
  margin: 0;                                 /* anula el margin de .article-body img */
}
.ebook-cta .ebook-cover__ribbon {
  position: absolute;
  top: 0;
  right: clamp(18px, 3vw, 26px);
  width: clamp(22px, 3vw, 28px);
  height: clamp(40px, 6vw, 54px);
  background: var(--gray-11);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 78%, 0 100%);
}
.ebook-cta .ebook-cover__title {
  font-family: var(--font-sans);
  font-size: clamp(var(--text-body-lg), 2.4vw, var(--text-heading-sm));  /* 22→24 */
  font-weight: 600;
  line-height: 1.1;
  color: var(--gray-11);                     /* texto oscuro sobre amarillo */
  text-wrap: balance;                        /* ajusta las 2 líneas solo, sin <br> */
}

/* Alineación responsive del banner (va al final para ganar por orden de cascada):
   · Teléfono (base): apilado, TODO centrado.
   · Tablet y escritorio (≥721): 2 columnas y CTA a la izquierda (QA 1034). */
@media (min-width: 721px) {
  .ebook-cta .ebook-cta__cta { justify-content: flex-start; }
}

/* 3 · BARRA STICKY (slim · abajo) · aparece (JS `.is-active`) al pasar el 1er
   banner y se oculta cuando el banner final entra en viewport. -------------
   Layout: todos los elementos en un contenedor, 40px de separación, centrados. */
.ebook-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 70;
  background: var(--bg-bold);
  border-top: 1px solid var(--border-neutral-soft);
  transform: translateY(110%);               /* oculta por debajo por defecto */
  /* APAGADA = INEXISTENTE (a11y, oct 2026) · la barra vive fuera de pantalla
     casi toda la lectura, pero lleva dentro un enlace de descarga y un botón
     de cerrar: sin esto se podían tabular mientras la barra estaba escondida.
     Antes se tapaba con `aria-hidden` fijo, que era peor: la ocultaba para
     el lector de pantalla incluso cuando SÍ estaba a la vista, así que un
     usuario de lector nunca sabía que la tenía delante ni podía cerrarla. */
  visibility: hidden;
  transition: transform var(--dur-soft) var(--ease-out),
              visibility 0s linear var(--dur-soft);
}
.ebook-bar.is-active {
  transform: translateY(0);
  visibility: visible;
  transition-delay: 0s;
}
.ebook-bar__inner {
  max-width: var(--max-wide);
  margin: 0 auto;
  /* Tamaño RECUPERADO (Irene, 2026-10-14): la versión de un paso menos quedó
     demasiado apretada. Vuelve a 52px de alto y 8px de aire. */
  min-height: 52px;
  padding: 8px var(--gutter);
  padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));  /* notch/home-indicator */
  display: flex;
  align-items: center;
  justify-content: center;                    /* bloque centrado en la página */
  gap: 12px;                                  /* separación equitativa entre todos los elementos */
}
.ebook-bar__logo {
  width: 22px;
  height: 22px;
  display: block;
  flex-shrink: 0;
}
.ebook-bar__title {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);            /* 16 · tamaño recuperado (Irene 2026-10-14).
    La barra es chrome, no prosa: el mínimo de 16px del DS protege el cuerpo
    del artículo, no una etiqueta de una línea dentro de una barra. */
  font-weight: 600;
  color: var(--content-text-default);
  white-space: nowrap;
}
.ebook-bar__lead {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);            /* 16 · tamaño recuperado (Irene 2026-10-14) */
  font-weight: 400;
  color: var(--content-text-default-weak);
  white-space: nowrap;
}
.ebook-bar__cta { flex-shrink: 0; }
/* Cerrar (Irene 2026-10) · `.icon-btn` del sistema, talla S para que pegue
   con el CTA en `.btn-xs`. Mide 32 pero se toca como 44 (el área táctil va
   de serie en la talla S desde que se incorporó `.btn-xs` al DS). */
/* EL ASPA, EN LA ESQUINA (Irene, 14-10-2026 · primero en el teléfono, luego
   también en escritorio) · montada a caballo del borde superior de la barra,
   como en las barras de aviso de las apps. Sale del renglón, así que no le
   quita sitio al texto —que era el problema en el teléfono— y en escritorio
   deja el bloque centrado de verdad, sin el peso del aspa tirando a un lado.
   Sólida (no cristal): detrás pasa el contenido de la página y el cristal se
   leería distinto sobre cada cosa. */
.ebook-bar__close {
  position: absolute;
  top: -14px;
  right: var(--gutter);
  flex-shrink: 0;
  margin-inline-start: 0;
  background: var(--gray-8);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: 1px solid var(--border-neutral-soft);
}
.ebook-bar__close:hover { background: var(--gray-7); }
.ebook-bar.is-dismissed { display: none; }

/* Móvil · UNA SOLA FILA, con el titular partiendo en dos líneas si hace falta
   (Irene, 14-10-2026) -----------------------------------------------------
   Intento anterior: dos filas con el botón a todo el ancho. Ocupaba 89px y no
   se parecía a la pieza — la barra es chrome fino, no un bloque de venta.
   Con el texto definitivo («Enjoy this ebook — it's free!» + «Download», el
   mismo que el banner del artículo) la frase no cabe de una tirada en los
   343 de un iPhone: 241 solo el titular. Así que el titular —y SOLO él—
   envuelve. El libro, el botón y la × mantienen su sitio; la barra crece de
   53 a ~56px, que sigue siendo una barra fina.
   El lead se retira: en el teléfono la frase corta ya dice lo que hay que
   decir, y la larga duplicaría el mensaje. */
/* Tablet · la frase larga no cabe junto al titular y el botón (QA 1035). */
@media (max-width: 1024px), (orientation: portrait) and (max-width: 1199px) { .ebook-bar__lead { display: none; } }
@media (max-width: 720px) {
  .ebook-bar__inner { gap: 8px; justify-content: flex-start; }
  .ebook-bar__title {
    min-width: 0;
    flex: 1 1 auto;
    line-height: 1.2;
    /* Sin `nowrap`: a 375 y más la frase entra de una tirada y no envuelve
       sola. En una pantalla de 320 (iPhone SE) no cabe de ninguna manera, y
       ahí preferimos que baje a dos líneas antes que cortarla a la mitad. */
    white-space: normal;
  }
  .ebook-bar__cta { flex: 0 0 auto; }
  /* La cuenta que obligó a sacar el aspa del renglón, medida a 375 (343
     útiles): el titular mide 204 de una tirada y con el aspa dentro solo le
     quedaban 166 — por eso partía en dos. Fuera quedan 212, y entra con 8 de
     margen. La regla vive arriba, con el resto del componente. */
}
@media (prefers-reduced-motion: reduce) {
  .ebook-bar { transition: none; }
}

/* ── Índice de contenidos · panel (QA #130) ───────────────────────────────
   Irene: «no me gusta el diseño de Contents dentro del párrafo… ¿alguna forma
   de hacerlo más visual? Quizás con un fondo de color, algún icono que indique
   que nos lleva a esa parte de la página, y que siga estando subrayado».
   Hasta ahora `.article-toc` no tenía NI UNA línea de CSS: era un <ol> suelto
   en el flujo, y por eso se leía como un párrafo más. Aquí se le da superficie
   propia (panel claro del DS), numeración mono, subrayado amarillo igual que
   el resto de enlaces del cuerpo, y una flecha ↓ que dice «te bajo ahí».
   El <h2> «Contents» vive DENTRO del panel, no encima. */
/* SIN FILETE Y SIN SOMBRA (Irene, 2026-10-16). Primero se probó quitar el
   filete y poner la sombra de fondo claro; al verlo: «quita la sombra, creo
   que me gusta más así». Lo separa solo el fondo. */
.article-body .article-toc {
  background: var(--bg-light-default);
  /* El radio de las imágenes del cuerpo (16 · Irene, 2026-10-19); antes 24. */
  border-radius: var(--radius-m);
  padding: clamp(1.25rem, 3vw, 1.75rem) clamp(1.1rem, 3vw, 1.6rem);
}
.article-body .article-toc__title {
  margin: 0 0 0.75rem;
  font-size: var(--text-heading-sm);          /* más contenido que un h2 de sección */
  letter-spacing: -0.01em;
}
.article-body .article-toc__list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* El panel solo lista lo que queda POR LEER: lo que va antes de él (la
     introducción) no entra (Irene, 2026-10-19). Para que su numeración siga
     siendo la del índice lateral, la lista declara cuántas secciones se salta:
     `style="--toc-antes: 1"` → la primera entrada es la 02. */
  counter-reset: toc var(--toc-antes, 0);
  display: grid;
  gap: 2px;
}
.article-body .article-toc__list > li { counter-increment: toc; margin: 0; }
.article-body .article-toc__list > li::marker { content: none; }

.article-body .article-toc__list a {
  /* El índice NO es prosa: anula el lavado amarillo de los enlaces inline, que
     lo alcanzaba por vivir dentro de <li> y teñía la lista entera. */
  background: none;
  box-shadow: none;
  display: grid;
  /* La columna del número se ajusta al número (antes era fija de 2.2rem con
     el «01» dentro a la izquierda: la distancia real al título era ~26px, no
     los 8 del gap). Así la separación es exactamente la que se declara. */
  grid-template-columns: auto minmax(0, 1fr) 26px;
  align-items: baseline;
  gap: 1rem;
  padding: 0.5rem 0.6rem;
  border-radius: var(--radius-s);
  color: var(--content-text-on-light-default);
  /* El subrayado NO va aquí: en CSS se propaga a los hijos y no hay forma de
     quitarlo desde dentro — el número y la flecha salían subrayados. Vive en
     `__label`, que es lo único que debe parecer enlace. */
  text-decoration: none;
  transition: background-color var(--dur-instant) var(--ease-out);
}
/* Subrayado NEUTRO (QA #136): el amarillo aquí competía con el propio texto.
   El amarillo se reserva para los enlaces inline del cuerpo, donde sí hace
   falta distinguirlos de la prosa; en el índice TODO es enlace, así que no
   hay nada que distinguir y basta con el filete discreto. */
.article-body .article-toc__label {
  text-decoration: underline;
  text-decoration-color: var(--content-text-on-light-weak);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: text-decoration-color var(--dur-instant) var(--ease-out);
}
/* el número es cronología, no el enlace */
.article-body .article-toc__list a::before {
  content: counter(toc, decimal-leading-zero);
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  color: var(--content-text-on-light-weak);
}
/* Icono de salto · círculo con flecha (QA #140). El chevron anterior era de
   7px en gris y quedaba más flojo que la flecha que sustituía: el cambio no se
   leía. Un círculo con flecha sí se reconoce como «te llevo ahí» de un vistazo,
   y al ir en gris neutro no compite con el texto. */
.article-body .article-toc__list a::after {
  content: "";
  justify-self: end;
  align-self: center;
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: var(--radius-circle);
  border: 1px solid var(--border-on-light);
  background: var(--gray-white) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14M19 12l-7 7-7-7'/%3E%3C/svg%3E") center / 13px 13px no-repeat;
  transition:
    border-color var(--dur-instant) var(--ease-out),
    transform var(--dur-instant) var(--ease-out);
}
/* Hover discreto (QA #136): antes se encendía en amarillo y era demasiado. */
/* El realce de pasar por encima SOLO donde hay ratón. En una pantalla táctil
   el navegador deja ese estado puesto después del toque: Irene tocó la entrada
   05, volvió, y la 05 se quedaba marcada como si estuviera seleccionada —
   pareciendo «estás aquí», que el índice no dice en ningún momento. */
@media (hover: hover) {
  .article-body .article-toc__list a:hover { background: var(--gray-white); }
}
.article-body .article-toc__list a:hover .article-toc__label {
  text-decoration-color: var(--content-text-on-light-default);
}
.article-body .article-toc__list a:hover::after {
  border-color: var(--content-text-on-light-default);
  transform: translateY(2px);
}
/* La flecha apunta a DONDE ESTÁ la sección, no a su posición en la lista.
   Antes se giraba la del PRIMER elemento, porque en la guía la introducción
   vive encima del panel. En Article y en la referencia el panel va arriba del
   todo y todo queda debajo: la flecha del 01 apuntaba hacia arriba señalando
   contenido que estaba abajo (Irene, 2026-10-14). Ahora lo marca el motor con
   `.is-up`, comparando en el DOM dónde está cada destino. */
.article-body .article-toc__list > li.is-up a::after { transform: rotate(180deg); }
.article-body .article-toc__list > li.is-up a:hover::after { transform: rotate(180deg) translateY(2px); }

/* APARTADOS (opcionales · Irene, 2026-10-19) · cada sección puede llevar sus
   apartados debajo, SIEMPRE a la vista, uno por línea y numerados (2.1, 2.2…:
   el 0 del capítulo sobra aquí). Sin chevron ni flecha: solo se subrayan al
   pasar por encima; el subrayado fijo es de las entradas numeradas. */
.article-body .article-toc__sub {
  list-style: none;
  margin: 0 0 var(--sp-xs);
  /* El número del apartado arranca donde el título de su sección: el ancho
     del «01» (2ch en la mono del número) + el hueco de la fila. La lista va en
     esa mono SOLO para que `ch` mida el número; los enlaces vuelven a sans. */
  font-family: var(--font-mono);
  font-size: var(--text-mono-md);
  padding: 0 0 0 calc(2ch + 1rem);
  counter-reset: sub;
}
.article-body .article-toc__sub li { margin: 0; counter-increment: sub; }
.article-body .article-toc__sub a {
  font-family: var(--font-sans);
  /* Número en su columna fija («7.15» cabe en 4ch): la segunda línea de un
     apartado largo arranca con el texto, no debajo del número. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  column-gap: 0.5em;
  padding: 0.35rem 0.6rem;
  font-size: var(--text-body-sm);
  line-height: 1.3;
  color: var(--content-text-on-light-weak);
}
.article-body .article-toc__sub a::before {
  content: counter(toc) "." counter(sub);
  display: block;
  min-width: 4ch;
  font-family: var(--font-mono);
  font-size: var(--text-mono-sm);
  color: var(--content-text-on-light-weak);
}
.article-body .article-toc__sub a::after { content: none; display: none; }
.article-body .article-toc__sub .article-toc__label { text-decoration-color: transparent; }

/* SUBSECCIONES PLEGABLES (Irene, 2026-10-28 · QA 1004) · el motor pone un
   botón + / − en cada sección que tiene apartados, y los deja CERRADOS de
   entrada. La fila pasa a dos columnas: el enlace y el botón. Discreto: mono,
   gris débil, sin caja; con su área táctil de 40. */
/* Orden de la fila: número · título · + / − · flecha (QA 1005). Si alguna
   fila del panel tiene botón, TODOS los enlaces reservan una columna de 40
   delante de la flecha —así las flechas siguen alineadas— y el botón se
   coloca encima de ese hueco; el motor mide dónde cae (relleno y flecha). */
.article-body .article-toc__list:has(.article-toc__toggle) > li { position: relative; }
.article-body .article-toc__list:has(.article-toc__toggle) > li > a { grid-template-columns: auto minmax(0, 1fr) var(--sp-xl) auto; }
.article-body .article-toc__list:has(.article-toc__toggle) > li > a::after { grid-column: 4; }
.article-body .article-toc__list > li > .article-toc__toggle { position: absolute; }
/* Móvil · sin flechas (se ocultan en ≤720): las filas a todo el ancho y solo
   las que tienen botón se reservan su hueco, con el botón pegado a la derecha. */
@media (max-width: 720px) {
  .article-body .article-toc__list:has(.article-toc__toggle) > li > a { grid-template-columns: auto minmax(0, 1fr); }
  .article-body .article-toc__list > li:has(> .article-toc__toggle) > a { padding-right: var(--sp-xl); }
}
.article-body .article-toc__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-xl);
  height: var(--sp-xl);
  padding: 0;
  border: 0;
  background: none;
  border-radius: var(--radius-s);
  font-family: var(--font-mono);
  font-size: var(--text-mono-lg);
  line-height: 1;
  color: var(--content-text-on-light-weak);
  cursor: pointer;
  transition: color var(--dur-instant) var(--ease-out);
}
.article-body .article-toc__toggle::before { content: "+"; }
.article-body .article-toc__toggle[aria-expanded="true"]::before { content: "\2212"; }
@media (hover: hover) { .article-body .article-toc__toggle:hover { color: var(--content-text-on-light-default); } }
@media (prefers-reduced-motion: reduce) { .article-body .article-toc__toggle { transition: none; } }

/* TOPE DE MEDIA PANTALLA (opcional) · si el panel lleva `.article-toc__more`,
   nunca mide más que media pantalla (menos la topbar): la lista se corta con
   el fundido del catálogo (`--mask-fade-bottom`) y el botón lo despliega
   entero. Lo mide y lo recalcula `script.js`; sin JavaScript se ve todo. */
.article-body .article-toc.is-cortado .article-toc__list {
  overflow: hidden;
  -webkit-mask-image: var(--mask-fade-bottom);
          mask-image: var(--mask-fade-bottom);
}
.article-body .article-toc__more { margin-top: var(--sp-default); }
/* Qué parte de la pantalla (menos la topbar) puede ocupar el panel antes de
   cortarse: media en tablet y escritorio; tres cuartos en el móvil, que es
   más vertical (QA 1009). El motor lo lee de aquí. */
.article-body .article-toc { --toc-tope: 0.5; }
@media (max-width: 720px) { .article-body .article-toc { --toc-tope: 0.75; } }
/* En móvil no hay flecha y la fila aprieta: hueco número → título a 10px
   (ver más abajo). */
@media (max-width: 720px) {
  .article-body .article-toc__sub { padding-left: calc(2ch + 10px); }
}

@media (prefers-reduced-motion: reduce) {
  .article-body .article-toc__list a::after { transition: none; }
}

/* ── El panel en móvil (Irene, 2026-10-14: «queda todo muy apretado, no es
   muy útil tal y como está») ─────────────────────────────────────────────
   Con 375 de pantalla el panel tiene 287 útiles, y de esos el disco de la
   flecha más su separación se comían 42 — el 15%. La columna de texto
   quedaba en 155px y los títulos caían a tres y cuatro líneas, con solo 2px
   entre entradas: un muro.
   En el teléfono la flecha SOBRA: el panel entero son enlaces y nadie duda de
   que un toque lleva a algún sitio. Quitándola el título recupera ese 15%, y
   el aire entre entradas pasa a separarlas de verdad. */
@media (max-width: 720px) {
  .article-body .article-toc__list a::after { display: none; }
  .article-body .article-toc__list a {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 10px;                      /* el número pega más al título */
    padding: 10px 8px;              /* 10 + 10 + 20 de línea = 40px justos, el
                                       mínimo táctil del sistema. Con 9 el
                                       panel encogía 22px más, pero las filas
                                       bajaban a 38 y esa regla no se salta. */
    line-height: 1.25;              /* es navegación, no prosa */
  }
  /* ALTO · el panel ocupaba media pantalla (Irene, 14-10). Tres ajustes, y
     ninguno baja del mínimo para el dedo:
       · las entradas se tocan (sin hueco entre ellas)
       · el interlineado baja de 1,47 a 1,25 — es una lista de navegación, no
         prosa, y ahí el aire generoso no ayuda a leer, estorba
       · el relleno de cada entrada baja a 9px
     Medido en la guía de eclipses a 375 (11 entradas, títulos largos):
     el panel pasa de 789px a 694px. Lo que queda es la LONGITUD de los
     títulos — con nombres de tres líneas no hay interlineado que lo arregle. */
  .article-body .article-toc__list { gap: 0; }
  /* El texto, al tamaño del cuerpo también en móvil (Irene, 2026-10-19: «pon
     el mismo tamaño al texto que el resto del texto base»). Antes bajaba a 16. */
  /* La caja del panel y su título también aprietan un paso. */
  .article-body .article-toc { padding: 1.1rem 1rem; }
  .article-body .article-toc__title { margin-bottom: 0.5rem; }
}

/* ── Cita destacada oscura · texto al ancho de lectura (QA #135) ──────────
   La banda sigue yendo a sangre (es ritmo editorial), pero su contenido se
   acota a `--max-article` y se centra, igual que un párrafo. Antes el texto
   llegaba a ~900px contra los 720 del cuerpo. Se resuelve con el padding, no
   con un wrapper: dentro del <blockquote> el texto es un nodo suelto. */
.article-body blockquote.dark {
  padding-inline: max(var(--gutter), calc((100% - var(--max-article)) / 2));
}

/* El botón flotante «Contents» se aparta mientras hay una cabecera de tabla
   pegada: vive 16px bajo la topbar, justo donde ahora va la cabecera, y le
   tapaba la primera columna (medido: 125px). Dos elementos flotantes no pueden
   disputarse la misma esquina — mientras lees una tabla, manda la tabla.
   Vuelve solo al salir de ella. */
body.has-pinned-table .index-fab {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* ── Móvil · tabla de datos (QA #149) ─────────────────────────────────────
   El texto vuelve al tamaño de escritorio: reducirlo ganaba poco y la tabla
   se desplaza igualmente. Lo que sí se mantiene es que la columna de texto
   largo NO envuelva: si envuelve, es ella —que está fuera de pantalla— la que
   decide el alto de cada fila, y se ven huecos desiguales sin motivo. */
@media (max-width: 720px) {
  /* La columna de texto largo se acota (QA #151). Con `nowrap` las filas
     quedaban iguales, pero UNA sola celda de 538px estiraba la columna a 573
     —la mediana es 222— y las otras 22 filas se veían medio vacías: ese era
     el «espacio en blanco al final» que se notaba al desplazar.
     Acotada a 34ch: 21 de 23 celdas siguen cabiendo en una línea, y solo las
     dos excepcionales envuelven. La columna baja de 573 a ~300px. */
  /* EL BLOQUE DE CÓDIGO ENVUELVE, no se desplaza (Irene, 15-10-2026). En el
     teléfono el bloque de exposición (ISO / Aperture / Shutter / WB) medía 510
     de ancho en 358 de pantalla, así que se quedaba a medias y había que
     arrastrarlo de lado — y ahí no hay nada que indique que se puede. Son
     cuatro pares dato-valor: caben partidos. */
  /* UNA FOTO VERTICAL CABE EN LA PANTALLA (Irene, 15-10-2026: «son tan altas
     que habrá pantallas en las que haya que hacer scroll para verlas por
     completo»). Se acota al alto del viewport del teléfono dejando sitio para
     la topbar y un respiro: con `svh` la medida es la del viewport pequeño,
     así que la foto cabe incluso con la barra del navegador desplegada. */
  .article-body figure.portrait img,
  .article-body .two-figures:not(.screenshot) .portrait img {
    /* IMAGEN **Y** PIE EN LA MISMA PANTALLA (Irene, 15-10-2026). El 78svh de
       antes dejaba la figura en 669px y no cabía con su pie en una pantalla
       corta; 62svh se quedó corto y asomaba ya la foto siguiente. Con 84svh
       la figura entera —foto MÁS pie— llena la pantalla por debajo de la
       barra sin dejar ver lo que viene detrás. */
    max-height: calc(84svh - var(--nav-height));
    width: auto;
    margin-inline: auto;
  }

  /* «Tap any frame to inspect it full-size:» llevaba la miniatura PEGADA al
     final de la frase, dentro del mismo párrafo: parecía un error de montaje.
     La miniatura pasa a su propia línea con aire. */
  .article-body p > a.fancybox {
    display: block;
    margin-top: 1rem;
    /* Al convertirlo en bloque se llevó consigo el realce amarillo de los
       enlaces de prosa, y quedó una banda amarilla detrás de la miniatura que
       parecía un error (Irene, 15-10-2026). Una foto que se amplía no es un
       enlace de texto: no lleva ese tratamiento. */
    background: none;
    box-shadow: none;
    width: fit-content;
  }

  /* LAS VIÑETAS Y LOS NÚMEROS SE QUEDAN DONDE ESTABAN (Irene, 15-10-2026:
     «lo que tenías que hacer era darle un poquito de margen izquierdo a los
     checks, no mover los bullet points a la izquierda»). El intento de llevar
     el marcador al margen de la página queda retirado; la sangría del ✓ se
     resuelve en la caja de aviso, que es donde estaba el desajuste. */

  :is(.article-body, .calculator) .stripped-table th:last-child,
  :is(.article-body, .calculator) .stripped-table td:last-child,
  :is(.article-body, .calculator) .stripped-table .wrap {
    white-space: normal;
    /* Ancho FIJADO, no solo acotado: con `max-width` a secas el navegador la
       encogía al mínimo (118px) y las filas se partían en 3-6 líneas. Con
       suelo y techo iguales la columna mide lo que debe. */
    min-width: 38ch;
    max-width: 38ch;
  }
}

/* ── Cabecera flotante de tabla (QA #148) ─────────────────────────────────
   El problema: con desplazamiento lateral, la cabecera no puede fijarse al
   scroll de la página — un contenedor con `overflow` lo impide. Probadas las
   5 combinaciones de overflow: la única que mantiene el pegado es `clip`, y
   `clip` no deja desplazar. En CSS puro no hay salida.
   La salida es una COPIA de la fila de encabezados, flotando por encima de la
   tabla y desplazándose en horizontal a la vez que ella. La tabla real no se
   parte en dos (eso rompería la relación encabezado↔celda para los lectores
   de pantalla): la copia es puramente decorativa y va con `aria-hidden`, así
   que quien usa lector sigue leyendo la tabla original. */
.table-sticky-head {
  position: fixed;
  z-index: 60;                /* bajo la topbar (100), sobre el contenido */
  overflow: hidden;
  pointer-events: none;       /* no intercepta el desplazamiento de la tabla */
  background: var(--gray-white);
  border-bottom: 1px solid var(--border-on-light);
  /* SOLO por abajo: una sombra difusa derrama también por los lados y se veía
     un filo oscuro pegado al margen izquierdo (Irene 2026-10). El desplazado
     negativo recorta el derrame lateral. */
  box-shadow: 0 6px 6px -6px rgba(10, 10, 10, 0.12);
}
/* Vive DENTRO de `.article-body` para heredar los estilos de la tabla, y eso
   la hacía caer en `.article-body > * + *` (margin-top 2-3rem) — que empujaba
   la copia 56px por debajo de la topbar: ESE era el salto al relevarse
   (y variaba, porque tras un `.u-full` el margen sube a 3.5-5rem). */
/* El inflado de texto de iOS se apaga para TODO el sitio en reset.css
   (antes se apagaba solo aquí, en las tablas). Ver QA #152 y #159. */
:is(.article-body, .calculator) .table-sticky-head { margin: 0; }
.table-sticky-head[hidden] { display: none !important; }
.table-sticky-head__rail { will-change: transform; }
.table-sticky-head table { border-collapse: collapse; margin: 0; }
/* En la copia el título ya está fuera del carril, así que no debe pegarse a
   nada: si quedara `sticky` buscaría el scroll de la PÁGINA y se iría al 0. */
.table-sticky-head .table-title { position: static; }

/* La copia flotante reproduce lo que queda fijo en escritorio: el título con
   su aire por arriba y, debajo, la fila de columnas con su espaciado. */
.table-sticky-head caption {
  text-align: left;
  /* Mismo sangrado lateral que el título real: ese `padding-inline` vive en
     una regla colgada de `.table-scroll`, y la copia no está ahí dentro. */
  padding: 12px var(--gutter) 0;   /* = sangrado de la tabla real */
}

/* ============================================================================
   TABLA EN EL PANEL DE CALCULADORA · `.calculator--table` (Irene, 20-10-2026 ·
   QA 642-652). El resultado de la calculadora ES la Tabla de datos, dentro del
   panel y sobre su degradado. La usan Depth of Field Table y Hyperfocal
   Distance Table. Marcado: `figure.table-figure[data-tabla-plegable]` (valor =
   filas visibles plegada, 8 por defecto) > `.table-scroll` > viewport > tabla,
   dos `.tabla-carril` con su `.tabla-flecha`, y `.table-more` con `[data-mas]`.
   El motor vive en `script.js` («Tablas en el panel»); la página solo pinta
   las filas y avisa con el evento `tabla:cambio`.
   ============================================================================ */
.calculator--table .table-figure { margin: var(--sp-l) 0 0; }
/* Título en mono (el de los rótulos de resultado) y su pie debajo, en sans. */
.calculator .stripped-table caption { margin-bottom: var(--sp-default); font-family: var(--font-mono); font-size: var(--text-mono-xl); font-weight: 500; text-transform: uppercase; letter-spacing: 0.08em; color: var(--content-text-default-weak); }
.calculator .stripped-table caption .tabla-pie { display: block; margin-top: var(--sp-xs); font-family: var(--font-sans); font-size: var(--text-body-xs); font-weight: 400; text-transform: none; letter-spacing: 0; }
/* Sobre el panel, sin fondo propio: colores de la tabla en oscuro. */
.calculator .table-scroll__viewport { background: none; }
.calculator .stripped-table th { color: var(--content-text-default); }
.calculator .stripped-table td { color: var(--content-text-default-weak); }
.calculator .stripped-table th,
.calculator .stripped-table td { border-bottom-color: var(--border-neutral-soft); }
.calculator .stripped-table tbody th { font-weight: 600; }
@media (hover: hover) { .calculator .stripped-table tbody tr:hover { background: var(--white-a04); } }
.calculator .stripped-table tbody tr.is-marcada { background: var(--white-a04); }
/* Fondo de lo que se queda PEGADO (título, fila de títulos, primera columna):
   el degradado del panel recortado a la altura de cada pieza. Así tapa las
   filas sin que se vea un corte; un fondo plano o un cristal se notan.
   Las alturas las publica el motor: --panel-h, --fy (inicio de la caja),
   --row-y (cada fila), --sy (desplazamiento interno), --fy-flotante y --cap-h. */
.calculator [data-tabla-plegable].is-plegada caption { background: var(--gradient-subtle) 0 calc(-1 * var(--fy, 0px)) / 100% var(--panel-h, 100%) no-repeat; }
.calculator [data-tabla-plegable].is-plegada thead th { background: var(--gradient-subtle) 0 calc(-1 * (var(--fy, 0px) + var(--table-cap-h, 0px))) / 100% var(--panel-h, 100%) no-repeat; }
.calculator .table-sticky-head { background: var(--gradient-subtle) 0 calc(-1 * var(--fy-flotante, 0px)) / 100% var(--panel-h, 100%) no-repeat; box-shadow: none; }
/* Móvil: celdas más estrechas, caben unos tres valores junto a la columna fija. */
@media (max-width: 720px) { .calculator .stripped-table :is(th, td) { padding: 0.7rem 0.75rem; } }
/* La unidad del título de la primera columna va en su propia línea, en todos
   los tamaños («Focal / length / (mm)»): la columna fija se estrecha y deja
   ver más columnas de datos (QA 1100). */
.calculator .tabla-unidad { display: block; }
/* PRIMERA COLUMNA FIJA en todos los tamaños: la magnitud de cada fila no se va
   al deslizar de lado. */
.calculator [data-tabla-plegable] tbody > tr > th { position: sticky; left: 0; z-index: 1; box-shadow: inset -1px 0 0 var(--border-neutral-soft); background: var(--gradient-subtle) 0 calc(-1 * (var(--fy, 0px) + var(--row-y, 0px) - var(--sy, 0px))) / 100% var(--panel-h, 100%) no-repeat; }
.calculator [data-tabla-plegable] thead > tr > th:first-child { position: sticky; left: 0; z-index: 3; box-shadow: inset -1px 0 0 var(--border-neutral-soft); background: var(--gradient-subtle) 0 calc(-1 * (var(--fy, 0px) + var(--table-cap-h, 0px))) / 100% var(--panel-h, 100%) no-repeat; }
/* La fila señalada (hover con ratón, toque en táctil) se aclara ENTERA, también
   la celda fija: su aclarado va encima de su fondo opaco. */
.calculator [data-tabla-plegable] tbody > tr.is-marcada > th { background: linear-gradient(var(--white-a04), var(--white-a04)) 0 0 / 100% 100% no-repeat, var(--gradient-subtle) 0 calc(-1 * (var(--fy, 0px) + var(--row-y, 0px) - var(--sy, 0px))) / 100% var(--panel-h, 100%) no-repeat; }
@media (hover: hover) {
  .calculator [data-tabla-plegable] tbody > tr:hover > th { background: linear-gradient(var(--white-a04), var(--white-a04)) 0 0 / 100% 100% no-repeat, var(--gradient-subtle) 0 calc(-1 * (var(--fy, 0px) + var(--row-y, 0px) - var(--sy, 0px))) / 100% var(--panel-h, 100%) no-repeat; }
}
.calculator .table-sticky-head thead th:first-child { position: relative; z-index: 1; background: var(--gradient-subtle) 0 calc(-1 * (var(--fy-flotante, 0px) + var(--cap-h, 0px))) / 100% var(--panel-h, 100%) no-repeat; }
/* El título no se va al deslizar la tabla de lado (en la tabla y en su copia). */
.calculator .stripped-table > caption > .table-title { position: sticky; left: 0; display: inline-block; }
/* PLEGADA: caja baja con desplazamiento propio en los dos sentidos; título y
   fila de títulos pegados arriba de la caja. La copia flotante solo sirve
   desplegada. */
.calculator [data-tabla-plegable].is-plegada .table-scroll__viewport { max-height: var(--alto-plegado, 400px); overflow: auto; }
.calculator [data-tabla-plegable].is-plegada caption { position: sticky; top: 0; z-index: 3; padding-top: var(--sp-xs); }
.calculator [data-tabla-plegable].is-plegada thead th { position: sticky; top: var(--table-cap-h, 0px); z-index: 2; }
.calculator:has([data-tabla-plegable].is-plegada) .table-sticky-head { display: none !important; }
/* Corte por la derecha y por abajo: se desvanece el CONTENIDO (sirve sobre el
   degradado). Al llegar a cada final, ese corte se quita. */
.calculator [data-tabla-plegable].fade-r .table-scroll__viewport { -webkit-mask-image: linear-gradient(to left, transparent, var(--gray-black) 4.5em); mask-image: linear-gradient(to left, transparent, var(--gray-black) 4.5em); }
.calculator [data-tabla-plegable].fade-b .table-scroll__viewport { -webkit-mask-image: var(--mask-fade-bottom); mask-image: var(--mask-fade-bottom); }
.calculator [data-tabla-plegable].fade-r.fade-b .table-scroll__viewport {
  -webkit-mask-image: linear-gradient(to left, transparent, var(--gray-black) 4.5em), var(--mask-fade-bottom);
          mask-image: linear-gradient(to left, transparent, var(--gray-black) 4.5em), var(--mask-fade-bottom);
  -webkit-mask-composite: source-in; mask-composite: intersect;
}
.calculator .table-more { margin-top: var(--sp-default); display: flex; justify-content: center; }
/* FLECHAS a los lados (sin trackpad no hay gesto horizontal). Cada una va en un
   CARRIL a todo el alto de la tabla y se queda a media pantalla con `sticky`:
   si la moviera el JS al desplazar, iría un fotograma por detrás y temblaría. */
.calculator .table-scroll { position: relative; }
.calculator .tabla-carril { position: absolute; top: 0; bottom: 0; width: var(--sp-xl); z-index: 4; pointer-events: none; }
.calculator .tabla-carril--izq { left: calc(-1 * var(--sp-l)); }
.calculator .tabla-carril--der { right: calc(-1 * var(--sp-l)); }
.calculator .tabla-flecha { position: sticky; top: calc(50vh - var(--sp-l)); pointer-events: auto; color: var(--color-action-default); }
/* Plegada: centrada en la caja. */
.calculator [data-tabla-plegable].is-plegada .tabla-flecha { position: absolute; top: 50%; transform: translateY(-50%); }
.calculator .tabla-flecha[hidden] { display: none; }
