/* rx-core.css — the shared visual vocabulary of the recommendation room on /styling.
 *
 * WHAT THIS OWNS (BUILD_SPEC §2.12): the page-local motion and depth tokens, the paper room, the
 * type roles every movement uses, the button and chip systems, the garment FACE (photograph, the
 * drawn card, and the colour / neutral field it falls back to), the skeleton sheen, the scroll
 * reveal, the look transition, and the one rule that keeps a hidden element inside the room from
 * being laid out. Component sheets (board, hero, sections, drawer, filters) load after this one and
 * own their own classes.
 *
 * THE PALETTE IS LOCKED TO WHITE AND INK. No colour literal is written in this file — every value
 * is a leorve.css token, a black or white alpha, or the neutral rgba(127,127,127,a) rim. The only
 * hue on the page is a garment's: the fill of a drawn flat (an SVG attribute rx-flats.js writes
 * from a hex rx-core.js has checked with isHex), a photograph, or the inline background of the
 * fallback colour field. So a future theme only has to re-point tokens; nothing here would need a
 * second cut except the alpha shadows (05 §17).
 *
 * EVERY HOVER THAT MOVES IS GATED TWICE (CLAUDE.md "The motion system"): (hover:hover) and
 * (pointer:fine), because a touch browser fires :hover on tap and leaves it stuck — a lifted
 * button that stays lifted after the tap reads as a broken control — and prefers-reduced-motion,
 * because a travelling box is the clearest case there is. Colour-only hovers sit inside the first
 * gate alone. leorve.css already zeroes every duration under reduced motion; the rules below add an
 * explicit END state wherever a START state would hide something.
 */

/* ═══ 1. PAGE-LOCAL TOKENS (05 §1) ═══════════════════════════════════════════════════════════
   On :root, not on #sSet, because the drawer and the tip are mounted on <body> (08 #2/#3) and
   must resolve the same durations. Every literal is achromatic by construction. */
:root{
  --rx-d1:180ms;      /* hover, colour, border, press release, chip fill */
  --rx-d2:240ms;      /* tile light / zoom, label morph, callout open, tip */
  --rx-d3:320ms;      /* drawer in, look in, seg thumb travel */
  --rx-dx:200ms;      /* drawer out, look out: exits are faster than entrances */
  --rx-dr:600ms;      /* scroll reveal, callout line draw-in, silhouette "dress" */
  --rx-dp:90ms;       /* press-in: instant feedback under the pointer */
  --rx-press:.98;
  --rx-zoom:1.04;     /* the pointer resting on a tile or a card image */
  --rx-zoom-lit:1.03; /* a reason, annotation or colour pointing at a tile */
  --rx-dim:.4;        /* the tiles NOT being talked about while one is lit */
  --rx-lift:0 10px 22px -16px rgba(0,0,0,.42);
  --rx-scrim:rgba(0,0,0,.42);
  /* The sticky offset: below the SETTLED capsule (v7 NAV_SPEC §8) — its --lv-nav-safe, 80px on a
     desktop and 72px on a phone, from /static/lv-capsule.css. It was the measured height of the old
     in-flow .topbar (rx-core.js init() wrote it); the capsule's height is fixed, so it is a token
     now, not a measurement. 72px is the fallback for a page without the capsule sheet. */
  --rx-top:var(--lv-nav-safe, 72px);
}

/* ═══ 2. THE ROOM (05 §3.4) ═════════════════════════════════════════════════════════════════
   The results are judged on white, not on the Studio's dot field: a garment colour read over a
   field of grey dots is a different colour, and the meta greys had to be lifted to --ink-2 there
   because a glyph could land on a dot. The paper runs full-bleed through a ::before so the room
   keeps .wrap's column; body{overflow-x:hidden} (leorve.css §3) clips the 100vw overhang. */
.rx-room{position:relative;isolation:isolate}
.rx-room::before{content:"";position:absolute;z-index:-1;top:0;bottom:0;
  left:calc(50% - 50vw);right:calc(50% - 50vw);background:var(--paper)}

