/* ─────────────────────────────────────────────────────────────────────
   ConferenceApp · Blockchain Education 2026
   Author: Viktor Georgiev
   ───────────────────────────────────────────────────────────────────── */
/* ============================================================
   globalEffects.css — the global ambient layer of the site.

   1) #ambient-fx — the background. Several kinds, all of them the same DOM
      (two layers plus the cursor glow) with a different drawing; which kind is
      chosen by ONE attribute:

        <div id="ambient-fx" data-gfx-bg="grid"> … </div>

      So the admin panel stores one string per page and nothing else has to
      change — neither the CSS nor the markup. The list of valid values lives
      in Services/Styles/AmbientBackgrounds.

   2) #scroll-progress — the thin progress bar along the top.

   Entirely CSS: there is no canvas and no render loop for the background. The
   script (globalEffects.js) writes three variables only — --gfx-cx / --gfx-cy
   for the cursor, --gfx-progress for the scroll, and --gfx-shield for the band
   a banner occupies — and the CSS turns them into transforms, translations and
   masks. Only transform and opacity are animated.

   Settings, overridable anywhere — in a page's Styles section, say, without
   touching this file:

     html[data-gfx="off"]      → the whole layer is not rendered
     --gfx-intensity: 0.6      → the same layer, quieter
     --gfx-ink / --gfx-glow    → the alphas of the lines and of the glow
     --gfx-grid / --gfx-step   → the pitch of the grids

   The banner shield is an attribute: put data-gfx-shield on a section that has
   effects of its own (a banner, a hero) and the layer is cut away above it.
   ============================================================ */

:root {
    /* The overall strength. 0 makes the layer invisible; every alpha below
       goes through it. */
    --gfx-intensity: 1;

    /* The ink of the drawing (white) and the strength of the accent glow. */
    --gfx-ink: 0.05;
    --gfx-glow: 0.13;
    --gfx-cursor-alpha: 0.05;

    /* The two pitches: the large grid and the fine graph paper. */
    --gfx-grid: 48px;
    --gfx-step: 8px;

    /* The progress bar */
    --gfx-bar-height: 2px;
}

/* ── 1. The ambient layer ────────────────────────────────────
   WHY it sits above the content (z-index: 1) rather than behind it: several
   pages paint an opaque background of their OWN across the whole wrapper
   (.faq-page, auth, travel, profile, submit, payment — all of them
   background: var(--bg)), so a layer underneath them is physically invisible
   exactly there. The layer is purely additive (mix-blend-mode:
   var(--blend-ambient, screen) — it only lightens), takes no clicks, and stays
   below .topbar (20), below the mobile menu (25) and below the cookie banner.

   The mask cuts away the band a banner currently occupies (--gfx-shield), with
   a soft transition below it. On a page without a banner both are 0. */
#ambient-fx {
    position: fixed;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    overflow: hidden;
    contain: strict;
    opacity: var(--gfx-intensity);
    -webkit-mask-image: linear-gradient(to bottom,
        transparent 0,
        transparent var(--gfx-shield, 0px),
        #000 calc(var(--gfx-shield, 0px) + var(--gfx-shield-fade, 0px)));
    mask-image: linear-gradient(to bottom,
        transparent 0,
        transparent var(--gfx-shield, 0px),
        #000 calc(var(--gfx-shield, 0px) + var(--gfx-shield-fade, 0px)));
}

/* An explicit opt-out, for a page, a tool, or an admin-like view. */
html[data-gfx="off"] #ambient-fx,
html[data-gfx="off"] #scroll-progress,
#ambient-fx[data-gfx-bg="off"] {
    display: none;
}

/* The two drawing layers. Keeping transform (the animations) and translate
   (the scroll parallax) apart is deliberate: the slow drift and the parallax
   then do not fight over one property. */
#ambient-fx > span {
    position: absolute;
    inset: -2px;
    display: block;
    mix-blend-mode: var(--blend-ambient, screen);
    /* A compositor layer of its own; otherwise the browser computes the
       blend on the CPU on every scroll. Not through translateZ(0), because a
       rule with an #id selector would then beat the transform of
       .afx-cursor. */
    backface-visibility: hidden;
    will-change: transform;
}

.afx-a { translate: 0 calc(var(--gfx-progress, 0) * -3vh); }
.afx-b { translate: 0 calc(var(--gfx-progress, 0) * -7vh); }

/* The shared mask of the drawn kinds: solid at the top, a long middle, a soft
   fade downwards. Four stops rather than two, which is noticeably smoother
   than an abrupt "gone by 60%". */
