/* Sinsonte — landing MOTION + INTERACTIVITY layer (Sep 2026).
 *
 * Loaded only by the home page, on top of tokens.css + site2.css. Everything
 * here serves one brief: "the page should be a working, scrubbable simulation
 * of the real product, not decoration" — so the three sections this file
 * dresses (#llamada, #pregunta, #informe) are a single act sharing one clock,
 * and the rest is the page-wide motion craft that makes them feel of a piece
 * with the sections around them.
 *
 * Four rules this file is written against, in priority order:
 *
 *   1. NOTHING IS HIDDEN THAT ONLY JS CAN REVEAL. Every selector that hides
 *      content is gated on `html.js` (set before first paint by the inline
 *      <head> script) or on a class JS itself adds. A visitor with blocked or
 *      broken JS reads the full transcript, all four answers and the finished
 *      report as plain HTML. This is the same contract site2.css's `.reveal`
 *      already follows (website-review-2026-09.md §3) and the reason the
 *      2026-09 round existed at all.
 *   2. NO LAYOUT SHIFT. The transcript "types" by fading pre-laid-out word
 *      spans in, never by inserting text; the report's skeleton is sized to
 *      the document it becomes; every frame reserves its own height.
 *   3. `prefers-reduced-motion: reduce` gets a fully usable page, not a
 *      degraded one: no typing, no autoplay, no scroll-driving, and every
 *      state still reachable by clicking.
 *   4. Zero network dependencies. No CDN, no font, no image — the CSP in
 *      landing/_headers (default-src 'none') forbids all three, and that is a
 *      feature.
 */

:root {
  /* One shared easing set for everything added this round. Spring is a real
   * overshoot (the 1.28 control point) and is only ever used on transforms of
   * small elements — never on opacity, never on anything text sits still in. */
  --ease-spring: cubic-bezier(0.22, 1.28, 0.36, 1);
  --ease-emph: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-quick: 150ms;
  --dur-mid: 320ms;
  --dur-long: 620ms;

  /* Page-wide cursor wash. Grana at document weight on Papel; on Tinta the
   * brand goes to the measured rose (tokens.css: Grana never sits on Tinta). */
  --lift-shadow: 0 2px 4px rgba(25, 21, 25, 0.06), 0 26px 52px -22px rgba(25, 21, 25, 0.34);
}
:root[data-theme="dark"] { --lift-shadow: 0 2px 4px rgba(0, 0, 0, 0.4), 0 26px 52px -20px rgba(0, 0, 0, 0.7); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --lift-shadow: 0 2px 4px rgba(0, 0, 0, 0.4), 0 26px 52px -20px rgba(0, 0, 0, 0.7); }
}

/* ══ Page-wide cursor wash ═════════════════════════════════════════════════
 * Founder (Sep 2026 round 1): "the effect of the mouse Grana colour behind
 * the first view — I love it — can we have it for all the background
 * everywhere in the page?" Round 2, after that first pass shipped as two
 * layers (an intense one clipped to the hero box + a fainter one behind the
 * rest of the page): "in the first view … where the grana colour is more
 * intense I like that kind of colour, but after that it fades and looks
 * like a rectangle contains it, not natural." The seam was the hero box's
 * own edge. Fix: ONE layer, full intensity, no clip — a single fixed,
 * full-viewport, pointer-events:none layer built from JS (js/motion.js — no
 * HTML change needed per page, so any page that loads this file gets it)
 * and painted BEHIND every other element (z-index: -1, an explicit negative
 * stacking context — the only way to guarantee it paints under plain,
 * non-positioned flow content like <main>/<footer>, not just under other
 * positioned elements). Same radius and alpha the founder liked in the
 * hero's first view, now simply carried everywhere instead of stopping at a
 * box edge — over the hero it looks identical to before; scroll past it and
 * it just continues. GPU-cheap: one element, one property changes per
 * frame, so nothing else on the page repaints. Idles into a slow drift when
 * the pointer stops so it's never a dead flat wash on a page nobody is
 * scrubbing. Off on coarse pointers and reduced motion. ══════════════════ */
