/* =============================================================================
   signal-fx.css — THE CHANNEL SWITCH. (terminal-theme, 2026-08-11, NEW)
   Owner-neutral. Pairs with `signal-fx.js`. Nothing here names an owner, a
   domain or a page.

   THE THESIS, IN ONE SENTENCE
     This theme's whole conceit is that the site IS a display the visitor is
     logged into — so when the visitor changes tab, the DISPLAY changes channel:
     the picture collapses to a bright line, and the next one opens back out of
     it. One physical idea, borrowed from the world the theme already commits
     to, used for every switch on the site so that page tabs and in-page tabs
     speak the same sentence.

   WHY IT IS NOT A SLIDE, A FADE OR A PUSH
     A slide says "these two views sit beside each other in space". They do
     not — LIVE and MAP are not neighbours, they are channels. A cross-fade
     says "one thing is becoming another". It is not; it is being replaced.
     The collapse says exactly what happened: this picture ended, that one
     began, on the same machine.

   ---------------------------------------------------------------------------
   THE FOUR RULES THIS FILE KEEPS, AND WHY EACH ONE EXISTS
   ---------------------------------------------------------------------------

   1. NEVER TRANSFORM, FILTER OR CLIP AN ANCESTOR OF THE SHARED SPECIMEN CANVAS.
      The theme's 3D engine appends ONE canvas to `document.body` at
      `position:fixed; inset:0; z-index:5` and scissors it per mount. `transform`,
      `filter`, `perspective`, `backdrop-filter` and `contain` on an ancestor all
      make that element the containing block for its fixed descendants, so a
      200ms flourish on `body` or on a page wrapper would tear every specimen
      off its well — and `clip-path` on an ancestor clips it outright. So the
      motion lives in ONE fixed overlay that is a SIBLING of everything, and in
      the browser's own view-transition snapshots, which are pictures and cannot
      disturb layout at all. `opacity` is the one property allowed on a wrapper:
      it makes a stacking context, never a containing block.

   2. THE DEFAULT STATE IS VISIBLE. Every rule here animates FROM a state to
      the page's real, resting state, and no animation uses `forwards` fill on
      content. A dropped declaration, a refused script, a killed animation or a
      browser that has never heard of view transitions all land on exactly the
      same picture: the site, fully drawn. A transition that can hide content
      when it fails is not a transition, it is an outage.

   3. REDUCED MOTION GETS THE SAME INFORMATION, WITHOUT THE TRAVEL. The switch
      still reads — the arriving region still marks itself — it simply does it
      in one short opacity step with no collapse, no beam and no scanline burst.
      `prefers-reduced-motion` is a request about MOTION, not a request to be
      told less.

   4. NO LOOPS, EVER. Every animation in this file runs once, on a real user
      action or on arrival, and every one is under 500ms. There is no idle
      motion, nothing that runs offscreen, and nothing that keeps a compositor
      layer alive after it lands — `will-change` is set by the .on class and
      dies with it.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   TOKENS. Everything is expressed against the theme's own palette, with
   literal fallbacks so this file is still correct if it is dropped onto a
   consumer that has not defined them yet.
   --------------------------------------------------------------------------- */
:root{
  --fx-cut:170ms;          /* the picture collapsing — an exit, so it is fast  */
  --fx-open:340ms;         /* the picture opening — an arrival, so it settles  */
  --fx-beam:460ms;         /* one pass of the scan beam                        */
  --fx-pane:340ms;         /* an in-page panel resolving                       */
  --fx-in:cubic-bezier(.4,0,1,1);        /* accelerate away                    */
  --fx-out:cubic-bezier(.16,1,.3,1);     /* decelerate in — confident arrival  */
  --fx-hot:#9FE2FF;        /* the beam core: hotter than the type, on purpose  */
  --fx-lit:75,180,252;     /* the theme blue, as parts, for alpha work         */
  --fx-dark:1,4,10;        /* the ground, as parts                             */
}

/* ===========================================================================
   PART 1 — THE CROSS-DOCUMENT SWITCH (the nav tabs)

   Cross-document view transitions do the whole job in the browser: it holds a
   picture of the page being left, paints the page being entered, and hands us
   both as pseudo-elements to animate. That is the ONLY mechanism on the web
   that can collapse the actual outgoing picture, because the outgoing document
   is gone by then. Browsers without it fall to PART 2, which is the same idea
   drawn by hand on the overlay.

   `navigation: auto` only ever applies to same-origin navigations, so an
   outbound link to Discord or Steam is untouched by construction.
   =========================================================================== */