.afx-a,
.afx-b {
    -webkit-mask-image: radial-gradient(130% 108% at 50% -4%,
        #000 0%, rgba(0, 0, 0, 0.72) 34%, rgba(0, 0, 0, 0.3) 62%, rgba(0, 0, 0, 0.08) 84%, transparent 100%);
    mask-image: radial-gradient(130% 108% at 50% -4%,
        #000 0%, rgba(0, 0, 0, 0.72) 34%, rgba(0, 0, 0, 0.3) 62%, rgba(0, 0, 0, 0.08) 84%, transparent 100%);
}

/* ── Kind 1: grid — the technical grid ───────────────────────
   Static: no movement means no shimmer on 1px lines at subpixel positions,
   including on phones at 1.5x and 2.75x device pixel ratios. */
#ambient-fx[data-gfx-bg="grid"] .afx-a {
    background:
        linear-gradient(rgba(255, 255, 255, var(--gfx-ink)) 1px, transparent 1px) 0 0 / 100% var(--gfx-grid),
        linear-gradient(90deg, rgba(255, 255, 255, var(--gfx-ink)) 1px, transparent 1px) 0 0 / var(--gfx-grid) 100%;
}

/* ── Kind 2: paper — graph paper ─────────────────────────────
   A fine pitch with every sixth line heavier. It reads as a drawing sheet
   rather than as a table. */
#ambient-fx[data-gfx-bg="paper"] .afx-a {
    background:
        linear-gradient(rgba(255, 255, 255, calc(var(--gfx-ink) * 0.45)) 1px, transparent 1px) 0 0 / 100% var(--gfx-step),
        linear-gradient(90deg, rgba(255, 255, 255, calc(var(--gfx-ink) * 0.45)) 1px, transparent 1px) 0 0 / var(--gfx-step) 100%;
}

#ambient-fx[data-gfx-bg="paper"] .afx-b {
    background:
        linear-gradient(rgba(255, 255, 255, var(--gfx-ink)) 1px, transparent 1px) 0 0 / 100% calc(var(--gfx-step) * 6),
        linear-gradient(90deg, rgba(255, 255, 255, var(--gfx-ink)) 1px, transparent 1px) 0 0 / calc(var(--gfx-step) * 6) 100%;
}

/* ── Kind 3: hatch — diagonal hatching ───────────────────────
   The drift runs along the strokes themselves, 135s per cycle: the drawing
   moves without the lines crossing the eye. The layer is wider than the screen
   so that no edge shows while it moves. */
#ambient-fx[data-gfx-bg="hatch"] .afx-a {
    inset: -30% -30%;
    background: repeating-linear-gradient(45deg,
        rgba(255, 255, 255, var(--gfx-ink)) 0 1px,
        transparent 1px 9px);
    animation: afxSlide 135s linear infinite;
}

#ambient-fx[data-gfx-bg="hatch"] .afx-b {
    inset: -30% -30%;
    background: repeating-linear-gradient(45deg,
        rgba(255, 54, 54, calc(var(--gfx-ink) * 0.6)) 0 1px,
        transparent 1px 54px);
    animation: afxSlide 210s linear infinite reverse;
}

/* ── Kind 4: glow — the accent glow ──────────────────────────
   The gradient has many stops along a Gaussian curve, and all of them are
   rgba(255,54,54,…) down to 0. Two things remove the visible circle: few stops
   mean an abrupt change in the slope, and the eye sees the slope rather than
   the value; and the keyword `transparent` is rgba(0,0,0,0), which on a red
   spot interpolates through black and draws an edge. */
#ambient-fx[data-gfx-bg="glow"] .afx-a {
    inset: auto;
    top: -32vh;
    left: 50%;
    width: 124vmax;
    height: 124vmax;
    margin-left: -62vmax;
    background: radial-gradient(circle closest-side,
        rgba(255, 54, 54, var(--gfx-glow)) 0%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.94)) 8%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.8)) 16%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.63)) 24%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.46)) 32%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.32)) 40%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.21)) 48%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.13)) 56%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.075)) 64%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.04)) 72%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.018)) 80%,
        rgba(255, 54, 54, calc(var(--gfx-glow) * 0.006)) 88%,
        rgba(255, 54, 54, 0) 100%);
    -webkit-mask-image: none;
    mask-image: none;
    animation: afxGlowDrift 68s ease-in-out infinite, afxBreathe 78s ease-in-out infinite;
    will-change: transform, opacity;
}

/* ── Kind 5: contour — contour lines ─────────────────────────
   Topographic rings from a point off screen. The scale breathes very slowly
   (96s), so the lines flow outwards like contours on a map rather than
   rotating. */
#ambient-fx[data-gfx-bg="contour"] .afx-a {
    background: repeating-radial-gradient(circle at 18% -8%,
        transparent 0 52px,
        rgba(255, 255, 255, var(--gfx-ink)) 52px 53px);
    animation: afxSwell 96s ease-in-out infinite;
}

#ambient-fx[data-gfx-bg="contour"] .afx-b {
    background: repeating-radial-gradient(circle at 88% 108%,
        transparent 0 74px,
        rgba(255, 54, 54, calc(var(--gfx-ink) * 0.8)) 74px 75px);
    animation: afxSwell 132s ease-in-out infinite reverse;
}

/* ── The cursor, for every kind ──────────────────────────────
   One soft accent spot centred under the pointer. Three things make it work:
   a low alpha, many stops for a soft edge, and slower chasing in the script —
   the spot lags slightly behind the mouse, which makes it part of the
   background rather than a cursor. */
.afx-cursor {
    inset: auto;
    top: 0;
    left: 0;
    width: 38vmax;
    height: 38vmax;
    margin: -19vmax 0 0 -19vmax;
    background: radial-gradient(circle closest-side,
        rgba(255, 54, 54, var(--gfx-cursor-alpha)) 0%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.92)) 10%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.74)) 20%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.54)) 30%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.36)) 40%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.22)) 50%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.12)) 60%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.055)) 70%,
        rgba(255, 54, 54, calc(var(--gfx-cursor-alpha) * 0.02)) 82%,
        rgba(255, 54, 54, 0) 100%);
    transform: translate3d(var(--gfx-cx, -9999px), var(--gfx-cy, -9999px), 0);
    opacity: var(--gfx-cursor, 0);
    transition: opacity 520ms ease;
    will-change: transform, opacity;
}