:root {
  --glow-page: rgb(192 20 65 / 0.085);
  --glow-page-idle: rgb(192 20 65 / 0.077);
}
:root[data-theme="dark"] { --glow-page: rgb(240 137 159 / 0.10); --glow-page-idle: rgb(240 137 159 / 0.09); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --glow-page: rgb(240 137 159 / 0.10); --glow-page-idle: rgb(240 137 159 / 0.09); }
}
@property --pwx { syntax: '<percentage>'; inherits: true; initial-value: 50%; }
@property --pwy { syntax: '<percentage>'; inherits: true; initial-value: 42%; }
.pagewash {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(26rem 18rem at var(--pwx, 50%) var(--pwy, 42%), var(--glow-page), rgb(0 0 0 / 0) 70%);
  transition: background-position var(--dur-long) linear;
}
.pagewash.is-idle {
  background: radial-gradient(30rem 22rem at var(--pwx, 50%) var(--pwy, 42%), var(--glow-page-idle), rgb(0 0 0 / 0) 70%);
}
@media (prefers-reduced-motion: no-preference) {
  .pagewash.is-idle { animation: pagewash-drift 26s var(--ease-emph) infinite alternate; }
}
@keyframes pagewash-drift {
  0%   { --pwx: 38%; --pwy: 30%; }
  50%  { --pwx: 64%; --pwy: 54%; }
  100% { --pwx: 42%; --pwy: 66%; }
}
@media (pointer: coarse), (prefers-reduced-motion: reduce) { .pagewash { display: none; } }

/* ══ Page-wide micro-interactions ═════════════════════════════════════════
 * Hover lift on the surfaces that are already cards, using the shadow token
 * above so the four framed things on this page (mockup, screenshots, download
 * cards, project card) rise the same way. Transform-only, so no repaint of
 * the image inside. ═════════════════════════════════════════════════════ */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .appmock, .shot__media, .shareshot, .get__i, .projcard, .callframe, .reportframe {
    transition: transform var(--dur-mid) var(--ease-spring), box-shadow var(--dur-mid) var(--ease-out);
  }
  .shot__media:hover, .shareshot:hover, .get__i:hover, .projcard:hover {
    transform: translateY(-3px); box-shadow: var(--lift-shadow);
  }
  .btn, .simbtn, .colibri__chip, .cite { transition: transform var(--dur-quick) var(--ease-spring), background-color var(--dur-quick) var(--ease-out), border-color var(--dur-quick) var(--ease-out), color var(--dur-quick) var(--ease-out); }
  .btn--primary:hover, .btn--ghost:hover { transform: translateY(-1px); }
  .proof__item { transition: transform var(--dur-quick) var(--ease-out); }
  .proof__item:hover { transform: translateX(2px); }
}

/* Entrance for the two non-sticky simulation frames, done the 2026-idiomatic
 * way where the browser has it: a scroll-driven animation with no JS at all,
 * no observer, and therefore no window in which content could be left hidden
 * by a script that failed. Gated on `html.js` only so that a no-JS visitor
 * never enters the `both` fill state; gated on @supports so nothing else
 * sees it. The sticky call frame deliberately does NOT use this — a sticky
 * subject's view() progress freezes while it is pinned, which is exactly the
 * kind of half-faded ghost §3 was written about. It gets the IO class below. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    html.js .reportframe, html.js .colibri {
      animation: sim-rise linear both;
      animation-timeline: view();
      animation-range: entry 12% entry 62%;
    }
  }
}
@keyframes sim-rise {
  from { opacity: 0.25; transform: translateY(22px) scale(0.99); }
  to { opacity: 1; transform: none; }
}

/* ══ The simulation — shared shell ════════════════════════════════════════ */
.sim { max-width: var(--page-max); margin-inline: auto; padding: var(--space-2xl) var(--space-gutter); }
.sim__head { max-width: 54ch; }
.sim__head .h2 { margin-top: var(--space-3xs); font-size: var(--text-xl); line-height: 1.2; }
.sim__head .body { margin-top: var(--space-2xs); }
.sim__note { margin-top: var(--space-sm); font-size: var(--text-xs); color: var(--color-muted); max-width: 46ch; line-height: 1.5; }

