
.ech-evb, .ech-evb * { box-sizing: border-box; }

.ech-evb {
  /* ── the two lights ─────────────────────────────────────────────────── */
  --evb-orchid: var(--nilla-accent);
  --evb-aqua: var(--theme-accent-2);
  /* the reacting light keeps a memory of the structural one — never pure aqua */
  --evb-live: color-mix(in srgb, var(--evb-aqua) 86%, var(--evb-orchid));

  /* ── depth ──────────────────────────────────────────────────────────────
     All four point at the palette's own floor rather than at black: depth in a
     user-editable palette has to be derived from the ground, or an inverted
     theme grows shadows that glow. --evb-shade is the well — the disc that
     darkens whatever is under the pointer so the reticle has somewhere to sit. */
  --evb-base: color-mix(in srgb, var(--evb-orchid) 20%, var(--nilla-bg));
  --evb-cast: color-mix(in srgb, var(--nilla-bg) 72%, transparent);
  --evb-bevel: color-mix(in srgb, var(--nilla-text) 12%, transparent);
  --evb-shade: color-mix(in srgb, var(--nilla-bg) 62%, transparent);

  /* ── geometry ───────────────────────────────────────────────────────── */
  --evb-box: auto;
  --evb-pad: 7px 14px;
  --evb-size: 9px;
  --evb-track: 0.24em;
  --evb-cut: 8px;
  --evb-gut: 8px;
  --evb-skirt: 3px;
  --evb-brk: 10px;
  --evb-brk-w: 1px;
  --evb-brk-off: -3px;
  --evb-brk-throw: 9px;
  --evb-cross-x: 250px;
  --evb-cross-y: 92px;
  /* The well is sized in ABSOLUTE pixels on every variant, because what it has
     to clear is the reticle — which is the same size over a 24px icon button
     as over a 200px one. The core is the part that is FULLY shaded, and it has
     to reach past 20px: the hive reticle's hover containment arcs sit at r+11,
     about 20px out, and at 55% of 26px the core stopped at 14.3px and left them
     standing on unshaded plate. 80% puts the solid edge at 20.8px. */
  --evb-well: 26px;
  --evb-well-core: 80%;
  --evb-pool: 92px;
  /* A mask reads ALPHA only — the hue of an opaque stop is discarded — so this
     stands in for "keep this pixel" and keeps the stylesheet free of literals.
     Any token that is opaque on every preset would do; the ink is. */
  --evb-keep: var(--nilla-text);

  /* ── timing ─────────────────────────────────────────────────────────── */
  --evb-arm-step: 130ms;
  --evb-scan-ms: 620ms;
  /* the interval-cta pause: the housing considers, then flips all at once */
  --evb-hold: 0.24s;

  /* ── resting values, and the hot value each one flips to ────────────── */
  --evb-ink: var(--nilla-text);
  --evb-ink-hot: var(--nilla-text);
  --evb-edge: color-mix(in srgb, var(--evb-orchid) 40%, transparent);
  --evb-edge-hot: var(--evb-live);
  /* The plate is OPAQUE on every variant, and it has to be: the body is filled
     with the edge colour and the plate covers all but a 1px rim of it, so a
     see-through plate would tint the entire face with the outline. That is the
     one constraint this technique imposes over a plain border. */
  --evb-plate: var(--theme-panel-bg);
  --evb-plate-hot: color-mix(in srgb, var(--nilla-bg) 90%, var(--evb-live));
  /* how much of the edge shows — one step thicker is the whole verdict */
  --evb-rim: 1px;
  --evb-wash: transparent;
  --evb-wash-hot: color-mix(in srgb, var(--evb-orchid) 16%, transparent);
  --evb-lat-hot: 0.5;

  /* ── the switches the hot rule throws ───────────────────────────────── */
  --evb-lat-o: 0;
  --evb-well-o: 0;
  --evb-scan-o: 0;
  --evb-cross-o: 0;
  --evb-brk-o: 0.24;
  --evb-brk-c: var(--evb-orchid);
  /* One scalar for all four corners: 1 = thrown out and rotated 90deg, 0 =
     home. Each corner multiplies it by its own throw vector, so the sequence
     stays a single value to reason about. */
  --evb-brk-k: 1;
  --evb-gut-a: 0.55;
  --evb-gut-b: 0;
  --evb-gut-bg: transparent;
  --evb-run: paused;

  position: relative;
  display: inline-flex;
  vertical-align: middle;
  /* Makes this a stacking context so the crosshair's z-index:-1 lands behind
     the button but still in front of whatever the button is sitting on. */
  isolation: isolate;
}