@keyframes afxSlide {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(18%, 0, 0); }
}

@keyframes afxSwell {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.16); }
}

@keyframes afxGlowDrift {
    0%   { transform: translate3d(-4vw, 0, 0); }
    50%  { transform: translate3d(4vw, 2.5vh, 0); }
    100% { transform: translate3d(-4vw, 0, 0); }
}

@keyframes afxBreathe {
    0%, 100% { opacity: 0.82; }
    50%      { opacity: 1; }
}

/* afxTurn — a slow sway of the orbits.
   IMPORTANT: the orbit layers carry a static transform: rotate(…). An animation
   on transform REPLACES it entirely, so the keyframes contain the whole angle
   rather than only the delta. The values are ±3deg around the base. */
@keyframes afxTurn {
    0%, 100% { transform: rotate(6deg); }
    50%      { transform: rotate(12deg); }
}


/* A browser without mask-image (older Safari and Firefox builds) would draw
   the pattern at full height with no fade. The opacity is halved instead:
   quieter, but not broken. */
@supports not ((mask-image: linear-gradient(#000, #000)) or (-webkit-mask-image: linear-gradient(#000, #000))) {
    #ambient-fx {
        opacity: calc(0.5 * var(--gfx-intensity));
    }
}

/* ── 2. The progress bar ─────────────────────────────────────
   Above the top bar (20) and below the mobile menu (25), so that an open menu
   does not get a red line across its overlay.

   It is not a solid line: a gradient that starts almost transparent and
   thickens to the right, with a thin glow at the head. The value is chased
   smoothly in the script, which is what makes the movement a glide. */
#scroll-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--gfx-bar-height);
    z-index: 30;
    pointer-events: none;
    transition: opacity 240ms ease;
}

#scroll-progress > span {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--accent) 12%, transparent) 0%,
        color-mix(in srgb, var(--accent) 45%, transparent) 45%,
        var(--accent, #FF3636) 100%);
    transform: scaleX(var(--gfx-progress, 0));
    transform-origin: 0 50%;
    will-change: transform;
}

/* The head of the bar. The scaleX on the parent would stretch it, so the
   scale is compensated back out: it stays a spot rather than a smeared
   line. */
#scroll-progress > span::after {
    content: "";
    position: absolute;
    top: 50%;
    right: 0;
    width: 42px;
    height: 6px;
    transform: translateY(-50%) scaleX(calc(1 / max(var(--gfx-progress, 0.001), 0.02)));
    transform-origin: 100% 50%;
    background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 60%, transparent), transparent 100%);
    opacity: calc(var(--gfx-progress, 0) * 1.4);
}

/* The script sets hidden when the page has nothing to scroll: an empty bar on
   a short page is not a state worth showing. */
#scroll-progress[hidden] {
    display: none;
}

body.mobile-nav-open #scroll-progress {
    opacity: 0;
}

/* ── Accessibility, weaker devices, print ────────────────────
   Under prefers-reduced-motion EVERY movement stops: the drift, the breathing,
   the scroll parallax and the cursor glow. The drawing stays — it is static,
   and removing it would cost the page its background for no reason.

   The bar is an indicator rather than decoration and keeps working, only
   without the smooth chase (see globalEffects.js). */
@media (prefers-reduced-motion: reduce) {
    #ambient-fx > span {
        animation: none !important;
        translate: none !important;
    }

    .afx-cursor {
        display: none;
    }
}

/* Phones and tablets: there is no cursor glow, because there is no cursor,
   and the graph paper is coarsened — an 8px pitch on a 2.75x screen merges
   into a flat grey. */
@media (hover: none), (max-width: 768px) {
    .afx-cursor { display: none; }
    :root { --gfx-step: 12px; --gfx-grid: 40px; }
}

@media print {
    #ambient-fx,
    #scroll-progress {
        display: none;
    }
}


/* ── The motions of the custom backgrounds ─────────────────────────────
   These keyframes existed only in adminPanelStyles.css, that is, only in the
   panel's preview. The public site did not have them, so a custom background
   with a motion was simply static, with no error anywhere. They are here as
   well now. A change has to be made in both places. */
@keyframes gfx-c-slide {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(18%, 0, 0); }
}

@keyframes gfx-c-swell {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.16); }
}

@keyframes gfx-c-drift {
    0%   { transform: translate3d(-3%, 0, 0); }
    50%  { transform: translate3d(3%, 2%, 0); }
    100% { transform: translate3d(-3%, 0, 0); }
}

/* The same reduced-motion rule as for the built-in backgrounds. */
@media (prefers-reduced-motion: reduce) {
    #ambient-fx .afx-a,
    #ambient-fx .afx-b { animation: none !important; }
}

