/* ═══════════════════════════════════════════════════════════════════════════
   EXPOSCALE COMMAND DECK — the shared premium layer
   ═══════════════════════════════════════════════════════════════════════════

   Loaded on ALL FOUR surfaces, between the shared sheet and the per-surface
   skins:

       exposcale.css  →  deck.css  →  portal.css | admin.css

   That order is deliberate and load-bearing:
     · exposcale.css is the shared structure (.layout/.sidebar/.main/.topbar)
       and is UNTOUCHABLE — four surfaces depend on it byte for byte.
     · deck.css only ADDS: new tokens, and new class names under three
       namespaces that exist nowhere else in the app — `cmd-`, `deck-`, `hud-`
       — plus the `[data-reveal]` motion attributes. It cannot collide.
     · portal.css / admin.css load AFTER, so every per-surface correction that
       already exists keeps the final word. Nothing here can regress them.

   THE DIRECTION — "cinematic shell, calm data".
   The frame is theatre; the instruments are calm. Heroes, page-header bands,
   KPI tiles, live indicators and empty states carry the HUD language (corner
   brackets, a hairline field, one scan sweep, telemetry rails). The 800-row
   top-up queue an admin works in all day gets micro-motion and nothing more.
   A command screen that animates its own data is a toy; one that frames its
   data and leaves it still is an instrument.

   ⛔ Nothing in this file may animate a layout property. transform + opacity.
   ⛔ Nothing here may style .layout/.sidebar/.ni/.topbar/.main/.badge — those
      are exposcale.css's, shared by every surface including the ones a
      stranger sees.
   ⛔ The rail and the topbar deliberately have NO entrance motion. They are
      seen on every single page load; per LAWS.md §4 the delight budget belongs
      to rare moments, not to the chrome you pass a hundred times a day.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. TOKENS ─────────────────────────────────────────────────────────────
   Declared on :root so all four surfaces inherit them. Named `--deck-*` /
   `--ease-*` / `--dur-*`; the surface sheets' own `--ease-out`, `--r-*` and
   `--shadow-*` are untouched and still win where they are redeclared. */

:root {
  /* Curves. The built-in CSS easings are too weak for UI. */
  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-back: cubic-bezier(0.34, 1.4, 0.64, 1);

  /* Durations. Frequency decides which one a thing is allowed to use. */
  --dur-micro: 140ms;   /* press, hover — seen constantly */
  --dur-ui: 240ms;      /* dropdowns, toggles — the sub-300ms rule */
  --dur-macro: 560ms;   /* a band revealing — seen once per page */

  --reveal-dy: 12px;    /* travel. Past ~24px a reveal reads cheap */

  /* The surface ramp. exposcale.css ships one flat --card on one flat --bg,
     which is why every panel reads as the same plane. Three planes, and the
     top-edge highlight is what makes a dark panel read as a machined face
     rather than a hole. */
  --deck-panel: linear-gradient(180deg, #16161f 0%, #121219 100%);
  --deck-panel-raised: linear-gradient(180deg, #1b1b26 0%, #15151e 100%);
  --deck-edge: inset 0 1px 0 rgba(255, 255, 255, 0.045);
  --deck-edge-hot: inset 0 1px 0 rgba(168, 85, 247, 0.22);

  /* Hairlines. --deck-rule is quieter than --border on purpose: a divider
     inside a panel should be felt, not read. */
  --deck-rule: rgba(255, 255, 255, 0.055);
  --deck-rule-hot: rgba(168, 85, 247, 0.28);

  /* The instrument field — the faint diagonal grid behind a hero. The angle
     is the one already used by portal.css's --hairline-grid (derived from the
     X mark's arms), so the two read as one system. */
  --deck-field: repeating-linear-gradient(64deg,
                  rgba(140, 82, 253, 0.075) 0 1px, transparent 1px 15px);
  --deck-field-fine: repeating-linear-gradient(64deg,
                  rgba(140, 82, 253, 0.05) 0 1px, transparent 1px 8px);

  --deck-glow: 0 0 0 1px rgba(140, 82, 253, 0.35), 0 18px 48px -18px rgba(140, 82, 253, 0.5);
  --deck-lift: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px -14px rgba(0, 0, 0, 0.65);
}

/* ── 2. MOTION — reveal, stagger, micro-interaction ────────────────────────
   ⛔⛔ THE ONE THING THAT IS DIFFERENT HERE from every scroll-reveal you have
   written before: on this app the DOCUMENT DOES NOT SCROLL. exposcale.css
   pins html/body to 100vh + overflow:hidden and the real scroller is `.main`
   (portal) or `.page-body` (admin). An IntersectionObserver left on its
   default root therefore observes a viewport nothing moves in, and every
   below-the-fold band stays at opacity 0 forever — content simply gone.
   deck.js finds the scroll parent and roots the observer on it. This comment
   is here because the CSS looks ordinary and the failure is silent. */

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-dy), 0);
}