/* The scroll driver. Height only exists when JS is running AND motion is
 * allowed — otherwise this is a plain, auto-height block with the frame
 * sitting in it, which is the whole no-JS/reduced-motion story. */
.sim__scroller { position: relative; margin-top: var(--space-lg); }
html.js .sim__scroller { height: var(--sim-h, 230vh); }
@media (prefers-reduced-motion: reduce) { html.js .sim__scroller { height: auto; } }
@media (max-width: 40rem) { :root { --sim-h: 190vh; } }
.sim__sticky { position: sticky; top: calc(var(--bar-h) + var(--space-sm)); }

/* ══ 1 · The call frame ═══════════════════════════════════════════════════ */
.callframe {
  border: var(--rule-hair) solid var(--color-rule-strong);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(25, 21, 25, 0.05), 0 20px 44px -20px rgba(25, 21, 25, 0.28);
}
/* Entrance: TRANSFORM ONLY, deliberately. An opacity entrance would need a
 * timed failsafe to guarantee it can never leave the frame dimmed (the §3
 * bug), and a failsafe that fires two seconds after load ends up firing for
 * almost everyone, which means paying for the risk and not getting the
 * animation. A 14 px lift cannot hide anything, so the worst case if the
 * class never arrives is a frame sitting 14 px low — invisible, and no
 * failsafe required. */
@media (prefers-reduced-motion: no-preference) {
  html.js .callframe { transform: translateY(14px) scale(0.995); transition: transform var(--dur-long) var(--ease-out); }
  html.js .callframe.is-in { transform: none; }
}