[data-gfx-bg="fiber"] .afx-a {
    inset: -30% -30%;
    background:
        repeating-linear-gradient(82deg, transparent 0 20px, rgba(255,255,255,calc(var(--gfx-ink) * 0.4)) 21px, rgba(255,255,255,calc(var(--gfx-ink) * 1.2)) 22px, rgba(255,255,255,calc(var(--gfx-ink) * 0.4)) 23px, transparent 24px 43px),
        repeating-linear-gradient(83.5deg, transparent 0 30px, rgba(255,255,255,calc(var(--gfx-ink) * 0.3)) 31px, rgba(255,255,255,calc(var(--gfx-ink) * 0.8)) 32px, rgba(255,255,255,calc(var(--gfx-ink) * 0.3)) 33px, transparent 34px 61px),
        repeating-linear-gradient(81deg, transparent 0 44px, rgba(255,255,255,calc(var(--gfx-ink) * 0.5)) 46px, transparent 48px 79px);
    animation: afxSlide 420s linear infinite;
}
[data-gfx-bg="fiber"] .afx-b {
    inset: -30% -30%;
    background:
        repeating-linear-gradient(82.6deg, transparent 0 60px, rgba(255,54,54,calc(var(--gfx-glow) * 0.1)) 62px, rgba(255,54,54,calc(var(--gfx-glow) * 0.28)) 64px, rgba(255,54,54,calc(var(--gfx-glow) * 0.1)) 66px, transparent 68px 103px),
        repeating-linear-gradient(80.4deg, transparent 0 70px, rgba(255,255,255,calc(var(--gfx-ink) * 0.35)) 72px, transparent 74px 151px);
    animation: afxSlide 610s linear infinite reverse;
}

/* ══════════════════════════════════════════════════════════════════════
   CLIPPED BACKGROUNDS WHEN A LAYER IS ROTATED OR ENLARGED
   ══════════════════════════════════════════════════════════════════════
   The mask on .afx-a / .afx-b fades to TRANSPARENT at the edge of the layer
   itself. While the layer matches the screen that is exactly what is wanted:
   the background fades downwards.

   A preset that enlarges the layer (inset: -30%) and then ROTATES it, however,
   brings that fading edge into the screen at an angle: a diagonal line appears
   beyond which the background simply ends. The same happens to a heavily
   enlarged layer without rotation — the mask stretches and its solid top
   covers the whole visible screen, so the fade disappears.

   The fix: a layer that is enlarged or rotated gets a mask of its OWN,
   computed against the SCREEN (vmax) rather than against itself. Its edge then
   always stays outside the visible area, whatever the angle.  */

#ambient-fx [class*="afx-"][style],
[data-gfx-bg="hatch"] .afx-a,
[data-gfx-bg="hatch"] .afx-b,
[data-gfx-bg="fiber"] .afx-a,
[data-gfx-bg="fiber"] .afx-b {
    -webkit-mask-image: radial-gradient(190vmax 150vmax at 50% -8vmax,
        #000 0%, rgba(0,0,0,0.72) 34%, rgba(0,0,0,0.3) 62%,
        rgba(0,0,0,0.08) 84%, transparent 100%);
    mask-image: radial-gradient(190vmax 150vmax at 50% -8vmax,
        #000 0%, rgba(0,0,0,0.72) 34%, rgba(0,0,0,0.3) 62%,
        rgba(0,0,0,0.08) 84%, transparent 100%);
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

/* The rotated ones also get more margin: at 13° a 1920×1080 rectangle needs
   at least 19% above and below to avoid an empty corner. -30% was enough on
   paper but left nothing for the mask; it now has double the reserve. */
/* ══════════════════════════════════════════════════════════════════════
   THE INTERACTIVE PRESETS
   ══════════════════════════════════════════════════════════════════════
   These do not draw a pattern that sits still: they draw relative to the
   person. They read --gfx-cx / --gfx-cy (the cursor) and --gfx-progress (the
   scroll).

   THAT IS WHY THE MOTION SETTING IS LOCKED FOR THEM (see the block below):
   they compute their position in screen coordinates, while a motion moves the
   whole layer. With both active the drawing shifts and the cursor does not —
   the torch stops being under the mouse. Their movement comes from the hand
   and from the scroll, which is better than any animation.
   ══════════════════════════════════════════════════════════════════════ */

/* ── lantern — a torch over a drawing ─────────────────────────
   The gesture: the drawing is visible only where you are looking.

   A fine grid sits under the layer, and the mask shows it in a circle around
   the cursor. The effect is a magnifier over graph paper: the page looks empty
   until the mouse moves.

   The cross in the second layer is an instrument's gesture — it says "this is
   a tool", not "this is decoration". */
#ambient-fx[data-gfx-bg="lantern"] .afx-a {
    background:
        linear-gradient(rgba(255,255,255,calc(var(--gfx-ink) * 1.4)) 1px, transparent 1px) 0 0 / 100% 13px,
        linear-gradient(90deg, rgba(255,255,255,calc(var(--gfx-ink) * 1.4)) 1px, transparent 1px) 0 0 / 13px 100%,
        linear-gradient(rgba(255,255,255,calc(var(--gfx-ink) * 2.2)) 1px, transparent 1px) 0 0 / 100% 65px,
        linear-gradient(90deg, rgba(255,255,255,calc(var(--gfx-ink) * 2.2)) 1px, transparent 1px) 0 0 / 65px 100%;
    -webkit-mask-image: radial-gradient(circle 300px at var(--gfx-cx, 32vw) var(--gfx-cy, 26vh),
        #000 0 22%, rgba(0,0,0,0.72) 44%, rgba(0,0,0,0.34) 66%, rgba(0,0,0,0.1) 84%, transparent 100%);
    mask-image: radial-gradient(circle 300px at var(--gfx-cx, 32vw) var(--gfx-cy, 26vh),
        #000 0 22%, rgba(0,0,0,0.72) 44%, rgba(0,0,0,0.34) 66%, rgba(0,0,0,0.1) 84%, transparent 100%);
}

#ambient-fx[data-gfx-bg="lantern"] .afx-b {
    /* A cross centred on the cursor: two 1px bands whose position is the
       variable itself. No animation — the movement is the hand. */
    background:
        linear-gradient(90deg, transparent 0, rgba(255,54,54,calc(var(--gfx-glow) * 0.34)) 50%, transparent 100%) calc(var(--gfx-cx, 32vw) - 90px) 0 / 180px 100% no-repeat,
        linear-gradient(180deg, transparent 0, rgba(255,54,54,calc(var(--gfx-glow) * 0.34)) 50%, transparent 100%) 0 calc(var(--gfx-cy, 26vh) - 90px) / 100% 180px no-repeat;
}

/* ── prism — a fan from one point ─────────────────────────────
   The gesture: the rays come out of wherever the cursor is.

   repeating-conic-gradient is the one gradient that repeats by ANGLE rather
   than by distance: the spacing between the rays grows with the distance from
   the centre, so the pattern has no visible pitch. Nothing else in the system
   uses a conic gradient, which is exactly why this looks different.

   It differs from the dropped "beam" in that the rays are not parallel bands
   but diverge from a point — and that point is under your hand. */
#ambient-fx[data-gfx-bg="prism"] .afx-a {
    background: repeating-conic-gradient(from 8deg at var(--gfx-cx, 22vw) var(--gfx-cy, 14vh),
        rgba(255,255,255,calc(var(--gfx-ink) * 1.1)) 0 0.35deg,
        rgba(255,255,255,calc(var(--gfx-ink) * 0.4)) 0.7deg,
        transparent 1.1deg 4.6deg);
}