[data-reveal="fade"] { transform: none; }
[data-reveal="left"] { transform: translate3d(calc(var(--reveal-dy) * -1.4), 0, 0); }
[data-reveal="right"] { transform: translate3d(calc(var(--reveal-dy) * 1.4), 0, 0); }
[data-reveal="scale"] { transform: scale(0.985); }

[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-macro) var(--ease-out-quart),
    transform var(--dur-macro) var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}

/* Already on screen at load ⇒ final, untransitioned. The LCP element never
   animates in; fading a heading IS delaying the LCP. */
[data-reveal].is-instant {
  opacity: 1;
  transform: none;
  transition: none;
}

/* ⛔⛔ THE NO-JAVASCRIPT FLOOR. If deck.js fails to load, 404s behind a stale
   cache, or throws before it runs, every [data-reveal] element on the page is
   permanently invisible — the whole console, blank, with no error a user can
   see. This is the exact failure mode that cost us two invisible sections on
   another build. `.deck-js` is set on <html> by an inline script in each
   layout BEFORE the first paint; without it, reveals render final. */
html:not(.deck-js) [data-reveal] {
  opacity: 1;
  transform: none;
}

/* Press feedback. Buttons are clicked constantly, so this is the only motion
   they get and it is 140ms. */
.u-press { transition: transform var(--dur-micro) var(--ease-out-quart); }
.u-press:active { transform: scale(0.975); }

@media (hover: hover) and (pointer: fine) {
  .u-lift {
    transition:
      transform var(--dur-ui) var(--ease-out-quart),
      box-shadow var(--dur-ui) var(--ease-out-quart),
      border-color var(--dur-ui) linear;
  }
  .u-lift:hover { transform: translate3d(0, -2px, 0); box-shadow: var(--deck-lift); }
}

/* ── 3. THE PAGE HEAD — `.cmd-head` ────────────────────────────────────────
   The band 25 of 26 admin pages did not have. They opened cold on a filter
   row: no statement of where you are, no numbers, nothing to look at while
   the page settled. Structure:

     .cmd-head            the band
       .cmd-head-main     eyebrow + title + sub
       .cmd-rail          the telemetry readout (this page's own live numbers)
       .cmd-actions       buttons

   It is deliberately NOT a hero. A hero is a destination; this is a header on
   a working screen, so it is ~96px, not ~180px, and its type is one step
   above the body rather than three. */

.cmd-head {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 22px;
  flex-wrap: wrap;
  padding: 15px 20px 16px;
  margin-bottom: 14px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--deck-panel);
  box-shadow: var(--deck-edge), var(--deck-lift);
}

/* The field + the accent wash. Both are pseudo-elements so the band needs no
   extra markup, and both are pointer-events:none so nothing they cover
   becomes unclickable. */
.cmd-head::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--deck-field);
  -webkit-mask-image: linear-gradient(105deg, #000 0%, transparent 62%);
  mask-image: linear-gradient(105deg, #000 0%, transparent 62%);
}

.cmd-head::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(90% 150% at 0% 0%, rgba(124, 58, 237, 0.16), transparent 62%);
}

.cmd-head > * { position: relative; z-index: 1; }

.cmd-head-main { min-width: 0; flex: 1 1 320px; }

.cmd-eyebrow {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--muted2);
  margin-bottom: 7px;
}

/* The eyebrow's leading tick — a 10px rule instead of a bullet or an emoji.
   Real marks, per LAWS.md §1 tell 6. */
.cmd-eyebrow::before {
  content: '';
  width: 12px;
  height: 1px;
  background: var(--pl);
  flex: none;
  opacity: 0.8;
}

.cmd-title {
  font-size: 21px;
  font-weight: 800;
  letter-spacing: -0.015em;
  line-height: 1.15;
}