/* 08 #1, inside the room: an author `display:` rule beats the UA's [hidden]{display:none}, so a
   movement painter that sets display on a host and then marks it hidden would still lay it out —
   an empty 72px-margined band, or worse, an invisible box over a control. One rule covers every
   host in the room; overlays live on <body> and carry their own. */
.rx-room [hidden]{display:none!important}

/* ═══ 3. TYPE ROLES (05 §2) ═════════════════════════════════════════════════════════════════
   Scale is the hierarchy lever on a page with no hue. Eyebrows are written in sentence case and
   uppercased HERE, so a screen reader reads a word rather than spelling letters. */
.rx-mov{margin:var(--sp-mov) 0 0}
.rx-mov-head{display:flex;align-items:end;justify-content:space-between;gap:16px;margin:0 0 var(--sp-head)}
.rx-mov-head > div{min-width:0}
.rx-eyebrow{margin:0 0 10px;font:600 11px/1.2 var(--body);letter-spacing:.22em;text-transform:uppercase;color:var(--ink-2)}
.rx-sec{margin:0;font:600 clamp(26px,2.8vw,40px)/1.1 var(--display);letter-spacing:-.02em;text-wrap:balance;color:var(--ink)}
.rx-sec:focus{outline:none}
.rx-sec:focus-visible{outline:2px solid var(--ink);outline-offset:4px}
.rx-body{margin:0;max-width:56ch;font:400 15px/1.6 var(--body);text-wrap:pretty;color:var(--ink-2)}
.rx-meta{margin:0;font:400 13px/1.5 var(--body);color:var(--ink-dim)}
.rx-micro{font:600 10.5px/1.3 var(--body);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-dim)}
.rx-num{font:500 13px/1 var(--display);font-variant-numeric:tabular-nums;letter-spacing:.06em;color:var(--ink-dim)}

/* ═══ 4. ICONS (05 §5.6) ════════════════════════════════════════════════════════════════════
   A safe default for every icon RX.core.icon() draws: an inline SVG with only a viewBox renders
   at 300×150 in HTML, so an icon a component forgot to size would fill its row. Component rules
   (".rx-btn svg") are as specific and load later, so they win.
   pointer-events:none so elementFromPoint on an icon button returns the BUTTON, not its <path> —
   the binding harness asserts exactly that on #drawerX, and a delegated click handler that tests
   `e.target.matches(…)` would otherwise miss every click that lands on the stroke. */
svg[data-i]{width:1em;height:1em;flex:none;fill:none;stroke:currentColor;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round;pointer-events:none;overflow:visible}
.rx-ico{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex:none}
.rx-ico > svg{width:16px;height:16px}

/* ═══ 5. BUTTONS (05 §5.1–§5.4) ═════════════════════════════════════════════════════════════
   Sentence case, this page's convention (the uppercase .lv-btn belongs to marketing pages).
   Pressed at .98 — the house presses at .985 and the gap is below perception; .98 is the brief's
   number and applies to rx- controls only. */
.rx-btn{--h:48px;position:relative;display:inline-flex;align-items:center;justify-content:center;gap:10px;
  min-height:var(--h);padding:0 22px;border-radius:var(--r-pill);border:1px solid var(--line-2);
  background:transparent;color:var(--ink);font:600 14px/1 var(--body);letter-spacing:.01em;
  white-space:nowrap;text-decoration:none;cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:background-color var(--rx-d1) var(--ease),border-color var(--rx-d1) var(--ease),
    color var(--rx-d1) var(--ease),box-shadow var(--rx-d1) var(--ease),transform var(--rx-d1) var(--ease)}
.rx-btn svg{width:16px;height:16px;flex:0 0 16px;transition:transform var(--rx-d2) var(--ease)}
.rx-btn.is-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.rx-btn.is-quiet{border-color:transparent;padding:0 6px;text-decoration:underline;
  text-underline-offset:4px;text-decoration-thickness:1px}
