/*
  Infin8 shared effect layer.

  Everything here is page-agnostic and surface-agnostic: the animated panel
  frames, the drifting aurora, the scroll-entry choreography, the painted
  nebula field, the button nebula, the Fin sprite player, the comic balloon and
  the docked assistant. It was extracted from assets/style.css so the Arcade,
  the leaderboard, the player profile and the marketing pages can all share one
  copy instead of growing three divergent ones.

  It carries no palette of its own. Every colour reads a custom property with a
  fallback, so the marketing pages get their tokens from assets/style.css and
  the Arcade surfaces get their own brighter profile from assets/arcade.css
  without either file being changed.

  Load it AFTER the page's own stylesheet - it is written to sit at the end of
  the cascade, exactly where it lived inside style.css.

  Page-specific styling that merely uses these effects - the home page hero
  mark, the plan panels, the Fin showcase band - deliberately stayed behind in
  assets/style.css.
*/

/* ---------------------------------------------------------------------------
   Shared brand texture

   The Infin8 nebula screened over every surface that loads this file - the
   marketing pages, the Arcade, the boards, the profile and the game shells -
   so the whole product shares one ground. It is a fixed layer behind all
   content rather than a body background, so it does not scroll, repeat, or
   shift as sections change height. `screen` lifts the dark page toward the
   texture's magentas without ever darkening text, and the low opacity keeps
   body copy at full contrast.

   Note the negative z-index: the texture sits behind content without any
   page needing to lift its own shell. The painted nebula field sits above it
   at z-index 0, which is why that one does need the lift.
   --------------------------------------------------------------------------- */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("infin8-texture.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  mix-blend-mode: screen;
  opacity: 0.16;
}

/* Reduced transparency preferences and print both want the texture gone. */
@media (prefers-reduced-transparency: reduce) {
  body::before { opacity: 0.07; }
}
@media print {
  body::before { display: none; }
}

/* --------------------------------------------------------------------------
   Fin, the hero mark

   The same spritesheet the Control Panel ships (assets/assistant-pets/fin),
   trimmed to the three rows the site plays: 0 idle, 1 speaking, 2 working, six
   frames wide. infin8-motion.js owns the timing and writes --fin-col /
   --fin-row; CSS owns the look. With the script absent or reduced motion on,
   the declared 0/0 leaves Fin standing on his idle frame.
   -------------------------------------------------------------------------- */

.fin-mark {
  --fin-cols: 6;
  --fin-rows: 3;
  --fin-col: 0;
  --fin-row: 0;
  /* Where the top of Fin's head sits, as a fraction of the mark's height.
     Measured, not guessed: his artwork begins 2.4% down inside its own sprite
     frame, and the sprite is 76% of the mark anchored to the bottom, so the
     crown lands at 24% + (2.4% x 76%) = 25.8%. The balloon is positioned from
     this number, so the two can never drift apart - change the sprite height
     and this value is the one other thing to update. */
  --fin-crown: 25.8%;
  position: relative;
  display: grid;
  /* Fin stands in the lower left of the mark so the bubble has clear air to
     its upper right. Nothing is centred here on purpose. */
  place-items: end start;
  width: 100%;
  height: 100%;
}

.fin-mark-glow {
  position: absolute;
  inset: auto auto 0 0;
  width: 74%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 55%, rgba(94, 226, 255, 0.28), rgba(150, 108, 255, 0.16) 45%, transparent 70%);
  filter: blur(14px);
  animation: fin-breathe 5.6s ease-in-out infinite;
}

.fin-mark-sprite {
  position: relative;
  display: block;
  height: 76%;
  aspect-ratio: 192 / 208;
  background-image: url("fin-sprite.webp");
  background-repeat: no-repeat;
  background-size: calc(var(--fin-cols) * 100%) calc(var(--fin-rows) * 100%);
  /* n frames across means n-1 steps between 0% and 100%. */
  background-position:
    calc(var(--fin-col) / (var(--fin-cols) - 1) * 100%)
    calc(var(--fin-row) / (var(--fin-rows) - 1) * 100%);
  image-rendering: auto;
  filter: drop-shadow(0 18px 34px rgba(12, 6, 32, 0.55));
}

