/* ============================================================================
   ATOMIC LAB — FX LAYER
   ----------------------------------------------------------------------------
   The instrument aesthetic in tokens.css defines the *substrate*: near
   monochrome, colour reserved for meaning. This layer adds the *room* the
   instrument sits in — a CRT-lit underground lab.

   Everything here is decorative and additive. It paints behind or above the
   content, never inside the layout, so removing this stylesheet leaves a fully
   working site. Every animated element is inside a
   `prefers-reduced-motion: no-preference` guard, and every overlay is
   `pointer-events: none`.

   Budget: the ambient layer is 3 fixed elements and 2 compositor-only
   animations. No per-frame JS is required for any of it.
   ========================================================================== */

:root {
  --hud-c:      rgba(89, 230, 255, 0.42);
  --hud-c-hot:  rgba(89, 230, 255, 0.95);
  --scan-op:    0.075;
  --grid-op:    0.42;
}

/* ---------------------------------------------------------------------------
   1. AMBIENT — the lab itself
   Three fixed layers, injected once by fx.js into <body>:
     .fx-floor  perspective grid receding to a horizon (behind content)
     .fx-crt    scanlines + vignette + refresh bar (above content)
     .fx-noise  film grain (above content, below the CRT bar)
   ------------------------------------------------------------------------- */
.fx-floor,
.fx-crt,
.fx-noise {
  position: fixed;
  inset: 0;
  pointer-events: none;
  contain: strict;
}

/* --- 1a. Horizon grid ---------------------------------------------------- */
.fx-floor {
  z-index: 0;
  overflow: hidden;
  opacity: var(--grid-op);
  /* Sits behind .al-shell (z-index:1) but above the body fill. */
  mask-image: linear-gradient(180deg, transparent 40%, #000 78%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 40%, #000 78%, transparent 100%);
}

.fx-floor::before {
  content: '';
  position: absolute;
  left: -60%;
  right: -60%;
  bottom: -18vh;
  height: 90vh;
  background-image:
    linear-gradient(to right,  rgba(89,230,255,0.30) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(89,230,255,0.30) 1px, transparent 1px);
  background-size: 68px 68px;
  transform: perspective(320px) rotateX(74deg);
  transform-origin: 50% 100%;
}

/* A second grid running the other way gives the horizon depth without a
   second element: the violet plane sits slightly above the cyan one. */
.fx-floor::after {
  content: '';
  position: absolute;
  left: -60%;
  right: -60%;
  top: -22vh;
  height: 70vh;
  background-image:
    linear-gradient(to right,  rgba(169,139,255,0.20) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(169,139,255,0.20) 1px, transparent 1px);
  background-size: 92px 92px;
  transform: perspective(340px) rotateX(-72deg);
  transform-origin: 50% 0%;
  opacity: 0.55;
}

@media (prefers-reduced-motion: no-preference) {
  .fx-floor::before { animation: fx-floor-run 7s linear infinite; }
  .fx-floor::after  { animation: fx-floor-run 11s linear infinite reverse; }
}
@keyframes fx-floor-run {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 0 68px, 0 68px; }
}

/* --- 1b. CRT scanlines + refresh bar + vignette --------------------------- */
.fx-crt {
  z-index: 9996;
  background-image:
    repeating-linear-gradient(
      180deg,
      rgba(0, 0, 0, 0) 0px,
      rgba(0, 0, 0, 0) 2px,
      rgba(0, 0, 0, 0.55) 3px,
      rgba(0, 0, 0, 0.55) 3px
    );
  opacity: var(--scan-op);
  mix-blend-mode: multiply;
}

/* Vignette + corner bloom. Separate element so it can use screen blending
   while the scanlines multiply. */
.fx-crt::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 90% at 50% 50%, transparent 52%, rgba(0, 0, 0, 0.55) 100%);
  mix-blend-mode: normal;
  opacity: 0.9;
}

/* The refresh bar — one soft band drifting down the screen. This is the single
   most recognisable CRT cue and costs one transform per frame. */
.fx-crt::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 38vh;
  top: -40vh;
  background: linear-gradient(
    180deg,
    transparent,
    rgba(89, 230, 255, 0.05) 42%,
    rgba(255, 255, 255, 0.07) 50%,
    rgba(169, 139, 255, 0.05) 58%,
    transparent
  );
  mix-blend-mode: screen;
}
@media (prefers-reduced-motion: no-preference) {
  .fx-crt::after { animation: fx-refresh 9s linear infinite; }
}
@keyframes fx-refresh {
  0%   { transform: translateY(0); }
  100% { transform: translateY(180vh); }
}