.rx-btn.is-icon{--h:44px;width:44px;padding:0;gap:0}
.rx-btn.is-sm{--h:36px;padding:0 14px;font-size:13px}
.rx-btn.is-lg{--h:52px;padding:0 28px;font-size:15px}
@media (pointer:coarse){ .rx-btn{min-height:max(var(--h),44px)} }

/* Colour-only hovers sit in the touch gate; the lift and the icon nudges MOVE, so they sit in the
   motion gate nested inside it. Nested rather than one combined query on purpose: every hover-gate
   check in the suite looks for the exact `@media (hover:hover) and (pointer:fine)` block and
   treats everything inside its braces as gated. */
@media (hover:hover) and (pointer:fine){
  .rx-btn:hover{border-color:var(--ink)}
  .rx-btn.is-primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
  .rx-btn.is-quiet:hover{border-color:transparent;text-decoration-thickness:2px}
  .rx-btn.is-icon:hover{background:var(--paper-2);border-color:var(--line-2)}
  @media (prefers-reduced-motion:no-preference){
    .rx-btn:hover{transform:translateY(-1px)}
    .rx-btn.is-primary:hover{box-shadow:var(--rx-lift)}
    .rx-btn.is-quiet:hover,.rx-btn.is-icon:hover{transform:none}
    .rx-btn:hover svg[data-i="arrow"]{transform:translateX(3px)}
    .rx-btn:hover svg[data-i="heart"]{transform:scale(1.1)}
    .rx-btn:hover svg[data-i="swap"]{transform:rotate(-40deg)}
    .rx-btn:hover svg[data-i="plus"]{transform:rotate(90deg)}
    .rx-btn:hover svg[data-i="spark"]{transform:rotate(45deg)}
    .rx-btn:hover svg[data-i="prev"]{transform:translateX(-2px)}
    .rx-btn:hover svg[data-i="next"]{transform:translateX(2px)}
  }
}
.rx-btn:active{transform:scale(var(--rx-press));transition-duration:var(--rx-dp)}
.rx-btn.is-primary:active{background:var(--accent-press);border-color:var(--accent-press)}
/* An ink ring, never white: .hero's leorve.css rule turned every focus ring in the old hero white
   on a white page (05 F4), which is why no rx- root is called "hero". */
.rx-btn:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.rx-btn[disabled]{opacity:.42;cursor:not-allowed;transform:none;box-shadow:none}
/* "Not yet" (house): still focusable, still explains itself, visibly quieter. */
.rx-btn[aria-disabled="true"]{opacity:.66;cursor:default}

/* LOADING IS A SHEEN, NOT A SPINNER (05 §5.3), and the label never changes: a spinner swapped in
   for the words shrinks the button and moves everything after it. The sheen is transform-only. */
.rx-btn[aria-busy="true"]{cursor:progress;overflow:hidden}
.rx-btn[aria-busy="true"]::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.22) 50%,transparent 70%);
  transform:translateX(-100%);animation:rx-sheen 1.2s var(--ease) infinite}
.rx-btn:not(.is-primary)[aria-busy="true"]::after{
  background:linear-gradient(100deg,transparent 30%,rgba(0,0,0,.06) 50%,transparent 70%)}
@keyframes rx-sheen{to{transform:translateX(100%)}}

/* THE SUCCESS MORPH, WITH NO WIDTH CHANGE (05 §5.4). Both labels share ONE grid cell, so the
   button is the width of the wider label from the first paint and "Save look" → "Saved" moves
   nothing beside it. Only opacity and transform animate. The positional selectors carry the rule
   whatever the two spans are called; .a / .b are the names 05 §5.4's markup uses. Pressed states
   are matched as data-state "done" (05) or "saved" (the setSave state name, BUILD_SPEC §2.5). */