.callframe__bar {
  display: flex; align-items: center; gap: var(--space-2xs);
  padding: 0.55rem var(--space-sm);
  background: var(--color-panel);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.callframe__title { font-size: var(--text-xs); color: var(--color-muted-strong); font-weight: 600; margin-right: auto; }
.callframe__who { display: inline-flex; }
.callframe__av {
  width: 20px; height: 20px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.6rem; font-style: normal; font-weight: 700; color: var(--color-bg);
  border: 2px solid var(--color-panel);
}
.callframe__av--1 { background: var(--color-hablante-1); }
.callframe__av--2 { background: var(--color-hablante-2); margin-left: -6px; }
/* 3/4 (Sep 2026, real per-locale audio): en/es clips surface 2-4 distinct
 * real speakers, not always exactly 2 — motion.js caps display at 4, the
 * same 4-hue budget CLAUDE.md §14b already designs around. */
.callframe__av--3 { background: var(--color-hablante-3); margin-left: -6px; }
.callframe__av--4 { background: var(--color-hablante-4); margin-left: -6px; }
.recpill--sim { padding: 2px var(--space-2xs); }
/* The call is over: the pill stops being Grana and becomes Quetzal "Listo" —
 * the same done-state colour the app uses, and the visual handoff to the
 * report section below. */
.callframe.is-done .recpill--sim { border-color: var(--color-quetzal); }
.callframe.is-done .recpill__dot { background: var(--color-quetzal); animation: none; }
.callframe.is-done .recpill__t { color: var(--color-quetzal-ink); }

/* ── Transcript ─────────────────────────────────────────────────────────
 * Height is constant from first paint: every word span is laid out from the
 * start and only its opacity moves, so nothing reflows while it "types". */
.turns {
  list-style: none; margin: 0; padding: var(--space-sm) var(--space-md);
  display: grid; gap: var(--space-sm);
  font-size: var(--text-sm); line-height: 1.55;
}
@media (min-width: 48rem) { .turns { padding: var(--space-md) var(--space-lg); font-size: var(--text-base); } }
.turn { --sp: var(--color-hablante-1); border-left: 2px solid var(--sp); padding-left: var(--space-sm); }
.turn--s1 { --sp: var(--color-hablante-1); }
.turn--s2 { --sp: var(--color-hablante-2); }
.turn--s3 { --sp: var(--color-hablante-3); }
.turn--s4 { --sp: var(--color-hablante-4); }
.turn__meta { display: flex; align-items: center; gap: var(--space-2xs); }
.turn__who { font-size: var(--text-xs); font-weight: 700; color: var(--sp); letter-spacing: 0.01em; }
.turn__text { margin-top: 2px; color: var(--color-ink); max-width: 74ch; }
/* Spanglish, left where it was spoken — the wedge, made visible (CLAUDE.md
 * §14b.7). Underline, not a highlight: it is a language mark, not a citation. */
.cs { font-style: normal; box-shadow: inset 0 -2px 0 0 var(--color-ambar); padding: 0 1px; }
/* While the text is typing, the mark has to belong to the WORDS, not to the
 * span around them — otherwise the underline is drawn under a gap where the
 * words have not arrived yet, and reads as a stray orange line. */
html.js .turns.is-live .cs, html.js .colibri.is-js .ans .cs { box-shadow: none; padding: 0; }
html.js .turns.is-live .cs .w, html.js .colibri.is-js .ans .cs .w { box-shadow: inset 0 -2px 0 0 var(--color-ambar); padding: 0 1px; }

html.js .turn { transition: opacity var(--dur-mid) var(--ease-out); }
html.js .turns.is-live .turn { opacity: 0.45; }
html.js .turns.is-live .turn.is-on { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  html.js .turns.is-live .w { opacity: 0; transform: translateY(0.16em); transition: opacity 240ms var(--ease-out), transform 240ms var(--ease-out); }
  html.js .turns.is-live .w.is-on { opacity: 1; transform: none; }
}
.turn.is-cited { background: var(--color-ambar-wash); border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: no-preference) { .turn.is-cited { transition: background-color 200ms var(--ease-out); } }

/* ── Citation chip — Ámbar, and Ámbar never carries light text (tokens.css).
 * Ink on the Ámbar wash with an Ámbar rule reads at full contrast in both
 * grounds and still says "cita verificable" at a glance. ───────────────── */
.cite {
  appearance: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 2px;
  border: var(--rule-hair) solid var(--color-ambar);
  background: var(--color-ambar-wash); color: var(--color-ink);
  border-radius: var(--radius-pill);
  padding: 1px 7px;
  font-family: var(--font-mono); font-size: 0.68rem; line-height: 1.5;
  white-space: nowrap; vertical-align: 1px;
}
.cite:hover { background: var(--color-ambar); }
@media (hover: hover) and (prefers-reduced-motion: no-preference) { .cite:hover { transform: translateY(-1px); } }
.cite:active { transform: none; }

/* ── Transport ──────────────────────────────────────────────────────────── */
.callframe__foot {
  display: flex; align-items: center; gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-sm);
  border-top: var(--rule-hair) solid var(--color-rule);
  background: var(--color-panel);
  flex-wrap: wrap;
}
.simbtn {
  appearance: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 34px; padding: 0 var(--space-xs);
  border: var(--rule-hair) solid var(--color-rule-strong);
  border-radius: var(--radius-pill);
  background: var(--color-bg); color: var(--color-ink);
  font-size: var(--text-xs); font-weight: 600;
}
.simbtn:hover { border-color: var(--color-ink); }
.simbtn--accent { color: var(--color-accent-ink); border-color: var(--color-accent-ink); }
.simbtn--accent:hover { background: var(--color-accent-wash); }
.simbtn--end { margin-left: auto; }
.simbtn[disabled] { opacity: 0.45; cursor: default; pointer-events: none; }
/* ▶ / ❚❚ drawn in CSS so no icon font or SVG sprite is needed for one glyph. */
.simbtn__glyph { width: 0; height: 0; border-left: 8px solid currentColor; border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
.simbtn--play[aria-pressed="true"] .simbtn__glyph { width: 8px; height: 10px; border: 0; background: currentColor; box-shadow: inset 2px 0 0 0 var(--color-bg), inset -2px 0 0 0 var(--color-bg); }

.simrange { appearance: none; -webkit-appearance: none; flex: 1 1 8rem; min-width: 5rem; height: 20px; background: transparent; cursor: pointer; --p: 0; }
.simrange::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: linear-gradient(to right, var(--color-grana) 0 calc(var(--p) * 100%), var(--color-rule-strong) calc(var(--p) * 100%) 100%); }
.simrange::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; margin-top: -4px; border-radius: 50%; background: var(--color-grana); border: 2px solid var(--color-bg); }
.simrange::-moz-range-track { height: 6px; border-radius: 3px; background: linear-gradient(to right, var(--color-grana) 0 calc(var(--p) * 100%), var(--color-rule-strong) calc(var(--p) * 100%) 100%); }
.simrange::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: var(--color-grana); border: 2px solid var(--color-bg); }
.simtime { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--color-muted-strong); }
@media (max-width: 30rem) {
  .simrange { flex-basis: 100%; order: 3; }
  .simbtn--end { margin-left: 0; }
}