.ech-evb[data-variant="quiet"] {
  --evb-pad: 6px 11px;
  --evb-size: 8px;
  --evb-track: 0.22em;
  --evb-cut: 7px;
  --evb-ink: var(--theme-ink-dim);
  --evb-edge: color-mix(in srgb, var(--nilla-border) 88%, transparent);
  /* below the panel it sits on, so a quiet control reads as a recessed slot
     rather than competing with the one button that matters */
  --evb-plate: color-mix(in srgb, var(--nilla-bg) 78%, var(--theme-panel-bg));
  --evb-lat-hot: 0.34;
  --evb-skirt: 2px;
  --evb-brk: 8px;
  --evb-brk-off: -2px;
  --evb-brk-throw: 7px;
  --evb-cross-x: 180px;
  --evb-cross-y: 74px;
  --evb-arm-step: 110ms;
  --evb-scan-ms: 540ms;
}

/* 24px square leaves five clear pixels top and bottom inside a 34px bar plate,
   and the brackets are inset rather than thrown outside so a row of these
   cannot overlap its neighbours. The crosshair overhangs by 6px, which is
   inside the 4px slot gap plus the neighbour's own bracket inset — and only
   one button is ever hovered at a time. */
.ech-evb[data-variant="icon"] {
  --evb-box: 24px;
  --evb-pad: 0;
  --evb-size: 11px;
  /* a centred single glyph is pushed off-centre by its own trailing letter-space */
  --evb-track: 0;
  --evb-cut: 6px;
  --evb-ink: var(--theme-ink-dim);
  --evb-edge: color-mix(in srgb, var(--evb-orchid) 34%, transparent);
  /* the panel surface sits well above the bar it is docked in, which is what
     makes 24px of keycap read as raised rather than as a hole */
  --evb-plate: var(--theme-panel-bg);
  --evb-lat-hot: 0.3;
  --evb-skirt: 2px;
  --evb-brk: 6px;
  --evb-brk-off: 1px;
  --evb-brk-throw: 5px;
  --evb-cross-x: 36px;
  --evb-cross-y: 30px;
  --evb-arm-step: 90ms;
  --evb-scan-ms: 460ms;
}

/* ── the one hot rule ─────────────────────────────────────────────────────
   :has() rather than :focus-within, because a mouse click leaves the button
   focused and a control that stayed lit after every press would be lying about
   where the pointer is. The focus RING itself is on the button and needs no
   :has(), so an engine without it loses keyboard-focus styling and nothing
   else.

   The argument is wrapped in :where() to strip its specificity, and that is
   not cosmetic: :has() takes the weight of its most specific argument, so the
   bare form would score a class higher than the verdict rule below and a
   keyboard-driven commit would silently never show its stamp. */
.ech-evb[data-disabled="0"]:hover,
.ech-evb[data-disabled="0"]:has(:where(.ech-evb-btn:focus-visible)) {
  --evb-ink: var(--evb-ink-hot);
  --evb-edge: var(--evb-edge-hot);
  --evb-plate: var(--evb-plate-hot);
  --evb-wash: var(--evb-wash-hot);
  --evb-lat-o: var(--evb-lat-hot);
  --evb-well-o: 1;
  --evb-cross-o: 1;
  --evb-brk-o: 1;
  --evb-brk-c: var(--evb-live);
  --evb-brk-k: 0;
  --evb-gut-a: 0.75;
  --evb-gut-b: 0.9;
  --evb-run: running;
}