.cmd-sub {
  margin-top: 5px;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--muted2);
  /* ~62 real characters. ⛔ NOT `50ch` — the ch unit is the advance of the
     digit zero and runs ~1.32× the average glyph, so `62ch` renders ~82
     characters. Measured, not assumed. */
  max-width: 47ch;
}

/* The SLIM band — for the pages whose body is `page-body--locked`.
   Those pages promise ONE SCREEN with no scroll (the dashboard, the support
   inbox, the wallet, announcements, tools, partners), so every pixel this band
   takes is a pixel a chart or a table loses. ⛔ Never put a `sub` on a slim
   band: the sentence is what makes it tall. The rail moves onto the title's
   own line and the whole thing lands at ~52px instead of ~96px. */
.cmd-head--slim {
  align-items: center;
  padding: 9px 16px 10px;
  margin-bottom: 11px;
  border-radius: 12px;
}
.cmd-head--slim .cmd-eyebrow { margin-bottom: 4px; }
.cmd-head--slim .cmd-title { font-size: 16px; }
.cmd-head--slim .cmd-rail { gap: 20px; padding-bottom: 0; }
.cmd-head--slim .cmd-stat-value { font-size: 15px; }
.cmd-head--slim .cmd-stat-label { font-size: 8px; margin-bottom: 2px; }

/* ── 3b. The telemetry rail ────────────────────────────────────────────────
   The page's own live numbers, read at a glance. Mono, tabular, small label
   over big figure — the shape of an instrument, not of a KPI card. */

.cmd-rail {
  display: flex;
  align-items: flex-end;
  gap: 26px;
  flex-wrap: wrap;
  padding-bottom: 2px;
}

.cmd-stat { min-width: 0; }

.cmd-stat-label {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
  white-space: nowrap;
}

.cmd-stat-value {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1;
  white-space: nowrap;
}

.cmd-stat-value.is-quiet { color: var(--muted2); font-weight: 500; }
.cmd-stat-value.is-good { color: var(--green); }
.cmd-stat-value.is-warn { color: var(--amber); }
.cmd-stat-value.is-bad { color: var(--red); }

.cmd-stat-note {
  margin-top: 4px;
  font-size: 10px;
  color: var(--muted);
  white-space: nowrap;
}