.fin-mark[data-fin-state="working"] .fin-mark-glow { animation-duration: 2.4s; }

@keyframes fin-breathe {
  0%, 100% { opacity: 0.65; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.06); }
}

/* --------------------------------------------------------------------------
   The comic balloon

   One balloon shape, shared by everything Fin says anywhere on the site: the
   home page greeting and the docked purchasing guide. It is a comic speech
   balloon rather than a chat bubble - a heavy ink outline, an outer halo ring
   that reads as the second pen pass, softly uneven corners so it looks drawn
   rather than generated, and a pointed tail with two trailing beads.

   Each user of the balloon points the tail itself by repositioning ::after and
   ::before; the shape, ink and halo are never redefined.
   -------------------------------------------------------------------------- */
.comic-balloon {
  --balloon-ink: rgba(196, 176, 255, 0.82);
  --balloon-fill: rgba(20, 16, 42, 0.96);
  --balloon-halo: rgba(57, 215, 238, 0.22);
  /* Deliberately no `position`. This class is a shape, and every user places
     it differently - the home page greeting is absolute inside Fin's mark, the
     docked panel is a flex child. Owning `position` here once turned the
     greeting into a grid item, which split the mark into two rows and shrank
     Fin to a third of his height. Each user establishes its own containing
     block for the tail; both do so below. */
  /* Corners are deliberately not all equal: an inked balloon never is. */
  border: 2.5px solid var(--balloon-ink);
  border-radius: 34px 28px 32px 30px;
  background: linear-gradient(150deg, rgba(30, 22, 58, 0.96), var(--balloon-fill));
  box-shadow:
    0 0 0 5px var(--balloon-halo),
    0 20px 46px rgba(6, 3, 20, 0.62);
  color: #f4f0ff;
  text-align: left;
}

/* The tail: a square rotated into a point, wearing two of the balloon's own
   borders so the ink line runs unbroken from the balloon into the point. */
.comic-balloon::after {
  position: absolute;
  width: 20px;
  height: 20px;
  border-right: 2.5px solid var(--balloon-ink);
  border-bottom: 2.5px solid var(--balloon-ink);
  border-bottom-right-radius: 4px;
  background: var(--balloon-fill);
  content: "";
}

/* The two trailing beads that make a comic balloon read as speech. They ride
   the same axis as the tail and are drawn on one element as two gradients so
   the balloon never needs extra markup. */
.comic-balloon::before {
  position: absolute;
  width: 34px;
  height: 22px;
  content: "";
  pointer-events: none;
  background:
    radial-gradient(circle 6.5px at 8px 7px, var(--balloon-fill) 0 68%, var(--balloon-ink) 69% 100%, transparent 100%),
    radial-gradient(circle 4px at 25px 17px, var(--balloon-fill) 0 60%, var(--balloon-ink) 61% 100%, transparent 100%);
}

/* --------------------------------------------------------------------------
   Nebula panel frames

   A cyan-to-violet light travels the edge of every product screenshot, so the
   images read as live windows rather than flat stills. The ring is a rotating
   conic gradient masked down to a one-pixel band, plus a blurred copy of
   itself for the bloom. Where @property is unsupported the angle simply never
   animates and the panel keeps a static brand-lit edge.
   -------------------------------------------------------------------------- */

@property --nebula-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.nebula-panel {
  position: relative;
  display: block;
  border-radius: var(--nebula-radius, 16px);
}

.nebula-ring,
.nebula-ring::after {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: var(--nebula-width, 2px);
  background: conic-gradient(
    from var(--nebula-angle),
    transparent 0turn,
    rgba(57, 215, 238, 0.95) 0.12turn,
    rgba(150, 108, 255, 1) 0.3turn,
    rgba(57, 215, 238, 0.45) 0.48turn,
    transparent 0.62turn,
    transparent 1turn
  );
  /* Paint the padding band only: the two masks cancel everywhere except the
     1px ring between the border box and the content box. */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: nebula-orbit 8s linear infinite;
}

.nebula-ring {
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  content: "";
}

