/* ═══════════════════════════════════════════════════════════════════════════
   THE WORLD BELOW — motion layer
   ───────────────────────────────────────────────────────────────────────────
   Loaded after twb-theme.css. Colour lives there; nothing in this file sets a
   hue that is not already a --twb-* token, so the two stay in step and the
   light variant follows for free.

   THE MOTION IDEA, so later edits keep it
   This world is stone, depth, and a storm happening somewhere overhead.
   Therefore:
     - things RISE. Content arrives from below and settles. Nothing drops in,
       nothing bounces. Rock does not bounce.
     - motion has WEIGHT. The easing starts slow, commits, and lands without
       overshoot. An elastic curve would read as plastic.
     - light is the only fast thing. Cyan is a strike: a seam of light crosses
       an edge in a fraction of the time the panel itself takes to move.
     - ambient motion is nearly invisible on purpose. If you notice the
       background moving while reading, it is too strong.

   PERFORMANCE RULE, non-negotiable
   Every animation here is transform / opacity / filter only. Nothing animates
   width, height, top, left or margin, so nothing here can trigger layout on a
   scroll or a tab switch. The one exception is the sub-nav row's height, which
   is a single 48px element switching between two known values.

   ACCESSIBILITY
   The whole file collapses under prefers-reduced-motion at the end. That block
   is last on purpose so it wins on order as well as specificity.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Weight. Slow to leave, decisive, lands flat. */
  --twb-ease: cubic-bezier(.16, .84, .28, 1);
  /* For things going away — leaves quickly, no lingering. */
  --twb-ease-out: cubic-bezier(.4, 0, .7, .35);
  /* Light. Near-linear so a strike does not appear to accelerate. */
  --twb-ease-light: cubic-bezier(.3, .1, .3, 1);

  --twb-fast: 140ms;
  --twb-mid: 280ms;
  --twb-slow: 520ms;
  --twb-rise: 16px;      /* how far content climbs into place */
}

/* ══ 1. ARRIVAL ══════════════════════════════════════════════════════════
   Scroll reveals. twb-motion.js marks candidates .twb-reveal and adds .twb-in
   when they cross into view. The observer never marks canvases, media, 3d
   viewers or anything already mid-animation — see the JS for the skip list.
   ───────────────────────────────────────────────────────────────────────── */
.twb-reveal {
  opacity: 0;
  transform: translate3d(0, var(--twb-rise), 0);
  /* A hair of blur on arrival reads as "surfacing", and it hides the 1px
     jitter that a sub-pixel translate leaves on text. */
  filter: blur(3px);
  will-change: opacity, transform;
}
.twb-reveal.twb-in {
  opacity: 1;
  transform: none;
  filter: none;
  transition:
    opacity var(--twb-slow) var(--twb-ease) var(--twb-d, 0ms),
    transform var(--twb-slow) var(--twb-ease) var(--twb-d, 0ms),
    filter var(--twb-mid) var(--twb-ease) var(--twb-d, 0ms);
}
/* Once it has arrived, stop promoting it — a page full of permanently
   composited layers costs memory for no benefit. */
.twb-reveal.twb-settled { will-change: auto; }

/* ══ 2. VIEW CHANGES ═════════════════════════════════════════════════════
   The generic panel transition. Applied by the JS whenever ANY element goes
   from hidden to shown, whatever mechanism the page used to do it — a class,
   a style.display, the hidden attribute. That is why it is one animation on
   one element rather than a per-page implementation: pages hand-roll their tab
   strips half a dozen different ways and a new one should not need new code.

   Children are deliberately NOT staggered here. A shop grid can hold sixty
   cards; a per-child delay would still be running seconds after the click.
   The reveal observer above gives cards their entrance the first time they are
   seen, which is where it belongs.
   ───────────────────────────────────────────────────────────────────────── */
@keyframes twb-panel-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0); filter: blur(4px); }
  60%  { filter: blur(0); }
  to   { opacity: 1; transform: none; filter: none; }
}
.twb-panel-in {
  animation: twb-panel-in 340ms var(--twb-ease) both;
}

/* The strike. A seam of light crossing the top edge of whatever just changed.
   It is drawn on a ::before so it needs no extra element inside a panel the
   page owns, and it is pointer-events:none so it can never eat a click. */