/* Centred in the shared cell: the cell is as wide as "Save look", so a start-aligned "Saved" left a
   lopsided gap on the right of the done button (seen at 1440). The wider label fills the cell, so
   centring moves only the narrower one, and still by no width at all. */
.rx-swap{display:grid;justify-items:center}
.rx-swap > span{grid-area:1 / 1;transition:opacity var(--rx-d2) var(--ease),transform var(--rx-d2) var(--ease)}
.rx-swap > span + span,.rx-swap > .b{opacity:0;transform:translateY(6px)}
.rx-btn[data-state="done"] .rx-swap > span:first-child,.rx-btn[data-state="saved"] .rx-swap > span:first-child,
.rx-btn[data-state="done"] .rx-swap > .a,.rx-btn[data-state="saved"] .rx-swap > .a{opacity:0;transform:translateY(-6px)}
.rx-btn[data-state="done"] .rx-swap > span + span,.rx-btn[data-state="saved"] .rx-swap > span + span,
.rx-btn[data-state="done"] .rx-swap > .b,.rx-btn[data-state="saved"] .rx-swap > .b{opacity:1;transform:none;transition-delay:60ms}
/* A done action stops competing with the next one: the fill recedes to an outline. */
.rx-btn.is-primary[data-state="done"],.rx-btn.is-primary[data-state="saved"]{background:transparent;
  border-color:var(--line-2);color:var(--ink);box-shadow:none}
.rx-btn[data-state="done"] svg[data-i="heart"] path,.rx-btn[data-state="saved"] svg[data-i="heart"] path{fill:currentColor}
.rx-done{margin:12px 0 0;font:400 13px/1.5 var(--body);color:var(--ink-2)}
.rx-done a{color:var(--ink);text-decoration:underline;text-underline-offset:3px}
.rx-done.is-err{color:var(--danger)}

/* ═══ 6. CHIPS (05 §7.2) ════════════════════════════════════════════════════════════════════
   A selected chip is an ink fill: the strongest "chosen" a white-and-ink palette can say. */
.rx-chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 16px;border-radius:var(--r-pill);
  border:1px solid var(--line-2);background:var(--card);color:var(--ink-2);font:500 13.5px/1 var(--body);
  white-space:nowrap;cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:background-color var(--rx-d1) var(--ease),color var(--rx-d1) var(--ease),
    border-color var(--rx-d1) var(--ease),transform var(--rx-d1) var(--ease)}