#ambient-fx[data-gfx-bg="prism"] .afx-b {
    background: repeating-conic-gradient(from 2deg at var(--gfx-cx, 22vw) var(--gfx-cy, 14vh),
        rgba(255,54,54,calc(var(--gfx-glow) * 0.22)) 0 0.8deg,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.06)) 1.6deg,
        transparent 2.6deg 19deg);
    animation: afxBreathe 160s ease-in-out infinite;
}

/* ── sonar — rings under the hand ─────────────────────────────
   The gesture: rings spread out from the cursor.

   The spacing is 58px in the first layer and 79px in the second — coprime, so
   the two never line up into one image. The scale breathes over 190s, so the
   rings stay alive even with the mouse still.

   It differs from contour in that the centre is the person. */
#ambient-fx[data-gfx-bg="sonar"] .afx-a {
    background: repeating-radial-gradient(circle at var(--gfx-cx, 50%) var(--gfx-cy, 28vh),
        transparent 0 52px,
        rgba(255,255,255,calc(var(--gfx-ink) * 0.45)) 55px,
        rgba(255,255,255,calc(var(--gfx-ink) * 1.4)) 57px,
        rgba(255,255,255,calc(var(--gfx-ink) * 0.45)) 59px,
        transparent 62px 58px);
    animation: afxSwell 190s ease-in-out infinite;
}

#ambient-fx[data-gfx-bg="sonar"] .afx-b {
    background: repeating-radial-gradient(circle at var(--gfx-cx, 50%) var(--gfx-cy, 28vh),
        transparent 0 72px,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.14)) 75px,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.38)) 77px,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.14)) 79px,
        transparent 82px 79px);
    animation: afxSwell 310s ease-in-out infinite reverse;
}

/* ── caustic — interference under the hand ────────────────────
   The gesture: two waves meet and you move one of them.

   The first layer is rings at the cursor, the second rings from a fixed point
   off screen. What is visible is the beat between them: every movement of the
   mouse rearranges the nodes across the whole height of the screen. This is
   the effect that looks least like CSS.

   The pitches are 43 and 61px — primes, so the beat is wide. */
#ambient-fx[data-gfx-bg="caustic"] .afx-a {
    background: repeating-radial-gradient(circle at var(--gfx-cx, 62vw) var(--gfx-cy, 30vh),
        transparent 0 38px,
        rgba(255,255,255,calc(var(--gfx-ink) * 0.5)) 40px,
        rgba(255,255,255,calc(var(--gfx-ink) * 1.5)) 41.5px,
        rgba(255,255,255,calc(var(--gfx-ink) * 0.5)) 43px,
        transparent 45px 43px);
}

#ambient-fx[data-gfx-bg="caustic"] .afx-b {
    background: repeating-radial-gradient(circle at 8% 106%,
        transparent 0 55px,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.12)) 57px,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.3)) 58.5px,
        rgba(255,54,54,calc(var(--gfx-glow) * 0.12)) 60px,
        transparent 62px 61px);
    animation: afxSwell 340s ease-in-out infinite;
}

/* ── spine — a column of light ────────────────────────────────
   The gesture: a column of light stands wherever the cursor is.

   It is 460px wide with soft ends, so it does not read as a line but as a lit
   band of the page — as though there were a lamp behind the content that you
   move. The second layer adds the column's horizontal sister, much quieter:
   together they make a cross of light without drawing a cross.

   Static. The movement is your hand. */