.nebula-ring::after {
  inset: -14px;
  padding: 16px;
  border-radius: calc(var(--nebula-radius, 16px) + 14px);
  filter: blur(18px);
  opacity: 0.8;
  /* Half speed and reversed, so the bloom and the hard edge never sit on top
     of each other twice in the same place. */
  animation-duration: 16s;
  animation-direction: reverse;
  content: "";
}

@keyframes nebula-orbit {
  to { --nebula-angle: 360deg; }
}

/* Drifting brand light. Two independent gradient plates on long, offset cycles
   never line up the same way twice, which keeps the hero from feeling looped. */
.aurora-field {
  position: absolute;
  inset: -20% -10% auto;
  height: 900px;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.aurora-field span {
  position: absolute;
  display: block;
  border-radius: 50%;
  filter: blur(70px);
}

.aurora-field span:nth-child(1) {
  top: 6%;
  left: 4%;
  width: 46vw;
  height: 46vw;
  background: radial-gradient(circle, rgba(139, 92, 246, 0.22), transparent 68%);
  animation: aurora-a 30s ease-in-out infinite;
}

.aurora-field span:nth-child(2) {
  top: 0;
  right: 2%;
  width: 40vw;
  height: 40vw;
  background: radial-gradient(circle, rgba(57, 215, 238, 0.16), transparent 68%);
  animation: aurora-b 38s ease-in-out infinite;
}

.aurora-field span:nth-child(3) {
  top: 42%;
  left: 38%;
  width: 30vw;
  height: 30vw;
  background: radial-gradient(circle, rgba(57, 223, 160, 0.1), transparent 70%);
  animation: aurora-c 44s ease-in-out infinite;
}

@keyframes aurora-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(7vw, 5vh, 0) scale(1.14); }
}

@keyframes aurora-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.08); }
  50% { transform: translate3d(-6vw, 7vh, 0) scale(0.94); }
}

@keyframes aurora-c {
  0%, 100% { transform: translate3d(0, 0, 0) scale(0.96); }
  50% { transform: translate3d(4vw, -6vh, 0) scale(1.18); }
}

/* The headline sheen re-uses the existing .gradient-text ramp and simply moves
   it, so the brand colours stay exactly as specified. */

@keyframes sheen {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

/* Scroll choreography. Elements start displaced and are released by the
   observer in assets/infin8-motion.js; `--reveal-index` staggers siblings. */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition:
    opacity 760ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 760ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--reveal-index, 0) * 90ms);
  will-change: opacity, transform;
}

[data-reveal="left"] { transform: translate3d(-28px, 0, 0); }
[data-reveal="right"] { transform: translate3d(28px, 0, 0); }
[data-reveal="scale"] { transform: scale(0.965); }