/* ── crosshair, piercing from behind ──────────────────────────────────── */

.ech-evb-cross {
  position: absolute;
  z-index: -1;
  left: 50%;
  top: 50%;
  pointer-events: none;
  opacity: var(--evb-cross-o);
  transition: opacity 300ms ease;
}
.ech-evb-cross i {
  position: absolute;
  left: 50%;
  top: 50%;
  display: block;
  background: color-mix(in srgb, var(--evb-live) 40%, transparent);
  transform: translate(-50%, -50%);
}
.ech-evb-cross i:first-child { width: var(--evb-cross-x); height: 1px; }
.ech-evb-cross i:last-child { width: 1px; height: var(--evb-cross-y); }

/* ── corner brackets, which double as the arming indicator ────────────── */

.ech-evb-brk {
  position: absolute;
  z-index: 1;
  width: var(--evb-brk);
  height: var(--evb-brk);
  border: 0 solid var(--evb-brk-c);
  opacity: var(--evb-brk-o);
  pointer-events: none;
  transition: transform 300ms cubic-bezier(.2,1.2,.4,1), opacity 220ms ease,
    border-color 220ms ease, filter 220ms ease;
}
.ech-evb-brk[data-c="tl"] {
  --evb-brk-i: 0;
  top: var(--evb-brk-off); left: var(--evb-brk-off);
  border-top-width: var(--evb-brk-w); border-left-width: var(--evb-brk-w);
  transform:
    translate(calc(var(--evb-brk-throw) * -1 * var(--evb-brk-k)), calc(var(--evb-brk-throw) * -1 * var(--evb-brk-k)))
    rotate(calc(-90deg * var(--evb-brk-k)));
}
.ech-evb-brk[data-c="tr"] {
  --evb-brk-i: 1;
  top: var(--evb-brk-off); right: var(--evb-brk-off);
  border-top-width: var(--evb-brk-w); border-right-width: var(--evb-brk-w);
  transform:
    translate(calc(var(--evb-brk-throw) * var(--evb-brk-k)), calc(var(--evb-brk-throw) * -1 * var(--evb-brk-k)))
    rotate(calc(90deg * var(--evb-brk-k)));
}
.ech-evb-brk[data-c="br"] {
  --evb-brk-i: 2;
  bottom: var(--evb-brk-off); right: var(--evb-brk-off);
  border-bottom-width: var(--evb-brk-w); border-right-width: var(--evb-brk-w);
  transform:
    translate(calc(var(--evb-brk-throw) * var(--evb-brk-k)), calc(var(--evb-brk-throw) * var(--evb-brk-k)))
    rotate(calc(-90deg * var(--evb-brk-k)));
}
.ech-evb-brk[data-c="bl"] {
  --evb-brk-i: 3;
  bottom: var(--evb-brk-off); left: var(--evb-brk-off);
  border-bottom-width: var(--evb-brk-w); border-left-width: var(--evb-brk-w);
  transform:
    translate(calc(var(--evb-brk-throw) * -1 * var(--evb-brk-k)), calc(var(--evb-brk-throw) * var(--evb-brk-k)))
    rotate(calc(90deg * var(--evb-brk-k)));
}

/* The stagger is applied ONLY while arriving. A retract has no sequence to
   tell — a rig that unlatched one corner at a time while the pointer was
   already gone would be describing a state that had ended. */
.ech-evb[data-state="scan"] .ech-evb-brk {
  transition-delay: calc(var(--evb-brk-i) * var(--evb-arm-step));
}
/* tactical-keys blooms the brackets as they land; here it is the only signal
   that the sequence COMPLETED, so it waits for armed rather than hover — and
   holds through the commit, since a rig does not unlatch to fire. */
.ech-evb[data-state="armed"] .ech-evb-brk,
.ech-evb[data-state="fired"] .ech-evb-brk {
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--evb-live) 55%, transparent));
}
.ech-evb[data-disabled="1"] .ech-evb-brk { opacity: 0; }