@view-transition{ navigation:auto }

/* The two halves must not cross-fade over each other: the old picture has to
   be GONE before the new one opens, or the collapse reads as a smear. So the
   old runs alone, the new starts after it, and neither is allowed to blend. */
::view-transition-image-pair(root){ isolation:auto }
::view-transition-old(root){
  animation:fx-off var(--fx-cut) var(--fx-in) both;
  mix-blend-mode:normal;
}
::view-transition-new(root){
  animation:fx-on var(--fx-open) var(--fx-out) var(--fx-cut) both;
  mix-blend-mode:normal;
}
/* The group must last as long as both halves, or the browser cuts the tail. */
::view-transition-group(root){ animation-duration:calc(var(--fx-cut) + var(--fx-open)) }

/* THE COLLAPSE. Vertical squeeze toward the centre line, a horizontal
   over-run as the deflection gives out, and the phosphor flaring as the whole
   frame's energy lands in one line — that is what a CRT actually does, and
   every number here is chosen to read at 170ms rather than to be accurate. */
@keyframes fx-off{
  0%  { transform:scaleY(1)    scaleX(1);    filter:brightness(1)   contrast(1);    opacity:1 }
  55% { transform:scaleY(.16)  scaleX(1.012);filter:brightness(1.85) contrast(1.22); opacity:1 }
  100%{ transform:scaleY(.004) scaleX(1.05); filter:brightness(3.1)  contrast(1.5);  opacity:.92 }
}
/* THE OPEN. The line becomes a picture again. It overshoots nothing — a bounce
   would turn a machine into a toy — it simply decelerates into place while the
   flare drains out of it. */
@keyframes fx-on{
  0%  { transform:scaleY(.004) scaleX(1.05); filter:brightness(3.1) contrast(1.4); opacity:.9 }
  22% { transform:scaleY(.18)  scaleX(1.012);filter:brightness(1.7) contrast(1.18);opacity:1 }
  100%{ transform:scaleY(1)    scaleX(1);    filter:brightness(1)   contrast(1);   opacity:1 }
}

/* ===========================================================================
   PART 2 — THE OVERLAY

   ONE fixed element, created by signal-fx.js, holding three drawn parts. It is
   a sibling of the page's own frame and of the specimen canvas, so nothing it
   does can reach layout. It is `aria-hidden`, never focusable, and it never
   takes a pointer event.

   z-index 190 puts it above every page surface this theme ships (the console
   sits at 150, the detail sheet at 80) and BELOW `.crt` at 200, because the
   shadow mask is glass in front of the tube: a beam that paints over the
   scanlines is a beam floating in the room.
   --------------------------------------------------------------------------- */
.fxsig{
  position:fixed; inset:0; z-index:190; pointer-events:none;
  display:block; opacity:1;
}
.fxsig > *{ position:absolute; opacity:0 }

/* THE REGION. Every part draws inside a rectangle that defaults to the whole
   viewport and is re-pointed at a panel for an in-page switch, so the same
   three parts serve both scales. */
.fxsig{ --fx-x:0px; --fx-y:0px; --fx-w:100vw; --fx-h:100vh }

/* (a) THE VEIL — the hand-drawn stand-in for the collapse, used where the
       browser cannot snapshot the document. Ground-coloured, so a browser
       without view transitions still sees the picture switch off rather than
       a white flash. */
.fxveil{
  left:var(--fx-x); top:var(--fx-y); width:var(--fx-w); height:var(--fx-h);
  background:
    linear-gradient(180deg,
      rgba(var(--fx-dark),0) 0%,
      rgba(var(--fx-dark),.92) 22%,
      rgba(var(--fx-dark),.98) 50%,
      rgba(var(--fx-dark),.92) 78%,
      rgba(var(--fx-dark),0) 100%);
  transform-origin:50% 50%;
}
/* (b) THE LINE — the beam-width core the veil collapses onto. It is the only
       thing on screen at the bottom of the cut, so it carries the flare. */
