/* ============================================================================
   THE CALCORAS SEQUENCE — presentation
   Premium, cinematic, restrained. Colour carries stage: cyan (signal) →
   pale steel (archive) → gold (convergence). Glitch appears only when the
   narrative says the system is unstable.
   ========================================================================= */

/* Nothing from the surface site comes through: no nav, no footer, and no
   market ticker scrolling stock prices across a first-contact event. */
nav, footer, .market-pulse { display: none !important; }
/* Lock horizontal drift, but never trap vertical scroll — the finale is taller
   than a phone. The puzzles guard themselves with touch-action instead. */
body { background: #04060a; margin: 0; overflow-x: hidden; }

.nx {
  --bg: #04060a;
  --ink: #8fa6b0;
  --ac: #00f0ff;
  --ac2: #d4af37;
  --panel: rgba(10, 16, 22, .55);
  --bd: rgba(0, 240, 255, .16);
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--ink);
  overflow: hidden;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  transition: --ac 1.2s ease;
}
.nx.s-stageTwo   { --ac: #9fc2cf; --bd: rgba(159, 194, 207, .18); --bg: #05070a; }
.nx.s-stageThree { --ac: #d4af37; --bd: rgba(212, 175, 55, .22); --bg: #06050a; }
.nx.s-completed  { --ac: #d4af37; --bd: rgba(212, 175, 55, .3);  --bg: #050408; }

#nxCanvas { position: fixed; inset: 0; z-index: 0; width: 100%; height: 100%; }
/* The peripheral geometry sits between the starfield and the puzzle, and must
   never intercept a click — the puzzle owns every pointer event. */
#nxStrata { position: fixed; inset: 0; z-index: 1; width: 100%; height: 100%;
            pointer-events: none; }

/* --------------------------------------------------------------- chrome -- */
/* The three progress dashes are gone (2026-07-16). They lived here, at top
   centre, and the exit now has that space — see .nx-exit below. */

.nx-mute, .nx-reset {
  position: fixed; top: 14px; z-index: 45; background: transparent;
  border: 1px solid var(--bd); color: var(--ac); border-radius: 999px;
  cursor: pointer; opacity: .4; transition: opacity .3s, border-color .3s;
  font-family: inherit; font-size: .68rem; letter-spacing: 1px;
}
.nx-mute { right: 16px; width: 34px; height: 34px; }
.nx-reset { right: 60px; height: 34px; padding: 0 12px; }
.nx-mute:hover, .nx-reset:hover { opacity: 1; border-color: var(--ac); }
.nx-mute.off { opacity: .22; text-decoration: line-through; }

/* TOP centre, in the space the progress dashes used to occupy.
   It was bottom centre and fixed, so on any viewport where the stage is taller
   than the screen it sat ON the puzzle — the five ARRAY cards stack on mobile
   and the link landed across ARRAY 04/05. The old fix was a gradient painted
   behind the link so it stayed *legible* over the content it covered, which
   concedes the overlap instead of removing it. Nothing else is at top centre,
   so now nothing collides. */
.nx-exit { position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 40; }
/* The mirror screen carries its own exit (.nx-mirror-exit), and the global fixed
   one does not know that — so the finale showed "return to the surface" twice,
   stacked. The screen that owns an exit wins. */
.nx.s-mirror .nx-exit { display: none; }
/* RESET and the mute toggle are pinned top-right and the exit is centred, so on
   a narrow screen the two meet. Shorten the link rather than let them touch —
   the arrow alone still reads as "back" and the title carries the rest. */
@media (max-width: 560px) {
  .nx-exit a { font-size: .68rem; letter-spacing: 1px; }
  .nx-exit .nx-exit-long { display: none; }
}
.nx-exit a { color: var(--ink); opacity: .5; font-size: .82rem; letter-spacing: 1.6px; text-decoration: none; transition: opacity .3s; }
.nx-exit a:hover { opacity: .9; color: var(--ac); }

/* ------------------------------------------------------------ awakening -- */
.nx-boot {
  position: fixed; inset: 0; z-index: 50; background: #04060a;
  display: flex; align-items: center; justify-content: center;
  color: var(--ac); font-size: clamp(.85rem, 2.2vw, 1.3rem);
  letter-spacing: 4px; text-align: center; padding: 30px;
  opacity: 0; pointer-events: none; transition: opacity .6s ease;
}
.nx-boot.on { opacity: 1; pointer-events: all; }

/* --------------------------------------------------------------- stage --- */
.nx-core {
  position: relative; z-index: 2; min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 72px 20px 64px; opacity: 0; transform: translateY(14px);
  transition: opacity .7s ease, transform .7s ease;
}
.nx-core.in { opacity: 1; transform: none; }

.nx-head { text-align: center; max-width: 720px; }
.nx-eyebrow { font-size: .78rem; letter-spacing: 4px; color: var(--ac); opacity: .85; }
.nx-h { font-size: clamp(1.3rem, 3.6vw, 2rem); font-weight: 800; letter-spacing: 1px; color: #eef6f8; margin: 10px 0 6px; }
.nx-sub { font-size: .92rem; color: var(--ink); opacity: .72; font-style: italic; margin: 0 0 4px; }
.nx-say { min-height: 1.4em; color: var(--ac); letter-spacing: 2px; font-size: .8rem; margin: 10px 0 2px; }
.nx-status { min-height: 1.2em; color: var(--ac2); letter-spacing: 2px; font-size: .74rem; opacity: .8; transition: color .3s; }
.nx-status.pulse { text-shadow: 0 0 14px currentColor; }
.nx-status.wrong { color: #e8794a; }

.nx-stage { width: 100%; max-width: 900px; margin-top: 22px; }
.nx-lead { text-align: center; color: var(--ink); opacity: .7; font-size: .86rem; line-height: 1.7; margin: 0 auto 22px; max-width: 560px; }

/* transient corner message */
.nx-flash-msg {
  position: fixed; right: 22px; bottom: 42px; z-index: 44;
  color: var(--ac); font-size: .72rem; letter-spacing: 3px;
  opacity: 0; transform: translateY(6px); pointer-events: none;
  transition: opacity .25s ease, transform .25s ease; text-align: right; max-width: 46vw;
}
.nx-flash-msg.show { opacity: .85; transform: none; }

/* instability — narrative only */
.nx.disturb .nx-core { animation: nxShake .42s ease; }
.nx.disturb #nxCanvas { filter: hue-rotate(40deg) saturate(1.4); }
@keyframes nxShake {
  0%, 100% { transform: none }
  20% { transform: translate(-5px, 2px) }
  45% { transform: translate(4px, -3px) }
  70% { transform: translate(-3px, 1px) }
}

/* =============================================== STAGE I — THE SIGNAL ==== */
/* --n is set from CFG.stageOne.kinds.length by stages.js. One row, no orphan.
   Below 820px the array stacks to a single column — still no orphan, and the
   cards stay large enough to read the waveform, which is the whole puzzle. */
.nx-wave-row { display: grid; grid-template-columns: repeat(var(--n, 5), 1fr); gap: 12px; }
@media (max-width: 820px) { .nx-wave-row { grid-template-columns: 1fr; gap: 10px; } }
.nx-wave {
  position: relative; background: var(--panel); border: 1px solid var(--bd);
  border-radius: 14px; padding: 0; height: 190px; cursor: pointer; overflow: hidden;
  transition: border-color .3s, transform .3s, box-shadow .3s; font-family: inherit;
}
.nx-wave:hover { border-color: var(--ac); transform: translateY(-3px); box-shadow: 0 18px 44px rgba(0, 0, 0, .5); }
.nx-wave:focus-visible { outline: 2px solid var(--ac); outline-offset: 3px; }
.nx-wave-cv { width: 100%; height: 100%; display: block; transition: opacity .6s ease; }
.nx-wave-tag {
  position: absolute; left: 12px; top: 10px; font-size: .6rem; letter-spacing: 2px;
  color: var(--ink); opacity: .5;
}
.nx-wave-glyph {
  position: absolute; inset: 0; display: grid; place-items: center;
  opacity: 0; transform: scale(.7); transition: opacity .8s ease, transform .8s cubic-bezier(.2, .8, .2, 1);
}
.nx-wave-glyph.show { opacity: 1; transform: none; }
/* The four that were never candidates. They do not fail — they simply stop
   pretending. Kept visible on purpose: an absence you can still see is louder
   than one that is removed. */
.nx-wave.inert { opacity: .28; filter: grayscale(1) blur(.6px); pointer-events: none; }
.nx-wave.inert .nx-wave-tag { text-decoration: line-through; }
.nx-wave.rejected { animation: nxReject .7s ease; border-color: rgba(232, 121, 74, .5); }
@keyframes nxReject {
  0% { transform: none; filter: none }
  30% { transform: translateX(-4px) skewX(3deg); filter: blur(2px) brightness(1.6) }
  60% { transform: translateX(4px) skewX(-2deg); filter: blur(1px) }
  100% { transform: none; filter: none }
}
.nx-wave.accepted { border-color: var(--ac); box-shadow: 0 0 0 1px var(--ac), 0 0 44px rgba(0, 240, 255, .3); }

/* ============================================== STAGE II — THE ARCHIVE === */
/* --- Stage II: the lightbox ------------------------------------------------
   Four quadrant slots. Records get laid ONTO this, which is where four
   civilisations visibly agree — the assembly is the mechanic now, not a
   post-solve reward animation. */
.nx-lightbox {
  /* Sized against viewport HEIGHT as well as width. At a flat 360px the lightbox
     plus the tray overflowed a 889px-tall viewport by 210px and the sequence —
     which is meant to be a fixed, full-screen experience — grew a scrollbar. */
  position: relative; width: min(320px, 74vw, 34vh); aspect-ratio: 1; margin: 0 auto 20px;
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
  border: 1px solid var(--bd); border-radius: 8px;
  background: radial-gradient(circle at 50% 50%, rgba(20, 28, 36, .55), rgba(4, 7, 10, .9) 70%);
  transition: box-shadow .8s, border-color .8s;
}
/* Explicit placement: a 2x2 grid flows 0,1,2,3 = TL,TR,BL,BR, but the quadrant
   indices are TL,TR,BR,BL (clockwise). Left implicit, slot 2 and 3 would swap
   and the symbol would assemble scrambled. */
.nx-slot { position: relative; border: 1px dashed rgba(159, 194, 207, .14); margin: 3px;
           border-radius: 4px; overflow: hidden;
           transition: background .25s, border-color .25s; }
.nx-slot[data-q="0"] { grid-row: 1; grid-column: 1; }
.nx-slot[data-q="1"] { grid-row: 1; grid-column: 2; }
.nx-slot[data-q="2"] { grid-row: 2; grid-column: 2; }
.nx-slot[data-q="3"] { grid-row: 2; grid-column: 1; }
.nx-slot.hot { background: rgba(212, 175, 55, .10); border-color: rgba(212, 175, 55, .5); }
.nx-lightbox.whole { border-color: var(--ac2); box-shadow: 0 0 0 1px var(--ac2), 0 0 70px rgba(212, 175, 55, .3); }
.nx-lightbox.whole .nx-slot { border-color: transparent; }
/* the symbol burning through the stone once all four agree */
.nx-lightbox.burn::after {
  content: ""; position: absolute; inset: -6%; pointer-events: none; border-radius: 12px;
  background: radial-gradient(circle at 50% 50%, rgba(212, 175, 55, .34), transparent 62%);
  animation: nxBurn 2.4s ease-out forwards;
}
@keyframes nxBurn { 0% { opacity: 0; transform: scale(.7) } 45% { opacity: 1 } 100% { opacity: .5; transform: scale(1) } }

/* --- Stage II: the fifth record -------------------------------------------
   Four quadrants complete the symbol and leave its centre empty. The fifth
   record is not stone or bronze and not from any century — it is a live
   projection, dated today, filed from wherever the observer actually is.

   It sits ABOVE the four slots (z-index) rather than inside the grid: the grid
   is 2x2 by construction and the centre is the seam between all four cells, not
   a fifth cell. Positioned, not flowed. */
.nx-slot-core {
  position: absolute; left: 50%; top: 50%; width: 44%; aspect-ratio: 1;
  transform: translate(-50%, -50%); z-index: 3;
  /* A solid, softly-glowing ring, not a dashed one — the dashes read as a
     rough "cut" edge against the dense disc that drops into it. */
  border: 1px solid rgba(0, 240, 255, .35); border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(0, 240, 255, .07), transparent 70%);
  transition: border-color .25s, background .25s, box-shadow .25s;
}
.nx-lightbox.hollow .nx-slot-core { animation: nxCoreWait 2.6s ease-in-out infinite; }
@keyframes nxCoreWait { 50% { border-color: rgba(0, 240, 255, .8); box-shadow: 0 0 26px rgba(0, 240, 255, .3); } }
.nx-slot-core.hot { border-color: #00f0ff; background: rgba(0, 240, 255, .16); box-shadow: 0 0 34px rgba(0, 240, 255, .5); }
.nx-lightbox.sealed { border-color: #00f0ff; box-shadow: 0 0 0 1px #00f0ff, 0 0 90px rgba(0, 240, 255, .35); }
/* Once the disc is seated it IS the edge; drop the core's own ring so there is
   no doubled/offset outline behind it. */
.nx-lightbox.sealed .nx-slot-core { border-color: transparent; background: none; box-shadow: none; }
.nx-slot-core .nx-frag-five { position: static; width: 100%; cursor: default; overflow: visible; }
.nx-slot-core .nx-frag-tag, .nx-slot-core .nx-frag-era { display: none; }

/* The record itself: light, not matter. Cyan against four dead browns.

   It lands in the tray, which is a 4-column grid built for the four quadrants —
   so a fifth child falls into row 2 column 1 and sits off in the bottom-left
   corner looking like a mistake. It spans the row and centres instead: by the
   time it exists the other four are gone from the tray anyway, and this is the
   only thing left on the table. */
/* .nx-frag.nx-frag-five, not .nx-frag-five: `.nx-tray .nx-frag { max-width: 22vh }`
   lives further down this file at equal specificity, so a plainer selector here
   loses on source order and the card came out 198px instead of 135 — which is
   what pushed the question off the bottom of the screen. */
.nx-tray .nx-frag.nx-frag-five { grid-column: 1 / -1; justify-self: center;
  width: 20vh; max-width: 20vh; margin: 8px auto 40px; }

/* THE ARCHIVE CLEARS THE TABLE for the last record. The lead paragraph is
   instructions for a puzzle that is now solved, and the proof panel has had its
   beat — keeping both on screen pushed the fifth record's question past the
   bottom edge and grew the scrollbar this stage is explicitly not allowed to
   have (see .nx-lightbox). They go quietly when the fifth arrives. */
.nx-archive.five-on .nx-lead,
.nx-archive.five-on .nx-archive-proof {
  opacity: 0; max-height: 0; margin: 0; padding: 0; border-width: 0;
  overflow: hidden; pointer-events: none;
  transition: opacity .6s ease, max-height .6s ease, margin .6s ease, padding .6s ease;
}
.nx-frag-five {
  border-color: rgba(0, 240, 255, .45);
  background: #000;
  cursor: default;                      /* not grabbable until it is filed */
  opacity: 0; transform: scale(.82);
  transition: opacity .9s ease, transform .9s ease, box-shadow .5s, border-color .5s;
}
.nx-frag-five.arrive { opacity: 1; transform: none; }
.nx-frag-five.grabbable { cursor: grab; }
.nx-five-plate {
  position: absolute; inset: 0; border-radius: 50%;
  background: url("frag-five.webp") center/cover no-repeat;
  /* The art is lit out of pure black, so it composites by being ADDED to what
     is under it rather than by a blend mode — the same discipline the orb had to
     learn: mix-blend-mode dies to any ancestor stacking context, and this card
     is inside three of them. */
  opacity: .9;
}

/* FIVE independent bands, one source image. Each mask reveals a different ring
   of the same art (with a soft feathered edge so the seams do not show), and
   each band gets its own direction and speed — alternating CW/CCW so the disc
   reads as counter-rotating machinery, not one sticker spinning as a unit. The
   core does not rotate at all: at that radius the art is nearly radially
   symmetric, so spin is invisible there and a pulse reads as "alive" instead. */
.nx-five-core {
  -webkit-mask-image: radial-gradient(circle, #000 0%, #000 21%, transparent 25%);
  mask-image: radial-gradient(circle, #000 0%, #000 21%, transparent 25%);
  animation: nxFivePulse 3.2s ease-in-out infinite;
  transform-origin: 50% 50%;
}
.nx-five-b1 {
  -webkit-mask-image: radial-gradient(circle, transparent 0%, transparent 19%, #000 23%, #000 37%, transparent 41%);
  mask-image: radial-gradient(circle, transparent 0%, transparent 19%, #000 23%, #000 37%, transparent 41%);
  animation: nxFiveSpin 22s linear infinite;
}
.nx-five-b2 {
  -webkit-mask-image: radial-gradient(circle, transparent 0%, transparent 39%, #000 43%, #000 57%, transparent 61%);
  mask-image: radial-gradient(circle, transparent 0%, transparent 39%, #000 43%, #000 57%, transparent 61%);
  animation: nxFiveSpinRev 34s linear infinite;
}
.nx-five-b3 {
  -webkit-mask-image: radial-gradient(circle, transparent 0%, transparent 59%, #000 63%, #000 79%, transparent 83%);
  mask-image: radial-gradient(circle, transparent 0%, transparent 59%, #000 63%, #000 79%, transparent 83%);
  animation: nxFiveSpin 46s linear infinite;
}
.nx-five-b4 {
  -webkit-mask-image: radial-gradient(circle, transparent 0%, transparent 81%, #000 85%, #000 100%);
  mask-image: radial-gradient(circle, transparent 0%, transparent 81%, #000 85%, #000 100%);
  animation: nxFiveSpinRev 64s linear infinite;
}
@keyframes nxFiveSpin { to { transform: rotate(360deg); } }
@keyframes nxFiveSpinRev { to { transform: rotate(-360deg); } }
@keyframes nxFivePulse {
  0%, 100% { transform: scale(1); opacity: .85; }
  50% { transform: scale(1.09); opacity: 1; }
}
.nx-frag-five.live { box-shadow: 0 0 34px rgba(0, 240, 255, .35); }
.nx-frag-five.knows { border-color: #00f0ff; box-shadow: 0 0 46px rgba(0, 240, 255, .55); }
.nx-frag-five.seated { border-color: #00f0ff; box-shadow: 0 0 60px rgba(0, 240, 255, .5); }
/* Its era line is the only cyan text in the stage: everything else here is old. */
.nx-frag-five .nx-frag-era { color: #00f0ff; letter-spacing: 1.4px; }
/* The disc is dense, bright art now — no text can sit ON it without looking
   cut into it. So the two labels sit OUTSIDE the circle instead: era above,
   tag below, on the plain black around the card. The card's own plate layers
   are each circular (border-radius:50%), so dropping the card's overflow clip
   costs nothing but lets the labels escape. Hidden entirely once seated
   (.nx-slot-core rule below), so this only styles the tray-reveal state. */
.nx-frag-five { overflow: visible; }
.nx-frag-five .nx-frag-tag, .nx-frag-five .nx-frag-era {
  opacity: 1; z-index: 2; background: none; padding: 0;
  left: -20%; right: -20%;              /* room to breathe past the disc edge */
  text-shadow: 0 0 6px #000, 0 1px 4px #000;
}
.nx-frag-five .nx-frag-era { top: auto; bottom: 100%; margin-bottom: 12px; }
.nx-frag-five .nx-frag-tag { bottom: auto; top: 100%; margin-top: 12px; }

/* The question. One field, four digits. */
.nx-five-ask {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin: 18px auto 0; max-width: 92vw;
}
.nx-five-q { color: #00f0ff; font-size: .74rem; letter-spacing: 2px; }
.nx-five-in {
  width: 92px; background: rgba(0, 240, 255, .05); color: #00f0ff;
  border: 1px solid rgba(0, 240, 255, .4); border-radius: 6px;
  padding: 8px 10px; font-family: inherit; font-size: .9rem; letter-spacing: 3px;
  text-align: center; text-transform: uppercase;
}
.nx-five-in:focus { outline: none; border-color: #00f0ff; box-shadow: 0 0 18px rgba(0, 240, 255, .35); }
.nx-five-ask.shake { animation: nxShake .42s; }
.nx-five-ask.done { opacity: 0; pointer-events: none; transition: opacity .8s; }
@media (prefers-reduced-motion: reduce) {
  .nx-lightbox.hollow .nx-slot-core { animation: none; border-color: rgba(0, 240, 255, .8); }
  .nx-five-ask.shake { animation: none; }
  .nx-five-plate, .nx-five-core, .nx-five-b1, .nx-five-b2, .nx-five-b3, .nx-five-b4 { animation: none; }
}

/* --- Stage II: the tray --------------------------------------------------- */
.nx-tray { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
           max-width: min(680px, 92vw); margin: 0 auto; }
.nx-tray .nx-frag { max-width: 22vh; justify-self: center; width: 100%; }
.nx-frag {
  position: relative; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, rgba(24, 32, 40, .9), rgba(6, 10, 14, .95) 72%);
  border: 1px solid var(--bd); color: var(--ink); cursor: grab; overflow: hidden;
  transition: border-color .4s, box-shadow .4s, color .4s, opacity .4s;
  touch-action: none;
}
.nx-frag:focus-visible { outline: 2px solid var(--ac); outline-offset: 3px; }
.nx-frag.dragging { position: fixed; z-index: 60; cursor: grabbing; pointer-events: none;
                    box-shadow: 0 24px 60px rgba(0,0,0,.6); border-color: var(--ac); }
.nx-frag.placed { position: absolute; inset: 0; aspect-ratio: auto; border-radius: 4px;
                  border-color: transparent; background: none; cursor: default; }
/* Fades rather than the `display:none` cut this used to be — that read as a
   glitch (the label there one frame, gone the next) rather than a deliberate
   "this record is part of the whole now, its own label no longer matters".
   A short delay before the fade starts gives the placement itself a beat to
   register before anything else changes. */
.nx-frag.placed .nx-frag-tag, .nx-frag.placed .nx-frag-era {
  opacity: 0; pointer-events: none; transition: opacity .6s ease .3s;
}
/* wrong slot: the record knows where it belongs, and this is not it */
.nx-frag.repel { animation: nxRepel .6s cubic-bezier(.36,.07,.19,.97); border-color: rgba(232,121,74,.6); }
@keyframes nxRepel {
  0%,100% { transform: translateX(0) }
  15% { transform: translateX(-9px) rotate(-3deg) } 35% { transform: translateX(8px) rotate(2deg) }
  55% { transform: translateX(-5px) } 75% { transform: translateX(3px) }
}
.nx-frag-svg { position: absolute; inset: 6%; width: 88%; height: 88%;
               transform-origin: 50% 50%; transition: transform .32s cubic-bezier(.34,1.3,.5,1); }
/* Placed: blow the record up to twice the slot and slide it so ITS quadrant sits
   exactly in this slot. Without this the four pieces never form one continuous
   symbol — which is the entire point of the stage.

   Done with transform, not width/left percentages. The percentage version was
   measurably wrong: `width:200%` resolved to 100% of the containing block (149px
   where 298px was intended), so `left:-100%` shoved quadrants 1-3 clean off their
   slots — 1% intersection, i.e. invisible, while the status line still counted
   them as placed. Transform math is unambiguous here and needs no containing-block
   assumptions.

   With transform-origin 0 0, `scale(2)` runs first (rightmost), so the record
   spans 0..2W; the translate then shifts by whole slot-widths (% of the element's
   own border box) to bring quadrant (col,row) into 0..W. */
.nx-frag.placed .nx-frag-svg { inset: 0; width: 100%; height: 100%;
                               transform-origin: 0 0; transition: opacity .5s; }
.nx-slot[data-q="0"] .nx-frag.placed .nx-frag-svg { transform: translate(0, 0) scale(2); }
.nx-slot[data-q="1"] .nx-frag.placed .nx-frag-svg { transform: translate(-100%, 0) scale(2); }
.nx-slot[data-q="2"] .nx-frag.placed .nx-frag-svg { transform: translate(-100%, -100%) scale(2); }
.nx-slot[data-q="3"] .nx-frag.placed .nx-frag-svg { transform: translate(0, -100%) scale(2); }
.nx-frag-tag { position: absolute; left: 0; right: 0; bottom: 8px; text-align: center;
               font-size: .52rem; letter-spacing: 1.5px; color: var(--ink); opacity: .55; }
.nx-frag-era { position: absolute; left: 0; right: 0; top: 8px; text-align: center;
               font-size: .46rem; letter-spacing: 1px; color: var(--ac2); opacity: .5; }
@media (max-width: 820px) { .nx-tray { grid-template-columns: repeat(2, 1fr); } }

.nx-archive-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
  transition: transform 1.1s cubic-bezier(.3, .7, .2, 1);
}
.nx-disc {
  position: relative; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, rgba(24, 32, 40, .9), rgba(6, 10, 14, .95) 72%);
  border: 1px solid var(--bd); color: var(--ink); cursor: grab; overflow: hidden;
  transition: border-color .4s, box-shadow .4s, color .4s;
  touch-action: none;
}
.nx-disc:focus-visible { outline: 2px solid var(--ac); outline-offset: 3px; }
.nx-disc.dragging { cursor: grabbing; }
.nx-disc.near { border-color: rgba(212, 175, 55, .45); color: #b8cfd8; }
.nx-disc.locked { border-color: var(--ac2); color: var(--ac2); box-shadow: 0 0 0 1px var(--ac2), 0 0 40px rgba(212, 175, 55, .25); }
.nx-disc-svg { position: absolute; inset: 6%; width: 88%; height: 88%; transition: transform .12s linear; }
.nx-disc.locked .nx-disc-svg { transition: transform .8s cubic-bezier(.2, .8, .2, 1); }
.nx-disc-glyph { color: var(--ac); opacity: .85; }
.nx-disc.locked .nx-disc-glyph { color: var(--ac2); }
.nx-disc-dress { color: currentColor; }
.nx-disc-tag {
  position: absolute; left: 50%; bottom: 7px; transform: translateX(-50%);
  font-size: .5rem; letter-spacing: 1.5px; opacity: .45; white-space: nowrap; pointer-events: none;
}
/* environmental assist: the room leans toward the answer */
.nx-stage.assist .nx-disc.near { animation: nxNear 2.4s ease-in-out infinite; }
@keyframes nxNear {
  0%, 100% { box-shadow: 0 0 0 0 rgba(212, 175, 55, 0) }
  50% { box-shadow: 0 0 26px rgba(212, 175, 55, .32) }
}
/* The four eras collapse into one record: each disc carries one quadrant of
   the same glyph, so they must land exactly on top of each other. The era
   dressing fades on the way in — what survives the centuries is the symbol. */
.nx-archive-grid.converge .nx-disc {
  animation: nxConverge 1.3s cubic-bezier(.3, .7, .2, 1) forwards;
  background: transparent; border-color: transparent; box-shadow: none;
}
.nx-archive-grid.converge .nx-disc-dress,
.nx-archive-grid.converge .nx-disc-tag { opacity: 0; transition: opacity .5s ease; }
.nx-archive-grid.converge .nx-disc-glyph { color: var(--ac2); }
/* 4-up row: shift by 1.5 / 0.5 track widths (+ the 14px gaps between them) */
.nx-archive-grid.converge .nx-disc:nth-child(1) { --tx: calc(150% + 21px); --ty: 0 }
.nx-archive-grid.converge .nx-disc:nth-child(2) { --tx: calc(50% + 7px);   --ty: 0 }
.nx-archive-grid.converge .nx-disc:nth-child(3) { --tx: calc(-50% - 7px);  --ty: 0 }
.nx-archive-grid.converge .nx-disc:nth-child(4) { --tx: calc(-150% - 21px); --ty: 0 }
@keyframes nxConverge {
  to { transform: translate(var(--tx), var(--ty)); opacity: 1; }
}
.nx-archive-proof {
  margin-top: 26px; text-align: center; opacity: 0; transform: translateY(10px);
  transition: opacity 1s ease, transform 1s ease;
}
.nx-archive-proof.show { opacity: 1; transform: none; }
.nx-proof-inner {
  display: inline-flex; align-items: center; gap: 14px; padding: 14px 20px;
  border: 1px solid rgba(212, 175, 55, .28); border-radius: 14px;
  background: rgba(20, 16, 8, .45); color: #d9c98f;
  font-size: .68rem; letter-spacing: 1.6px; line-height: 1.6; max-width: 620px; text-align: left;
}

/* =========================================== STAGE III — CONVERGENCE ===== */
.nx-conv-stage {
  position: relative; width: 100%; aspect-ratio: 1 / .74; max-height: 52vh;
  margin: 0 auto; border-radius: 18px; overflow: hidden;
  border: 1px solid var(--bd); background: radial-gradient(circle at 50% 50%, rgba(12, 10, 6, .8), rgba(4, 4, 8, .96) 70%);
  touch-action: none;
}
.nx-conv-stage:focus-visible { outline: 2px solid var(--ac2); outline-offset: 3px; }
.nx-conv-cv { display: block; cursor: grab; }
.nx-conv-cv:active { cursor: grabbing; }
/* announced to screen readers, invisible to everyone else */
.nx-conv-live {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.nx-conv-glyphs {
  display: flex; justify-content: center; gap: 14px; margin-top: 20px;
  opacity: 0; transform: translateY(10px); pointer-events: none;
  transition: opacity .8s ease, transform .8s ease;
}
.nx-conv-glyphs.show { opacity: 1; transform: none; pointer-events: all; }
.nx-conv-glyph {
  width: 68px; height: 68px; display: grid; place-items: center; cursor: pointer;
  background: rgba(10, 8, 4, .6); border: 1px solid var(--bd); border-radius: 14px;
  color: var(--ink); transition: color .3s, border-color .3s, box-shadow .3s, transform .3s;
}
.nx-conv-glyph:hover { color: var(--ac2); border-color: var(--ac2); transform: translateY(-3px); }
.nx-conv-glyph:focus-visible { outline: 2px solid var(--ac2); outline-offset: 3px; }
.nx-conv-glyph.on { color: var(--ac2); border-color: var(--ac2); box-shadow: 0 0 0 1px var(--ac2), 0 0 34px rgba(212, 175, 55, .35); }
.nx-conv-glyph[disabled] { cursor: default; }
/* the machine twitches a beat before you commit */
.nx-conv-glyph.anticipate { transform: translateY(-3px) scale(1.04); color: #ffe9a8; }

/* ================================================= THE FINAL SEQUENCE ==== */
.nx-reveal {
  position: relative; z-index: 3; min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 60px 20px; transition: opacity .5s ease;
}
/* The blackout is a hard cut — no easing out, nothing to watch. */
.nx-reveal.blackout { opacity: 0; transition: opacity .06s linear; }
/* The arrival: it comes up out of the black, but it comes up SHARP.
   This used to run 4s of blur(14px) -> blur(5px) -> none, which was a mistake:
   the observer has just solved three puzzles and the reward for that is the
   screen refusing to be legible for four seconds. "Emerging" does not require
   defocus — the brightness lift and the scale settle carry it, and now the thing
   they earned is readable the moment it exists. */
.nx-reveal.bleed { animation: nxBleed 2.2s cubic-bezier(.22, .61, .36, 1) forwards; }
@keyframes nxBleed {
  0%   { opacity: 0;   filter: brightness(.35); transform: scale(1.03); }
  45%  { opacity: .8;  filter: brightness(.8);  transform: scale(1.01); }
  100% { opacity: 1;   filter: none;            transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .nx-reveal.bleed { animation: none; opacity: 1; }
}
.nx-reveal-bg {
  position: absolute; inset: 0; z-index: 0;
  /* webp q92: 2.46MB -> 0.42MB (83% off) at SSIM 0.989 against the PNG, i.e.
     visually indistinguishable. q85 would have saved another 0.15MB but measured
     0.977 — under the bar, and this image is pure atmosphere, so it isn't worth it. */
  background: url(reveal-bg.webp) center / cover no-repeat;
  opacity: 0; animation: nxRevealBg 3.4s ease forwards;
}
@keyframes nxRevealBg { to { opacity: .42 } }
/* a scrim so the transmission stays readable against the city */
.nx-reveal::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(ellipse at 50% 46%, rgba(2, 3, 6, .84), rgba(2, 3, 6, .4) 58%, rgba(2, 3, 6, .1) 85%);
}
.nx-sigil-glyph {
  position: relative; z-index: 2; opacity: 0; transform: scale(.6) rotate(-12deg);
  transition: opacity 1.2s ease, transform 1.4s cubic-bezier(.2, .8, .2, 1);
  filter: drop-shadow(0 0 30px rgba(212, 175, 55, .6));
}
.nx-sigil-glyph.show { opacity: 1; transform: none; }
.nx-final-lines { position: relative; z-index: 2; margin-top: 26px; max-width: 640px; }
.nx-final-line {
  color: var(--ac2); font-size: clamp(.86rem, 2.2vw, 1.05rem); letter-spacing: 2px;
  line-height: 1.9; margin: 6px 0; opacity: 0; transition: opacity .8s ease;
}
.nx-final-line.show { opacity: .92; }
.nx-count {
  font-size: clamp(1.1rem, 3.4vw, 1.8rem); font-weight: 800; letter-spacing: 2px;
  color: #fff; text-shadow: 0 0 26px var(--ac2); font-variant-numeric: tabular-nums; margin: 14px 0;
}
.nx-count small { font-size: .5em; opacity: .65; }

.nx-sigil { position: relative; z-index: 2; margin-top: 30px; opacity: 0; transition: opacity 1s ease; }
.nx-sigil.show { opacity: 1; }
/* The card sits on top of a very busy sky — it needs to win that fight. */
.nx-sg-card {
  display: inline-block; background: rgba(3, 5, 9, .92); border: 1px solid rgba(212, 175, 55, .45);
  border-radius: 18px; padding: 26px 32px; backdrop-filter: blur(14px); max-width: 460px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, .7), 0 0 0 1px rgba(0, 0, 0, .5);
}
.nx-sg-h { color: var(--ac2); font-size: .64rem; letter-spacing: 3px; opacity: .9; }
.nx-sg-code {
  font-size: clamp(1.3rem, 4vw, 1.9rem); font-weight: 800; letter-spacing: 4px;
  color: #fff; margin: 10px 0; text-shadow: 0 0 20px var(--ac2);
}
.nx-sg-locked {
  display: inline-flex; align-items: center; gap: 8px; margin: 6px 0 4px;
  color: rgba(212, 175, 55, .5); font-size: .58rem; letter-spacing: 2px;
  border: 1px dashed rgba(212, 175, 55, .28); border-radius: 999px; padding: 6px 12px;
}
.nx-sg-locked.unsealed {
  color: var(--ac2); border-style: solid; border-color: rgba(212, 175, 55, .6);
  animation: nxUnseal 2.6s ease-in-out infinite;
}
@keyframes nxUnseal {
  0%, 100% { box-shadow: 0 0 0 0 rgba(212, 175, 55, 0) }
  50%      { box-shadow: 0 0 22px rgba(212, 175, 55, .45) }
}
.nx-sg-coord { color: var(--ink); font-size: .62rem; letter-spacing: 2px; opacity: .6; margin-top: 8px; }
.nx-sg-sub { color: var(--ink); font-size: .78rem; opacity: .8; font-style: italic; margin-top: 6px; }
.nx-sg-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 18px; }
.nx-btn {
  background: transparent; border: 1px solid var(--ac2); color: var(--ac2); font-family: inherit;
  padding: 11px 20px; border-radius: 11px; letter-spacing: 1.6px; text-transform: uppercase;
  font-size: .78rem; cursor: pointer; text-decoration: none; transition: background .3s, color .3s;
}
.nx-btn:hover { background: var(--ac2); color: #06050a; }
.nx-sg-copied { color: var(--ac2); font-size: .7rem; margin-top: 12px; display: none; line-height: 1.5; }

/* ============================================ THE FOURTH SYMBOL ========== */
/* It breaks into real pieces, heals, and then will not stay where you put it. */

/* The dial and the inner interlock counter-rotate while it is whole — the
   thing is never quite still, even before it comes apart. These live on inner
   wrappers so the shards' own transform stays free for the break. */
.nx-spin-cw, .nx-spin-ccw { transform-box: view-box; transform-origin: 50px 50px; }
.nx-spin-cw  { animation: nxSpinCw 44s linear infinite; }
.nx-spin-ccw { animation: nxSpinCcw 30s linear infinite; }
@keyframes nxSpinCw  { to { transform: rotate(360deg) } }
@keyframes nxSpinCcw { to { transform: rotate(-360deg) } }

/* The shards fly past the viewBox — without this they get guillotined at the
   edge of their own square. */
.nx-sg-svg { overflow: visible; }
.nx-shard {
  transform-box: fill-box; transform-origin: center;
  transition: transform 1.15s cubic-bezier(.2, .8, .2, 1), opacity 1.15s ease;
}
/* Each shard carries its own bearing (--dx/--dy), spin and scale, so the
   break looks like a break and not a zoom. */
.nx-sigil-glyph.broken .nx-shard {
  transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(var(--sc));
  opacity: .22;
  transition: transform .62s cubic-bezier(.5, -0.4, .8, .5), opacity .62s ease;
}
.nx-sigil-glyph.broken .nx-shard-core { opacity: .9; }
.nx-sigil-glyph.broken { filter: drop-shadow(0 0 52px rgba(212, 175, 55, .95)) brightness(1.35); }
.nx-sigil-glyph.healing { filter: drop-shadow(0 0 38px rgba(212, 175, 55, .8)); }
.nx-sigil-glyph.roaming {
  position: fixed; left: 50%; top: 50%; z-index: 30; cursor: pointer;
  animation: nxLoose 3.4s ease-in-out infinite;
  will-change: left, top;
}
.nx-sigil-glyph.roaming:hover { filter: drop-shadow(0 0 44px rgba(212, 175, 55, 1)) brightness(1.25); }
.nx-sigil-glyph.roaming:focus-visible { outline: 2px solid var(--ac2); outline-offset: 8px; }
@keyframes nxLoose {
  0%, 100% { filter: drop-shadow(0 0 22px rgba(212, 175, 55, .5)) }
  50%      { filter: drop-shadow(0 0 42px rgba(212, 175, 55, .95)) }
}
.nx-sigil-glyph.opening { animation: nxOpening .9s cubic-bezier(.6, 0, .85, .2) forwards; }
@keyframes nxOpening { to { transform: scale(26); opacity: 0; filter: blur(10px) } }
.nx-sigil-hint {
  position: fixed; left: 50%; bottom: 52px; transform: translateX(-50%); z-index: 31;
  color: var(--ac2); font-size: .66rem; letter-spacing: 3px; opacity: 0;
  pointer-events: none; transition: opacity 1.2s ease; text-align: center;
}
.nx-sigil-hint.show { opacity: .55; animation: nxHintBreath 3s ease-in-out infinite; }
@keyframes nxHintBreath { 0%, 100% { opacity: .35 } 50% { opacity: .8 } }

/* ---------------------------------- STAGE IV — the resonance sequencer -- */
.nx-mirror-row { display: flex; justify-content: center; gap: 10px; margin: 8px 0 22px; transition: opacity .3s ease; }
.nx-mirror-row.shake { animation: nxShake .43s ease; }
/* while the archive is speaking, the row does not take input — say so */
.nx-mirror-row.busy { pointer-events: none; }
.nx-mirror-row.busy .nx-cell { cursor: default; opacity: .82; }
.nx-cell {
  width: 62px; height: 76px; border-radius: 12px; cursor: pointer;
  background: rgba(10, 14, 20, .6); border: 1px solid var(--bd);
  display: grid; place-items: center; transition: border-color .25s, background .25s, transform .18s;
}
.nx-cell i {
  width: 12px; height: 12px; border-radius: 50%; display: block;
  background: rgba(160, 190, 200, .25); transition: background .25s, box-shadow .25s, transform .25s;
}
.nx-cell:hover { border-color: rgba(232, 226, 208, .6); transform: translateY(-2px); }
.nx-cell:focus-visible { outline: 2px solid var(--ac); outline-offset: 3px; }
.nx-cell.on i { background: #e8e2d0; box-shadow: 0 0 16px rgba(232, 226, 208, .9); transform: scale(1.5); }
.nx-cell.echo { border-color: rgba(0, 240, 255, .8); background: rgba(0, 240, 255, .07); }
.nx-cell.echo i { background: #00f0ff; box-shadow: 0 0 20px #00f0ff; transform: scale(1.7); }
.nx-cell.live { border-color: rgba(232, 226, 208, .9); background: rgba(232, 226, 208, .08); }
.nx-cell.locked {
  border-color: var(--ac2); background: rgba(212, 175, 55, .1);
  box-shadow: 0 0 0 1px var(--ac2), 0 0 28px rgba(212, 175, 55, .3);
}
.nx-cell.locked i { background: var(--ac2); box-shadow: 0 0 22px var(--ac2); }
.nx-mirror-acts { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
.nx-btn-key { border-color: var(--ac2); color: var(--ac2); }
.nx-btn-key:hover { background: var(--ac2); color: #06050a; }
.nx-btn-ghost { opacity: .55; }

/* ================================================= THE MIRROR SCREEN ===== */
.nx-mirror-screen {
  position: relative; z-index: 4; min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 60px 20px; gap: 26px; opacity: 0; transition: opacity 1.6s ease;
  background: radial-gradient(ellipse at 50% 50%, rgba(6, 5, 4, .5), rgba(2, 2, 3, .92) 70%);
}
.nx-mirror-screen.in { opacity: 1; }
.nx-mirror-frame {
  position: relative; width: min(760px, 92vw); aspect-ratio: 16 / 9;
  border-radius: 16px; overflow: hidden;
  border: 1px solid rgba(212, 175, 55, .25);
  box-shadow: 0 40px 120px rgba(0, 0, 0, .8), 0 0 90px rgba(212, 175, 55, .12);
  animation: nxMirrorIn 2.2s cubic-bezier(.2, .8, .2, 1) both;
}
@keyframes nxMirrorIn {
  from { transform: scale(.86); opacity: 0; filter: blur(14px) }
  to   { transform: none; opacity: 1; filter: blur(0) }
}
.nx-mirror-vid { width: 100%; height: 100%; object-fit: cover; display: block; }
.nx-mirror-sound {
  position: absolute; right: 12px; bottom: 12px; display: none;
  background: rgba(0, 0, 0, .6); border: 1px solid rgba(212, 175, 55, .5);
  color: var(--ac2); border-radius: 999px; padding: 7px 13px; cursor: pointer;
  font-family: inherit; font-size: .64rem; letter-spacing: 1.5px;
}
.nx-mirror-sound.show { display: block; }
.nx-mirror-lines { max-width: 720px; text-align: center; }
.nx-mirror-line {
  /* Everything up to here is monospace, because everything up to here is a
     machine. This screen is not the machine talking — it is the thing the
     machine was carrying. A high-contrast serif reads old and spoken rather
     than typed, which is what these lines actually are.
     System stack only: the site ships zero dependencies, and a webfont here
     would mean a network round-trip before the most important text in the
     piece could render. */
  font-family: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua',
               Georgia, 'Times New Roman', serif;
  color: #f4ecd8; font-size: clamp(1.05rem, 2.7vw, 1.55rem); line-height: 1.85;
  letter-spacing: .8px; font-weight: 400; font-style: italic;
  margin: 14px 0; opacity: 0;
  transform: translateY(-22px); filter: blur(8px);
  text-shadow: 0 0 30px rgba(240, 230, 200, .35), 0 2px 20px rgba(0, 0, 0, .9);
}
/* THE HUMAN RECORD — 40,000 years drifting down the margins.
   Ghosted hard on purpose: these are not photographs on this screen, they are
   what the archive remembers, and a crisp JPEG of Rosa Parks next to "there is
   only one consciousness" would read as a slideshow. Mirrored like everything
   else here. Pinned outside the centre column and inert to input. */
.nx-hist {
  position: absolute; z-index: 0; pointer-events: none; user-select: none;
  width: clamp(124px, 14.5vw, 210px); height: auto; top: -22vh;
  /* A lane between the text column and the edge — not the edge itself.
     Bigger images need pushing further out: the video is min(760px, 92vw), so
     its half-width is ~26vw, and a picture centred too close puts its inner edge
     back over the frame. The lane was 32vw + 0..8vw for a 168px image; at 210px
     it needs another ~1.5vw of clearance, hence 34vw. Verified by measuring the
     real intersection against the video and the text, not by eye. */
  inset-inline-start: calc(50% + (var(--side) * (34vw + var(--x))));
  margin-inline-start: -6vw;
  opacity: 0; border-radius: 3px;
  /* Brighter and sharper. At brightness(.72) with a 0.34 opacity ceiling these
     were technically present and practically unreadable — the whole point is
     that the observer RECOGNISES what went past, and you cannot recognise a
     smudge. Still ghosted, still a memory rather than a photograph, but now
     legible. */
  filter: grayscale(1) contrast(1.05) brightness(1.05) sepia(.22);
  mix-blend-mode: screen;
  transform: scaleX(-1) rotate(var(--rot));
  animation: nxHist var(--dur) linear var(--delay) both;
}
/* top -> bottom, the way a record falls past you.
   Brighter than it was (.62/.5 -> .78/.66): the entire point is that the
   observer RECOGNISES what went past — Chauvet, the tank, the moon — and you
   cannot recognise a smudge. Still ghosted, still a memory rather than a
   photograph. */
@keyframes nxHist {
  0%   { opacity: 0; transform: scaleX(-1) rotate(var(--rot)) translateY(0) scale(.94); }
  14%  { opacity: .78; }
  80%  { opacity: .66; }
  100% { opacity: 0; transform: scaleX(-1) rotate(var(--rot)) translateY(128vh) scale(1.02); }
}
/* Nothing interrupts the fall any more — histCollapse() is a deliberate no-op.
   The record used to be swept sideways off the screen on "One essence."; the
   glyphs still converge there, but the human record keeps going, because the
   screen has been promising exactly that since launch: THE SEQUENCE IS COMPLETE
   / THE TRANSMISSION IS NOT. */
@media (max-width: 820px) { .nx-hist { display: none; } }
@media (prefers-reduced-motion: reduce) { .nx-hist { display: none; } }

/* THE REFLECTIONS — the line, thrown back at you from the margins, reversed.
   Pinned to the left/right edges and never over the centre column: the real line
   must stay perfectly readable, and these are for the corner of the eye. They are
   inert to the pointer and invisible to screen readers — the centre column is the
   text of record, and a reader hearing every line six times would be a nightmare. */
.nx-echo {
  position: absolute; z-index: 1; pointer-events: none; user-select: none;
  top: var(--y);
  /* A narrow wrapping column, NOT nowrap: a nowrap line is as wide as the
     sentence, and scaleX(-1) pivots that box about its own centre — which walked
     the long lines straight back over the video and the text they were echoing.
     Wrapping inside a fixed narrow column keeps them in the margin by construction. */
  width: 15vw; white-space: normal; text-align: center;
  /* Same face as the line they reflect — a reflection in a different typeface
     is not a reflection. */
  font-family: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua',
               Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: clamp(.55rem, .95vw, .82rem); letter-spacing: .6px; line-height: 1.5;
  color: #f4ecd8; opacity: 0;
  transform: scaleX(-1) rotate(var(--rot));
  text-shadow: 0 0 18px rgba(240, 230, 200, .5);
  animation: nxEcho var(--dur) ease-out var(--delay) both;
}
/* --side: -1 = left margin, 1 = right. Anchored from the viewport edge inward,
   so they can never encroach on the centre column whatever the text length. */
.nx-echo { left: auto; right: auto;
           inset-inline-start: calc(50% + (var(--side) * (26vw + var(--x))));
           margin-inline-start: -7.5vw; }
@keyframes nxEcho {
  0%   { opacity: 0; filter: blur(9px); transform: scaleX(-1) rotate(var(--rot)) translateX(calc(var(--side) * 26px)) scale(.94); }
  22%  { opacity: var(--a); filter: blur(0); transform: scaleX(-1) rotate(var(--rot)) translateX(0) scale(1); }
  70%  { opacity: calc(var(--a) * .8); }
  100% { opacity: 0; filter: blur(5px); transform: scaleX(-1) rotate(var(--rot)) translateX(calc(var(--side) * -18px)) scale(1.05); }
}
/* "One essence." — the margins fill, and then everything falls inward at once. */
.nx-echo.final {
  font-size: clamp(.72rem, 1.5vw, 1.15rem); color: #fff;
  text-shadow: 0 0 26px rgba(255, 255, 255, .8), 0 0 60px rgba(212, 175, 55, .5);
  animation-name: nxEchoFinal;
}
@keyframes nxEchoFinal {
  0%   { opacity: 0; filter: blur(12px); transform: scaleX(-1) rotate(var(--rot)) translateX(calc(var(--side) * 34px)); }
  18%  { opacity: calc(var(--a) * 1.8); filter: blur(0); transform: scaleX(-1) rotate(var(--rot)) translateX(0); }
  62%  { opacity: calc(var(--a) * 1.5); }
  /* inward, toward the centre, with the glyphs */
  100% { opacity: 0; filter: blur(7px);
         transform: scaleX(-1) rotate(0deg) translateX(calc(var(--side) * -26vw)) scale(.55); }
}
@media (max-width: 820px) { .nx-echo { display: none; } }
@media (prefers-reduced-motion: reduce) { .nx-echo { display: none; } }

/* THE IMPACT.
   The line lands and the frame takes it — a short, hard, decaying shake. Real
   impacts move the camera, not the subject: shaking the text alone reads as a
   wobbly font, shaking the whole scene reads as force. Sub-pixel and brief on
   purpose; anything longer becomes seasickness rather than weight. */
.nx-mirror-screen.quake { animation: nxQuake .42s cubic-bezier(.36, .07, .19, .97); }
@keyframes nxQuake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  12% { transform: translate3d(-5px, 2px, 0); }
  28% { transform: translate3d(4px, -3px, 0); }
  46% { transform: translate3d(-3px, 1px, 0); }
  66% { transform: translate3d(2px, -1px, 0); }
  84% { transform: translate3d(-1px, 0, 0); }
}
/* "One essence." — the last line the sequence ever says. Longer, wider, and it
   drags a chromatic tear through the frame on the way out. */
.nx-mirror-screen.quake-hard { animation: nxQuakeHard .78s cubic-bezier(.36, .07, .19, .97); }
@keyframes nxQuakeHard {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); filter: none; }
  8%  { transform: translate3d(-11px, 5px, 0) scale(1.012);
        filter: drop-shadow(3px 0 0 rgba(255,60,60,.5)) drop-shadow(-3px 0 0 rgba(60,200,255,.5)); }
  20% { transform: translate3d(9px, -6px, 0) scale(1.008);
        filter: drop-shadow(-2px 0 0 rgba(255,60,60,.4)) drop-shadow(2px 0 0 rgba(60,200,255,.4)); }
  36% { transform: translate3d(-7px, 3px, 0) scale(1.004); filter: none; }
  54% { transform: translate3d(5px, -2px, 0); }
  72% { transform: translate3d(-3px, 1px, 0); }
  88% { transform: translate3d(1px, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .nx-mirror-screen.quake, .nx-mirror-screen.quake-hard { animation: none; }
}

/* The line drifts down at the old pace, then lands hard: a flash of white,
   a scale kick, and it settles brighter than it arrived. */
.nx-mirror-line.show { animation: nxLineStrike 2.05s both; }
@keyframes nxLineStrike {
  0% {
    opacity: 0; transform: translateY(-22px) scale(1.03); filter: blur(8px);
    text-shadow: 0 0 30px rgba(240, 230, 200, .15), 0 2px 20px rgba(0, 0, 0, .9);
    animation-timing-function: cubic-bezier(.2, .8, .2, 1);
  }
  73% {
    opacity: 1; transform: translateY(0) scale(1); filter: blur(0);
    text-shadow: 0 0 30px rgba(240, 230, 200, .3), 0 2px 20px rgba(0, 0, 0, .9);
    animation-timing-function: cubic-bezier(.1, .9, .2, 1);
  }
  79% {
    transform: scale(1.055);
    text-shadow: 0 0 6px rgba(255, 255, 255, .95), 0 0 26px rgba(255, 255, 255, .8),
                 0 0 70px rgba(212, 175, 55, .7), 0 2px 20px rgba(0, 0, 0, .9);
  }
  88% { transform: scale(.992); }
  100% {
    opacity: 1; transform: none; filter: blur(0);
    text-shadow: 0 0 12px rgba(255, 255, 255, .28), 0 0 34px rgba(240, 230, 200, .55),
                 0 2px 20px rgba(0, 0, 0, .9);
  }
}
.nx-mirror-line:nth-child(4),
.nx-mirror-line:nth-child(5),
.nx-mirror-line:nth-child(6) { color: #fff; font-weight: 600; letter-spacing: 1.5px; }

/* "One essence." — nothing follows it, so it is allowed to be bigger, slower and
   brighter than everything before it. `.final` is set in JS from the array's last
   index, not hard-coded to a child number: the copy is config-driven and an
   nth-child would silently point at the wrong line the moment a line is added. */
.nx-mirror-line.final {
  font-size: clamp(1.2rem, 3.4vw, 1.9rem); letter-spacing: 3px; margin-top: 22px;
}
.nx-mirror-line.final.show { animation: nxLineStrike 2.6s both; }
.nx-mirror-exit { margin-top: 8px; }
.nx-mirror-exit a { color: var(--ink); opacity: .35; font-size: .74rem; letter-spacing: 2px; text-decoration: none; }
.nx-mirror-exit a:hover { opacity: .9; color: var(--ac2); }

@media (max-width: 860px) {
  .nx-cell { width: 36px; height: 58px; }
  .nx-mirror-row { gap: 6px; }
  .nx-mirror-frame { width: 96vw; }
}

.nx.reduced .nx-sigil-glyph.roaming { animation: none; }
.nx.reduced .nx-spin-cw, .nx.reduced .nx-spin-ccw { animation: none; }
.nx.reduced .nx-mirror-line { transform: none; filter: none; }
.nx.reduced .nx-mirror-line.show { animation: none; opacity: 1; }

/* the other observer, at the edge of the frame */
.nx-other {
  position: fixed; left: 14px; bottom: 14px; z-index: 46; pointer-events: none;
  color: rgba(212, 175, 55, .5); font-family: ui-monospace, monospace;
  font-size: .58rem; letter-spacing: 2px; opacity: 0; transition: opacity .5s ease;
}
.nx-other.show { opacity: 1; }

/* ------------------------------------------------------------- responsive */
@media (max-width: 860px) {
  .nx-core { padding: 64px 14px 72px; }
  .nx-wave-row { grid-template-columns: 1fr; gap: 12px; }
  .nx-wave { height: 116px; }
  /* 2x2 on small screens: each disc crosses half a track (+ half the gap) */
  .nx-archive-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .nx-archive-grid.converge .nx-disc:nth-child(1) { --tx: calc(50% + 6px);  --ty: calc(50% + 6px) }
  .nx-archive-grid.converge .nx-disc:nth-child(2) { --tx: calc(-50% - 6px); --ty: calc(50% + 6px) }
  .nx-archive-grid.converge .nx-disc:nth-child(3) { --tx: calc(50% + 6px);  --ty: calc(-50% - 6px) }
  .nx-archive-grid.converge .nx-disc:nth-child(4) { --tx: calc(-50% - 6px); --ty: calc(-50% - 6px) }
  .nx-conv-stage { aspect-ratio: 1; max-height: 46vh; }
  .nx-conv-glyph { width: 58px; height: 58px; }
  .nx-flash-msg { right: 14px; bottom: 54px; max-width: 70vw; font-size: .64rem; }
  .nx-proof-inner { font-size: .6rem; }
}
@media (max-width: 420px) {
  /* Reset must stay reachable on a phone — shrink it, never hide it. */
  .nx-reset { right: 54px; height: 30px; padding: 0 9px; font-size: .58rem; }
  .nx-mute { width: 30px; height: 30px; right: 14px; }
}

/* --------------------------------------------------------- reduced motion */
.nx.reduced #nxCanvas { opacity: .55; }
.nx.reduced *,
.nx.reduced *::before,
.nx.reduced *::after {
  animation-duration: .01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .12s !important;
}
.nx.reduced .nx-reveal-bg { opacity: .3; animation: none; }
@media (prefers-reduced-motion: reduce) {
  .nx-core { transition: none; opacity: 1; transform: none; }
}

/* The credits link is a licence condition, not decoration — it stays reachable,
   but it is not allowed to compete with the last line of the piece. */
.nx-credit-link {
  display: inline-block; margin-inline-start: 18px;
  color: var(--ink); opacity: .5; font-size: .78rem; letter-spacing: 1.2px;
  text-decoration: none; transition: opacity .3s;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}
.nx-credit-link:hover { opacity: .85; color: var(--ac2); }

/* ------------------------------------------------ the invitation (mirror) -- */
/* Fades up only after she has finished speaking. Everyone who sees this solved
   four puzzles and sat through the ending, so it is allowed to simply ask. */
.nx-invite {
  position: relative; z-index: 3; max-width: 560px; margin: 30px auto 0;
  text-align: center; opacity: 0; transform: translateY(10px);
  transition: opacity 2.2s ease, transform 2.2s ease; pointer-events: none;
}
.nx-invite.show { opacity: 1; transform: none; pointer-events: auto; }
.nx-invite-h {
  font-family: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
  font-style: italic; color: #fff; font-size: clamp(1rem, 2.2vw, 1.25rem);
  margin: 0 0 10px; text-shadow: 0 0 24px rgba(240, 230, 200, .5);
}
.nx-invite-p {
  color: var(--ink); font-size: .88rem; line-height: 1.8; margin: 0 0 16px;
  opacity: .9;
}
.nx-invite-f { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.nx-invite-f.busy { opacity: .45; pointer-events: none; }
.nx-invite-f input {
  flex: 1 1 240px; max-width: 300px; background: rgba(8, 12, 16, .8);
  border: 1px solid var(--bd); border-radius: 999px; padding: 10px 16px;
  color: #eef6f8; font-family: inherit; font-size: .82rem;
}
.nx-invite-f input:focus { outline: none; border-color: var(--ac2); }
.nx-invite-f button {
  background: transparent; border: 1px solid var(--ac2); color: var(--ac2);
  border-radius: 999px; padding: 10px 20px; cursor: pointer;
  font-family: inherit; font-size: .76rem; letter-spacing: 1.5px;
  transition: background .3s, color .3s;
}
.nx-invite-f button:hover { background: var(--ac2); color: #06050a; }
.nx-invite-note { color: var(--ink); font-size: .8rem; opacity: .72; margin: 12px 0 0; }
.nx-invite-note a { color: var(--ink); }
.nx-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* --------------------------------------------------- the gate (boot screen) --
   A browser will not make a sound until this document is touched, and arriving
   from the orb is a navigation, not a touch. Without this the whole sequence ran
   in silence — every sound working, the engine never switched on, and nothing on
   screen asking. So the gate is explicit, and it is the sigil, because the one
   thing nobody needs instructions for is a thing that is clearly waiting. */
.nx-boot.nx-gate #nxBootTxt {
  display: flex; flex-direction: column; align-items: center; gap: 18px;
  letter-spacing: 0;
}
.nx-gate-orb {
  background: none; border: none; padding: 0; cursor: pointer; line-height: 0;
  filter: drop-shadow(0 0 30px rgba(212, 175, 55, .55));
  animation: nxGateBeat 2.1s ease-in-out infinite;
  transition: transform .3s ease, filter .3s ease;
}
.nx-gate-orb:hover { transform: scale(1.08); filter: drop-shadow(0 0 52px rgba(212, 175, 55, .95)); }
.nx-gate-orb:focus-visible { outline: 2px solid var(--ac2); outline-offset: 12px; border-radius: 50%; }
.nx-gate-orb svg { width: 132px; height: 132px; display: block; }
@keyframes nxGateBeat {
  0%, 100% { transform: scale(1);    filter: drop-shadow(0 0 26px rgba(212, 175, 55, .5)); }
  50%      { transform: scale(1.055); filter: drop-shadow(0 0 46px rgba(212, 175, 55, .85)); }
}
.nx-gate-h {
  color: #f4ecd8; font-size: clamp(.9rem, 2.2vw, 1.15rem); letter-spacing: 5px;
  text-shadow: 0 0 24px rgba(240, 230, 200, .5);
}
.nx-gate-p {
  color: var(--ink); font-size: .72rem; letter-spacing: 2px; opacity: .6;
  font-style: italic;
}
@media (prefers-reduced-motion: reduce) { .nx-gate-orb { animation: none; } }

/* ---------------------------------- STAGE V — the other observer -------- */
/* Six records and a transcript, and it has to fit a phone without turning the
   sequence into a scrolling document. The type is small on purpose: this is a
   file being read, not a UI. */
.nx-dossier { max-width: 860px; margin: 0 auto; }

/* THE TRANSCRIPT. Not a hint panel — o-1, twenty minutes ago, was "Don't say
   anything. It logs the room." This is the archive keeping the only promise it
   ever made, so it is presented as evidence, not as help. */
.nx-transcript {
  border: 1px solid rgba(143, 166, 176, .22); border-radius: 12px;
  padding: 14px 16px; margin: 0 auto 20px; max-width: 620px;
  background: rgba(143, 166, 176, .04);
}
.nx-transcript h3 {
  margin: 0 0 10px; font-size: .62rem; letter-spacing: 2.4px; font-weight: 400;
  color: var(--ink); opacity: .55; text-transform: uppercase;
}
.nx-t-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; padding: 5px 0; }
.nx-t-line q { color: var(--ink); font-size: .8rem; font-style: italic; opacity: .82; quotes: '"' '"'; }
.nx-t-line.empty { color: var(--ink); opacity: .4; font-size: .72rem; letter-spacing: 2px; }
/* What the archive read out of the line. Hidden until the observer has earned
   it — the archive relents slowly here exactly as it does on the keypad.

   `visibility`, not opacity alone: an opacity-0 element is still in the
   accessibility tree, so a screen-reader user would be handed the archive's
   own reading of every line on arrival while a sighted user has to fail twice
   for it. That is the same parity bug REDESIGN already fixed once in Stage
   III's announce(), and it is worth stating plainly — hiding by opacity hides
   it from exactly one kind of observer. */
.nx-t-reads {
  font-size: .58rem; letter-spacing: 2px; color: var(--ac2);
  opacity: 0; visibility: hidden;
  transition: opacity .6s ease; white-space: nowrap;
}
.nx-transcript.marked .nx-t-reads { opacity: .75; visibility: visible; }

/* THE RECORDS. Six divides by 3, 2 and 1, so every breakpoint lands clean —
   unlike the five-card array, which an auto-fit grid always orphans 4+1. */
.nx-recs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.nx-rec {
  background: rgba(200, 212, 220, .03); border: 1px solid rgba(143, 166, 176, .2);
  border-radius: 12px; padding: 12px 13px; text-align: left; cursor: pointer;
  font-family: inherit; color: var(--ink); transition: border-color .3s, background .3s, transform .3s;
}
.nx-rec:hover { border-color: rgba(200, 212, 220, .55); transform: translateY(-2px); }
.nx-rec:focus-visible { outline: 2px solid var(--ac); outline-offset: 3px; }
.nx-rec.shake { animation: nxShake .43s ease; }
.nx-rec-id {
  font-size: .62rem; letter-spacing: 2px; color: #c8d4dc; opacity: .9;
  padding-bottom: 8px; margin-bottom: 8px; border-bottom: 1px solid rgba(143, 166, 176, .16);
}
.nx-rec dl {
  display: grid; grid-template-columns: auto 1fr; gap: 3px 8px; margin: 0;
  font-size: .58rem; letter-spacing: 1.4px;
}
.nx-rec dt { opacity: .42; }
.nx-rec dd { margin: 0; text-align: right; color: #c8d4dc; opacity: .88; }
.nx-rec dd.gold { color: var(--ac2); opacity: 1; }
/* Never resolved, because she never said the thing that would have resolved it.
   The masked rows are an honest record of what the observer was not listening
   to — o-12: "There's a version of this where you don't hear me at all." */
.nx-rec dd.masked { opacity: .3; letter-spacing: 0; }
.nx-rec-bio { padding-bottom: 8px; margin-bottom: 8px; border-bottom: 1px solid rgba(143, 166, 176, .16); }

/* the archive marks it itself when the observer was never given the evidence */
.nx-rec.marked { border-color: rgba(212, 175, 55, .55); background: rgba(212, 175, 55, .05); }

/* the lock: everything settles, the chosen one stays lit */
.nx-recs .nx-rec.settled { pointer-events: none; }
.nx-rec.settled:not(.found) { opacity: .5; }
.nx-rec.found { border-color: var(--ac2); background: rgba(212, 175, 55, .06); }
/* And then every record resolves to the same identity, one at a time. Nothing
   announces it. The reveal is that the reveal is not special. */
.nx-rec.resolved { opacity: 1; border-color: rgba(200, 212, 220, .5); }
.nx-rec.resolved .nx-rec-id { color: #f0e6c8; opacity: 1; animation: nxResolve .9s ease; }
@keyframes nxResolve {
  0%   { opacity: 0; letter-spacing: 8px; filter: blur(4px); }
  100% { opacity: 1; letter-spacing: 2px; filter: blur(0); }
}

/* PINNED TO THE VIEWPORT, because this is the one control that leaves the stage
   and it arrives under a transcript and six record cards — measured at top:810
   on a 720px window, i.e. off-screen, at the end of twenty minutes of work.
   Present and unreachable is the same failure as not being there.

   It is `fixed`, not `sticky`: sticky was tried first and did NOTHING, because
   .nx has overflow:hidden — a clipping ancestor leaves sticky with no scroll
   range to stick within, so it silently degrades to static. Same family as the
   mix-blend-mode note above: an ancestor's property quietly killing a child's.
   fixed answers to the viewport and cannot be undone from up the tree.

   Harmless before the stage resolves: the bar is empty and opacity:0 until
   answerHer() puts the button in it. */
.nx-dossier-acts {
  display: flex; justify-content: center; gap: 10px;
  opacity: 0; transition: opacity 1.2s ease;
  position: fixed; left: 0; right: 0; bottom: 22px; z-index: 6;
  pointer-events: none;
}
/* It floats over whatever the dossier ends with, so it carries its own darkness
   rather than trusting the background it happens to land on. */
.nx-dossier-acts.show {
  opacity: 1;
  pointer-events: auto;
  padding: 14px 0 6px;
  background: radial-gradient(50% 180% at 50% 100%, rgba(4, 6, 9, .95) 45%, rgba(4, 6, 9, 0) 100%);
}
/* your cadence going out, and the same cadence coming back */
.nx-dossier.pulse { animation: nxDossierPulse .16s ease; }
@keyframes nxDossierPulse { 50% { filter: brightness(1.5); } }

@media (max-width: 820px) { .nx-recs { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 460px) {
  .nx-recs { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .nx-rec { padding: 9px 10px; }
  .nx-rec dl { font-size: .54rem; gap: 2px 5px; letter-spacing: 1px; }
  .nx-t-line q { font-size: .74rem; }
  .nx-t-reads { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
  .nx-rec.resolved .nx-rec-id { animation: none; }
  .nx-dossier.pulse { animation: none; }
}

/* ------------------------------- the archive's count + 2030 countdown --- */
/* Both sit in the invite block on the mirror, because that block has claimed
   both since launch and shown neither: "Almost nobody gets here" (no number) and
   "The archive has a countdown on it" (no clock). Filled from real data only —
   if the count cannot be fetched the element stays [hidden] rather than printing
   an invented number under the last line of the piece. */
.nx-arch-count {
  color: var(--ac2); font-size: .8rem; letter-spacing: 1.8px;
  margin: 0 0 12px; opacity: .95;
}
.nx-arch-clock {
  display: inline-flex; flex-direction: column; gap: 5px; align-items: center;
  margin: 0 0 18px; padding: 9px 18px;
  border: 1px solid rgba(212, 175, 55, .22); border-radius: 11px;
  background: rgba(212, 175, 55, .04);
}
.nx-arch-clock-l { font-size: .7rem; letter-spacing: 2.2px; color: var(--ink); opacity: .72; }
.nx-arch-clock b {
  font-family: ui-monospace, monospace; font-weight: 400;
  font-size: .92rem; letter-spacing: 2px; color: #f0e6c8;
  font-variant-numeric: tabular-nums;   /* or the seconds jitter the whole box */
  text-shadow: 0 0 18px rgba(240, 230, 200, .35);
}
[hidden] { display: none !important; }
@media (max-width: 460px) {
  .nx-arch-clock b { font-size: .8rem; letter-spacing: 1px; }
  .nx-arch-count { font-size: .6rem; letter-spacing: 1.6px; }
}

/* =================================================== THE VEIL — Stage I ==== */
/* Two figures at the edges of the room and the instrument each stands behind.
   Everything here is periphery: it never takes a pointer event, never sits above
   the puzzle, and never competes with the array for attention. Same discipline
   as strata.js — the reason most "atmospheric background" work reads as noise is
   that it skips exactly this. */
/* NO position:fixed, NO z-index, NO opacity ON THIS CONTAINER. All three
   absences are load-bearing, and every one of them cost a round of this bug.

   mix-blend-mode only blends against the backdrop within its own stacking
   context. If any ancestor forms one, the images blend against nothing, their
   black stops disappearing, and it paints as an opaque rectangle. THREE separate
   ancestor properties formed one here, and each was found only by measuring:
     1. `z-index:1`                  -> step of 4.4 in background luminance
     2. `transform` on the wrapper   -> step of 3.45   (see .nx-veil-figwrap)
     3. `position:fixed`             -> step of 3.45 -> 0.12 when made absolute
   Chrome forms a stacking context for a fixed element even with z-index:auto,
   which is the one that looks completely innocent.

   The symptom is specific and easy to misread as a bad export: hard-edged panels
   down the sides with the starfield MISSING inside them while the middle of the
   screen still has stars, and the veil region measuring DARKER than the page —
   which `screen` cannot do, because it was never screening.

   So: absolute, no z-index, and `display` is the only thing toggled. The fade-in
   lives on the images (an element's own opacity does not isolate its own
   blending; it composites the already-blended result). Painting order still
   lands right — z-index:auto puts this above #nxCanvas (z0) by DOM order and
   below .nx-core (z2). If you ever need this to survive a scrolling page, fix it
   with layout, NOT with position:fixed. */
.nx-veil {
  position: absolute; inset: 0;
  pointer-events: none; overflow: hidden;
  display: none;
  /* The room either side of the puzzle. .nx-stage is 900px, so this is the space
     that actually exists to put someone in — and every figure's height is derived
     from it below, because a fixed height silently walks the art across the
     cards as the window narrows. Measured: content reaches 63% of the Stage I
     frame and 93% of the procession's, so at 1366 (an extremely common laptop)
     a fixed size put the queue straight through the array. */
  --gap: max(0px, calc((100vw - 940px) / 2));
}
/* One veil per stage, and never two at once. Stage I is two figures; Stage II is
   the same figure counted backwards into the dark. The escalation IS the
   argument — two, then many, then Stage V resolves every record in the line-up
   to the observer's own. Beyond Stage II they stop: from the astrolabe onward
   the observer is being asked something, and company at the edge of the room
   would answer it. */
.nx.s-stageOne .nx-veil-1 { display: block; }
.nx.s-stageTwo .nx-veil-2 { display: block; }

/* flex centring, NOT top:50%+translateY(-50%) — a transform on an ancestor forms
   a stacking context just as surely as z-index does, and blows the blend exactly
   the same way (see .nx-veil). It cost a second round of this bug: the fix was
   applied to the container, then a wrapper was added with a centring transform
   and reintroduced it. The tell was a 3.19 -> 7.38 brightness step measured at
   x=437, precisely the wrapper's right edge — the veil region rendering DARKER
   than the page, which `screen` cannot do, because it was never screening. */
.nx-veil-side {
  position: absolute; top: 0; bottom: 0; width: min(38vw, 460px);
  display: flex; align-items: center;
}
.nx-veil-l { left: 0; }
.nx-veil-r { right: 0; justify-content: flex-end; }

/* mix-blend-mode: screen is why the generated images needed a PURE BLACK
   background — screen maps black to nothing, so the figure emerges out of the
   starfield itself with no box, no edge and no cutout. Verified at generation:
   74-80% of every source image is exactly #000000. */
.nx-veil-fig, .nx-veil-pat {
  position: absolute; mix-blend-mode: screen; user-select: none;
}
/* THE FIGURE'S BOX. It carries the source's exact aspect ratio (768x1408), which
   is what lets the glints be placed as percentages of HER rather than of the
   viewport — otherwise every viewport height slides them off her face, and the
   slide is invisible in a screenshot taken at the size they were authored on.
   The wrapper owns the centring transform so the image is free to animate its
   own. */
/* HEIGHT IS DERIVED FROM THE GAP, never from the viewport alone.
   The frame is 768x1408, so width = height * 0.545, and the art's visible
   content stops at a known fraction of that width (measured, per image). The
   multipliers below are 1/(0.545 * that fraction) — i.e. the tallest this figure
   can be while its light still cannot reach the array:
     Stage I    content to 63% -> 1/(0.545*0.63) = 2.9
     Stage II   content to 93% -> 1/(0.545*0.93) = 1.97
   The negative margins pull her further out still, so they only ever add slack.
   If you replace the art, re-measure the fraction — do not reuse these. */
.nx-veil-figwrap {
  position: relative;                 /* no transform, no z-index: see above */
  height: min(96vh, 940px, calc(var(--gap) * 2.9));
  aspect-ratio: 768 / 1408; flex: none;
}
/* She is flush with the edge — her outer half is off-screen on purpose, so she
   reads as continuing past the window rather than as a picture placed on it.
   (It also puts the one bright spike that touches the gold source's frame edge
   safely outside the viewport.) Margins, because a transform here would isolate
   the blend. */
.nx-veil-l .nx-veil-figwrap { margin-left: -12%; }
.nx-veil-r .nx-veil-figwrap { margin-right: -12%; }

/* The fade-in is an animation on the image, not on an ancestor — see the note on
   .nx-veil. `both` holds the entrance until the breath takes over, and the breath
   is delayed to start exactly where the entrance ends and opens on the same
   value, so there is no step between them. `drift` is on transform, so it
   composes with the opacity animations instead of fighting them: she is never
   quite still, which is most of the difference between a presence and a print. */
.nx-veil-fig {
  inset: 0; width: 100%; height: 100%;
  animation: nxVeilIn 2.8s ease both,
             nxVeilBreath 13s ease-in-out 2.8s infinite,
             nxVeilDrift 29s ease-in-out infinite;
}

/* THE SHEEN — a slow wave of light travelling down her veils.
   It is the SAME image again, masked to a narrow moving band, so the light can
   only ever appear where she already is: no bar crossing empty space, no shape
   invented that the artwork does not have. The browser decodes one file for both
   layers. This is the single biggest thing separating "a beautiful picture" from
   "something in the room with you" — light moving over a form reads as depth, and
   costs one masked layer. */
.nx-veil-sheen {
  inset: 0; width: 100%; height: 100%;
  opacity: .5;
  -webkit-mask-image: linear-gradient(to bottom, transparent 34%, #000 46%, #000 54%, transparent 66%);
          mask-image: linear-gradient(to bottom, transparent 34%, #000 46%, #000 54%, transparent 66%);
  -webkit-mask-size: 100% 260%;
          mask-size: 100% 260%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  animation: nxSheen 17s ease-in-out 3s infinite;
}

/* THE EMBERS — the empty air of the room, moving.
   Everything above was pinned to her; this is the space around her, which was
   the part that still read as a still image. They rise, because nothing else in
   the sequence rises: the starfield drifts, the strata dissolve, the record
   falls. */
.nx-ember {
  position: absolute; bottom: -6%; left: var(--x);
  width: var(--s); height: var(--s); border-radius: 50%;
  mix-blend-mode: screen; opacity: 0;
  background: radial-gradient(circle closest-side,
              rgba(255, 244, 214, .95) 0%, rgba(255, 220, 150, .5) 45%, rgba(255, 200, 110, 0) 100%);
  animation: nxEmber var(--t) linear var(--d) infinite;
}
.nx-veil-r .nx-ember {
  background: radial-gradient(circle closest-side,
              rgba(255, 255, 255, .95) 0%, rgba(214, 232, 246, .5) 45%, rgba(180, 208, 230, 0) 100%);
}

/* THE GLINTS — temple, brow, cheekbone, jaw, ear, throat, collar.
   Light catching her as she breathes. Each one has its own delay and duration
   (see _VEIL_GLINTS in build.py) and no two are shared, so they never pulse in
   unison and the loop cannot be found. This is the difference between "a lit
   figure" and "a figure that is alive": the eye is very good at spotting a still
   image and very bad at spotting eight unsynchronised 3-7s cycles. */
.nx-glint {
  position: absolute; top: var(--y); width: var(--s); height: var(--s);
  margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2);
  border-radius: 50%; mix-blend-mode: screen; opacity: 0;
  background: radial-gradient(circle closest-side,
              rgba(255, 248, 224, 1) 0%, rgba(255, 232, 170, .75) 38%, rgba(255, 214, 120, 0) 100%);
  animation: nxGlint var(--t) ease-in-out var(--d) infinite;
}
/* Same coordinates serve both figures: the silver one is a true mirror of the
   gold, so anchoring from the opposite edge lands on the same features. */
.nx-veil-l .nx-glint { left: var(--x); }
.nx-veil-r .nx-glint { right: var(--x); }
/* Her side of the room is cold; the light on her is too. */
.nx-veil-r .nx-glint {
  background: radial-gradient(circle closest-side,
              rgba(255, 255, 255, 1) 0%, rgba(224, 238, 248, .75) 38%, rgba(190, 214, 232, 0) 100%);
}

/* THE INSTRUMENT — in front of her, in the orb's own language: fine-line
   geometry that quietly reconfigures. Two sigils sharing a centre and a radius,
   cross-faded against each other while both slowly counter-rotate. Neither one
   is ever "the" shape: it is one instrument that will not hold still. */
.nx-veil-pat {
  top: 50%; width: min(30vw, 340px); height: auto;
  margin-top: -1vh;
}
.nx-veil-l .nx-veil-pat { left: 8%; transform-origin: 50% 50%; }
.nx-veil-r .nx-veil-pat { right: 8%; transform-origin: 50% 50%; }
/* Spin animates `rotate`, morph animates `opacity` — different properties, so
   they compose instead of fighting. Each morph is delayed past its entrance and
   opens on the value that entrance ends at. */
.nx-veil-pat-a {
  animation: nxVeilInA 2.8s ease both,
             nxVeilSpinA 96s linear infinite,
             nxVeilMorphA 19s ease-in-out 2.8s infinite;
}
.nx-veil-pat-b {
  animation: nxVeilInB 2.8s ease both,
             nxVeilSpinB 132s linear infinite,
             nxVeilMorphB 19s ease-in-out 2.8s infinite;
}

/* A glint is mostly absent. It sits dark for roughly three quarters of its cycle
   and then arrives — a flash that is always on is a highlight, not a glint. */
@keyframes nxGlint {
  0%, 62%, 100% { opacity: 0; transform: scale(.6) }
  74%           { opacity: .95; transform: scale(1.35) }
  86%           { opacity: .18; transform: scale(.9) }
}
/* Barely a millimetre on screen — enough that she is never twice in the same
   place, never enough to read as a moving picture. */
@keyframes nxVeilDrift {
  0%, 100% { transform: translate3d(0, 0, 0) }
  50%      { transform: translate3d(.7%, -1.1%, 0) }
}
/* Down her, then a long dark pause. The pause is what makes it an event rather
   than a loop you can set your watch by. */
@keyframes nxSheen {
  0%        { -webkit-mask-position: 0 -160%; mask-position: 0 -160%; opacity: 0 }
  12%       { opacity: .5 }
  46%       { opacity: .5 }
  58%, 100% { -webkit-mask-position: 0 160%;  mask-position: 0 160%;  opacity: 0 }
}
/* Up, wandering, and out. Never a straight line — a straight line is a bullet. */
@keyframes nxEmber {
  0%   { opacity: 0;   transform: translate3d(0, 0, 0) scale(.7) }
  12%  { opacity: .75 }
  50%  { transform: translate3d(14px, -46vh, 0) scale(1) }
  78%  { opacity: .5 }
  100% { opacity: 0;   transform: translate3d(-10px, -94vh, 0) scale(.55) }
}
@keyframes nxVeilIn  { from { opacity: 0 } to { opacity: .44 } }
@keyframes nxVeilInA { from { opacity: 0 } to { opacity: .34 } }
@keyframes nxVeilInB { from { opacity: 0 } to { opacity: .04 } }
@keyframes nxVeilBreath { 0%, 100% { opacity: .44 } 50% { opacity: .64 } }
@keyframes nxVeilSpinA { from { rotate: 0deg }   to { rotate: 360deg } }
@keyframes nxVeilSpinB { from { rotate: 360deg } to { rotate: 0deg } }
/* The two never sum to a constant — there is a moment where both are half
   present and the instrument is genuinely ambiguous. That moment is the point. */
@keyframes nxVeilMorphA { 0%, 42% { opacity: .34 } 58%, 100% { opacity: .04 } }
@keyframes nxVeilMorphB { 0%, 42% { opacity: .04 } 58%, 100% { opacity: .34 } }

/* THE PROCESSION (Stage II) is wider than Stage I's pair — it has a queue behind
   it, and cropping the queue would cost the only thing it says. It also sits
   deeper: the dead ones should be at the top edge of vision, not at eye level. */
.nx-veil-2 .nx-veil-side { width: min(42vw, 520px); }
/* 1.97, not 2.9: the queue reaches 93% of its frame where Stage I's lone figure
   reaches 63%, so the same height would push the dead ones across the array. */
.nx-veil-2 .nx-veil-figwrap { height: min(104vh, 1010px, calc(var(--gap) * 1.97)); }
.nx-veil-2 .nx-veil-l .nx-veil-figwrap { margin-left: -8%; }
.nx-veil-2 .nx-veil-r .nx-veil-figwrap { margin-right: -8%; }
/* Bone, not gold. Stage II's own palette — and the room is colder than the one
   with two people in it. */
.nx-veil-2 .nx-glint {
  background: radial-gradient(circle closest-side,
              rgba(255, 255, 255, 1) 0%, rgba(214, 230, 240, .7) 38%, rgba(170, 198, 220, 0) 100%);
}
.nx-veil-2 .nx-ember {
  background: radial-gradient(circle closest-side,
              rgba(240, 246, 252, .9) 0%, rgba(201, 162, 39, .45) 45%, rgba(201, 162, 39, 0) 100%);
}
/* Slower. Everything in the archive has been here a very long time. */
.nx-veil-2 .nx-veil-fig { animation-duration: 2.8s, 19s, 41s; }
.nx-veil-2 .nx-veil-sheen { animation-duration: 26s; }

/* There is no periphery left to put her in: the array needs the full width, and
   a figure behind the cards is not mystery, it is clutter. Removed rather than
   shrunk — display:none also stops the browser fetching the art at all.

   Two breakpoints, not one, and they are set by what the art NEEDS rather than
   by a round number. The gap-derived height above keeps the art off the puzzle
   at any width, but "safe" is not "worth showing": measured, the procession
   comes out 256px tall at 1200 and 420px at 1366 — a hall of the dead reduced to
   a postage stamp, which is lint, not atmosphere. It needs more room than Stage
   I's lone figure because it has a queue behind her.
     Stage I  >= 1280  (~536px tall)
     Stage II >= 1400  (~490px tall) */
@media (max-width: 1279px) { .nx-veil-1 { display: none !important; } }
@media (max-width: 1399px) { .nx-veil-2 { display: none !important; } }
@media (prefers-reduced-motion: reduce) {
  /* Still present, just still. `animation: none` also drops the entrance, so the
     resting opacities have to be restated or she vanishes entirely. */
  .nx-veil-fig, .nx-veil-pat-a, .nx-veil-pat-b, .nx-glint { animation: none; }
  .nx-veil-fig { opacity: .44; }
  .nx-veil-pat-a { opacity: .3; }
  .nx-veil-pat-b { opacity: .1; }
  /* The glints hold lit rather than flashing — someone who asked for less motion
     should still get the jewellery, just not the twinkle. */
  .nx-glint { opacity: .5; }
}

/* ==================================================== THE LAST ORB ======== */
/* Five layers on one centre, each turning at its own rate, each its own metal.
   Centres were recentred to the exact middle pixel at encode time (measured: all
   five land at 0,0) — a layer even 20px off makes the object wobble apart.

   THESE LAYERS DO NOT BLEND: the art carries REAL alpha instead. mix-blend-mode
   was the obvious choice and it was unwinnable here — the orb lives inside
   #nxMirror, which is position:relative + z-index:4, a stacking context that is
   not mine to remove, so the layers painted their black squares and the
   counter-rotating corners swept a black star around the orb. Alpha
   (luminance -> alpha, colour un-premultiplied) reproduces exactly what screen
   produced, works inside any stacking context, and makes filters safe here.

   Every animation below is written out per layer. An earlier version put the
   spin in a custom property and fed it to the `animation` shorthand via var() —
   it silently killed four of the five layers dead. Nothing here is clever, on
   purpose. See docs/nexus/LASTQUESTION.md. */
.nx-last {
  position: fixed; left: 0; top: 0; z-index: 6;
  width: clamp(150px, 17vw, 240px); aspect-ratio: 1;
  background: none; border: 0; padding: 0; cursor: pointer;
  filter: drop-shadow(0 0 30px rgba(212, 175, 55, .28));
  animation: nxLastIn 4s ease both;
  transition: filter .6s ease;
}
.nx-last[hidden] { display: none; }
.nx-last:focus-visible { outline: 2px solid var(--ac2); outline-offset: 8px; border-radius: 50%; }
.nx-last-l {
  position: absolute; inset: 0; width: 100%; height: 100%;
  transform-origin: 50% 50%;
}

/* Counter-rotating, no two periods sharing a factor, so the object never
   returns to the same configuration twice in any sitting. And each ring is a
   different metal: the heart stays cold cyan — it is the only cold thing left —
   and everything around it is gold, rose, green-gold, steel and violet. */
.nx-last-1 { animation: nxSpinCW  41s linear infinite; }
.nx-last-2 { animation: nxSpinCCW 67s linear infinite;  filter: hue-rotate(-34deg) saturate(1.1); }
.nx-last-3 { animation: nxSpinCW  97s linear infinite;  filter: hue-rotate(54deg)  saturate(1.1); }
.nx-last-4 { animation: nxSpinCCW 149s linear infinite; filter: hue-rotate(166deg) saturate(1.15); }
.nx-last-5 { animation: nxSpinCW  211s linear infinite; filter: hue-rotate(226deg) saturate(1.1); }

.nx-last:hover { filter: drop-shadow(0 0 50px rgba(212, 175, 55, .55)); }
.nx-last:hover .nx-last-1 { animation-duration: 9s; }

@keyframes nxSpinCW  { from { rotate: 0deg }   to { rotate: 360deg } }
@keyframes nxSpinCCW { from { rotate: 360deg } to { rotate: 0deg } }
@keyframes nxLastIn  { from { opacity: 0 } to { opacity: 1 } }
/* it is leaving, and it takes the page with it */
.nx-last.opening { animation: nxLastOpen 1.1s ease forwards; }
@keyframes nxLastOpen {
  0%   { transform: scale(1);   opacity: 1 }
  40%  { transform: scale(.86); opacity: 1 }
  100% { transform: scale(14);  opacity: 0 }
}
@media (prefers-reduced-motion: reduce) {
  .nx-last-l { animation: none; }
  .nx-last.opening { animation: none; opacity: 0; }
}

/* ============================================ THE LAST QUESTION ========== */
/* /nexus/question/ — reached only through the orb at the end of the mirror.

   THIS SCREEN HAS NO CARD, and that is the entire design. It used to be a 560px
   bordered panel with the vault dimmed to 26% behind frosted glass. A card is a
   form; a form is a newsletter signup; and no amount of atmosphere painted
   around one will ever land. Everything here follows from removing it:

     - the vault IS the room: full strength, larger than the screen
     - the mark is the interface, at the size of the room, not a thumbnail
     - the gauges are pinned to the edges, the way an instrument carries its own
     - the question stands in the middle with nothing behind it at all

   The observer is not filling in a field. They are standing inside the thing and
   writing on the dark. */

/* -------------------------------------------------------------- the room -- */
.nx-q, .nx-q .nx-core { min-height: 100vh; min-height: 100dvh; }
.nx-q-room { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.nx-q-vault, .nx-q-aperture {
  position: absolute; top: 50%; left: 50%;
  height: auto; transform-origin: 50% 50%;
}
/* Larger than the viewport on purpose: you are inside it, not looking at it.
   Two copies turning against each other — a single ring at 420s was a
   seven-minute revolution and nobody ever saw 1% of one. */
.nx-q-vault-1 {
  width: min(128vh, 128vw);
  margin: calc(min(128vh, 128vw) / -2) 0 0 calc(min(128vh, 128vw) / -2);
  opacity: .62; animation: nxVaultTurn 130s linear infinite;
}
.nx-q-vault-2 {
  width: min(210vh, 210vw);
  margin: calc(min(210vh, 210vw) / -2) 0 0 calc(min(210vh, 210vw) / -2);
  opacity: .34; animation: nxVaultTurn2 240s linear infinite;
}
.nx-q-aperture {
  width: min(58vh, 58vw);
  margin: calc(min(58vh, 58vw) / -2) 0 0 calc(min(58vh, 58vw) / -2);
  opacity: .3; animation: nxIris 14s ease-in-out infinite;
}
@keyframes nxVaultTurn  { from { rotate: 0deg }   to { rotate: 360deg } }
@keyframes nxVaultTurn2 { from { rotate: 360deg } to { rotate: 0deg } }
@keyframes nxIris { 0%, 100% { opacity: .18; scale: 1 } 50% { opacity: .42; scale: 1.06 } }

/* -------------------------------------------------------------- the mark -- */
/* The size of the room. It is the only thing on this page that belongs to the
   observer, so it is the largest thing on it. It was 230px, inside a box. */
.nx-q-mark {
  position: fixed; top: 50%; left: 50%; z-index: 1;
  width: min(86vh, 86vw); aspect-ratio: 1;
  margin: calc(min(86vh, 86vw) / -2) 0 0 calc(min(86vh, 86vw) / -2);
  pointer-events: none; opacity: 0;
  transition: opacity 1.4s ease, filter 1.4s ease;
  filter: drop-shadow(0 0 40px rgba(212, 175, 55, .45));
}
.nx-q-mark:not(:empty) { opacity: 1; }
.nx-sigil-svg { width: 100%; height: 100%; display: block; }
.nx-sigil-node { animation: nxSigilNode .5s ease both; animation-delay: calc(var(--i) * .09s); }
@keyframes nxSigilNode { from { opacity: 0; r: 0 } to { opacity: .95 } }

/* --------------------------------------------------------- the question --- */
/* THREE BANDS: the question above, the machine in the middle, the answer in the
   black below it. Everything was centred, so the orb, the mark, the question and
   the number were all fighting over the same 400 pixels and whichever one got
   bigger crushed the rest. Nothing is dimmed to fix it — they are simply not in
   the same place any more. */
.nx-q-room-txt {
  position: relative; z-index: 3;
  min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center;
  gap: 20px; padding: 9vh 20px 11vh; text-align: center;
}

.nx-q-ask {
  /* margin-bottom:auto eats the free space, so everything after it drops to the
     bottom of the screen — the question above the orb, the answer in the black
     below it. Declared HERE and not earlier: an earlier `margin-bottom:auto`
     was silently overridden by this rule's `margin: 0` and the answer never
     moved at all. */
  margin: 0 0 auto; color: #fff; letter-spacing: 2px; line-height: 1.5;
  font-size: clamp(1.05rem, 2.9vw, 1.7rem);
  /* Nothing behind it, so the glow IS the background: a wide soft shadow lifts
     it off the vault without putting a box around it. */
  text-shadow: 0 0 8px #06050a, 0 0 26px #06050a, 0 0 60px rgba(6, 5, 10, .9),
               0 0 90px rgba(240, 230, 200, .25);
}
/* A line of light, not a field. */
.nx-q-f { display: flex; gap: 18px; align-items: center; justify-content: center; flex-wrap: wrap; }
.nx-q-f[hidden] { display: none; }
.nx-q-f input {
  background: none; border: 0; border-bottom: 1px solid rgba(212, 175, 55, .4);
  color: #f0e6c8; font-family: inherit; text-align: center;
  font-size: clamp(1.8rem, 5vw, 3.2rem); letter-spacing: 8px;
  width: min(360px, 74vw); padding: 4px 0 8px;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 10px #06050a, 0 0 40px rgba(240, 230, 200, .45);
  transition: border-color .4s, text-shadow .4s, color .4s;
}
.nx-q-f input::placeholder { color: rgba(212, 175, 55, .5); letter-spacing: 6px; }
.nx-q-f input:focus { outline: none; border-bottom-color: #fff; }
.nx-q-f button {
  /* SOLID, not an outline. A hollow gold outline disappears into a gold machine —
     the only actionable thing on the screen was the least visible thing on it. */
  background: var(--ac2); border: 1px solid var(--ac2); color: #06050a;
  font-family: inherit; padding: 15px 30px; border-radius: 11px;
  letter-spacing: 2.4px; text-transform: uppercase; font-size: .86rem; font-weight: 700;
  cursor: pointer; transition: background .3s, color .3s, box-shadow .3s, transform .2s;
  box-shadow: 0 0 0 6px rgba(4, 3, 8, .75), 0 0 30px rgba(212, 175, 55, .45);
}
.nx-q-f button:hover {
  background: #f0e6c8; border-color: #f0e6c8; transform: translateY(-1px);
  box-shadow: 0 0 0 6px rgba(4, 3, 8, .75), 0 0 46px rgba(240, 230, 200, .7);
}
.nx-q-f.busy { opacity: .5; pointer-events: none; }
.nx-q-warn {
  margin: 0; color: var(--ac2); font-size: .76rem; letter-spacing: 1.8px; opacity: .9;
  text-shadow: 0 0 10px #06050a, 0 0 30px #06050a;
  transition: font-size .3s, color .3s;
}
.nx-q-warn[hidden] { display: none; }

/* ARMED — the point of no return has to look like one. With the card gone this
   cannot be a panel colour any more, so THE ROOM answers: the input burns, the
   button fills, and the vault itself flares up behind everything. */
.nx-q-f.armed input { border-bottom-color: #fff; color: #fff; text-shadow: 0 0 34px var(--ac2); }
.nx-q-f.armed button {
  background: var(--ac2); color: #06050a; font-weight: 700;
  box-shadow: 0 0 34px rgba(212, 175, 55, .6);
}
.nx-q.armed .nx-q-vault-1 { opacity: .9; }
.nx-q.armed .nx-q-aperture { opacity: .5; }
.nx-q.armed .nx-q-warn { font-size: .86rem; color: #f0e6c8; text-shadow: 0 0 24px var(--ac2); }

/* on record */
.nx-q-done { display: flex; flex-direction: column; gap: 10px; align-items: center; }
.nx-q-done[hidden] { display: none; }
.nx-q-done-l { font-size: .72rem; letter-spacing: 2.2px; color: var(--ink); opacity: .7; }
.nx-q-done b {
  font-size: clamp(2.4rem, 7vw, 4.4rem); font-weight: 400; color: #f0e6c8; letter-spacing: 5px;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 12px #06050a, 0 0 50px rgba(240, 230, 200, .6);
}
.nx-q-done-n { font-size: .8rem; color: var(--ink); opacity: .72; font-style: italic; }
/* THE ONLY STILLNESS IN THE PIECE. Everything has moved for twenty minutes —
   the starfield, the strata, the record, this room. When the answer is committed
   it all stops, and that stop is the loudest thing here. */
.nx-q.locked .nx-q-vault, .nx-q.locked .nx-q-aperture { animation-play-state: paused; }
.nx-q.locked .nx-q-mark { filter: drop-shadow(0 0 80px rgba(240, 230, 200, .75)); }

/* 1 January 2030 */
.nx-q-resolved { display: flex; flex-direction: column; gap: 12px; }
.nx-q-resolved span { display: block; }

/* ------------------------------------------------------------ the gauges -- */
/* An instrument carries its dials on its own frame, not in a table in the
   middle of the room. */
.nx-q-gauge {
  position: fixed; bottom: 34px; z-index: 3;
  display: flex; flex-direction: column; gap: 5px;
  font-size: .76rem; letter-spacing: 1.6px;
  text-shadow: 0 0 10px #06050a, 0 0 30px #06050a;
}
.nx-q-gauge-l { left: 82px; text-align: left; }
.nx-q-gauge-r { right: 82px; text-align: right; }
.nx-q-gauge-k { color: var(--ink); opacity: .62; font-size: .68rem; }
.nx-q-gauge b {
  color: #f0e6c8; font-weight: 400; font-variant-numeric: tabular-nums;
  letter-spacing: 2px;
}
.nx-q-foot {
  position: fixed; bottom: 34px; left: 50%; transform: translateX(-50%); z-index: 3;
  max-width: 430px; margin: 0; color: var(--ink); opacity: .55;
  font-size: .74rem; line-height: 1.7; text-align: center;
  text-shadow: 0 0 10px #06050a, 0 0 26px #06050a;
}
.nx-q-foot[hidden] { display: none; }

/* ------------------------------------------------------------ the others -- */
/* Real records only. If nobody has answered, nothing appears — the room does the
   moving instead. Nobody is ever invented. */
.nx-q-others { position: fixed; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
.nx-q-other {
  position: absolute; left: 50%; top: 0;
  width: 190px; height: 190px; margin-left: -95px;
  translate: var(--x) var(--y); scale: var(--s); opacity: 0;
  animation: nxOtherDrift var(--t) ease-in-out var(--d) infinite;
}
@keyframes nxOtherDrift {
  0%, 100% { opacity: 0;   rotate: 0deg }
  22%      { opacity: .3 }
  50%      { opacity: .42; rotate: 8deg }
  78%      { opacity: .26 }
}

@media (max-width: 900px) {
  .nx-q-others { display: none; }
  .nx-q-gauge { position: static; align-items: center; text-align: center; }
  .nx-q-foot { position: static; transform: none; margin: 18px auto 0; }
  .nx-q-room-txt { min-height: auto; padding: 90px 20px 34px; }
  .nx-q-mark { width: min(78vw, 420px); margin: calc(min(78vw, 420px) / -2) 0 0 calc(min(78vw, 420px) / -2); }
}
@media (prefers-reduced-motion: reduce) {
  .nx-q-vault, .nx-q-aperture, .nx-q-other, .nx-sigil-node { animation: none; }
  .nx-q-other { opacity: .3; }
}

/* ---------------------------------------------- the orb at the centre ----- */
/* The same five layers as the orb at the end of the mirror. Fast here: on the
   mirror it drifts (41-211s) because it is caught out of the corner of the eye;
   here you are standing in front of it. No hue filters — its gold, silver and
   cyan are already right, and rotating the hue only ever greyed it. */
.nx-q-heart {
  position: absolute; top: 50%; left: 50%;
  width: min(44vh, 44vw); height: min(44vh, 44vw);
  margin: calc(min(44vh, 44vw) / -2) 0 0 calc(min(44vh, 44vw) / -2);
  filter: drop-shadow(0 0 40px rgba(212, 175, 55, .45));
  animation: nxHeartBreath 7s ease-in-out infinite;
}
.nx-q-heart-l { position: absolute; inset: 0; width: 100%; height: 100%; transform-origin: 50% 50%; }
.nx-q-heart-1 { animation: nxSpinCW  7s  linear infinite; }
.nx-q-heart-2 { animation: nxSpinCCW 11s linear infinite; }
.nx-q-heart-3 { animation: nxSpinCW  17s linear infinite; }
.nx-q-heart-4 { animation: nxSpinCCW 23s linear infinite; }
.nx-q-heart-5 { animation: nxSpinCW  31s linear infinite; }
@keyframes nxHeartBreath {
  0%, 100% { filter: drop-shadow(0 0 34px rgba(212, 175, 55, .34)) brightness(1) }
  50%      { filter: drop-shadow(0 0 68px rgba(212, 175, 55, .6))  brightness(1.3) }
}
.nx-q.locked .nx-q-heart, .nx-q.locked .nx-q-heart-l { animation-play-state: paused; }
.nx-q.armed .nx-q-heart { filter: drop-shadow(0 0 90px rgba(212, 175, 55, .8)) brightness(1.45); }

/* ------------------------------------------------- the inscription -------- */
/* Ar-Rahman 55:29 in Elder Futhark under a Vigenere key. MEANT TO BE SEEN: an
   inscription nobody can find is never solved and may as well not be there.
   Seeing it gives nothing away — it is shifted, so you can stare at it for a
   year without the key. */
.nx-q-verse {
  position: fixed; top: 50%; left: 50%; z-index: 2;
  width: min(112vh, 112vw); aspect-ratio: 1;
  margin: calc(min(112vh, 112vw) / -2) 0 0 calc(min(112vh, 112vw) / -2);
  pointer-events: none;
  animation: nxVerseTurn 420s linear infinite;
}
.nx-q-verse svg { width: 100%; height: 100%; overflow: visible; }
.nx-q-verse text {
  fill: #f7edd2; font-size: 3.9px; letter-spacing: .3px; opacity: .72;
}
@keyframes nxVerseTurn { from { rotate: 0deg } to { rotate: 360deg } }
@media (max-width: 900px) { .nx-q-verse { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .nx-q-verse, .nx-q-heart, .nx-q-heart-l { animation: none; }
}

/* A neon per word on the inscription. Each glyph glows in its own colour rather
   than sharing one shadow, so the words separate at a glance instead of bleeding
   into a single band of light. */
.nx-q-verse text tspan {
  filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor);
}
/* the separators stay quiet — they are punctuation, not words */
.nx-q-verse text .nx-v-dot { fill: rgba(240, 230, 200, .45); filter: none; }

/* THE INSCRIPTION FALLING — one column down each edge of the room.
   The glyphs are the real ciphertext in order (see rain() in question.js), not
   random runes pretending to be a message.
   Dim on purpose: the rim carries the neon, these are the same text at the edge
   of vision. Two more neon columns would just be noise competing with it. */
/* THE COLUMN IS THE WINDOW; THE TRACK INSIDE IT IS WHAT MOVES.
   The animation used to sit on this element, which is fixed and as tall as the
   screen — so the clipping box slid down WITH its own contents and half the
   column sat empty for most of every cycle. It has to stay still and clip. */
.nx-q-rain {
  position: fixed; top: 0; bottom: 0; z-index: 2;
  width: 3.4rem; pointer-events: none; overflow: hidden;
  font-size: 1.9rem; line-height: 1.45;
  /* Fades at both ends of the SCREEN rather than at both ends of the text, so
     the glyphs arrive out of nothing and leave into nothing instead of the
     column starting and stopping at a visible edge. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
}
.nx-q-rain-run {
  display: flex; flex-direction: column; align-items: center;
  animation: nxRainFall 26s linear infinite;
  will-change: transform;
}
.nx-q-rain-l { left: 1.2vw; }
.nx-q-rain-r { right: 1.2vw; }
.nx-q-rain-r .nx-q-rain-run { animation-duration: 34s; }
/* NEON, and bright. These were dim gold on the theory that the rim should carry
   the colour and the columns should stay at the edge of vision — which just made
   them look like a rendering fault. They are the same ciphertext as the rim, so
   they get the same palette: each glyph its own colour, cycling through the nine,
   every one of them lit. */
.nx-q-rain i {
  font-style: normal;
  color: var(--c);
  text-shadow: 0 0 4px var(--c), 0 0 12px var(--c), 0 0 28px var(--c);
  transition: color .22s ease, text-shadow .22s ease;
}
/* the glyph that just changed flares white-hot, then settles back to its colour */
.nx-q-rain i.lit {
  color: #ffffff;
  text-shadow: 0 0 6px #fff, 0 0 20px var(--c), 0 0 44px var(--c);
  transition: none;
}
/* The track holds the verse TWICE, so -50% lands on a glyph identical to the one
   that was there — the wrap is invisible. This only holds while both halves stay
   identical, which is why nothing rewrites a glyph in place any more. */
@keyframes nxRainFall {
  from { transform: translateY(-50%) }
  to   { transform: translateY(0) }
}
/* No room for it, and a column of runes beside a phone-width form is clutter. */
@media (max-width: 900px) { .nx-q-rain { display: none; } }
@media (prefers-reduced-motion: reduce) { .nx-q-rain-run { animation: none; } }