#ambient-fx[data-gfx-bg="spine"] .afx-a {
    background:
        linear-gradient(90deg,
            transparent 0%,
            rgba(255,255,255,calc(var(--gfx-ink) * 0.3)) 22%,
            rgba(255,255,255,calc(var(--gfx-ink) * 1.3)) 50%,
            rgba(255,255,255,calc(var(--gfx-ink) * 0.3)) 78%,
            transparent 100%) calc(var(--gfx-cx, 46vw) - 230px) 0 / 460px 100% no-repeat,
        linear-gradient(90deg, transparent 0, rgba(255,255,255,calc(var(--gfx-ink) * 2)) 50%, transparent 100%) calc(var(--gfx-cx, 46vw) - 1px) 0 / 2px 100% no-repeat;
}

#ambient-fx[data-gfx-bg="spine"] .afx-b {
    background:
        linear-gradient(180deg,
            transparent 0%,
            rgba(255,54,54,calc(var(--gfx-glow) * 0.1)) 26%,
            rgba(255,54,54,calc(var(--gfx-glow) * 0.3)) 50%,
            rgba(255,54,54,calc(var(--gfx-glow) * 0.1)) 74%,
            transparent 100%) 0 calc(var(--gfx-cy, 32vh) - 130px) / 100% 260px no-repeat;
}

/* ── ascent — a horizon that rises with the scroll ────────────
   The gesture: as you read, the horizon comes up.

   The position of the band is --gfx-progress (0..1) — the same variable that
   drives the progress bar. At the top of the page the band is at the bottom;
   at the end it is at the top. So the background is an INDICATOR: you feel how
   far you have come without looking at the bar.

   It is the only preset whose drawing changes with the scroll rather than
   merely being displaced (the parallax is separate and untouched). It also
   works "stopped": with no animation the band simply stays wherever the scroll
   has left it. */
#ambient-fx[data-gfx-bg="ascent"] .afx-a {
    background:
        linear-gradient(180deg,
            transparent 0%,
            rgba(255,255,255,calc(var(--gfx-ink) * 0.4)) 26%,
            rgba(255,255,255,calc(var(--gfx-ink) * 1.5)) 47%,
            rgba(255,255,255,calc(var(--gfx-ink) * 0.4)) 68%,
            transparent 100%) 0 calc(88vh - var(--gfx-progress, 0) * 128vh) / 100% 78vh no-repeat,
        linear-gradient(rgba(255,255,255,calc(var(--gfx-ink) * 0.5)) 1px, transparent 1px) 0 0 / 100% 47px;
}

#ambient-fx[data-gfx-bg="ascent"] .afx-b {
    /* The horizon line itself, plus a glow just beneath it. */
    background:
        linear-gradient(180deg, transparent 0, rgba(255,54,54,calc(var(--gfx-glow) * 0.45)) 48%, rgba(255,54,54,calc(var(--gfx-glow) * 0.45)) 52%, transparent 100%) 0 calc(88vh - var(--gfx-progress, 0) * 128vh + 37vh) / 100% 4px no-repeat,
        linear-gradient(180deg, rgba(255,54,54,calc(var(--gfx-glow) * 0.18)) 0%, transparent 100%) 0 calc(88vh - var(--gfx-progress, 0) * 128vh + 39vh) / 100% 22vh no-repeat;
}

/* ══════════════════════════════════════════════════════════════════════
   THE MOTION SETTING FROM THE ADMIN PANEL
   ══════════════════════════════════════════════════════════════════════

   Why the chosen motion "did not appear":
   The first version reused the presets' OWN animations (afxSlide and the
   rest). Their amplitudes, however, are calculated for cycles of 96 to 610
   seconds: afxGlowDrift moves 8vw over half a cycle, which at 180s is 1.28
   pixels a second. That is below the threshold of noticing — you choose
   "slide", nothing visible happens, and it looks broken.

   So the panel's choice uses animations of its OWN (afxU*), with an amplitude
   and a duration calculated for the purpose: an administrator who asks for
   movement explicitly should be able to see it.

   The presets keep using theirs; they were not touched.

   Two independent settings on #ambient-fx:
     data-gfx-motion : off | drift | slide | swell | breathe | turn
     data-gfx-speed  : slower | slow | fast | faster
   ══════════════════════════════════════════════════════════════════════ */

/* The base duration of a CHOSEN motion. 70s is the compromise: visible within
   a few seconds of looking, without drawing attention to itself. */
#ambient-fx[data-gfx-motion] { --gfx-u-dur: 70s; }

#ambient-fx[data-gfx-speed="slower"] { --gfx-u-dur: 200s; }
#ambient-fx[data-gfx-speed="slow"]   { --gfx-u-dur: 120s; }
#ambient-fx[data-gfx-speed="fast"]   { --gfx-u-dur: 40s;  }
#ambient-fx[data-gfx-speed="faster"] { --gfx-u-dur: 22s;  }

/* ── Stopped ────────────────────────────────────────────────────────── */
#ambient-fx[data-gfx-motion="off"] .afx-a,
#ambient-fx[data-gfx-motion="off"] .afx-b,
#ambient-fx[data-gfx-motion="off"] .afx-cursor {
    animation: none !important;
}

/* ── Common to every chosen kind ────────────────────────────────────────
   The second layer runs at 1.4× the duration; otherwise the two move in step
   and the background reads as one flat sheet. */