.fxline{
  left:var(--fx-x); top:calc(var(--fx-y) + var(--fx-h) / 2 - 1px);
  width:var(--fx-w); height:2px;
  background:linear-gradient(90deg,
    rgba(var(--fx-lit),0) 0%,
    var(--fx-hot) 18%, #FFFFFF 50%, var(--fx-hot) 82%,
    rgba(var(--fx-lit),0) 100%);
  box-shadow:0 0 12px rgba(var(--fx-lit),.85), 0 0 42px rgba(var(--fx-lit),.5);
  transform-origin:50% 50%;
}
/* (c) THE BEAM — one pass down the region as the picture re-acquires. The
       coloured lips are a mis-converged tube's red and cyan fringes; they are
       drawn ON THE BEAM rather than on the type, because tinting the type
       would mean overriding the theme's per-glyph ink stroke, and the ink
       stroke is the only reason body copy is legible over the owner's
       photograph at all. Never trade legibility for a flourish. */
.fxbeam{
  left:var(--fx-x); top:var(--fx-y); width:var(--fx-w); height:88px;
  background:
    linear-gradient(180deg,
      rgba(255,64,96,0) 0%, rgba(255,64,96,.22) 34%,
      rgba(var(--fx-lit),.42) 46%,
      rgba(226,247,255,.88) 50%,
      rgba(var(--fx-lit),.42) 54%,
      rgba(64,190,255,.22) 66%, rgba(64,190,255,0) 100%);
  mix-blend-mode:screen;
}
/* (d) THE BURST — the shadow mask jumping as the raster re-locks. Wider, more
       separated scanlines for a moment, over the region only. */
.fxgrid{
  left:var(--fx-x); top:var(--fx-y); width:var(--fx-w); height:var(--fx-h);
  background:repeating-linear-gradient(180deg,
    rgba(var(--fx-lit),.24) 0 1px, transparent 1px 4px);
  mix-blend-mode:screen;
}

/* ---- the run states, set by signal-fx.js ---------------------------------- */
.fxsig.cut .fxveil { animation:fx-veil-off var(--fx-cut) var(--fx-in) both; will-change:transform,opacity }
.fxsig.cut .fxline { animation:fx-line-in  var(--fx-cut) var(--fx-in) both; will-change:transform,opacity }

.fxsig.open .fxveil{ animation:fx-veil-on var(--fx-open) var(--fx-out) both; will-change:transform,opacity }
.fxsig.open .fxline{ animation:fx-line-out var(--fx-open) var(--fx-out) both; will-change:transform,opacity }
.fxsig.open .fxbeam{ animation:fx-beam-run var(--fx-beam) linear both; will-change:transform,opacity }
.fxsig.open .fxgrid{ animation:fx-grid-burst var(--fx-open) linear both }

/* the beam alone — an in-page switch where nothing needs to be hidden */
.fxsig.scan .fxbeam{ animation:fx-beam-run var(--fx-beam) linear both; will-change:transform,opacity }
.fxsig.scan .fxgrid{ animation:fx-grid-burst var(--fx-pane) linear both }

@keyframes fx-veil-off{
  0%  { opacity:0;  transform:scaleY(1) }
  38% { opacity:1;  transform:scaleY(1) }
  100%{ opacity:1;  transform:scaleY(.012) }
}
@keyframes fx-veil-on{
  0%  { opacity:1;  transform:scaleY(.012) }
  62% { opacity:.85;transform:scaleY(1) }
  100%{ opacity:0;  transform:scaleY(1) }
}
@keyframes fx-line-in{
  0%  { opacity:0; transform:scaleX(.2) }
  40% { opacity:.5; transform:scaleX(.85) }
  100%{ opacity:1; transform:scaleX(1.02) }
}
@keyframes fx-line-out{
  0%  { opacity:1; transform:scaleX(1.02) }
  46% { opacity:.55;transform:scaleX(1) }
  100%{ opacity:0; transform:scaleX(.6) }
}
@keyframes fx-beam-run{
  0%  { opacity:0; transform:translate3d(0,-90px,0) }
  10% { opacity:.95 }
  86% { opacity:.8 }
  100%{ opacity:0; transform:translate3d(0,var(--fx-h),0) }
}
@keyframes fx-grid-burst{
  0%  { opacity:0 }
  18% { opacity:.9 }
  100%{ opacity:0 }
}

/* ===========================================================================
   PART 3 — THE ARRIVING REGION

   An in-page tab switch cannot be snapshotted (the page's own handler has
   already swapped the DOM by the time anything of ours runs), so the arriving
   panel announces itself instead: it opens from the centre line, the same
   gesture the whole-screen switch makes, one size down.

   `clip-path` is used rather than `height`, because animating height is a
   layout animation that would reflow the page 60 times a second — and it is
   applied to a PANEL, never to a page wrapper, so the "never clip an ancestor
   of the specimen canvas" rule is kept: the canvas is a child of <body>.
   The animation carries no fill, so the property reverts to `none` the moment
   it ends and nothing stays clipped.
   =========================================================================== */
