/* ==========================================================================
   Trovewall — the MagicUI effects layer
   --------------------------------------------------------------------------
   MagicUI's components rebuilt in vanilla CSS. The originals are React +
   Framer Motion; almost all of them are really one clever gradient plus one
   keyframe, so they port cleanly and cost nothing at runtime.

   Where a component genuinely needs measurement or per-frame work — the
   cursor-following spotlight, the number ticker, the scroll reveal, the
   animated beam between two DOM nodes — the CSS here exposes custom
   properties and effects.js drives them.

   Every effect degrades to something static and legible: with JavaScript off
   the spotlight simply never lights, the ticker shows its final number, and
   revealed elements are visible from the start. Nothing here is load-bearing.

   Contents
     01  Animation primitives
     02  Border beam · shine border
     03  Magic card (cursor spotlight)
     04  Shimmer · rainbow · ripple buttons
     05  Backgrounds: dot · grid · retro grid · ripple · flicker · noise
     06  Glows and spotlights
     07  Meteors
     08  Orbiting circles
     09  Marquee
     10  Text: aurora · gradient · shiny · reveal · ticker · typing
     11  Animated beam (node to node)
     12  Scroll reveal (blur fade)
     13  Device mocks
   ========================================================================== */


/* 01  Animation primitives ------------------------------------------------
   Registered custom properties so gradients can be animated at all — an
   unregistered --angle is a string and does not interpolate. */

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

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

@property --gradient-shift {
    syntax: '<percentage>';
    initial-value: 0%;
    inherits: false;
}


/* 02  Border beam · shine border ------------------------------------------
   MagicUI's border-beam: a short arc of light travelling the perimeter. Built
   as a conic gradient masked down to the border ring, rotating forever.

   Defaults follow the original — #FFAA40 to #9C40FF over 6s — but Trovewall
   overrides them to cyan/violet through the custom properties. */

.beam {
    position: relative;
    isolation: isolate;
}

.beam::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    padding: var(--beam-width, 1px);
    background: conic-gradient(
        from var(--beam-angle),
        transparent 0deg,
        var(--beam-from, var(--brand-accent)) 20deg,
        var(--beam-to, var(--brand-violet)) 50deg,
        transparent 70deg
    );
    /* Show only the ring: fill minus 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: beam-spin var(--beam-duration, 6s) linear infinite;
    animation-delay: var(--beam-delay, 0s);
    pointer-events: none;
}

.beam--reverse::after { animation-direction: reverse; }
.beam--slow::after    { --beam-duration: 12s; }
.beam--thick::after   { --beam-width: 2px; }
.beam--amber::after   { --beam-from: var(--brand-amber); --beam-to: var(--brand-violet); }

/* Only light up when the reader is on it. */
.beam--hover::after { opacity: 0; transition: opacity var(--speed) var(--ease); }
.beam--hover:hover::after { opacity: 1; }

@keyframes beam-spin {
    to { --beam-angle: 360deg; }
}

/* Shine border: the whole ring glows and breathes rather than one arc
   travelling round it. Cheaper, calmer, good for a focused form. */
.shine-border {
    position: relative;
    isolation: isolate;
}
.shine-border::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    padding: 1px;
    background: linear-gradient(
        var(--shine-angle),
        transparent 20%,
        var(--brand-accent) 45%,
        var(--brand-violet) 55%,
        transparent 80%
    );
    -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: shine-rotate 8s linear infinite;
    pointer-events: none;
}
@keyframes shine-rotate {
    to { --shine-angle: 360deg; }
}


/* 03  Magic card ----------------------------------------------------------
   The spotlight follows the cursor and the border lights nearest to it.
   effects.js writes --mx / --my as pixel offsets within the card; with no
   JavaScript the two layers stay at opacity 0 and the card is just a card. */

.magic-card {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    --mx: 50%;
    --my: 50%;
    --spotlight-size: 320px;
}