/* ── the cap ──────────────────────────────────────────────────────────── */

/* Deliberately unstyled: no background, no border, no clip. It carries the
   semantics, the focus outline and the hit rectangle, and nothing else. */
.ech-evb-btn {
  position: relative;
  display: flex;
  /* Both axes, explicitly. Without these the flex default left the face at its
     own 13px content height pinned to the top of a 24px cap, so an icon button
     rendered its glyph in the upper half with 11px of dead space beneath it —
     which read as the icons floating up into the bar's hazard strip rather
     than sitting in the bar. Measured before the fix: cap top and glyph top
     were the same 5.59px, and the glyph cleared the strip by 1.39px. */
  align-items: center;
  justify-content: center;
  width: var(--evb-box);
  height: var(--evb-box);
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--evb-ink);
  font: inherit;
  cursor: pointer;
  transition: color 0s var(--evb-hold);
}

/* The keycap, as two chained drop-shadows: a hard un-blurred offset is the
   cap's skirt, a soft one under it is the cast. They are cast from the
   RENDERED ALPHA, so both follow the chamfer — a box-shadow is cast from the
   border box and would print square corners under a cut cap. */
.ech-evb-lift {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  filter:
    drop-shadow(0 var(--evb-skirt) 0 var(--evb-base))
    drop-shadow(0 calc(var(--evb-skirt) + 3px) 6px var(--evb-cast));
  transition: filter 90ms ease, transform 90ms ease;
}

/* The cap drops by exactly the skirt height while the skirt collapses to zero,
   so the base stays planted and only the throw is spent. That reads as travel;
   moving the whole object reads as sliding. */
.ech-evb-btn:active:not(:disabled) .ech-evb-lift {
  transform: translateY(var(--evb-skirt));
  filter: drop-shadow(0 0 0 var(--evb-base)) drop-shadow(0 2px 4px var(--evb-cast));
}

/* ── the body: silhouette, edge and plate ─────────────────────────────────
   The body is filled with the EDGE colour and the plate is an inset ::before
   carrying the same clip, so the 1px delta is an outline that follows the
   chamfer. A border cannot, because clip-path cuts the border box. */
.ech-evb-body {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  align-items: stretch;
  min-width: 0;
  overflow: hidden;
  background-color: var(--evb-edge);
  clip-path: polygon(
    var(--evb-cut) 0, 100% 0,
    100% calc(100% - var(--evb-cut)), calc(100% - var(--evb-cut)) 100%,
    0 100%, 0 var(--evb-cut));
  /* the seele hard cut: consider, then flip — no 200ms fade into aqua while
     the reticle is still arriving */
  transition: background-color 0s var(--evb-hold);
}
.ech-evb-body::before {
  content: "";
  position: absolute;
  inset: var(--evb-rim);
  z-index: 0;
  pointer-events: none;
  clip-path: polygon(
    var(--evb-cut) 0, 100% 0,
    100% calc(100% - var(--evb-cut)), calc(100% - var(--evb-cut)) 100%,
    0 100%, 0 var(--evb-cut));
  background-color: var(--evb-plate);
  /* the hazard wash across the plate — a hint of tape without swamping the
     label, which is why it is a 16% mix rather than the gutters' full weight */
  background-image: repeating-linear-gradient(135deg,
    var(--evb-wash) 0 6px, transparent 6px 13px);
  background-size: 18.385px 100%;
  box-shadow: inset 0 1px 0 var(--evb-bevel);
  animation: ech-evb-crawl 1.15s linear infinite;
  animation-play-state: var(--evb-run);
  transition: background-color 0s var(--evb-hold);
}
/* permanent CRT texture, over the label rather than under it */
.ech-evb-body::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  background-image: repeating-linear-gradient(180deg,
    color-mix(in srgb, var(--evb-orchid) 8%, transparent) 0 1px, transparent 1px 3px);
}
/* A press is the one moment nothing should be deliberating about, so :active
   cancels the delay outright — the interval-cta trick, same as the source. */