/* ══ 2 · Colibrí ══════════════════════════════════════════════════════════ */
.colibri { margin-top: var(--space-lg); display: grid; gap: var(--space-md); }
.colibri__chips { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.colibri__chip {
  appearance: none; cursor: pointer; text-align: left;
  min-height: 40px; padding: var(--space-2xs) var(--space-sm);
  border: var(--rule-hair) solid var(--color-rule-strong);
  border-radius: var(--radius-pill);
  background: var(--color-bg); color: var(--color-muted-strong);
  font-size: var(--text-sm);
}
.colibri__chip:hover { color: var(--color-ink); border-color: var(--color-ink); }
@media (hover: hover) and (prefers-reduced-motion: no-preference) { .colibri__chip:hover { transform: translateY(-1px); } }
.colibri__chip[aria-expanded="true"] { background: var(--color-accent-wash); border-color: var(--color-accent-ink); color: var(--color-accent-ink); font-weight: 600; }

.colibri__answers { display: grid; gap: var(--space-md); }
.ans {
  border-left: 2px solid var(--color-accent-ink);
  padding-left: var(--space-md);
  max-width: 62ch;
}
.ans__q { font-size: var(--text-sm); font-weight: 600; color: var(--color-muted-strong); }
.ans__t { margin-top: var(--space-3xs); color: var(--color-ink); font-size: var(--text-md); line-height: 1.55; }
.ans__cites { margin-top: var(--space-2xs); display: flex; gap: var(--space-3xs); flex-wrap: wrap; }
/* Enhanced: one answer at a time, so it becomes a panel — the same card
 * language the rest of the page uses. The reserved min-height is what keeps
 * switching between a two-line and a four-line answer from moving the page
 * under the visitor's cursor. And with JS the chip above already carries the
 * question, so the echo inside the panel is noise; without JS it is the
 * answer's only label and it stays. */
html.js .colibri.is-js .ans {
  background: var(--color-panel);
  border: var(--rule-hair) solid var(--color-rule);
  border-left: 2px solid var(--color-accent-ink);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
}
html.js .colibri.is-js .ans__q { display: none; }
html.js .colibri.is-js .colibri__answers { min-height: 9rem; }
.ans--ungrounded, html.js .colibri.is-js .ans--ungrounded { border-left-color: var(--color-rule-strong); }
.ans__tag {
  display: inline-block; margin-top: var(--space-3xs);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--color-muted); border: var(--rule-hair) dashed var(--color-rule-strong);
  border-radius: var(--radius-pill); padding: 0 8px;
}
html.js .colibri.is-js .ans[hidden] { display: none; }
@media (prefers-reduced-motion: no-preference) {
  html.js .colibri.is-js .ans .w { opacity: 0; transition: opacity 200ms var(--ease-out); }
  html.js .colibri.is-js .ans .w.is-on { opacity: 1; }
}

/* ══ 3 · The report ═══════════════════════════════════════════════════════ */
.reportframe {
  margin-top: var(--space-lg);
  max-width: 44rem;
  border: var(--rule-hair) solid var(--color-rule-strong);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(25, 21, 25, 0.05), 0 20px 44px -20px rgba(25, 21, 25, 0.28);
}
.reportframe__bar {
  display: flex; align-items: center; gap: var(--space-2xs);
  padding: 0.55rem var(--space-sm);
  background: var(--color-panel);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.reportframe__bar .callframe__title { margin-right: auto; }
.statepill {
  display: inline-flex; align-items: center; gap: 6px;
  border: var(--rule-hair) solid var(--color-quetzal);
  border-radius: var(--radius-pill); padding: 1px var(--space-2xs);
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--color-quetzal-ink);
}
.statepill__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--color-quetzal); flex: none; }
.reportframe.is-writing .statepill { border-color: var(--color-ambar); color: var(--color-ambar-deep); }
.reportframe.is-writing .statepill__dot { background: var(--color-ambar); }
@media (prefers-reduced-motion: no-preference) {
  .reportframe.is-writing .statepill__dot { animation: pm-dot 1.2s ease-in-out infinite; }
}