/* The soft fill that follows the pointer. */
.magic-card::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background: radial-gradient(
        var(--spotlight-size) circle at var(--mx) var(--my),
        color-mix(in srgb, var(--brand-accent) 12%, transparent),
        transparent 70%
    );
    opacity: 0;
    transition: opacity var(--speed-slow) var(--ease);
    pointer-events: none;
}

/* The border segment nearest the pointer — MagicUI's #9E7AFF → #FE8BBB. */
.magic-card::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    padding: 1px;
    background: radial-gradient(
        260px circle at var(--mx) var(--my),
        var(--card-beam-from, var(--brand-violet-lt)),
        var(--card-beam-to, var(--brand-pink)) 40%,
        transparent 75%
    );
    -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;
    opacity: 0;
    transition: opacity var(--speed-slow) var(--ease);
    pointer-events: none;
}

.magic-card:hover::before { opacity: 1; }
.magic-card:hover::after  { opacity: 1; }
.magic-card > * { position: relative; z-index: 2; }

/* Orb variant: a tighter, brighter blob instead of a wash. */
.magic-card--orb::before {
    background: radial-gradient(
        140px circle at var(--mx) var(--my),
        color-mix(in srgb, var(--brand-accent) 30%, transparent),
        transparent 65%
    );
    filter: blur(14px);
}


/* 04  Buttons: shimmer · rainbow · ripple --------------------------------- */

/* Shimmer button — a spark travels the rim while the face stays solid.
   MagicUI's signature call to action.

   Built on the same masked conic ring as .beam rather than an oversized
   rotating square: the mask guarantees the spark is confined to the border
   whatever the button's size or radius, which the square approach gets wrong
   on wide buttons (the gradient's centre drifts off the visible rim). */
.btn--shimmer {
    position: relative;
    isolation: isolate;
    background: var(--surface-raised);
    border-color: var(--border-strong);
    color: var(--text);
}
.btn--shimmer::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    padding: 1.5px;
    background: conic-gradient(
        from var(--beam-angle),
        transparent 0deg,
        var(--brand-accent) 30deg,
        var(--brand-violet) 70deg,
        transparent 110deg
    );
    -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: beam-spin 3s linear infinite;
    pointer-events: none;
}
/* A highlight that sweeps the face, offset from the rim spark. */
.btn--shimmer::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.09) 50%, transparent 70%);
    background-size: 250% 100%;
    animation: shimmer-sweep 3s linear infinite;
    pointer-events: none;
}
.btn--shimmer > * { position: relative; z-index: 2; }
.btn--shimmer:hover { background: var(--surface-muted); border-color: var(--border-strong); }

@keyframes shimmer-sweep {
    from { background-position: 150% 0; }
    to   { background-position: -100% 0; }
}

/* Rainbow button — an animated multi-stop gradient with the same gradient
   blurred underneath as a glow. */
.btn--rainbow {
    position: relative;
    border: 0;
    color: #FFFFFF;
    background: linear-gradient(
        90deg,
        var(--brand-accent), var(--brand-violet), var(--brand-pink),
        var(--brand-amber), var(--brand-accent)
    );
    background-size: 300% 100%;
    animation: rainbow-pan 6s linear infinite;
    box-shadow: none;
}
.btn--rainbow::before {
    content: '';
    position: absolute;
    inset: -3px;
    z-index: -1;
    border-radius: inherit;
    background: inherit;
    background-size: 300% 100%;
    filter: blur(14px);
    opacity: 0.55;
    animation: rainbow-pan 6s linear infinite;
}
.btn--rainbow:hover { filter: brightness(1.08); }

@keyframes rainbow-pan {
    to { background-position: 300% 50%; }
}

/* Ripple button — expands a circle from the click point. effects.js sets
   --rx / --ry and toggles the class. */
.btn--ripple { position: relative; overflow: hidden; }
.btn--ripple .ripple-ink {
    position: absolute;
    left: var(--rx);
    top: var(--ry);
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.35;
    transform: translate(-50%, -50%) scale(0);
    animation: ripple-out 620ms var(--ease-out) forwards;
    pointer-events: none;
}
@keyframes ripple-out {
    to { transform: translate(-50%, -50%) scale(38); opacity: 0; }
}