.ech-evb-btn:active:not(:disabled),
.ech-evb-btn:active:not(:disabled) .ech-evb-body,
.ech-evb-btn:active:not(:disabled) .ech-evb-body::before,
.ech-evb-btn:active:not(:disabled) .ech-evb-gut { transition: none; }

/* ── the lattice, and the ring it blooms in ───────────────────────────────
   hex-cta pools this under the pointer. Here it is held OFF in a disc of
   --evb-well and blooms outside it, because the middle of that disc is where
   the reticle's filled core sits. */
.ech-evb-lat {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: var(--evb-lat-o);
  background-image:
    repeating-linear-gradient(0deg,
      color-mix(in srgb, var(--evb-live) 46%, transparent) 0 1px, transparent 1px 13px),
    repeating-linear-gradient(60deg,
      color-mix(in srgb, var(--evb-live) 34%, transparent) 0 1px, transparent 1px 13px),
    repeating-linear-gradient(-60deg,
      color-mix(in srgb, var(--evb-orchid) 34%, transparent) 0 1px, transparent 1px 13px);
  transition: opacity 260ms ease;
  animation: ech-evb-drift 2.6s linear infinite;
  animation-play-state: var(--evb-run);
}
/* the pooled glow, as an annulus. Kept off the parent so the drift keyframe
   (which animates background-position) cannot drag it around. */
.ech-evb-lat::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle var(--evb-pool) at var(--mx, 50%) var(--my, 50%),
    transparent 0 var(--evb-well),
    color-mix(in srgb, var(--evb-live) 26%, transparent) calc(var(--evb-well) + 26px),
    transparent var(--evb-pool));
}

/* ── the well ─────────────────────────────────────────────────────────────
   Above the lattice AND above the gutters, below the label. This is the piece
   that makes the reticle legible: everything under the pointer is pulled back
   toward the ground, while the label on top stays at full ink.

   Staying at z-index 3 is the point — it is BELOW the face at z-4 on purpose,
   so the glyphs keep full ink. That is also why it cannot be the only defence:
   see the punch-out below for the layers that would otherwise paint over it. */
.ech-evb-well {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: var(--evb-well-o);
  background: radial-gradient(circle var(--evb-well) at var(--mx, 50%) var(--my, 50%),
    var(--evb-shade) 0 var(--evb-well-core), transparent 100%);
  transition: opacity 200ms ease;
}

/* ── the punch-out ────────────────────────────────────────────────────────
   The well is a floor only if nothing escapes it, and three layers did. The
   rolling scan band and the read-head are handled by moving them below it (see
   each), but the press wipe has to stay on top to read as a wipe at all — so
   all three additionally have the well's own disc REMOVED from them. Shaded and
   unlit beats shaded alone: --evb-live at 26% painted over a darkened disc is
   still --evb-live, and the reticle's hex outline measured 2.90:1 on Bone
   Archive with the band running across the plain plate.

   One gradient, shared, so the hole can never drift away from the well's. */
.ech-evb-head, .ech-evb-scan, .ech-evb-wipe {
  -webkit-mask-image: radial-gradient(circle var(--evb-well) at var(--mx, 50%) var(--my, 50%),
    transparent 0 var(--evb-well-core), var(--evb-keep) 100%);
  mask-image: radial-gradient(circle var(--evb-well) at var(--mx, 50%) var(--my, 50%),
    transparent 0 var(--evb-well-core), var(--evb-keep) 100%);
}

/* ── hazard gutters ───────────────────────────────────────────────────── */

.ech-evb-gut {
  position: relative;
  z-index: 2;
  flex: 0 0 var(--evb-gut);
  overflow: hidden;
  background-color: var(--evb-gut-bg);
  transition: background-color 0s var(--evb-hold);
}
/* Two plies of identical geometry, cross-faded. A gradient cannot be
   interpolated by any engine, so a cross-fade is the only way to get a smooth
   orchid-to-aqua shift; sharing one animation keeps the plies in phase. */