.cmd-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* ⛔ `page-body--locked` is a flex column, and a flex child's default
   `flex-shrink: 1` lets a tall table squeeze the band until its own text
   clips. The band is chrome: it keeps its height and the content takes the
   remainder. (Written as a descendant rather than a child selector because
   the admin's grid-locked pages nest one level deeper.) */
.page-body--locked > .cmd-head,
.page-body--stack > .cmd-head { flex: none; }

/* ── 3c. THE ARRIVAL CASCADE — `.deck-cascade` ─────────────────────────────
   The page's own blocks settle in, one after another, on load.

   ⭐ Deliberately CSS-only and markup-free: one class on the scroller, and
   every block inside it inherits the cascade. The alternative — a
   `data-reveal` attribute on some hundreds of panels across 26 pages — is a
   much larger diff over money-bearing markup for an identical result.

   ⛔ `:not([data-reveal])` is load-bearing. An element carrying that attribute
   is driven by deck.js, which toggles opacity through a class; letting a CSS
   animation with `both` fill the same property means the two fight and the
   loser is whichever the engine resolves last. The `.cmd-head` at the top of
   every page is exactly that element.

   ⛔ It stops at the 8th child ON PURPOSE. Delays past ~380ms stop reading as
   "arriving" and start reading as "slow", and a page with thirty blocks would
   otherwise still be assembling itself two seconds after it loaded.

   ⛔ transform + opacity only, and `both` so nothing is left mid-animation. */

@keyframes deck-rise {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  /* ⛔ `.rise` is excluded for the same reason as `[data-reveal]`, one level
     up: the client portal already ships a hand-staged entrance
     (`portal-rise` + `.rise-1…8` delays, tuned per page). Two animations on
     one element means the cascade silently replaces that staging with a
     positional one — a regression dressed as a feature. */
  .deck-cascade > *:not([data-reveal]):not(.rise) {
    animation: deck-rise 500ms var(--ease-out-expo) both;
  }
  .deck-cascade > *:nth-child(1):not([data-reveal]):not(.rise) { animation-delay: 0ms; }
  .deck-cascade > *:nth-child(2):not([data-reveal]):not(.rise) { animation-delay: 55ms; }
  .deck-cascade > *:nth-child(3):not([data-reveal]):not(.rise) { animation-delay: 110ms; }
  .deck-cascade > *:nth-child(4):not([data-reveal]):not(.rise) { animation-delay: 165ms; }
  .deck-cascade > *:nth-child(5):not([data-reveal]):not(.rise) { animation-delay: 220ms; }
  .deck-cascade > *:nth-child(6):not([data-reveal]):not(.rise) { animation-delay: 275ms; }
  .deck-cascade > *:nth-child(7):not([data-reveal]):not(.rise) { animation-delay: 330ms; }
  .deck-cascade > *:nth-child(n+8):not([data-reveal]):not(.rise) { animation-delay: 380ms; }
}

/* ── 4. HUD ACCENTS ────────────────────────────────────────────────────────
   Used sparingly and on purpose. Every one of these is opt-in markup; none of
   them appears anywhere by default. */

/* Corner brackets. The single most legible "this is an instrument" mark, and
   the cheapest — two pseudo-elements, no extra nodes. Put them on the ONE
   panel per page that earns them, never on every card (that is LAWS tell 2
   wearing a different hat). */
.deck-brackets::before,
.deck-brackets::after {
  content: '';
  position: absolute;
  width: 13px;
  height: 13px;
  border: 1px solid var(--deck-rule-hot);
  pointer-events: none;
  z-index: 2;
}
.deck-brackets::before {
  top: 8px; left: 8px;
  border-right: 0; border-bottom: 0;
  border-top-left-radius: 4px;
}
.deck-brackets::after {
  bottom: 8px; right: 8px;
  border-left: 0; border-top: 0;
  border-bottom-right-radius: 4px;
}

/* ⭐ The same brackets as an ELEMENT, for a host whose own pseudo-elements
   are already spoken for. `.hero-mini` in portal.css uses `::before` for its
   hairline grid, and a later sheet wins at equal specificity — so
   `.deck-brackets` there would simply never paint. This variant owns its own
   pseudo-elements and works anywhere.

     <span class="deck-corners" aria-hidden="true"></span>

   ⛔ It is `position:absolute; inset:0` so it never becomes a flex item. The
   `.hero-mini` beam has a comment in portal.css about exactly this: a child
   forced back to `position:relative` there turned into a 260px in-flow item
   and shoved the band's text off the right edge of a phone. */
.deck-corners {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
}
.deck-corners::before,
.deck-corners::after {
  content: '';
  position: absolute;
  width: 13px;
  height: 13px;
  border: 1px solid var(--deck-rule-hot);
}
.deck-corners::before {
  top: 7px; left: 7px;
  border-right: 0; border-bottom: 0;
  border-top-left-radius: 4px;
}
.deck-corners::after {
  bottom: 7px; right: 7px;
  border-left: 0; border-top: 0;
  border-bottom-right-radius: 4px;
}
@media (max-width: 860px) { .deck-corners { display: none; } }

/* The live dot. A status light that breathes — the one looping animation in
   the system, because "live" is a claim that has to keep being made. It is
   2s and 6% of opacity range, so it reads as presence, not as a blink. */
.live-dot {
  position: relative;
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--green);
  flex: none;
}
.live-dot::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.18;
  animation: deck-pulse 2.4s var(--ease-out-quart) infinite;
}
.live-dot { color: var(--green); }
.live-dot.is-warn { background: var(--amber); color: var(--amber); }
.live-dot.is-bad { background: var(--red); color: var(--red); }
.live-dot.is-idle { background: var(--muted); color: var(--muted); }
.live-dot.is-idle::after { animation: none; opacity: 0.12; }

@keyframes deck-pulse {
  0% { transform: scale(0.75); opacity: 0.28; }
  70% { transform: scale(1.5); opacity: 0; }
  100% { transform: scale(1.5); opacity: 0; }
}

/* The status chip the dot usually lives in. */
.hud-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 10px 4px 9px;
  border: 1px solid var(--deck-rule);
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.02);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted2);
  white-space: nowrap;
}
.hud-chip.is-hot { border-color: var(--deck-rule-hot); color: #d8b4fe; }

/* The scan sweep. ONE pass across a band on load, then it is gone — not a
   loop. A looping sweep on a screen somebody works in all day is a
   distraction with a theme; a single pass on arrival is an instrument coming
   up. 1.6s, and it never runs under reduced motion. */
.deck-scan {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  border-radius: inherit;
}
.deck-scan::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 34%;
  left: -40%;
  background: linear-gradient(100deg, transparent, rgba(168, 85, 247, 0.13), transparent);
  animation: deck-sweep 1.6s var(--ease-out-quart) 240ms 1 both;
}