/* Pulsating call to action for genuinely urgent states (a blocked payout). */
.btn--pulse { animation: btn-pulse 2.4s var(--ease) infinite; }
@keyframes btn-pulse {
    0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand-accent) 55%, transparent); }
    50%      { box-shadow: 0 0 0 10px transparent; }
}


/* 05  Backgrounds ---------------------------------------------------------
   All of these are decorative and absolutely positioned. Put one inside a
   .section or .hero with position:relative and it fills it. */

.bg-layer {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
}

/* Dot pattern — MagicUI's default 16px spacing, 1px radius. */
.bg-dots {
    background-image: radial-gradient(var(--dot-color, var(--border-strong)) 1px, transparent 1px);
    background-size: var(--dot-gap, 16px) var(--dot-gap, 16px);
    -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 0%, #000 40%, transparent 100%);
            mask-image: radial-gradient(ellipse 70% 60% at 50% 0%, #000 40%, transparent 100%);
}
.bg-dots--wide { --dot-gap: 28px; }
/* Inside a bento cell the pattern is decoration, not structure, so it is
   tinted and tightened rather than left at the hairline border colour. */
.bento__art .bg-dots {
    --dot-color: color-mix(in srgb, var(--brand-accent) 28%, transparent);
    --dot-gap: 18px;
}
.bento__art .bg-grid {
    --grid-color: color-mix(in srgb, var(--brand-violet) 26%, transparent);
}
.bg-dots--center {
    -webkit-mask-image: radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 100%);
            mask-image: radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 100%);
}