#ambient-fx[data-gfx-motion="drift"] .afx-a,
#ambient-fx[data-gfx-motion="drift"] .afx-b,
#ambient-fx[data-gfx-motion="slide"] .afx-a,
#ambient-fx[data-gfx-motion="slide"] .afx-b,
#ambient-fx[data-gfx-motion="swell"] .afx-a,
#ambient-fx[data-gfx-motion="swell"] .afx-b,
#ambient-fx[data-gfx-motion="breathe"] .afx-a,
#ambient-fx[data-gfx-motion="breathe"] .afx-b,
#ambient-fx[data-gfx-motion="turn"] .afx-a,
#ambient-fx[data-gfx-motion="turn"] .afx-b {
    animation-duration: var(--gfx-u-dur, 70s) !important;
    animation-iteration-count: infinite !important;
    animation-play-state: running !important;
}

#ambient-fx[data-gfx-motion="drift"] .afx-b,
#ambient-fx[data-gfx-motion="slide"] .afx-b,
#ambient-fx[data-gfx-motion="swell"] .afx-b,
#ambient-fx[data-gfx-motion="breathe"] .afx-b,
#ambient-fx[data-gfx-motion="turn"] .afx-b {
    animation-duration: calc(var(--gfx-u-dur, 70s) * 1.4) !important;
}

/* ── The kinds ──────────────────────────────────────────────────────── */
#ambient-fx[data-gfx-motion="drift"] .afx-a,
#ambient-fx[data-gfx-motion="drift"] .afx-b {
    animation-name: afxUDrift !important;
    animation-timing-function: ease-in-out !important;
}

#ambient-fx[data-gfx-motion="slide"] .afx-a,
#ambient-fx[data-gfx-motion="slide"] .afx-b {
    animation-name: afxUSlide !important;
    animation-timing-function: linear !important;
}

#ambient-fx[data-gfx-motion="swell"] .afx-a,
#ambient-fx[data-gfx-motion="swell"] .afx-b {
    animation-name: afxUSwell !important;
    animation-timing-function: ease-in-out !important;
}

#ambient-fx[data-gfx-motion="breathe"] .afx-a,
#ambient-fx[data-gfx-motion="breathe"] .afx-b {
    animation-name: afxUBreathe !important;
    animation-timing-function: ease-in-out !important;
}

#ambient-fx[data-gfx-motion="turn"] .afx-a,
#ambient-fx[data-gfx-motion="turn"] .afx-b {
    animation-name: afxUTurn !important;
    animation-timing-function: linear !important;
}

/* ── The animations of a CHOSEN motion ──────────────────────────────────
   The amplitudes are about three times those of the presets, because these run
   at 70s rather than 200 or more. The point is that the choice should be
   SEEN. */

@keyframes afxUDrift {
    0%   { transform: translate3d(-9vw, -2vh, 0); }
    50%  { transform: translate3d(9vw, 4vh, 0); }
    100% { transform: translate3d(-9vw, -2vh, 0); }
}

@keyframes afxUSlide {
    from { transform: translate3d(-14%, 0, 0); }
    to   { transform: translate3d(14%, 0, 0); }
}

@keyframes afxUSwell {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.42); }
}

/* The opacity runs from 0.28 to 1: against a background at alpha 0.05 a
   narrower range simply cannot be seen. */
@keyframes afxUBreathe {
    0%, 100% { opacity: 0.28; }
    50%      { opacity: 1; }
}

@keyframes afxUTurn {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* ── The margin a motion needs ──────────────────────────────────────────
   By default the layer matches the screen. Any movement — sliding, drifting,
   rotating — displaces it and exposes the edge: the drawing ends abruptly and
   a gap shows.

   A percentage margin (inset: -45%) does NOT solve rotation, because it grows
   proportionally while the need is quadratic: to cover a 1920×1080 screen at
   any angle the layer has to be at least 2203px in BOTH directions. -45% gives
   3648×2052 — wide enough, not tall enough. Hence the diagonal band.

   So: for the displacing motions, a percentage margin; for rotation, a SQUARE
   of 160vmax. The diagonal of any screen is under 1.42vmax, so 160vmax covers
   every aspect ratio with room to spare. */
#ambient-fx[data-gfx-motion="drift"] .afx-a,
#ambient-fx[data-gfx-motion="drift"] .afx-b,
#ambient-fx[data-gfx-motion="slide"] .afx-a,
#ambient-fx[data-gfx-motion="slide"] .afx-b {
    inset: -25% -25%;
}

#ambient-fx[data-gfx-motion="turn"] .afx-a,
#ambient-fx[data-gfx-motion="turn"] .afx-b {
    inset: auto;
    top: 50%;
    left: 50%;
    width: 160vmax;
    height: 160vmax;
    margin: -80vmax 0 0 -80vmax;
    /* An explicit centre: without it the rotation depends on the size of the
       content and the axis drifts when the preset changes. */
    transform-origin: 50% 50%;
    -webkit-mask-image: radial-gradient(190vmax 150vmax at 50% -8vmax,
        #000 0%, rgba(0,0,0,0.72) 34%, rgba(0,0,0,0.3) 62%,
        rgba(0,0,0,0.08) 84%, transparent 100%);
    mask-image: radial-gradient(190vmax 150vmax at 50% -8vmax,
        #000 0%, rgba(0,0,0,0.72) 34%, rgba(0,0,0,0.3) 62%,
        rgba(0,0,0,0.08) 84%, transparent 100%);
}