.twb-seam { position: relative; }
.twb-seam::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 1px;
  pointer-events: none;
  z-index: 3;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(79, 216, 216, 0) 18%,
    var(--twb-cyan, #4FD8D8) 50%,
    rgba(79, 216, 216, 0) 82%,
    transparent 100%);
  transform: scaleX(0);
  transform-origin: 50% 50%;
  animation: twb-seam 460ms var(--twb-ease-light) both;
}
@keyframes twb-seam {
  0%   { transform: scaleX(0); opacity: 0; }
  22%  { opacity: 1; }
  70%  { transform: scaleX(1); opacity: .85; }
  100% { transform: scaleX(1); opacity: 0; }
}

/* ══ 3. THE NAVIGATION ═══════════════════════════════════════════════════ */

/* The moving mark under the active section. One absolutely positioned element
   that slides between tabs instead of an underline that teleports. The JS
   measures and writes --twb-mark-x / --twb-mark-w; if it cannot find the nav
   it never creates the element and nothing here applies. */
.twb-navmark {
  position: absolute;
  bottom: 0;
  height: 2px;
  left: 0;
  width: 1px;
  pointer-events: none;
  border-radius: 2px;
  background: linear-gradient(90deg,
    rgba(79, 216, 216, .15), var(--twb-cyan, #4FD8D8) 50%, rgba(79, 216, 216, .15));
  box-shadow: 0 0 12px rgba(79, 216, 216, .55), 0 0 3px rgba(79, 216, 216, .8);
  transform: translate3d(var(--twb-mark-x, 0), 0, 0) scaleX(var(--twb-mark-w, 1));
  transform-origin: 0 50%;
  opacity: 0;
  transition:
    transform 420ms var(--twb-ease),
    opacity 200ms var(--twb-ease);
}
.twb-navmark.twb-mark-on { opacity: 1; }
/* First placement must not slide in from the left edge of the bar. */
.twb-navmark.twb-mark-jump { transition: opacity 200ms var(--twb-ease); }

/* The contextual sub-tab row swapping its contents when the section changes. */
.twb-subrow-swap { animation: twb-subrow 300ms var(--twb-ease) both; }
@keyframes twb-subrow {
  from { opacity: 0; transform: translate3d(0, -6px, 0); }
  to   { opacity: 1; transform: none; }
}

/* Nav links: a cyan seam wipes in under the label on hover. Uses scaleX on a
   pseudo rather than a width transition so it stays off the layout path. */
.twb-navwipe { position: relative; }
.twb-navwipe::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -3px;
  height: 1px;
  background: var(--twb-cyan, #4FD8D8);
  opacity: .0;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 260ms var(--twb-ease), opacity 180ms var(--twb-ease);
  pointer-events: none;
}
.twb-navwipe:hover::after,
.twb-navwipe:focus-visible::after { transform: scaleX(1); opacity: .55; }

/* ══ 4. TOUCH ════════════════════════════════════════════════════════════
   Press states. Stone is heavy: it moves a single pixel and it moves fast.
   ───────────────────────────────────────────────────────────────────────── */
.sw-btn,
button:not(.twb-no-press),
.sw-card a.more,
[role="button"] {
  transition:
    transform var(--twb-fast) var(--twb-ease),
    box-shadow var(--twb-mid) var(--twb-ease),
    border-color var(--twb-mid) var(--twb-ease),
    background-color var(--twb-mid) var(--twb-ease),
    color var(--twb-mid) var(--twb-ease);
}
.sw-btn:active,
button:not(.twb-no-press):active,
[role="button"]:active { transform: translate3d(0, 1px, 0) scale(.994); }

/* Cards lift and catch the light. The glow is an accent and stays one, so the
   border brightens and the shadow stays cold. */
.sw-card {
  transition:
    transform var(--twb-mid) var(--twb-ease),
    box-shadow var(--twb-mid) var(--twb-ease),
    border-color var(--twb-mid) var(--twb-ease);
}
.sw-card:hover {
  transform: translate3d(0, -2px, 0);
  border-color: rgba(79, 216, 216, .30);
  box-shadow:
    0 10px 30px rgba(var(--twb-scrim-rgb, 5, 8, 11), .55),
    0 0 0 1px rgba(79, 216, 216, .10);
}
/* A card that is itself a link should not lift twice when the anchor inside
   it is also hovered. */
.sw-card .sw-card:hover { transform: none; }

/* ══ 4b. THINGS THAT OPEN OVER THE PAGE ══════════════════════════════════
   Every overlay on this site is toggled between display:none and
   display:flex/block by a class. That means the CLOSING direction cannot be
   animated from CSS at all — the element is gone from the frame the class
   comes off — so these are entrance-only, on purpose. It is still the
   difference between a modal that appears and a modal that snaps.

   ★ These animate the overlay, never its backdrop separately, and they use
   scale from 0.98 rather than a slide, because several of these overlays are
   position:fixed and a translate on the wrapper would drag a fixed child with
   it. 0.98 is small enough that no text renders visibly soft on the way in.
   ───────────────────────────────────────────────────────────────────────── */
.modal-overlay.show,
.cas-modal.show,
#shop-modal.show,
#inv-modal.show,
#vault-modal.show,
.modal-wrap.open,
#modal.open {
  animation: twb-over-in 200ms var(--twb-ease) both;
}
@keyframes twb-over-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* The card inside the overlay carries the movement, so the dimmed backdrop
   behind it stays put instead of scaling with it. */
.modal-overlay.show > *,
.cas-modal.show > *,
.modal-wrap.open > * {
  animation: twb-over-card 260ms var(--twb-ease) both;
}
@keyframes twb-over-card {
  from { opacity: 0; transform: translate3d(0, 10px, 0) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* The account dropdown in the topbar, which until now was a hard cut both
   ways. Anchored to its top edge so it reads as unfolding downward out of the
   bar rather than arriving from nowhere. */
header.sweet-nav .user-menu:not([hidden]) {
  animation: twb-menu-in 180ms var(--twb-ease) both;
  transform-origin: 100% 0;
}
@keyframes twb-menu-in {
  from { opacity: 0; transform: translate3d(0, -6px, 0) scale(.98); }
  to   { opacity: 1; transform: none; }
}

/* The mobile burger sheet already transitions its transform; it only ever
   lacked a matching fade, which is what made it read as a jump on a slow
   phone. Layout is untouched. */
@media (max-width: 860px) {
  header.sweet-nav nav.links { opacity: 0; transition: transform .35s var(--twb-ease), visibility .35s, opacity .28s var(--twb-ease); }
  header.sweet-nav nav.links.open { opacity: 1; }
}

/* ══ 5. AMBIENT ══════════════════════════════════════════════════════════
   The storm overhead. A faint band of light crosses the top bar every so
   often. Extremely low alpha on purpose: it should register as weather, not
   as a loading bar. Skipped entirely on low-end devices (html.fi-lite).
   ───────────────────────────────────────────────────────────────────────── */
.twb-topbar-storm { position: relative; overflow: hidden; }
.twb-topbar-storm::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent 40%,
    rgba(155, 175, 246, .055) 48%,
    rgba(223, 235, 242, .085) 50%,
    rgba(155, 175, 246, .055) 52%,
    transparent 60%);
  transform: translate3d(-60%, 0, 0);
  animation: twb-storm 17s var(--twb-ease-light) infinite;
}
@keyframes twb-storm {
  0%, 72%  { transform: translate3d(-60%, 0, 0); opacity: 0; }
  76%      { opacity: 1; }
  100%     { transform: translate3d(60%, 0, 0); opacity: 0; }
}
html.fi-lite .twb-topbar-storm::after { display: none; }

/* A live thing, pulsing. Used by the player-count dot. */
.twb-pulse {
  position: relative;
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--twb-mint, #63D2A8);
  box-shadow: 0 0 8px rgba(99, 210, 168, .8);
  vertical-align: middle;
}
.twb-pulse::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--twb-mint, #63D2A8);
  animation: twb-ping 2.6s var(--twb-ease-light) infinite;
}
@keyframes twb-ping {
  0%   { transform: scale(1); opacity: .7; }
  70%  { transform: scale(3.2); opacity: 0; }
  100% { transform: scale(3.2); opacity: 0; }
}
/* One beat per successful check, so a count that has not moved still shows
   that the page is talking to the server. */