/* --- 1c. Grain ------------------------------------------------------------
   Inline SVG turbulence: no network request, no image decode. Kept very faint;
   its job is to stop the large flat panels from banding. */
.fx-noise {
  z-index: 9997;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
@media (prefers-reduced-motion: no-preference) {
  .fx-noise { animation: fx-grain 640ms steps(4) infinite; }
}
@keyframes fx-grain {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-3%, 2%); }
  50%  { transform: translate(2%, -3%); }
  75%  { transform: translate(-2%, -2%); }
  100% { transform: translate(0, 0); }
}

/* Phones do the least of this: the grid is the expensive layer and the least
   legible on a 390px screen. */
@media (max-width: 720px) {
  .fx-floor { display: none; }
  :root { --scan-op: 0.035; }
}

/* ---------------------------------------------------------------------------
   2. HUD BRACKETS
   Corner brackets painted with eight 1px background bars on a single
   pseudo-element — four corners, no extra DOM, no per-corner children.
   Applied only to the large "stage" surfaces so it stays a signal rather than
   wallpaper. `.ticked` already owns ::before/::after, hence the :not().
   ------------------------------------------------------------------------- */
.atom__stage,
.lab__stage,
.el-tile,
.hero__viz,
.game__stage,
.tool__stage,
[data-fx~="hud"] {
  position: relative;
}

.atom__stage::before,
.lab__stage::before,
.hero__viz::before,
.game__stage::before,
[data-fx~="hud"]:not(.ticked)::before {
  content: '';
  position: absolute;
  inset: 8px;
  pointer-events: none;
  z-index: 3;
  background-image:
    /* top-left */
    linear-gradient(var(--hud-c), var(--hud-c)),
    linear-gradient(var(--hud-c), var(--hud-c)),
    /* top-right */
    linear-gradient(var(--hud-c), var(--hud-c)),
    linear-gradient(var(--hud-c), var(--hud-c)),
    /* bottom-left */
    linear-gradient(var(--hud-c), var(--hud-c)),
    linear-gradient(var(--hud-c), var(--hud-c)),
    /* bottom-right */
    linear-gradient(var(--hud-c), var(--hud-c)),
    linear-gradient(var(--hud-c), var(--hud-c));
  background-size:
    16px 1px, 1px 16px,
    16px 1px, 1px 16px,
    16px 1px, 1px 16px,
    16px 1px, 1px 16px;
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
  background-repeat: no-repeat;
  transition: background-image var(--d-2) var(--e-out);
}

.atom__stage:hover::before,
.lab__stage:hover::before,
.hero__viz:hover::before,
.game__stage:hover::before,
[data-fx~="hud"]:not(.ticked):hover::before {
  --hud-c: var(--hud-c-hot);
}

/* The existing corner ticks get a slow breath so a static page still has a
   pulse in it. */
@media (prefers-reduced-motion: no-preference) {
  .ticked::before, .ticked::after { animation: fx-tick 4.5s var(--e-inout) infinite; }
  .ticked::after { animation-delay: 2.25s; }
}
@keyframes fx-tick {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; border-color: rgba(89, 230, 255, 0.55); }
}

/* ---------------------------------------------------------------------------
   3. POINTER SPOTLIGHT
   fx.js writes --mx/--my (in px, element-local) onto the hovered surface. The
   highlight is a pseudo-element so it composites above the panel background
   and below the content.
   ------------------------------------------------------------------------- */
   `.card` already carries this treatment in components.css — it is not
   repeated here.
   ------------------------------------------------------------------------- */
.panel,
.el-tile,
.qa__item,
[data-fx~="lit"] { position: relative; }

.panel::after,
.qa__item::after,
[data-fx~="lit"]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--d-3) var(--e-out);
  background: radial-gradient(
    260px circle at var(--mx, 50%) var(--my, 50%),
    color-mix(in srgb, var(--fam, var(--beam)) 13%, transparent),
    transparent 62%
  );
}
.panel.is-lit::after,
.qa__item.is-lit::after,
[data-fx~="lit"].is-lit::after { opacity: 1; }