.ech-evb-gut::before, .ech-evb-gut::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(135deg,
    var(--evb-ply) 0 6px, transparent 6px 13px);
  background-size: 18.385px 100%;
  transition: opacity 200ms ease;
  animation: ech-evb-crawl 1.15s linear infinite;
  animation-play-state: var(--evb-run);
}
.ech-evb-gut::before { --evb-ply: var(--evb-orchid); opacity: var(--evb-gut-a); }
.ech-evb-gut::after  { --evb-ply: var(--evb-live); opacity: var(--evb-gut-b); }

/* ── face ─────────────────────────────────────────────────────────────── */

.ech-evb-face {
  position: relative;
  z-index: 4;
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: var(--evb-pad);
  font-size: var(--evb-size);
  font-weight: 700;
  letter-spacing: var(--evb-track);
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  /* A ground-derived contour hugging the glyph. It cannot separate two
     overlapping 1px strokes, but it does mean the reticle passing BESIDE a
     stroke crosses darkness rather than half-lit ink. */
  filter: drop-shadow(0 0 2px var(--nilla-bg));
}
/* an inline SVG sits on the text baseline and drags a descender gap with it */
.ech-evb-face > svg { display: block; }

/* ── read-head: the arming scan ───────────────────────────────────────────
   magi-biometric streams hex glyphs behind this on a canvas. A canvas inside
   every button in a kit is not a trade worth making, and at 24px there is
   nothing to read anyway — the bar and the region it leaves behind carry the
   whole signal.

   z-index 2 puts the head UNDER the well and under the face. It used to be at
   5, which meant its 2px bar of solid --evb-live crossed the whole label on
   every hover: --evb-live against --nilla-text is 1.02:1 on Pattern Blue and
   under 4.5:1 on all eight presets, and an opaque bar cannot be rescued by
   anything painted beneath it. A read head that occludes what it is reading is
   backwards anyway — magi-biometric streams its glyphs BEHIND the text. */
.ech-evb-head {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.ech-evb[data-state="scan"] .ech-evb-head { opacity: 1; }
.ech-evb-head::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 2px;
  background: var(--evb-live);
}
.ech-evb-head::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--evb-live) 18%, transparent));
}
.ech-evb[data-state="scan"] .ech-evb-head::before {
  animation: ech-evb-head var(--evb-scan-ms) linear forwards;
}
.ech-evb[data-state="scan"] .ech-evb-head::after {
  animation: ech-evb-read var(--evb-scan-ms) linear forwards;
}

/* ── rolling scan band, once armed ────────────────────────────────────────
   Opacity is what switches, not play-state alone: a paused periodic tape is
   invisible mid-cycle, but a paused sweep is a bright bar left frozen across
   the face. Play-state still gates the motion so the band resumes in place.

   z-index 2 for the same reason as the read-head, and this is the worse of the
   two: armed IS the steady hover state, so a layer above the well was painting
   --evb-live over the pointer's disc for the whole time the pointer was on the
   control — continuously, not for a 460ms arrival. Below the well it gets
   shaded like the lattice and the gutters, and the punch-out removes it from
   the disc outright. */
.ech-evb-scan {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  opacity: var(--evb-scan-o);
  pointer-events: none;
  transition: opacity 240ms ease;
}
.ech-evb[data-disabled="0"][data-state="armed"] { --evb-scan-o: 1; }
.ech-evb-scan::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -42%;
  height: 42%;
  background: linear-gradient(180deg, transparent,
    color-mix(in srgb, var(--evb-live) 26%, transparent), transparent);
  animation: ech-evb-roll 1.9s linear infinite;
  animation-play-state: var(--evb-run);
}

/* ── latch lamp ───────────────────────────────────────────────────────── */

.ech-evb-led {
  position: absolute;
  z-index: 6;
  top: 2px;
  right: 2px;
  width: 3px;
  height: 3px;
  background: color-mix(in srgb, var(--evb-orchid) 26%, transparent);
  pointer-events: none;
  transition: background 300ms ease, box-shadow 300ms ease;
}