[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Set by assets/infin8-motion.js once every reveal has had time to play. A
   transition only advances while the page composites, so a preview pane or a
   throttled tab can hold one at time zero forever and keep revealed content at
   its starting opacity of 0. Retiring the transition lets the end state apply
   directly, so content is never hostage to an animation that cannot run. */
.motion-settled [data-reveal] {
  transition: none;
}

/* --------------------------------------------------------------------------
   Nebula particle field

   A single canvas painted by assets/infin8-motion.js. It is drawn from the
   site tokens only - cyan, violet and mint motes over the existing dark base -
   so it reads as the Infin8 galaxy texture in motion rather than a generic
   starfield. It is fixed, non-interactive, and every element above it is
   lifted one stacking level so nothing is ever painted over content.
   -------------------------------------------------------------------------- */
.nebula-canvas {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1200ms ease;
}

.nebula-canvas.is-live { opacity: 1; }

/* Content above the field. Only static shells are listed: .arcade-header and
   .board-header are sticky with a z-index already, and overriding their
   position here would stop them sticking. */
main,
.arcade-footer,
.board-footer,
.profile-shell {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------------
   Nebula buttons

   The existing .button geometry, colour and focus ring are untouched. What is
   added is a nebula that lives inside the button: a violet/cyan cloud that
   drifts continuously, plus a light that follows the pointer through
   --pointer-x / --pointer-y written by the motion script.
   -------------------------------------------------------------------------- */
.button {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.button::before {
  position: absolute;
  inset: -60%;
  z-index: -1;
  content: "";
  opacity: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 30% 30%, rgba(139, 92, 246, 0.85), transparent 42%),
    radial-gradient(circle at 70% 62%, rgba(57, 215, 238, 0.7), transparent 46%),
    radial-gradient(circle at 48% 82%, rgba(57, 223, 160, 0.4), transparent 44%);
  filter: blur(12px);
  transition: opacity 320ms ease;
  animation: nebula-churn 12s linear infinite;
}

/* The pointer light is a separate layer so it can track the cursor without
   restarting the drift underneath it. */
.button::after {
  position: absolute;
  inset: 0;
  z-index: -1;
  content: "";
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(
    120px circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
    rgba(255, 255, 255, 0.2),
    transparent 62%
  );
  transition: opacity 260ms ease;
}

.button:hover::before,
.button:focus-visible::before { opacity: 0.72; }

.button:hover::after,
.button:focus-visible::after { opacity: 1; }

.button.primary:hover::before,
.button.primary:focus-visible::before { opacity: 0.75; }

.button:hover,
.button:focus-visible {
  border-color: var(--border-strong);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.42), 0 0 34px rgba(139, 92, 246, 0.28);
}

@keyframes nebula-churn {
  0% { transform: rotate(0deg) scale(1.05); }
  50% { transform: rotate(180deg) scale(1.22); }
  100% { transform: rotate(360deg) scale(1.05); }
}

/* --------------------------------------------------------------------------
   Fin, the purchasing guide

   The same character, the same spritesheet and the same `[data-fin]` player
   the home page hero uses - here as a docked assistant that explains the step
   the visitor is actually on, points at the Arcade between tips, and shows who
   is currently at the top of the Arcade boards.

   It is an aside, not a dialog: it never traps focus, never covers the primary
   action on any supported width, and can be closed for the session.
   -------------------------------------------------------------------------- */
.fin-guide {
  position: fixed;
  right: clamp(12px, 2vw, 26px);
  bottom: clamp(12px, 2vw, 26px);
  z-index: 40;
  display: flex;
  align-items: flex-end;
  gap: 10px;
  width: min(500px, calc(100% - 24px));
  pointer-events: none;
}

.fin-guide > * { pointer-events: auto; }
.fin-guide[hidden] { display: none; }

/* Fin himself. `.fin-mark` is the shared player surface: the same custom
   properties the home page hero writes, at dock scale. */
.fin-guide-mark {
  flex: 0 0 auto;
  width: clamp(92px, 10vw, 118px);
  height: clamp(92px, 10vw, 118px);
  border: 0;
  padding: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
}

.fin-guide-mark .fin-mark-sprite { height: 100%; }
.fin-guide-mark .fin-mark-glow { width: 88%; }

.fin-guide-mark:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 4px;
}

/* The same comic balloon as the home page, pointed left at Fin instead of down
   at him. Nothing about the shape is redefined here - only where the tail and
   its beads sit. */
.fin-guide-panel {
  /* Containing block for the balloon's tail and beads. */
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  padding: 16px 18px 15px;
  backdrop-filter: blur(8px);
}

/* Fin and the balloon are bottom-aligned, so his head sits a fixed distance up
   from the balloon's bottom edge. The tail is pinned to that same distance and
   the beads trail back down toward him. */
.fin-guide-panel::after {
  bottom: 70px;
  left: -12px;
  border-right: 0;
  border-bottom: 0;
  border-top: 2.5px solid var(--balloon-ink);
  border-left: 2.5px solid var(--balloon-ink);
  border-bottom-right-radius: 0;
  border-top-left-radius: 4px;
  transform: rotate(-45deg) skew(5deg, 5deg);
}

.fin-guide-panel::before {
  bottom: 44px;
  left: -32px;
  transform: scaleX(-1);
}

.fin-guide-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
}