/* ---------------------------------------------------------------------------
   4. SCAN SWEEP ON REVEAL
   When a section first enters the viewport a single light bar wipes down it,
   like a scanner reading the plate. It runs once, then the element is done.
   ------------------------------------------------------------------------- */
.fx-scan { position: relative; }
.fx-scan > .fx-scan__bar {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 140px;
  pointer-events: none;
  z-index: 6;
  opacity: 0;
  background: linear-gradient(
    180deg,
    transparent,
    rgba(89, 230, 255, 0.10) 45%,
    rgba(205, 246, 255, 0.32) 50%,
    rgba(89, 230, 255, 0.10) 55%,
    transparent
  );
}
@media (prefers-reduced-motion: no-preference) {
  .fx-scan.is-scanning > .fx-scan__bar { animation: fx-scan 950ms var(--e-inout) forwards; }
}
@keyframes fx-scan {
  0%   { opacity: 0;   transform: translateY(-140px); }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { opacity: 0;   transform: translateY(var(--scan-to, 600px)); }
}

/* ---------------------------------------------------------------------------
   5. GLITCH TYPE
   Two offset copies of the same string, clipped to moving horizontal slices.
   `data-text` is stamped by fx.js so the markup keeps a single text node and
   screen readers read it once (the copies are aria-hidden by being generated
   content).
   ------------------------------------------------------------------------- */
.fx-glitch { position: relative; display: inline-block; }
.fx-glitch::before,
.fx-glitch::after {
  content: attr(data-text);
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  pointer-events: none;
  opacity: 0;
  background: inherit;
  -webkit-background-clip: inherit;
  background-clip: inherit;
}
.fx-glitch::before { color: #ff3d68; }
.fx-glitch::after  { color: var(--beam); }

@media (prefers-reduced-motion: no-preference) {
  .fx-glitch.is-glitching::before {
    opacity: 0.85;
    animation: fx-glitch-a 420ms steps(2, end) 2;
    -webkit-text-fill-color: #ff3d68;
  }
  .fx-glitch.is-glitching::after {
    opacity: 0.85;
    animation: fx-glitch-b 420ms steps(2, end) 2;
    -webkit-text-fill-color: var(--beam);
  }
}
@keyframes fx-glitch-a {
  0%   { transform: translate(0, 0);    clip-path: inset(12% 0 62% 0); }
  25%  { transform: translate(-3px, 1px); clip-path: inset(48% 0 22% 0); }
  50%  { transform: translate(2px, -1px); clip-path: inset(8% 0 76% 0); }
  75%  { transform: translate(-2px, 0);   clip-path: inset(70% 0 8% 0); }
  100% { transform: translate(0, 0);    clip-path: inset(30% 0 46% 0); }
}
@keyframes fx-glitch-b {
  0%   { transform: translate(0, 0);    clip-path: inset(60% 0 14% 0); }
  25%  { transform: translate(3px, -1px); clip-path: inset(14% 0 64% 0); }
  50%  { transform: translate(-2px, 1px); clip-path: inset(72% 0 6% 0); }
  75%  { transform: translate(2px, 0);    clip-path: inset(26% 0 50% 0); }
  100% { transform: translate(0, 0);    clip-path: inset(44% 0 30% 0); }
}

/* Decode: the character shuffle fx.js runs.

   The shuffled string is painted from a data attribute through generated
   content, never written into the element's text node — see the note in
   fx.js. The real text stays in the DOM and is simply made transparent for
   the duration, so anything reading the document (a crawler mid-render, a
   screen reader, select-and-copy) gets the true string at every instant. */
.fx-decode { position: relative; }
.fx-decode.is-decoding { color: transparent; }
.fx-decode.is-decoding::before {
  content: attr(data-scramble);
  position: absolute;
  inset: 0;
  color: var(--beam);
  white-space: pre-wrap;
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   6. RETICLE
   A HUD crosshair that tracks the pointer and snaps a bracket around whatever
   interactive element is under it. Desktop + fine pointer only — on touch
   there is no hover state to report and the element would lag the tap.
   ------------------------------------------------------------------------- */
.fx-reticle {
  position: fixed;
  left: 0;
  top: 0;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  pointer-events: none;
  z-index: 9998;
  opacity: 0;
  border-radius: 5px;
  transition:
    width   200ms var(--e-out),
    height  200ms var(--e-out),
    margin  200ms var(--e-out),
    opacity 200ms linear,
    border-radius 200ms var(--e-out);
  background-image:
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot)),
    linear-gradient(var(--hud-c-hot), var(--hud-c-hot));
  background-size:
    8px 1px, 1px 8px, 8px 1px, 1px 8px,
    8px 1px, 1px 8px, 8px 1px, 1px 8px;
  background-position:
    left top, left top, right top, right top,
    left bottom, left bottom, right bottom, right bottom;
  background-repeat: no-repeat;
  filter: drop-shadow(0 0 6px rgba(89, 230, 255, 0.65));
}
.fx-reticle.is-on   { opacity: 0.9; }
.fx-reticle.is-snap { opacity: 1; }