/* ── press wipe ───────────────────────────────────────────────────────── */

.ech-evb-wipe {
  position: absolute;
  inset: 0;
  z-index: 8;
  overflow: hidden;
  pointer-events: none;
}
.ech-evb-wipe::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -34%;
  height: 34%;
  background: linear-gradient(180deg, transparent,
    color-mix(in srgb, var(--evb-live) 72%, transparent), transparent);
  animation: ech-evb-wipe 420ms ease-in forwards;
}
.ech-evb-wipe::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--evb-live);
  mix-blend-mode: screen;
  opacity: 0;
  animation: ech-evb-flash 420ms linear forwards;
}

/* ── the stamped verdict ──────────────────────────────────────────────────
   eject-abort inverts the whole housing on commit — accent field, ground text.
   Inverting the FACE would lay a solid sheet of aqua directly under a reticle
   that is itself aqua while pressed, so the inversion is relocated OUTWARD:
   the plate blacks out to the ground, the rim doubles in weight and goes live,
   the gutters swap ply for field, and the crosshair snaps on. The ink stays
   neutral for exactly the reason the hover state does — the centre is the
   reticle's. Written with the same specificity shape as the hot rule and
   declared after it, so it wins while the pointer is still on the control. */
.ech-evb[data-disabled="0"][data-state="fired"] {
  --evb-plate: var(--nilla-bg);
  --evb-ink: var(--evb-ink-hot);
  --evb-edge: var(--evb-live);
  --evb-rim: 2px;
  --evb-gut-bg: var(--evb-live);
  --evb-cross-o: 1;
}
.ech-evb[data-disabled="0"][data-state="fired"] .ech-evb-gut::before {
  --evb-ply: var(--nilla-bg);
  opacity: 1;
}
.ech-evb[data-disabled="0"][data-state="fired"] .ech-evb-gut::after { opacity: 0; }
.ech-evb[data-disabled="0"][data-state="fired"] .ech-evb-body {
  animation: ech-evb-stamp 300ms cubic-bezier(.2,1.3,.4,1);
}
/* The discharge: down 9px, overshoot 4px past the rest line, then two decaying
   bounces. A single translate reads as a press; this reads as a firing. */
.ech-evb[data-disabled="0"][data-state="fired"] .ech-evb-lift {
  animation: ech-evb-recoil 500ms cubic-bezier(.3,1.6,.4,1);
}

/* ── latched ──────────────────────────────────────────────────────────────
   A latch is persistent STATE, so it stays orchid — aqua is reserved for the
   live answer to a pointer, and a permanently aqua control would break that.
   Carried on aria-pressed rather than a class, so the accessible state and
   the visual state cannot drift apart. */
.ech-evb-btn[aria-pressed="true"] .ech-evb-body {
  box-shadow: inset 0 0 12px color-mix(in srgb, var(--evb-orchid) 18%, transparent);
}
.ech-evb-btn[aria-pressed="true"] .ech-evb-led {
  background: var(--evb-orchid);
  box-shadow: 0 0 6px color-mix(in srgb, var(--evb-orchid) 70%, transparent);
  animation: ech-evb-tick 1.1s steps(2, jump-none) infinite;
}

.ech-evb-btn:focus-visible { outline: 2px solid var(--evb-orchid); outline-offset: 2px; }

/* ── the arming status line ───────────────────────────────────────────────
   Only mounted when arm is on, because that is the only case where the phase
   is semantic rather than decorative. Visually hidden: the phase is already
   legible on screen from the brackets and the read-head. */