.repdoc { position: relative; padding: var(--space-md) var(--space-md) var(--space-sm); }
@media (min-width: 48rem) { .repdoc { padding: var(--space-lg); } }
.repdoc__t { font-family: var(--font-serif); font-weight: 600; font-size: var(--text-xl); line-height: 1.15; }
.repdoc__h { margin-top: var(--space-md); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--color-muted-strong); }
.repdoc__p { margin-top: var(--space-2xs); font-size: var(--text-sm); line-height: 1.55; }
.steps { list-style: none; padding: 0; margin-top: var(--space-2xs); display: grid; gap: var(--space-2xs); }
.steps__i {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2xs);
  padding: var(--space-2xs) 0; border-bottom: var(--rule-hair) dashed var(--color-rule);
  font-size: var(--text-sm);
}
.steps__i:last-child { border-bottom: 0; }
.steps__who { font-weight: 700; color: var(--color-quetzal-ink); min-width: 4.5rem; }
.steps__what { flex: 1 1 14rem; }
.repdoc__actions { margin-top: var(--space-md); display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.sharehint { margin-top: var(--space-2xs); display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2xs); }
.sharehint__url { font-family: var(--font-mono); font-size: var(--text-xs); background: var(--color-panel); border: var(--rule-hair) dashed var(--color-rule-strong); border-radius: var(--radius-sm); padding: 2px 8px; }
.sharehint__tag { font-size: var(--text-xs); color: var(--color-muted); }
.simtoast { margin-top: var(--space-2xs); font-size: var(--text-xs); color: var(--color-quetzal-ink); min-height: 1.2em; }
@media (prefers-reduced-motion: no-preference) {
  .simtoast { opacity: 0; transition: opacity var(--dur-quick) var(--ease-out); }
  .simtoast.is-on { opacity: 1; }
}

/* The rows arrive one after another as the document "fills in". Class added
 * by JS, so a no-JS reader has them solid from the start. */
@media (prefers-reduced-motion: no-preference) {
  html.js .reportframe.is-filling [data-rep-row] { opacity: 0; transform: translateY(10px); }
  html.js .reportframe.is-filling [data-rep-row].is-on { opacity: 1; transform: none; transition: opacity 360ms var(--ease-out), transform 360ms var(--ease-spring); }
}

/* Skeleton — built and removed by JS. It is an OVERLAY on the finished
 * document rather than a replacement for it: same box, same height, so the
 * crossfade costs zero layout shift, and a no-JS visitor never meets it. */
.repskel {
  position: absolute; inset: 0; z-index: 1;
  background: var(--color-bg);
  padding: var(--space-md); display: grid; gap: var(--space-2xs); align-content: start;
}
@media (min-width: 48rem) { .repskel { padding: var(--space-lg); } }
.repskel.is-out { opacity: 0; }
@media (prefers-reduced-motion: no-preference) { .repskel { transition: opacity 260ms var(--ease-out); } }
.repskel__b { height: 0.8rem; border-radius: var(--radius-sm); background: var(--color-panel); border: var(--rule-hair) solid var(--color-rule); }
@media (prefers-reduced-motion: no-preference) {
  .repskel__b { animation: skel 1.4s var(--ease-in-out) infinite; }
  .repskel__b:nth-child(2n) { animation-delay: 160ms; }
  .repskel__b:nth-child(3n) { animation-delay: 320ms; }
}
@keyframes skel { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
.repskel__step { font-size: var(--text-xs); color: var(--color-muted-strong); font-family: var(--font-mono); }