.fin-guide-name {
  color: var(--cyan);
  font-size: 0.7rem;
  font-weight: 900;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.fin-guide-kind {
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 750;
}

.fin-guide-close {
  min-width: 30px;
  min-height: 30px;
  margin-left: auto;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.045);
  color: var(--muted);
  font: inherit;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.fin-guide-close:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.fin-guide-panel { min-height: 118px; }
.fin-guide-body { color: #f4f0ff; font-size: 1.08rem; font-weight: 500; line-height: 1.42; }
.fin-guide-body p { margin: 0; }
.fin-guide-body strong { color: #fff; }

.fin-guide-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 11px;
}

/* Deliberately smaller than a page `.button` - this is a suggestion beside the
   page, never a competitor to a plan's own call to action. */
.fin-guide-action {
  display: inline-flex;
  min-height: 36px;
  align-items: center;
  padding: 6px 12px;
  border: 1px solid rgba(139, 92, 246, 0.5);
  border-radius: 10px;
  background: rgba(139, 92, 246, 0.14);
  color: #e6dcff;
  font: inherit;
  font-size: 0.84rem;
  font-weight: 800;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 180ms ease, background-color 180ms ease, transform 180ms ease;
}

.fin-guide-action:hover {
  border-color: var(--cyan);
  background: rgba(57, 215, 238, 0.16);
  color: #fff;
  transform: translateY(-1px);
}

/* The top-three board. Player identity is rendered by the shared rider-card
   module, never hand-built here, so a player's saved avatar, accent and title
   look the same in the dock as they do on the Arcade leaderboard. */
.fin-guide-riders {
  display: grid;
  gap: 7px;
  margin-top: 10px;
  padding: 0;
  list-style: none;
}

.fin-guide-riders li {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: rgba(8, 11, 24, 0.6);
}

.fin-guide-rank {
  flex: 0 0 auto;
  width: 22px;
  color: var(--cyan);
  font-size: 0.78rem;
  font-weight: 900;
  text-align: center;
}

.fin-guide-riders li:first-child .fin-guide-rank { color: #ffd479; }
.fin-guide-riders .rider-identity { min-width: 0; flex: 1 1 auto; }

/* Fin rotates on his own, and an auto-updating live region would interrupt a
   screen reader every time he did. The visible panel is therefore silent, and
   only a message the visitor asked for is announced through this region. */
.fin-guide-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

/* The balloon's bottom rail. The dots keep their place on the left and any
   docked tool sits hard right, on the balloon's own baseline - so the Arcade
   music button reads as part of what Fin is offering instead of as a second
   floating control competing with him for the same corner. */
.fin-guide-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* The spacing above the rail belongs to whichever child is actually showing,
   so a phone - which hides the dots - and a page with no docked tool both get
   a balloon that ends where its text ends. */
.fin-guide-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 11px 0 0 auto;
}

.fin-guide-tools:empty { display: none; }

.fin-guide-dots {
  display: flex;
  gap: 5px;
  margin-top: 11px;
}

/* Step through Fin's tips by hand. The arrows sit either side of the dots and
   stay visible on a phone, where the dots themselves are hidden. */
.fin-guide-nav {
  display: flex;
  align-items: center;
  gap: 8px;
}

.fin-guide-nav[hidden] { display: none; }

.fin-guide-nav .fin-guide-dots { margin-top: 0; }

.fin-guide-arrow {
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  margin-top: 11px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: rgba(8, 11, 24, 0.6);
  color: var(--cyan);
  font: inherit;
  font-size: 1.05rem;
  font-weight: 900;
  line-height: 1;
  cursor: pointer;
  transition: border-color 180ms ease, background-color 180ms ease, transform 180ms ease;
}

.fin-guide-arrow:hover,
.fin-guide-arrow:focus-visible {
  border-color: var(--cyan);
  background: rgba(57, 215, 238, 0.16);
  color: #fff;
  transform: translateY(-1px);
}

.fin-guide-nav .fin-guide-arrow + .fin-guide-dots,
.fin-guide-nav .fin-guide-dots + .fin-guide-arrow { margin-top: 11px; }

.fin-guide-dots span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(185, 161, 255, 0.28);
  transition: background-color 240ms ease, transform 240ms ease;
}

.fin-guide-dots span.is-current {
  background: var(--cyan);
  transform: scale(1.5);
}

/* Each new message arrives rather than swapping in place, which is what makes
   the dock read as Fin saying something instead of as text refreshing. */
.fin-guide-panel.is-entering {
  animation: fin-guide-in 460ms cubic-bezier(0.22, 1.3, 0.4, 1) both;
}

@keyframes fin-guide-in {
  from { opacity: 0; transform: translate3d(10px, 6px, 0) scale(0.96); }
  to { opacity: 1; transform: none; }
}

/* Closed for the session: Fin stays reachable as a small tab rather than
   disappearing with no way back. */
.fin-guide-reopen {
  position: fixed;
  right: clamp(12px, 2vw, 26px);
  bottom: clamp(12px, 2vw, 26px);
  z-index: 40;
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  gap: 9px;
  padding: 8px 15px 8px 10px;
  border: 1px solid rgba(185, 161, 255, 0.4);
  border-radius: 999px;
  background: rgba(18, 15, 36, 0.94);
  color: #e8e2ff;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 800;
  cursor: pointer;
  box-shadow: 0 16px 40px rgba(4, 2, 16, 0.6);
}

.fin-guide-reopen[hidden] { display: none; }
.fin-guide-reopen:hover { border-color: var(--cyan); }

.fin-guide-reopen img {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
}

/* With a form inside, the balloon can outgrow a short window. The scroll lives
   on an inner wrapper rather than on the panel itself, because the panel is
   what carries the tail and its beads - clipping there would cut the point off
   the balloon. */
.fin-guide-scroll {
  max-height: min(72vh, 640px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* --------------------------------------------------------------------------
   Fin as the support desk

   On support.html only, the same balloon becomes a conversation: a scrolling
   log and a composer instead of one rotating tip. Everything here is scoped to
   `.is-console`, which the dock adds only on that surface - the tip carousel on
   every other page is untouched by all of it.

   The balloon is wider and taller in this mode because a support conversation
   is not a one-line tip, and it stays inside `.fin-guide-scroll` so the tail
   and its beads are never clipped.
   -------------------------------------------------------------------------- */
.fin-guide-panel.is-console {
  min-height: 260px;
}

.fin-guide.has-console { width: min(560px, calc(100% - 24px)); }

.fin-support-status {
  margin: 0 0 10px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 750;
  letter-spacing: 0.04em;
}

/* The tier Fin is actually answering from, said in colour as well as words so
   "an assistant is online" and "queued" are never mistaken for each other. */
.fin-support-status.is-live { color: var(--cyan); }
.fin-support-status.is-live::before,
.fin-support-status.is-queued::before,
.fin-support-status.is-known::before {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 7px;
  border-radius: 50%;
  vertical-align: baseline;
  background: currentColor;
}

.fin-support-log {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-height: min(46vh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: 4px;
}

.fin-support-line { display: flex; flex-direction: column; gap: 3px; }

.fin-support-line p {
  margin: 0;
  /* Somebody pasting a stack trace or a licence error must not blow the
     balloon out sideways. */
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.fin-support-who {
  color: var(--muted);
  font-size: 0.66rem;
  font-weight: 900;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* The visitor's own words sit apart from everything said to them, so a long
   conversation stays readable without avatars or alignment tricks. */
.fin-support-line.is-customer {
  padding-left: 11px;
  border-left: 2px solid rgba(139, 92, 246, 0.55);
}
.fin-support-line.is-customer .fin-support-who { color: #c4b5fd; }
.fin-support-line.is-assistant .fin-support-who,
.fin-support-line.is-operator .fin-support-who,
.fin-support-line.is-discord .fin-support-who { color: var(--cyan); }
.fin-support-line.is-system p { color: var(--muted); font-size: 0.9rem; }

.fin-support-composer,
.fin-support-contact {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
}

.fin-support-composer textarea,
.fin-support-contact input {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(12, 10, 22, 0.55);
  color: var(--text);
  font: inherit;
  font-size: 0.98rem;
  resize: vertical;
}

.fin-support-composer textarea::placeholder,
.fin-support-contact input::placeholder { color: var(--muted); }

.fin-support-composer textarea:focus-visible,
.fin-support-contact input:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 1px;
  border-color: transparent;
}

.fin-guide-action.is-primary {
  align-self: flex-start;
  border-color: rgba(34, 211, 238, 0.55);
  background: rgba(34, 211, 238, 0.16);
  color: #d8fbff;
}

.fin-guide-action.is-primary[disabled] { opacity: 0.55; cursor: default; }

/* The honeypot. Off-screen rather than `display: none`, because a form control
   that is display-none is one a bot knows to skip. */
.fin-support-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.fin-support-exit { margin-top: 12px; }

@media (max-width: 620px) {
  .fin-guide-panel.is-console { min-height: 0; }
  .fin-support-log { max-height: 40vh; }
}

@media (max-width: 620px) {
  .fin-guide {
    right: 10px;
    left: 10px;
    bottom: 10px;
    width: auto;
  }

  /* A phone has no spare vertical room, so the dots go and the balloon keeps
     only what it is actually saying - and whatever tool is docked in it. */
  .fin-guide-dots { display: none; }

  .fin-guide-mark { width: 74px; height: 74px; }
  .fin-guide-body { font-size: 0.95rem; }
  .fin-guide-panel::after { bottom: 44px; }
  .fin-guide-panel::before { bottom: 22px; }
}


/* --------------------------------------------------------------------------
   Narrow screens

   Only overrides of selectors this file owns. Anything page-specific - the
   home page hero mark, its greeting balloon - stays in that page's stylesheet.
   -------------------------------------------------------------------------- */
@media (max-width: 860px) {
  .aurora-field { height: 620px; }

  /* Narrow screens have no spare width, so Fin gives some back: he shrinks,
     and because the sprite is shorter the top of his head moves down with it.
     Both numbers change together or the balloon lands on him. */
  .fin-mark-sprite { height: 68%; }
  .fin-mark-glow { width: 66%; }
  .fin-mark { --fin-crown: 33.6%; }
}

/* --------------------------------------------------------------------------
   Touch devices

   Cost reduction, not a redesign: the same effects in the same places, drawn
   in a way a phone can afford while it is also compositing a document the
   reader is scrolling.

   One rule accounts for most of it. `filter: blur()` is applied after the
   transform, so a blurred layer that is being scaled has to be re-rasterised
   every frame at its new size - and these are the largest layers on the page.
   Translating a blurred layer costs nothing, because the compositor just moves
   the texture it already has. On touch the drifting plates therefore translate
   and hold their size, which is the part of the motion that actually reads.
   -------------------------------------------------------------------------- */
@media (hover: none) and (pointer: coarse) {
  /* Blur cost climbs with the radius, and the plates are already smaller here
     (.aurora-field is 620px tall below 860px), so a smaller radius reads the
     same. */
  .aurora-field span { filter: blur(40px); }

  .aurora-field span:nth-child(1) { animation-name: aurora-a-drift; }
  .aurora-field span:nth-child(2) { animation-name: aurora-b-drift; }
  .aurora-field span:nth-child(3) { animation-name: aurora-c-drift; }

  /* The orbiting conic repaints the ring and its 18px blurred copy every
     frame, once per panel - and the purchasing page carries seventeen of them.
     A static brand-lit edge is already this effect's designed behaviour where
     @property is unsupported, so touch takes that same state deliberately. */
  .nebula-ring,
  .nebula-ring::after { animation: none; }
}

/* Translate-only counterparts of the three drift cycles. Same offsets, same
   durations, same easing - only the scale component is gone. */
@keyframes aurora-a-drift {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50% { transform: translate3d(7vw, 5vh, 0); }
}

@keyframes aurora-b-drift {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50% { transform: translate3d(-6vw, 7vh, 0); }
}

@keyframes aurora-c-drift {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50% { transform: translate3d(4vw, -6vh, 0); }
}

/* --------------------------------------------------------------------------
   Reduced motion

   Every effect in this file is decoration over content that is already in its
   final position, so freezing all of it removes movement without removing
   anything a visitor needs to read or operate.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .aurora-field span,
  .button::before,
  .fin-guide-panel.is-entering,
  .nebula-ring,
  .nebula-ring::after {
    animation: none !important;
  }

  .nebula-canvas { display: none; }
  .fin-mark-glow { animation: none; opacity: 0.8; transform: none; }

  [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