.ech-evb-say {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── dead ─────────────────────────────────────────────────────────────────
   Unmistakably so: the hue is gone, the label is at the faintest ink the
   palette has, and the skirt is removed entirely — a cap with nothing under it
   has no travel left to give, which is what dead looks like mechanically.

   No opacity step on top of that, and the reason is arithmetic: the ink token
   is ALREADY 28% alpha, so a 0.34 multiplier put the glyph at ~0.095 — about
   1.2:1 against the status-bar plate, with the edge at ~1.0:1 and the corner
   brackets zeroed. That is not a dead control, it is an absent one, and the
   icon variant has no visible word left to fall back on: a fresh load, where
   undo and redo are both unavailable, simply appeared to be missing two
   buttons. Grayscale plus the faint ink alone lands near 2:1 — dim, but
   plainly still occupying its slot. */
.ech-evb-btn:disabled { cursor: not-allowed; color: var(--theme-ink-faint); }
.ech-evb[data-disabled="1"] .ech-evb-lift { filter: none; transform: none; }
/* The plate stays where it was — it cannot go transparent, or the dimmed edge
   under it would flood the whole face (see --evb-plate above). Grayscale on the
   body reaches the plate and the tape through it in one pass. */
.ech-evb[data-disabled="1"] .ech-evb-body {
  background-color: color-mix(in srgb, var(--nilla-border) 60%, transparent);
  filter: grayscale(1);
}
.ech-evb[data-disabled="1"] .ech-evb-body::before { background-image: none; box-shadow: none; }
.ech-evb[data-disabled="1"] .ech-evb-gut::before { opacity: 0.22; }
.ech-evb[data-disabled="1"] .ech-evb-gut::after { opacity: 0; }
.ech-evb[data-disabled="1"] .ech-evb-cross { opacity: 0; }

@keyframes ech-evb-crawl { to { background-position: 18.385px 0; } }
@keyframes ech-evb-drift { to { background-position: 0 26px; } }
@keyframes ech-evb-roll { to { top: 100%; } }
@keyframes ech-evb-wipe { to { top: 134%; } }
@keyframes ech-evb-head { from { left: 0; } to { left: 100%; } }
@keyframes ech-evb-read { to { transform: scaleX(1); } }
@keyframes ech-evb-stamp { from { transform: scale(1.08); } to { transform: scale(1); } }
@keyframes ech-evb-recoil {
  0% { transform: translateY(0); }
  12% { transform: translateY(9px); }
  34% { transform: translateY(-4px); }
  55% { transform: translateY(5px); }
  78% { transform: translateY(-2px); }
  100% { transform: translateY(0); }
}
/* Two pops rather than one fade: a single decay reads as a glow, a double pop
   reads as a discharge. Capped at 0.14 rather than the 0.3 it started at,
   because this layer screens --evb-live over the WHOLE face — additive, which
   is the one operation the collision analysis rules out, and it lands on
   exactly the disc the reticle is standing in. The punch-out keeps it off that
   disc; the cap is what keeps the rest of the face from flaring past the
   figures quoted at the top of this file. Two pops still read at 0.14 because
   the eye is reading the RHYTHM, not the amplitude. */
@keyframes ech-evb-flash {
  0% { opacity: 0.14; }
  22% { opacity: 0; }
  44% { opacity: 0.14; }
  100% { opacity: 0; }
}
@keyframes ech-evb-tick { 0% { opacity: 1; } 100% { opacity: 0.35; } }

@media (prefers-reduced-motion: reduce) {
  .ech-evb-btn, .ech-evb-lift, .ech-evb-brk, .ech-evb-body, .ech-evb-body::before,
  .ech-evb-lat, .ech-evb-well, .ech-evb-scan, .ech-evb-head, .ech-evb-led,
  .ech-evb-cross, .ech-evb-gut, .ech-evb-gut::before, .ech-evb-gut::after {
    transition: none;
  }
  .ech-evb-body, .ech-evb-body::before, .ech-evb-lat, .ech-evb-lift,
  .ech-evb-gut::before, .ech-evb-gut::after, .ech-evb-scan::before,
  .ech-evb-head::before, .ech-evb-head::after, .ech-evb-wipe::before,
  .ech-evb-wipe::after, .ech-evb-btn[aria-pressed="true"] .ech-evb-led {
    animation: none;
  }
  /* the read-head never runs, so park it finished rather than at frame 0 */
  .ech-evb-head::after { transform: scaleX(1); }
}