/* The system's reduced-motion setting overrides everything above. */
@media (prefers-reduced-motion: reduce) {
    #ambient-fx .afx-a,
    #ambient-fx .afx-b,
    #ambient-fx .afx-cursor { animation: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════
   LOCKING THE INTERACTIVE PRESETS
   ══════════════════════════════════════════════════════════════════════
   The interactive presets draw relative to the cursor and the scroll, in
   SCREEN coordinates. The motion setting moves the whole layer with a
   transform. With both in effect the drawing shifts while the cursor stays
   where it is: the torch is no longer under the mouse, the rays no longer
   start from it.

   So the motion choice does NOT apply to them. These rules come last in the
   file and beat every motion rule above.

   One exception: prism, sonar and caustic have a slow breathing of their own
   that does not move the coordinate system. That is kept — except when the
   administrator has explicitly chosen "stopped".                        */

#ambient-fx[data-gfx-motion] .afx-a,
#ambient-fx[data-gfx-motion] .afx-b { }   /* a specificity anchor */

#ambient-fx[data-gfx-bg="lantern"] .afx-a,
#ambient-fx[data-gfx-bg="lantern"] .afx-b,
#ambient-fx[data-gfx-bg="spine"] .afx-a,
#ambient-fx[data-gfx-bg="spine"] .afx-b,
#ambient-fx[data-gfx-bg="ascent"] .afx-a,
#ambient-fx[data-gfx-bg="ascent"] .afx-b {
    animation: none !important;
    inset: 0 !important;
    transform: none !important;
}

/* These three keep their own breathing but accept no other kind of motion,
   no other duration, and do not move. */
#ambient-fx[data-gfx-bg="prism"] .afx-a,
#ambient-fx[data-gfx-bg="prism"] .afx-b,
#ambient-fx[data-gfx-bg="sonar"] .afx-a,
#ambient-fx[data-gfx-bg="sonar"] .afx-b,
#ambient-fx[data-gfx-bg="caustic"] .afx-a,
#ambient-fx[data-gfx-bg="caustic"] .afx-b {
    inset: 0 !important;
}

#ambient-fx[data-gfx-bg="prism"] .afx-a { animation: none !important; }
#ambient-fx[data-gfx-bg="prism"] .afx-b { animation: afxBreathe 160s ease-in-out infinite !important; }

#ambient-fx[data-gfx-bg="sonar"] .afx-a { animation: afxSwell 190s ease-in-out infinite !important; }
#ambient-fx[data-gfx-bg="sonar"] .afx-b { animation: afxSwell 310s ease-in-out infinite reverse !important; }

#ambient-fx[data-gfx-bg="caustic"] .afx-a { animation: none !important; }
#ambient-fx[data-gfx-bg="caustic"] .afx-b { animation: afxSwell 340s ease-in-out infinite !important; }

/* "Stopped" applies to them too. It is the one setting that reaches them, and
   it breaks nothing: the drawing stays, only the breathing stops. */
#ambient-fx[data-gfx-motion="off"][data-gfx-bg="prism"] .afx-b,
#ambient-fx[data-gfx-motion="off"][data-gfx-bg="sonar"] .afx-a,
#ambient-fx[data-gfx-motion="off"][data-gfx-bg="sonar"] .afx-b,
#ambient-fx[data-gfx-motion="off"][data-gfx-bg="caustic"] .afx-b {
    animation: none !important;
}

/* The cursor presets need the coordinates even under reduced motion: without
   them the drawing freezes in its fallback position. So .afx-cursor stops
   while the drawing stays alive — it is not an animation, it follows the
   hand. */
@media (prefers-reduced-motion: reduce) {
    #ambient-fx .afx-a,
    #ambient-fx .afx-b,
    #ambient-fx .afx-cursor { animation: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════
   PHONES, AND DEVICES WITHOUT A MOUSE
   ══════════════════════════════════════════════════════════════════════
   Two separate reasons, hence two separate rules.

   1) PHONES — the layer is hidden BY DEFAULT.
      On a small screen the banner fills almost the whole view and the
      background shows only in the narrow bands between sections, so the gain
      is small. Against it stands a cost: a full-screen gradient recomputed on
      scroll, on a device running from a battery. An administrator can switch
      it on for a given page from the panel.

   2) NO MOUSE — the cursor backgrounds are not rendered at all.
      lantern, prism, sonar, caustic and spine draw relative to the cursor
      position. Without a mouse that position is the fallback value and stays
      there for ever — a torch nailed to a corner. That is not a weaker version
      of the effect, it is a broken one. Better nothing.                */

@media (max-width: 820px) {
    /* Off by default. The data-gfx-mobile="on" attribute is set from the
       panel and brings the layer back for one page. */
    #ambient-fx:not([data-gfx-mobile="on"]) { display: none; }
}

/* pointer: fine means a real mouse or a stylus. Everything else is a
   finger. */
@media (pointer: coarse), (hover: none) {
    #ambient-fx[data-gfx-bg="lantern"],
    #ambient-fx[data-gfx-bg="prism"],
    #ambient-fx[data-gfx-bg="sonar"],
    #ambient-fx[data-gfx-bg="caustic"],
    #ambient-fx[data-gfx-bg="spine"] { display: none; }

    /* The glow under the cursor makes no sense without one either. */
    #ambient-fx .afx-cursor { display: none; }
}