/* ★ MEASURED BY LOOKING, NOT BY THE CLOCK. The first cut of this ran 300ms
   from `inset(49.4%)`, which is arithmetically a line opening — and on film it
   was invisible, because a tall panel spends almost the whole animation within
   a few per cent of open and the eye only reads the last stretch. The opening
   now holds near the line for the first third and does its travel where it can
   be seen. Same duration class, an order of magnitude more legible. */
.fx-open{ animation:fx-panel var(--fx-pane) var(--fx-out) both }
@keyframes fx-panel{
  0%  { opacity:.05; clip-path:inset(49.6% 0 49.6% 0) }
  22% { opacity:.55; clip-path:inset(44% 0 44% 0) }
  62% { opacity:1;   clip-path:inset(12% 0 12% 0) }
  100%{ opacity:1;   clip-path:inset(0 0 0 0) }
}

/* THE TAB THAT WAS PRESSED. A switch has a cause, and the cause should light
   up for exactly as long as the switch takes. This rides on the theme's own
   bracket marks, so it adds no new vocabulary. */
.fx-fired{ animation:fx-fired 380ms var(--fx-out) both }
@keyframes fx-fired{
  0%  { filter:brightness(2.4) }
  100%{ filter:brightness(1) }
}

/* THE FRAME ACKNOWLEDGES IT TOO. One hairline pulse on the screen border as
   the channel lands — the theme draws its frame in `--rule`, so the pulse is
   the frame's own colour going hot and back. `box-shadow` is used rather than
   `border-color` so nothing can shift by a pixel. */
html.fx-landing .screen{ animation:fx-frame 520ms var(--fx-out) both }
@keyframes fx-frame{
  0%  { box-shadow:0 0 0 1px rgba(var(--fx-lit),.85), 0 0 60px -10px rgba(var(--fx-lit),.55) }
  100%{ box-shadow:0 0 0 1px rgba(var(--fx-lit),0),   0 0 60px -10px rgba(var(--fx-lit),0) }
}

/* ===========================================================================
   PART 4 — THE ANSWERS TO EVERY DEGRADED STATE
   =========================================================================== */

/* No JS: the overlay is never created, `@view-transition` still runs the
   cross-document switch on its own, and nothing else is reachable. The site
   is complete either way — this whole file is enhancement over a page that
   was already finished. */

/* Reduced motion. The collapse, the beam and the burst all go. What survives
   is a single 120ms opacity step, so the visitor is still told that the view
   changed — they are simply not moved through it. */
@media(prefers-reduced-motion:reduce){
  ::view-transition-old(root){ animation:fx-quiet-out 90ms linear both }
  ::view-transition-new(root){ animation:fx-quiet-in 120ms linear 90ms both }
  ::view-transition-group(root){ animation-duration:210ms }
  @keyframes fx-quiet-out{ to{ opacity:0 } }
  @keyframes fx-quiet-in{ from{ opacity:0 } to{ opacity:1 } }

  .fxsig.cut .fxveil{ animation:fx-quiet-veil-in 90ms linear both }
  .fxsig.open .fxveil{ animation:fx-quiet-veil-out 120ms linear both }
  @keyframes fx-quiet-veil-in{ from{ opacity:0; transform:none } to{ opacity:1; transform:none } }
  @keyframes fx-quiet-veil-out{ from{ opacity:1; transform:none } to{ opacity:0; transform:none } }

  .fxsig .fxline,.fxsig .fxbeam,.fxsig .fxgrid{ animation:none !important; opacity:0 !important }
  .fx-open{ animation:fx-quiet-panel 120ms linear both }
  @keyframes fx-quiet-panel{ from{ opacity:.35 } to{ opacity:1 } }
  .fx-fired{ animation:none }
  html.fx-landing .screen{ animation:none }
}

/* A visitor who is dragging a selection, or a printer, gets none of it. */
@media print{ .fxsig{ display:none } }

/* An owner surface that must never be covered — a checkout, a live map drag —
   opts its region out by name rather than by us guessing. */
[data-fx="off"] .fx-open,.fx-open[data-fx="off"]{ animation:none }