.twb-pulse.twb-beat { animation: twb-beat 620ms var(--twb-ease); }
@keyframes twb-beat {
  0%   { transform: scale(1); }
  28%  { transform: scale(1.55); }
  100% { transform: scale(1); }
}

/* Nobody home: the dot goes cold and stops pinging. */
.twb-pulse.twb-cold { background: var(--twb-stone, #7C8D9A); box-shadow: none; }
.twb-pulse.twb-cold::after { display: none; }
/* The server did not answer. */
.twb-pulse.twb-stale { background: var(--twb-ember, #D77A5F); box-shadow: 0 0 8px rgba(215, 122, 95, .7); }
.twb-pulse.twb-stale::after { border-color: var(--twb-ember, #D77A5F); animation-duration: 4s; }

/* A number that just changed. The lift is upward for a rise and downward for
   a fall, so a busy server feels different from an emptying one. */
.twb-roll { display: inline-block; }
.twb-roll.twb-up   { animation: twb-roll-up 420ms var(--twb-ease); }
.twb-roll.twb-down { animation: twb-roll-down 420ms var(--twb-ease); }
@keyframes twb-roll-up {
  0%   { transform: translate3d(0, .5em, 0); opacity: 0; filter: blur(3px); }
  100% { transform: none; opacity: 1; filter: none; }
}
@keyframes twb-roll-down {
  0%   { transform: translate3d(0, -.5em, 0); opacity: 0; filter: blur(3px); }
  100% { transform: none; opacity: 1; filter: none; }
}

/* ══ 6. BETWEEN PAGES ════════════════════════════════════════════════════
   The five top-level sections are separate documents, so "changing tabs"
   really means a page load. Browsers that support cross-document view
   transitions get a real cross-fade from the at-rule below and the JS stays
   out of the way. Everything else gets the JS fallback, which fades the
   outgoing document and is cancelled on pageshow so the back button can never
   land on a faded page.
   ───────────────────────────────────────────────────────────────────────── */
@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: twb-vt-out 180ms var(--twb-ease-out) both;
}
::view-transition-new(root) {
  animation: twb-vt-in 380ms var(--twb-ease) both;
}
@keyframes twb-vt-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(0, -8px, 0); }
}
@keyframes twb-vt-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}

/* The fallback. Only ever activated by the JS on browsers without the above.
   ★ OPACITY ONLY, AND THAT IS DELIBERATE. A transform on <body> makes the body
   a containing block, which re-parents every position:fixed descendant — the
   topbar, the voice dock, any open modal — for as long as the animation runs.
   The rise belongs on the view-transition snapshots above, where it is a
   picture of the page and not the page itself. Do not "improve" this by adding
   a translate. */
html.twb-leaving body {
  opacity: 0;
  transition: opacity 170ms var(--twb-ease-out);
}

/* First paint of a document, for the same no-view-transition browsers. */
html.twb-entering body {
  animation: twb-doc-in 380ms var(--twb-ease) both;
}
@keyframes twb-doc-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ══ 7. STILLNESS ════════════════════════════════════════════════════════
   Last block in the file. Anyone who has asked their system for less motion
   gets the whole site, instantly, with no movement at all — including the
   ambient storm and the cross-document transition, which are the two that a
   naive reduced-motion block usually misses.
   ───────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .twb-reveal,
  .twb-reveal.twb-in {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
  .twb-panel-in,
  .twb-subrow-swap,
  .twb-roll.twb-up,
  .twb-roll.twb-down,
  .modal-overlay.show, .cas-modal.show, #shop-modal.show, #inv-modal.show,
  #vault-modal.show, .modal-wrap.open, #modal.open,
  .modal-overlay.show > *, .cas-modal.show > *, .modal-wrap.open > *,
  header.sweet-nav .user-menu:not([hidden]),
  html.twb-entering body { animation: none !important; }
  header.sweet-nav nav.links { opacity: 1 !important; transition: none !important; }
  .twb-seam::before,
  .twb-topbar-storm::after,
  .twb-pulse::after { animation: none !important; display: none !important; }
  .twb-pulse.twb-beat { animation: none !important; transform: none !important; }
  .twb-navmark { transition: none !important; }
  .sw-card:hover,
  .sw-btn:active,
  button:active,
  [role="button"]:active { transform: none !important; }
  html.twb-leaving body { opacity: 1 !important; transform: none !important; transition: none !important; }
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none !important; }
}