.rx-chip svg{width:14px;height:14px;flex:0 0 14px;transition:transform var(--rx-d2) var(--ease)}
.rx-chip[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
@media (hover:hover) and (pointer:fine){
  .rx-chip:not([aria-pressed="true"]):hover{border-color:var(--ink);color:var(--ink)}
  @media (prefers-reduced-motion:no-preference){
    .rx-chip:hover svg[data-i="spark"]{transform:rotate(45deg)}
  }
}
.rx-chip:active{transform:scale(var(--rx-press));transition-duration:var(--rx-dp)}
.rx-chip:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.rx-chip[aria-disabled="true"]{opacity:.66;cursor:default}
@media (pointer:coarse){ .rx-chip{min-height:44px} }
/* QUICK ASKS ARE ACTIONS, NOT FILTERS: no pressed state, transparent, a spark icon. Styled apart
   so a row of them never reads as a second filter bar with nothing selected. */
.rx-chip.is-ask{background:transparent;color:var(--ink)}

/* ═══ 7. THE FACE: one garment, two first-class states — photographed or drawn (05 §6.1) ═════
   THE ASPECT IS THE RENDERER'S. recommender/look_images draws every card at 600×750, so the box is
   4:5; a 3:4 box once cropped 6.7% off every garment photograph for no reason but a mismatch.
   This rule stands alone and first: tests/test_styling_gate_and_strip.py reads `.rx-face{…}` for
   the aspect. container-type lets the card's words drop out on a tile too small to hold them. */
.rx-face{position:relative;display:block;aspect-ratio:4/5;overflow:hidden;border-radius:var(--r);
  background:var(--paper-2);container-type:inline-size;
  outline:1px solid transparent;outline-offset:3px;
  transition:outline-color var(--rx-d2) var(--ease),transform var(--rx-d1) var(--ease)}
/* The neutral rim, OVER the media: it darkens a pale fill's edge and lightens a dark one's, so an
   ivory card and a black card both have an edge on white without a hue in the chrome. */
.rx-face::after{content:"";position:absolute;inset:0;z-index:1;border-radius:inherit;pointer-events:none;
  box-shadow:inset 0 0 0 1px rgba(127,127,127,.24)}
/* Photo and card fill the SAME box, and both are `.rx-face > *` — so every zoom, light and dim rule
   a component writes against that selector works on a drawn card exactly as on a photograph
   (LOCKED "HERO VISUAL": hover, light, zoom and annotation all work identically on a card). */
.rx-face > img,.rx-face > .rx-fill{position:absolute;inset:0;width:100%;height:100%;display:block;
  object-fit:cover;transform:scale(1);transform-origin:50% 50%;transition:transform var(--rx-d2) var(--ease)}
.rx-face > img{background:var(--paper-2)}
.rx-fill{color:var(--ink)}
.rx-fill.on-dark{color:var(--on-image)}
.rx-fill-name{position:absolute;left:12px;right:12px;bottom:12px;font:500 15px/1.25 var(--display);
  letter-spacing:-.01em;overflow-wrap:anywhere}
/* THE PLATE (the fallback colour field only): a colour that clears 4.5:1 with neither ink
   (Terracotta reaches 4.41) gets its words on a small white plate, which passes on every colour.
   Computed by RX.core.inkFor, never guessed. A drawn card never needs it — its words are on the
   paper. */
.rx-fill.needs-plate .rx-fill-name{width:max-content;max-width:calc(100% - 24px);
  padding:3px 8px;border-radius:10px;background:var(--card);color:var(--ink)}

/* THE DRAWN CARD (rx-core.js flatFace): the garment's flat, in its advised dye, on the quiet paper.
   WHAT KEEPS IT A DRAWING AND NOT A PHOTOGRAPH is the paper and the line: a flat colour inside a
   1.4px ink contour on an untextured ground, with the face's 1px rim around it, is read as an
   illustration at a glance, and nothing here adds a shadow, gradient or texture that would start
   imitating cloth. The ground is the face's own --paper-2 restated on the fill, so the paper zooms
   WITH the drawing under the board's hover and light rules instead of the drawing sliding over a
   still ground.
   THE DRAWING'S BOX IS INSET, NOT FULL-BLEED: a garment touching the rim reads as cropped, which
   is exactly the photographic cue a drawing must not give. The inset is the same fraction on all
   four sides so the box keeps the face's 4:5 and the SVG (viewBox 0 0 120 150, also 4:5) fills
   it without letterboxing; that also keeps the board's callout anchors (a fraction of the FACE's
   height, rx-board.js ANCHOR_AT) near the part of the drawing they name. */
.rx-fill.is-flat{background:var(--paper-2);color:var(--ink)}
.rx-flat{position:absolute;inset:6%;display:block;pointer-events:none}
/* Absolutely filling its box, in both layouts below: a percentage HEIGHT on a child of a flex item
   is the kind of thing engines have disagreed about, a containing block is not. */
.rx-flat > svg{position:absolute;inset:0;display:block;width:100%;height:100%;overflow:visible}
/* THE PLATE CAPTION (opts.name, the drawer's media): the words sit under a 1px rule as a fashion
   plate's caption does — the garment upright in the house's editorial serif (the shop's own
   product-name face), the dye in its italic, quieter.
   A COLUMN, NOT TWO ABSOLUTE BOXES. The caption is as tall as its name wraps to — one line for
   "Juttis", three for "Full-sleeved winter blouse / in Forest green" in the 209px phone drawer —
   and with the drawing in a box of fixed height the third line printed across the garment's hem.
   As a flex column the drawing takes whatever height the words leave, so the two can never meet. */
.rx-fill.is-flat.has-name{display:flex;flex-direction:column;padding:6% 14px 14px}
.rx-fill.is-flat.has-name .rx-flat{position:relative;inset:auto;flex:1 1 auto;min-height:0;margin:0 0 10px}
.rx-fill.is-flat .rx-fill-name{position:static;flex:none;padding-top:10px;border-top:1px solid var(--line-2);
  font:600 15px/1.3 var(--serif);letter-spacing:-.005em;color:var(--ink)}
.rx-fill.is-flat .rx-fill-name em{display:block;font:italic 400 14px/1.35 var(--serif);color:var(--ink-2)}
/* THE NEUTRAL FIELD (AMENDMENTS A4 — the fallback for a craft word when the flats did not load; with
   them, the same piece is drawn in outline): a craft word ("Kundan", "Meenakari") names a WORK, not
   a shade, and the engine leaves it unpainted by contract. It must never be an empty grey box —
   that is what a failed image looks like — so it carries the garment's name in quiet ink, framed by
   a dashed inner rule that says "described, not pictured". */
.rx-fill.is-neutral{background:var(--paper-2);color:var(--ink-2)}
.rx-fill.is-neutral::before{content:"";position:absolute;inset:8px;border:1px dashed var(--line-2);
  border-radius:calc(var(--r) - 6px);pointer-events:none}
.rx-fill.is-neutral .rx-fill-name{left:16px;right:16px;bottom:16px;color:var(--ink-2)}
/* The advised-colour dot every photograph carries (R16): the photo shows the CUT; the dot says the
   colour without a sentence. Ringed in white so it reads on a dark photograph and on a pale one. */
.rx-dot{position:absolute;top:8px;right:8px;z-index:2;width:14px;height:14px;border-radius:50%;
  box-shadow:0 0 0 2px var(--card),inset 0 0 0 1px rgba(127,127,127,.35)}
/* A tile too small to hold the words (a detail tile on a phone): the caption under the tile
   speaks instead, so a COLOURED card's name steps out — and a drawn card's too, whose drawing
   then takes the whole box back (the named column's 14px sides and the caption's 10px gap would
   otherwise frame a 56px thumbnail in paper). The drawing also comes closer to the rim: at 82px a
   6% inset is 5px of paper, and every pixel of a pair of earrings at that size is worth more than
   the margin.
   The NEUTRAL field keeps its name, smaller — without it that tile would be the empty grey box A4
   forbids. (A drawn card with no dye is an outline of the garment, which is not an empty box, so
   it needs no such exception.) The name sits 10px in, 5px inside the 5px dashed rule: at 8px (the
   first cut) the words touched the rule on a 106px phone tile and read as text spilling out of a
   frame. */
@container (max-width:159px){
  .rx-fill-name{display:none}
  .rx-flat{inset:4%}
  .rx-fill.is-flat.has-name{padding:4%}
  .rx-fill.is-flat.has-name .rx-flat{margin:0}
  .rx-fill.is-neutral .rx-fill-name{display:block;left:10px;right:10px;bottom:10px;
    font:500 11.5px/1.25 var(--body);letter-spacing:0}
  .rx-fill.is-neutral::before{inset:5px}
  .rx-dot{top:6px;right:6px;width:10px;height:10px}
}

/* ═══ 8. SKELETON (05 §12) ══════════════════════════════════════════════════════════════════
   An achromatic sheen, transform-only (no background-position repaint per frame), drawn in the
   FINAL geometry so nothing jumps when the answer lands. */
.rx-sk{position:relative;overflow:hidden;background:var(--paper-2);border-radius:var(--r)}
.rx-sk::after{content:"";position:absolute;inset:0;transform:translateX(-100%);pointer-events:none;
  background:linear-gradient(100deg,transparent 20%,rgba(255,255,255,.85) 50%,transparent 80%);
  animation:rx-shim 1.6s var(--ease) infinite}
@keyframes rx-shim{to{transform:translateX(100%)}}
.rx-sk-line{display:block;height:12px;margin:0 0 10px;border-radius:6px}
.rx-sk-line.is-w60{width:60%}
.rx-sk-line.is-w40{width:40%}
.rx-sk-name{display:block;height:clamp(38px,4.8vw,60px);width:72%;margin:0 0 14px;border-radius:10px}

/* ═══ 9. SCROLL REVEAL (05 §15) ═════════════════════════════════════════════════════════════
   HIDDEN ONLY UNDER html.rx-reveal, which rx-core.js adds AFTER `new IntersectionObserver`
   returned — never in a bare rule (tests/test_reveal_failsafe.py fails any bare class at
   opacity:0). A browser without the observer, a script that failed, or reduced motion: the class
   never arrives and every movement is simply there. A 2.5s watchdog reveals anything left in view. */
html.rx-reveal .rx-rv{transition:opacity var(--rx-dr) var(--ease),transform var(--rx-dr) var(--ease)}
html.rx-reveal .rx-rv:not(.is-in){opacity:0;transform:translate3d(0,16px,0)}
.rx-rv.is-in{opacity:1;transform:none}

/* ═══ 10. THE LOOK TRANSITION (05 §14) ══════════════════════════════════════════════════════
   The head and the stage are named by their owners (.rx-hero-head → rx-head, .rx-stage →
   rx-stage); the stage carries its callout lines with it, so the annotations travel with their
   board. html.rx-vt is set by RX.core.transition only for the duration of OUR transition, so the
   root cross-fade is switched off for it and for nothing else. The outgoing look leaves faster
   (200ms) than the new one arrives (320ms). The opacity here belongs to snapshots the browser
   discards on completion, so nothing can be stranded invisible. Off entirely under reduced
   motion: RX.core.transition then never starts one, and these rules do not exist. */
@media (prefers-reduced-motion:no-preference){
  html.rx-vt::view-transition-old(root),html.rx-vt::view-transition-new(root){animation:none}
  html.rx-vt::view-transition-old(rx-stage),html.rx-vt::view-transition-old(rx-head){animation:rx-out var(--rx-dx) var(--ease) both}
  html.rx-vt::view-transition-new(rx-stage),html.rx-vt::view-transition-new(rx-head){animation:rx-in var(--rx-d3) var(--ease) both}
  html.rx-vt[data-rx-dir="prev"]::view-transition-old(rx-stage),
  html.rx-vt[data-rx-dir="prev"]::view-transition-old(rx-head){animation-name:rx-out-prev}
  html.rx-vt[data-rx-dir="prev"]::view-transition-new(rx-stage),
  html.rx-vt[data-rx-dir="prev"]::view-transition-new(rx-head){animation-name:rx-in-prev}
}
@keyframes rx-out{to{opacity:0;transform:translateX(-24px)}}
@keyframes rx-in{from{opacity:0;transform:translateX(24px)}}
@keyframes rx-out-prev{to{opacity:0;transform:translateX(24px)}}
@keyframes rx-in-prev{from{opacity:0;transform:translateX(-24px)}}

/* ═══ 11. REDUCED MOTION: the end states (05 §16) ═══════════════════════════════════════════
   leorve.css zeroes every duration; these are the places where a START state would otherwise be
   what is left on screen. A revealed movement is visible, the busy sheen and the skeleton sweep
   are not drawn (a static block is the honest "still loading"), and the press does not shrink. */
@media (prefers-reduced-motion:reduce){
  .rx-rv,html.rx-reveal .rx-rv:not(.is-in){opacity:1!important;transform:none!important}
  .rx-btn[aria-busy="true"]::after,.rx-sk::after{display:none}
  .rx-btn[aria-busy="true"]{opacity:.72}
  .rx-btn:active,.rx-chip:active{transform:none}
  .rx-face > img,.rx-face > .rx-fill{transition:none}
}