@keyframes deck-sweep {
  from { transform: translateX(0); }
  to { transform: translateX(420%); }
}

/* ── 5. THE PANEL — `.deck` ────────────────────────────────────────────────
   The portal has `.pcard` and the admin has a dozen inline-styled boxes.
   `.deck` is the shared face both can wear where a section deserves to read
   as a machined surface rather than a rectangle. */

.deck {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 13px;
  background: var(--deck-panel);
  box-shadow: var(--deck-edge), var(--deck-lift);
}
.deck-pad { padding: 15px 17px; }
.deck-flush { padding: 0; overflow: hidden; }
.deck-raised { background: var(--deck-panel-raised); }

.deck-h {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 16px 11px;
  border-bottom: 1px solid var(--deck-rule);
}
.deck-h-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: -0.01em;
  min-width: 0;
}
.deck-h-title > svg { color: var(--pl); opacity: 0.9; flex: none; }

/* The section eyebrow, for use outside a .cmd-head. */
.deck-label {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ── 6. STATE COVERAGE — the empty screen ──────────────────────────────────
   LAWS.md §7 scores "state coverage (empty/loading/error)" and an empty table
   that renders as a blank rectangle is the single most template-looking thing
   a dashboard does. This is the shared empty state for every list. */

.deck-empty {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 38px 22px;
  text-align: center;
  border-radius: 12px;
}
.deck-empty::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--deck-field-fine);
  opacity: 0.5;
  -webkit-mask-image: radial-gradient(60% 70% at 50% 45%, #000, transparent 75%);
  mask-image: radial-gradient(60% 70% at 50% 45%, #000, transparent 75%);
}
.deck-empty > * { position: relative; }
.deck-empty-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--deck-rule-hot);
  border-radius: 11px;
  color: var(--pl);
  background: rgba(124, 58, 237, 0.07);
}
.deck-empty-title { font-size: 13px; font-weight: 700; }
.deck-empty-body {
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--muted2);
  max-width: 42ch;
}

/* ── 7. FOCUS FLOOR ────────────────────────────────────────────────────────
   admin.css already ships a ring and loads AFTER this file, so the console
   keeps its own. This is the floor for the three surfaces that had none: the
   portal, the auth shell and the partner area. `:where()` keeps it at
   specificity 0, so any real rule anywhere beats it. */

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg, #0d0d12), 0 0 0 4px rgba(168, 85, 247, 0.65);
  border-radius: 8px;
}

/* Selection: the default blue on a violet-on-near-black page is the one
   colour on screen that belongs to no palette. */
::selection { background: rgba(124, 58, 237, 0.35); color: #fff; }

/* ── 8. RESPONSIVE ─────────────────────────────────────────────────────────
   Mobile is the design, not the adaptation. At 390px the rail moves under the
   title and its figures step down — a telemetry readout that wraps mid-number
   is worse than no readout. */

@media (max-width: 860px) {
  .cmd-head {
    padding: 13px 14px 14px;
    border-radius: 12px;
    gap: 14px;
  }
  .cmd-head-main { flex-basis: 100%; }
  .cmd-title { font-size: 18px; }
  .cmd-sub { max-width: none; }
  .cmd-rail { gap: 16px 22px; width: 100%; }
  .cmd-stat-value { font-size: 16px; }
  .cmd-actions { width: 100%; }
  /* A 44px touch floor on every action in the band. */
  .cmd-actions > * { min-height: 40px; }
  .deck-brackets::before, .deck-brackets::after { display: none; }
}

@media (max-width: 560px) {
  .cmd-rail { gap: 14px 18px; }
  .cmd-stat-value { font-size: 15px; }
}

/* ── 9. REDUCED MOTION ─────────────────────────────────────────────────────
   Gentler, never zero. Movement goes; the fade stays so a state change is
   still legible. ⛔ [data-reveal] must resolve to VISIBLE here — the failure
   this guards against is content that stays at opacity 0 forever for the
   users least able to debug it. */

@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reveal].is-revealed,
  [data-reveal].is-instant {
    opacity: 1;
    transform: none;
    transition: opacity 180ms linear;
  }
  .u-press:active, .u-lift:hover { transform: none; }
  .deck-scan::before { animation: none; opacity: 0; }
  .live-dot::after { animation: none; opacity: 0.16; }
}