/* Grid pattern — hairline squares. */
.bg-grid {
    background-image:
        linear-gradient(to right, var(--grid-color, var(--border)) 1px, transparent 1px),
        linear-gradient(to bottom, var(--grid-color, var(--border)) 1px, transparent 1px);
    background-size: var(--grid-gap, 56px) var(--grid-gap, 56px);
    -webkit-mask-image: radial-gradient(ellipse 75% 65% at 50% 0%, #000 35%, transparent 100%);
            mask-image: radial-gradient(ellipse 75% 65% at 50% 0%, #000 35%, transparent 100%);
}
.bg-grid--small { --grid-gap: 28px; }
.bg-grid--center {
    -webkit-mask-image: radial-gradient(ellipse 60% 55% at 50% 50%, #000 25%, transparent 100%);
            mask-image: radial-gradient(ellipse 60% 55% at 50% 50%, #000 25%, transparent 100%);
}

/* The grid is anchored to the horizontal centre, not to the left edge, so a
   column line sits at a position a stylesheet can name at any viewport width:
   `50% - gap/2 + n·gap`. That is what lets `.spark--v` below ride one. The
   rows are anchored to the top, where they already were. */
.bg-grid { background-position: center top; }

/* Retro grid — the perspective floor rushing toward the horizon. */
.bg-retro {
    perspective: 240px;
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 55%, transparent);
            mask-image: linear-gradient(to bottom, transparent, #000 55%, transparent);
    opacity: 0.55;
}
.bg-retro__plane {
    position: absolute;
    inset: 0;
    transform: rotateX(62deg);
    transform-origin: 50% 100%;
}
.bg-retro__plane::before {
    content: '';
    position: absolute;
    inset: -120% 0 0 -50%;
    width: 200%;
    height: 320%;
    background-image:
        linear-gradient(to right, var(--brand-accent) 1px, transparent 0),
        linear-gradient(to bottom, var(--brand-accent) 1px, transparent 0);
    background-size: 60px 60px;
    opacity: 0.22;
    animation: retro-scroll 14s linear infinite;
}
@keyframes retro-scroll {
    to { transform: translateY(60px); }
}

/* Ripple — concentric rings breathing outward from a point. */
.bg-ripple {
    display: grid;
    place-items: center;
}
.bg-ripple__ring {
    position: absolute;
    border: 1px solid color-mix(in srgb, var(--brand-accent) 22%, transparent);
    border-radius: 50%;
    animation: ripple-breathe 5s var(--ease) infinite;
}
@keyframes ripple-breathe {
    0%   { transform: scale(0.85); opacity: 0.55; }
    100% { transform: scale(1.25); opacity: 0; }
}

/* Flickering grid — cells fade in and out at staggered intervals. The
   staggering comes from nth-child so no JavaScript is needed. */
.bg-flicker {
    display: grid;
    grid-template-columns: repeat(auto-fill, 26px);
    grid-auto-rows: 26px;
    gap: 2px;
    -webkit-mask-image: radial-gradient(ellipse 65% 60% at 50% 40%, #000 20%, transparent 100%);
            mask-image: radial-gradient(ellipse 65% 60% at 50% 40%, #000 20%, transparent 100%);
}
.bg-flicker span {
    background: var(--brand-accent);
    border-radius: 2px;
    opacity: 0;
    animation: flicker 4s steps(1, end) infinite;
}
.bg-flicker span:nth-child(3n)   { animation-delay: 0.4s; }
.bg-flicker span:nth-child(5n)   { animation-delay: 1.1s; }
.bg-flicker span:nth-child(7n)   { animation-delay: 1.9s; }
.bg-flicker span:nth-child(11n)  { animation-delay: 2.6s; }
.bg-flicker span:nth-child(13n)  { animation-delay: 3.3s; }
@keyframes flicker {
    0%, 92%  { opacity: 0; }
    94%, 98% { opacity: 0.22; }
    100%     { opacity: 0; }
}

/* Noise — a static film-grain overlay that keeps large flat areas from
   banding. An inline SVG turbulence, so no image request. */
.bg-noise {
    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'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}


/* 06  Glows and spotlights ------------------------------------------------ */

/* The hero wash. Three overlapping radial gradients painted straight onto a
   .bg-layer — no blur, no circle, and therefore no hard edge where the
   section clips them. Prefer this over stacking .glow elements in a section
   that has overflow:hidden. */
.bg-aurora {
    background:
        radial-gradient(1000px 520px at 50% -14%,
            color-mix(in srgb, var(--brand-accent) 17%, transparent), transparent 66%),
        radial-gradient(760px 460px at 10% 6%,
            color-mix(in srgb, var(--brand-violet) 15%, transparent), transparent 68%),
        radial-gradient(720px 420px at 92% 2%,
            color-mix(in srgb, var(--brand-pink) 9%, transparent), transparent 68%);
}


.glow {
    position: absolute;
    border-radius: 50%;
    filter: blur(90px);
    pointer-events: none;
    z-index: 0;
}
.glow--accent { background: color-mix(in srgb, var(--brand-accent) 32%, transparent); }
.glow--violet { background: color-mix(in srgb, var(--brand-violet) 34%, transparent); }
.glow--pink   { background: color-mix(in srgb, var(--brand-pink) 26%, transparent); }

.glow--tl { top: -180px; left: -140px;  width: 460px; height: 460px; }
.glow--tr { top: -200px; right: -120px; width: 520px; height: 520px; }
.glow--bl { bottom: -220px; left: -160px; width: 480px; height: 480px; }
.glow--br { bottom: -180px; right: -140px; width: 440px; height: 440px; }
.glow--top-center { top: -260px; left: 50%; transform: translateX(-50%); width: 720px; height: 520px; }

.glow--breathe { animation: glow-breathe 9s var(--ease) infinite; }
@keyframes glow-breathe {
    0%, 100% { opacity: 0.7;  transform: scale(1); }
    50%      { opacity: 1;    transform: scale(1.12); }
}
.glow--top-center.glow--breathe { animation-name: glow-breathe-centered; }
@keyframes glow-breathe-centered {
    0%, 100% { opacity: 0.7; transform: translateX(-50%) scale(1); }
    50%      { opacity: 1;   transform: translateX(-50%) scale(1.12); }
}

/* A thin luminous line under a sticky header or above a footer. */
.hairline-glow {
    height: 1px;
    background: linear-gradient(90deg,
        transparent,
        color-mix(in srgb, var(--brand-accent) 65%, transparent) 35%,
        color-mix(in srgb, var(--brand-violet) 65%, transparent) 65%,
        transparent);
}


/* 07  Grid sparks ---------------------------------------------------------
   Two or three points of light travelling along the lines of the grid behind
   them. They replaced a set of diagonal meteors, which looked wrong for a
   reason worth writing down: the streak was drawn horizontally and then
   rotated 215deg while the element itself translated down-left at 45deg, so
   the tail pointed somewhere the spark was not going. Nothing else on the
   page moves diagonally either — the grid does not — so the effect read as a
   glitch rather than as motion.

   A spark is not a dot that is moved. It is a hairline the width (or height)
   of the section, painted with a short gradient that is slid along it by
   `background-position`, which is why the head can leave one edge and enter
   the other without ever being positioned outside the box. `100%` in a
   background-position resolves against *area minus image*, so
   `calc(100% + var(--spark-length))` is exactly one spark-length past the far
   edge, at any width.

   Alignment is the whole point, so both directions are stated in grid units:
   `--spark-row` counts rows down from the top, `--spark-col` counts columns
   out from the centre (see the background-position note on `.bg-grid`). A
   container using these must carry the same `--grid-gap` as its grid — and
   the grid must not be animated, because a line that drifts is a line a
   spark cannot stay on. */

.sparks {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    --grid-gap: 56px;
    /* The same mask as .bg-grid, so a spark fades out exactly where the line
       it is riding does instead of running on into empty space. */
    -webkit-mask-image: radial-gradient(ellipse 75% 65% at 50% 0%, #000 35%, transparent 100%);
            mask-image: radial-gradient(ellipse 75% 65% at 50% 0%, #000 35%, transparent 100%);
}

.spark {
    position: absolute;
    background-repeat: no-repeat;
    --spark-length: 190px;
    --spark-duration: 9s;
    --spark-tint: var(--brand-accent);
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--spark-tint) 65%, transparent));
}

/* Along a row: full width, one pixel tall, head at the leading edge. */
.spark--h {
    left: 0;
    right: 0;
    height: 1px;
    top: calc(var(--spark-row, 2) * var(--grid-gap));
    background-image: linear-gradient(90deg,
        transparent 0,
        color-mix(in srgb, var(--spark-tint) 45%, transparent) 62%,
        var(--spark-tint) 92%,
        #fff 100%);
    background-size: var(--spark-length) 100%;
    animation: spark-h var(--spark-duration) linear infinite;
    animation-delay: var(--spark-delay, 0s);
}
@keyframes spark-h {
    from { background-position: calc(-1 * var(--spark-length)) 0; }
    to   { background-position: calc(100% + var(--spark-length)) 0; }
}

/* Along a column, counted out from the centre so it lands on a line at any width. */
.spark--v {
    top: 0;
    bottom: 0;
    width: 1px;
    left: calc(50% - (var(--grid-gap) / 2) + var(--spark-col, 0) * var(--grid-gap));
    background-image: linear-gradient(180deg,
        transparent 0,
        color-mix(in srgb, var(--spark-tint) 45%, transparent) 62%,
        var(--spark-tint) 92%,
        #fff 100%);
    background-size: 100% var(--spark-length);
    animation: spark-v var(--spark-duration) linear infinite;
    animation-delay: var(--spark-delay, 0s);
}
@keyframes spark-v {
    from { background-position: 0 calc(-1 * var(--spark-length)); }
    to   { background-position: 0 calc(100% + var(--spark-length)); }
}

.spark--violet { --spark-tint: var(--brand-violet); }


/* 08  Orbiting circles ----------------------------------------------------
   Icons circling a centre point — used on the integrations panel. Each child
   sets --orbit-radius, --orbit-duration and --orbit-delay inline. */

.orbit {
    position: relative;
    display: grid;
    place-items: center;
    aspect-ratio: 1;
}
.orbit__ring {
    position: absolute;
    border: 1px solid var(--border);
    border-radius: 50%;
    width: calc(var(--orbit-radius) * 2);
    height: calc(var(--orbit-radius) * 2);
}
.orbit__item {
    position: absolute;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    margin: calc(var(--orbit-radius) * -1) 0 0 -20px;
    border-radius: 50%;
    background: var(--surface-raised);
    border: 1px solid var(--border);
    box-shadow: var(--edge-light);
    transform-origin: 20px calc(var(--orbit-radius) + 20px);
    animation: orbit-spin var(--orbit-duration, 20s) linear infinite;
    animation-delay: var(--orbit-delay, 0s);
}
.orbit__item--reverse { animation-direction: reverse; }
/* Counter-rotate the contents so the icon itself stays upright. */
.orbit__item > * {
    animation: orbit-spin var(--orbit-duration, 20s) linear infinite reverse;
    animation-delay: var(--orbit-delay, 0s);
}
.orbit__item--reverse > * { animation-direction: normal; }

@keyframes orbit-spin {
    to { transform: rotate(360deg); }
}

.orbit__centre {
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    border-radius: var(--radius);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    box-shadow: var(--edge-light), var(--glow-accent);
}


/* 09  Marquee -------------------------------------------------------------
   Two identical tracks side by side; when the first has scrolled its whole
   width the second is exactly where it started, so the loop is seamless.
   The markup must therefore render the track twice. */

.marquee {
    display: flex;
    overflow: hidden;
    gap: var(--marquee-gap, var(--sp-4));
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.marquee__track {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: var(--marquee-gap, var(--sp-4));
    min-width: 100%;
    animation: marquee-scroll var(--marquee-duration, 42s) linear infinite;
}
.marquee--reverse .marquee__track { animation-direction: reverse; }
.marquee:hover .marquee__track { animation-play-state: paused; }

@keyframes marquee-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(calc(-100% - var(--marquee-gap, 1rem))); }
}

/* Vertical variant for testimonial columns. */
.marquee--vertical { flex-direction: column; height: var(--marquee-height, 420px);
    -webkit-mask-image: linear-gradient(180deg, transparent, #000 10%, #000 90%, transparent);
            mask-image: linear-gradient(180deg, transparent, #000 10%, #000 90%, transparent);
}
.marquee--vertical .marquee__track {
    flex-direction: column;
    min-width: 0;
    min-height: 100%;
    animation-name: marquee-scroll-y;
}
@keyframes marquee-scroll-y {
    from { transform: translateY(0); }
    to   { transform: translateY(calc(-100% - var(--marquee-gap, 1rem))); }
}

.marquee__item {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-5);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface-glass);
    box-shadow: var(--edge-light);
    white-space: nowrap;
    font-size: var(--text-md);
}


/* 10  Text effects -------------------------------------------------------- */

/* Aurora text — a slowly shifting multi-colour wash clipped to the glyphs.

   inline-block matters: on a wrapped inline element each line box gets its own
   slice of the gradient, so a two-line heading ends up violet on one line and
   pink on the next. As a block the ramp runs left-to-right across the whole
   element and every line reads the same way. */
.aurora-text {
    display: inline-block;
    background-image: linear-gradient(
        110deg,
        var(--brand-accent) 0%,
        var(--brand-violet-lt) 26%,
        var(--brand-pink) 44%,
        var(--brand-violet-lt) 64%,
        var(--brand-accent) 88%,
        var(--brand-violet-lt) 100%
    );
    background-size: 220% 100%;
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    animation: aurora-pan 8s linear infinite;
}
@keyframes aurora-pan {
    to { background-position: 220% 50%; }
}

/* Static gradient text for headlines that should not move. */
.gradient-text {
    background-image: linear-gradient(100deg, var(--brand-accent), var(--brand-violet-lt) 55%, var(--brand-pink));
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
}

/* Where a heading is mostly plain with one gradient phrase. */
.gradient-text--soft {
    background-image: linear-gradient(180deg, var(--text), color-mix(in srgb, var(--text) 55%, transparent));
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
}

/* Animated shiny text — a highlight sweeps across muted text. MagicUI uses
   this for the small announcement pill above a hero. */
.shiny-text {
    color: var(--text-muted);
    background: linear-gradient(
        110deg,
        transparent 35%,
        var(--text) 50%,
        transparent 65%
    ) no-repeat;
    background-size: 250% 100%;
    background-position: 150% 0;
    -webkit-background-clip: text;
            background-clip: text;
    animation: shiny-sweep 4.5s linear infinite;
}
@keyframes shiny-sweep {
    to { background-position: -50% 0; }
}

/* The announcement pill itself: gradient hairline border, shiny label. */
.announce {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-1);
    border-radius: var(--radius-full);
    background: var(--surface-glass);
    border: 1px solid var(--border);
    box-shadow: var(--edge-light);
    font-size: var(--text-sm);
    font-weight: 500;
    text-decoration: none;
    transition: border-color var(--speed) var(--ease), transform var(--speed) var(--ease);
}
.announce:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.announce__tag {
    padding: 2px var(--sp-2);
    border-radius: var(--radius-full);
    background: linear-gradient(100deg, var(--brand-accent), var(--brand-violet));
    color: #04202A;
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.announce svg { color: var(--text-faint); }

/* Number ticker — effects.js counts up on first view. The pre-rendered final
   value stays in the DOM so it is correct without JavaScript. */
.ticker { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum'; }

/* Typing animation — a monospace line that types itself, with a caret. */
.typing { display: inline-block; white-space: pre; }
.typing::after {
    content: '';
    display: inline-block;
    width: 0.6ch;
    height: 1em;
    margin-left: 1px;
    vertical-align: text-bottom;
    background: var(--brand-accent);
    animation: caret-blink 1.06s steps(1, end) infinite;
}
@keyframes caret-blink {
    0%, 49%   { opacity: 1; }
    50%, 100% { opacity: 0; }
}

/* Word rotate — a stack of words, one visible at a time. */
.word-rotate {
    display: inline-grid;
    vertical-align: bottom;
    overflow: hidden;
}
.word-rotate > span {
    grid-area: 1 / 1;
    opacity: 0;
    transform: translateY(0.6em);
    animation: word-cycle calc(var(--word-count, 3) * 2.4s) var(--ease-out) infinite;
}
@keyframes word-cycle {
    0%, 4%    { opacity: 0; transform: translateY(0.6em); }
    8%, 28%   { opacity: 1; transform: translateY(0); }
    33%, 100% { opacity: 0; transform: translateY(-0.6em); }
}

/* Text reveal — words fade up in sequence as the block scrolls into view.
   effects.js adds .is-revealed; the per-word delay is set inline. */
.text-reveal__word {
    display: inline-block;
    opacity: 0.18;
    transition: opacity 420ms var(--ease), filter 420ms var(--ease);
    filter: blur(2px);
}
.is-revealed .text-reveal__word {
    opacity: 1;
    filter: blur(0);
}


/* 11  Animated beam -------------------------------------------------------
   Connects two nodes with a curved line and runs a pulse of light along it.
   effects.js measures the nodes and writes the SVG path; this styles it.
   Used on the front page to draw the click → conversion → payout flow. */

.beam-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    overflow: visible;
}
.beam-canvas__path {
    fill: none;
    stroke: var(--border-strong);
    stroke-width: 1.5;
    stroke-linecap: round;
}
.beam-canvas__pulse {
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-dasharray: 40 460;
    animation: beam-travel var(--beam-travel-duration, 3.2s) linear infinite;
    animation-delay: var(--beam-travel-delay, 0s);
}
@keyframes beam-travel {
    from { stroke-dashoffset: 500; }
    to   { stroke-dashoffset: 0; }
}

/* The nodes the beams connect. */
.beam-node {
    position: relative;
    z-index: 2;
    display: grid;
    place-items: center;
    gap: var(--sp-2);
    width: 78px;
    height: 78px;
    border-radius: var(--radius);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    box-shadow: var(--edge-light), var(--shadow);
    color: var(--text-muted);
}
.beam-node--accent { color: var(--brand-accent); border-color: color-mix(in srgb, var(--brand-accent) 35%, transparent); }
.beam-node--violet { color: var(--brand-violet-lt); border-color: color-mix(in srgb, var(--brand-violet) 35%, transparent); }
.beam-node__label {
    position: absolute;
    top: calc(100% + var(--sp-2));
    left: 50%;
    transform: translateX(-50%);
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-muted);
    white-space: nowrap;
}

.beam-stage {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-6);
    padding: var(--sp-12) var(--sp-8) var(--sp-16);
    min-height: 260px;
}
@media (max-width: 780px) {
    .beam-stage { flex-direction: column; gap: var(--sp-12); }
    .beam-canvas { display: none; }
}


/* 12  Scroll reveal (blur fade) -------------------------------------------
   MagicUI's blur-fade. effects.js adds .is-visible when the element enters
   the viewport. Elements are only hidden once JavaScript has confirmed it is
   running (html.js), so with it off nothing is invisible. */

.js .reveal {
    opacity: 0;
    transform: translateY(14px);
    filter: blur(6px);
    transition: opacity 620ms var(--ease-out),
                transform 620ms var(--ease-out),
                filter 620ms var(--ease-out);
    transition-delay: var(--reveal-delay, 0ms);
}
.js .reveal.is-visible {
    opacity: 1;
    transform: none;
    filter: blur(0);
}

.js .reveal--left  { transform: translateX(-18px); }
.js .reveal--right { transform: translateX(18px); }
.js .reveal--scale { transform: scale(0.96); }
.js .reveal--left.is-visible,
.js .reveal--right.is-visible,
.js .reveal--scale.is-visible { transform: none; }

@media (prefers-reduced-motion: reduce) {
    .js .reveal { opacity: 1; transform: none; filter: none; }
    /* A spark is motion and nothing else — stopped, it is a smear of light
       sitting on a line, so it is removed rather than paused. */
    .sparks { display: none; }
}


/* 13  Device mocks --------------------------------------------------------
   A phone shell for showing the offerwall as a publisher's user sees it. */

.phone {
    position: relative;
    width: 300px;
    /* Fixed width, so it needs centring in whatever column it lands in — that
       was an inline margin-inline on the one place it is used. */
    margin-inline: auto;
    padding: 10px;
    border-radius: 42px;
    background: linear-gradient(160deg, var(--surface-muted), var(--surface-inset));
    border: 1px solid var(--border-strong);
    box-shadow: var(--shadow-xl), var(--edge-light);
}
.phone::before {
    content: '';
    position: absolute;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    width: 96px;
    height: 20px;
    border-radius: var(--radius-full);
    background: var(--bg-sunken);
    z-index: 3;
}
.phone__screen {
    position: relative;
    border-radius: 32px;
    overflow: hidden;
    background: #FFFFFF;
    aspect-ratio: 9 / 19;
}
.phone__screen iframe { width: 100%; height: 100%; border: 0; display: block; }

.browser {
    border-radius: var(--radius-lg);
    overflow: hidden;
    border: 1px solid var(--border);
    background: var(--surface);
    box-shadow: var(--edge-light), var(--shadow-lg);
}
.browser__bar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-glass);
    border-bottom: 1px solid var(--border);
}
.browser__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border-strong); }
.browser__url {
    flex: 1;
    margin-left: var(--sp-2);
    padding: var(--sp-1) var(--sp-3);
    border-radius: var(--radius-full);
    background: var(--surface-inset);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