@media (hover: none), (pointer: coarse) {
  .fx-reticle { display: none; }
}

/* ---------------------------------------------------------------------------
   7. MAGNETIC + RIPPLE
   fx.js writes --pull-x/--pull-y; the transform stays in CSS so the button's
   own :hover/:active transforms compose with it in one declaration.
   ------------------------------------------------------------------------- */
.btn {
  transform: translate(var(--pull-x, 0px), calc(var(--pull-y, 0px)));
  transition:
    transform var(--d-2) var(--e-spring),
    background-color var(--d-2) var(--e-out),
    border-color var(--d-2) var(--e-out),
    box-shadow var(--d-2) var(--e-out),
    color var(--d-2) var(--e-out);
}
.btn:hover  { transform: translate(var(--pull-x, 0px), calc(var(--pull-y, 0px) - 2px)); }
.btn:active { transform: translate(var(--pull-x, 0px), var(--pull-y, 0px)) scale(0.985); }

.fx-ripple {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0);
  background: radial-gradient(circle, rgba(205, 246, 255, 0.55), transparent 70%);
  mix-blend-mode: screen;
  z-index: 4;
}
@media (prefers-reduced-motion: no-preference) {
  .fx-ripple { animation: fx-ripple 620ms var(--e-out) forwards; }
}
@keyframes fx-ripple {
  to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

/* ---------------------------------------------------------------------------
   8. LINK + FOCUS TREATMENT
   ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  :focus-visible {
    outline: 1px solid var(--beam);
    outline-offset: 3px;
    animation: fx-focus 1.4s var(--e-inout) infinite;
  }
}
@keyframes fx-focus {
  0%, 100% { box-shadow: 0 0 0 0 rgba(89, 230, 255, 0.35); }
  50%      { box-shadow: 0 0 0 5px rgba(89, 230, 255, 0); }
}

/* Nav links get a chromatic split on hover — cheap, and it ties the nav to the
   glitch treatment on the headings. */
.nav__link,
.foot__links a {
  transition: color var(--d-1) var(--e-out), text-shadow var(--d-1) var(--e-out);
}
.nav__link:hover,
.foot__links a:hover {
  text-shadow: -0.7px 0 rgba(255, 61, 104, 0.55), 0.7px 0 rgba(89, 230, 255, 0.55);
}

/* ---------------------------------------------------------------------------
   9. TABLE POWER-ON
   The table already has hover lift, a period/group crosshair and phase cues in
   ptable.css. What it lacked was an arrival: 118 cells appearing at once reads
   as a static image. Staggering them by atomic number makes the grid *fill*,
   left to right, the way the table was actually discovered.

   fx.js stamps --i (0..117) once; the animation runs once and is then inert.
   ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .ptable.is-booting .el {
    opacity: 0;
    animation: el-boot 460ms var(--e-out) forwards;
    animation-delay: calc(var(--i, 0) * 5.5ms);
  }
}
@keyframes el-boot {
  from { opacity: 0; transform: translateY(9px) scale(0.86); filter: brightness(2.4) saturate(0.3); }
  to   { opacity: 1; transform: none;                        filter: none; }
}

/* Selecting a cell for comparison should feel like arming it. */
.el.is-sel { animation: el-arm 520ms var(--e-out); }
@keyframes el-arm {
  0%   { box-shadow: 0 0 0 2px #fff, 0 0 0 0 var(--c); }
  60%  { box-shadow: 0 0 0 2px #fff, 0 0 0 14px transparent; }
  100% { box-shadow: 0 0 0 2px #fff, 0 0 26px -4px var(--c); }
}

/* ---------------------------------------------------------------------------
   10. PRINT — strip the entire layer
   ------------------------------------------------------------------------- */
@media print {
  .fx-floor, .fx-crt, .fx-noise, .fx-reticle, .fx-scan__bar { display: none !important; }
}
