/* ============================================
   Riviant — Effects Lab (/CSS)
   Every demo is calm at rest and animates on
   hover (or tap — JS toggles .fx-on for touch).
   ============================================ */

.fx-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
    gap: 28px;
}

/* --- Card shell --- */
.fx-card {
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    transition: box-shadow var(--transition-base), transform var(--transition-base), border-color var(--transition-base);
}

.fx-card:hover,
.fx-card.fx-on {
    box-shadow: var(--shadow-elevated);
    border-color: var(--ink-300);
    transform: translateY(-3px);
}

.fx-stage {
    position: relative;
    height: 230px;
    display: grid;
    place-items: center;
    background: var(--ink-050);
    border-bottom: 1px solid var(--border-subtle);
    padding: 24px;
    overflow: hidden;
}

.fx-stage--dark { background: var(--ink-900); }
.fx-stage--white { background: #ffffff; }
.fx-stage--clip { isolation: isolate; }
.fx-stage--flush { padding: 0; }

.fx-info { padding: 20px 22px 22px; }

.fx-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}

.fx-title-row h3 {
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.01em;
}

.fx-tag {
    flex: none;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-600);
    background: var(--ink-100);
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    padding: 3px 10px;
    white-space: nowrap;
}

.fx-tag--js {
    color: var(--accent-blue);
    background: rgba(var(--accent-blue-rgb), 0.08);
    border-color: rgba(var(--accent-blue-rgb), 0.25);
}

.fx-info p {
    font-size: 0.9rem;
    color: var(--text-secondary);
    line-height: 1.55;
}

.fx-footnote {
    text-align: center;
    color: var(--text-muted);
    font-size: 0.9rem;
    margin-top: 40px;
}

/* ============================================
   Admin reordering — badge on each card plus a
   sticky save bar. Invisible to visitors.
   ============================================ */
.fx-admin-note {
    background: rgba(var(--accent-blue-rgb), 0.06);
    border: 1px solid rgba(var(--accent-blue-rgb), 0.25);
    border-radius: var(--radius-md);
    padding: 14px 18px;
    margin-bottom: 24px;
    font-size: 0.9rem;
    color: var(--text-secondary);
    line-height: 1.55;
}

.fx-admin-note strong { color: var(--text-primary); }

.fx-order-demo {
    display: inline-block;
    background: var(--ink-900);
    color: var(--ink-000);
    border-radius: 4px;
    padding: 0 5px;
    font-weight: 700;
    font-size: 0.8em;
}

.fx-saved-flash {
    background: rgba(var(--accent-blue-rgb), 0.1);
    border: 1px solid rgba(var(--accent-blue-rgb), 0.35);
    color: var(--accent-blue);
    border-radius: var(--radius-md);
    padding: 12px 18px;
    margin-bottom: 24px;
    font-size: 0.9rem;
    font-weight: 600;
}

.fx-order {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 5;
    display: inline-flex;
    align-items: center;
    gap: 2px;
    background: rgba(23, 23, 23, 0.9);
    color: var(--ink-000);
    border-radius: 999px;
    padding: 3px 4px 3px 10px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
    cursor: text;
}

.fx-order > span {
    font-size: 0.78rem;
    font-weight: 700;
    opacity: 0.6;
}

.fx-order input {
    width: 40px;
    border: 0;
    background: transparent;
    color: var(--ink-000);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 700;
    text-align: center;
    padding: 2px 0;
    border-radius: 999px;
}

.fx-order input:focus {
    outline: 0;
    background: rgba(255, 255, 255, 0.16);
}

/* The card is position:relative already via .fx-card's overflow context;
   make it explicit so the badge anchors to the card, not the page. */
.fx-card { position: relative; }

.fx-savebar {
    position: sticky;
    bottom: 20px;
    z-index: 20;
    margin: 26px auto 0;
    width: max-content;
    display: flex;
    align-items: center;
    gap: 16px;
    background: var(--ink-900);
    color: var(--ink-000);
    border-radius: 999px;
    padding: 10px 12px 10px 22px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

.fx-savebar-text {
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.fx-savebar.fx-dirty .fx-savebar-text::after {
    content: ' — unsaved changes';
    color: #FCD34D;
}

/* ============================================
   1. 3D Flip Card
   ============================================ */
.fx-flip {
    width: 200px;
    height: 150px;
    perspective: 900px;
}

.fx-flip-inner {
    position: relative;
    width: 100%;
    height: 100%;
    transform-style: preserve-3d;
    transition: transform 0.7s cubic-bezier(0.3, 0.05, 0.2, 1);
}

.fx-card:hover .fx-flip-inner,
.fx-card.fx-on .fx-flip-inner {
    transform: rotateY(180deg);
}

.fx-flip-face {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    border-radius: var(--radius-md);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 16px;
    text-align: center;
}

.fx-flip-front {
    background: var(--ink-900);
    color: var(--ink-000);
}

.fx-flip-mark {
    width: 42px;
    height: 42px;
    border: 1.5px solid var(--ink-500);
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-weight: 800;
    font-size: 1.1rem;
}

.fx-flip-hint {
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-400);
}

.fx-flip-back {
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    transform: rotateY(180deg);
}

.fx-flip-word {
    font-size: 1.3rem;
    font-weight: 800;
    color: var(--accent-blue);
}

.fx-flip-sub {
    font-size: 0.78rem;
    color: var(--text-secondary);
    line-height: 1.4;
}

/* ============================================
   2. Cursor Spotlight  (JS feeds --fx-x/--fx-y)
   ============================================ */
/* Registering these makes the touch-drift animation interpolate smoothly.
   Unregistered custom properties animate in discrete jumps. */
@property --fx-x {
    syntax: '<length-percentage>';
    initial-value: 50%;
    inherits: false;
}

@property --fx-y {
    syntax: '<length-percentage>';
    initial-value: 40%;
    inherits: false;
}

.fx-spot {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    background: radial-gradient(
        220px circle at var(--fx-x, 50%) var(--fx-y, 40%),
        rgba(var(--accent-blue-rgb), 0.35),
        rgba(var(--accent-blue-rgb), 0.08) 45%,
        transparent 70%
    );
    cursor: none;
    /* A touch of lag makes the glow trail the cursor instead of snapping,
       and softens its return to centre when the pointer leaves. */
    transition: --fx-x 0.18s ease-out, --fx-y 0.18s ease-out;
}

.fx-spot span {
    color: var(--ink-300);
    font-size: 0.9rem;
    letter-spacing: 0.04em;
    pointer-events: none;
}

/* A phone has no cursor to follow, so the light drifts on its own —
   the effect still sells itself. Touching it takes manual control
   (JS adds .fx-spot--held while a finger is down). */
@keyframes fx-spot-drift {
    0%   { --fx-x: 26%; --fx-y: 30%; }
    25%  { --fx-x: 74%; --fx-y: 36%; }
    50%  { --fx-x: 66%; --fx-y: 72%; }
    75%  { --fx-x: 30%; --fx-y: 66%; }
    100% { --fx-x: 26%; --fx-y: 30%; }
}

.fx-spot--held { animation: none !important; }

/* ============================================
   3. Shimmer Text
   ============================================ */
.fx-shimmer {
    font-size: clamp(2rem, 5vw, 2.6rem);
    font-weight: 900;
    letter-spacing: 0.04em;
    color: transparent;
    background: linear-gradient(
        110deg,
        var(--ink-900) 42%,
        var(--accent-blue) 50%,
        var(--ink-900) 58%
    );
    background-size: 250% 100%;
    background-position: 100% 0;
    -webkit-background-clip: text;
    background-clip: text;
}

.fx-card:hover .fx-shimmer,
.fx-card.fx-on .fx-shimmer {
    animation: fx-shimmer-sweep 1.6s linear infinite;
}

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

/* ============================================
   4. Animated Gradient Border
   ============================================ */
@property --fx-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
}

.fx-ring {
    width: 220px;
    border-radius: var(--radius-md);
    padding: 2px; /* the visible border thickness */
    background: conic-gradient(
        from var(--fx-angle),
        transparent 0%,
        rgba(var(--accent-blue-rgb), 0.9) 12%,
        transparent 28%
    ), var(--ink-200);
}

.fx-card:hover .fx-ring,
.fx-card.fx-on .fx-ring {
    animation: fx-ring-spin 2.4s linear infinite;
}

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

.fx-ring-inner {
    background: var(--bg-primary);
    border-radius: calc(var(--radius-md) - 2px);
    padding: 26px 20px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    text-align: center;
}

.fx-ring-title {
    font-weight: 800;
    font-size: 1rem;
    letter-spacing: 0.02em;
}

.fx-ring-sub {
    font-size: 0.78rem;
    color: var(--text-secondary);
    line-height: 1.45;
}

/* ============================================
   5. Frosted Glass
   ============================================ */
.fx-blob {
    position: absolute;
    width: 150px;
    height: 150px;
    border-radius: 50%;
    filter: blur(6px);
    z-index: -1;
    transition: transform 1.2s ease;
}

.fx-blob--a {
    background: rgba(var(--accent-blue-rgb), 0.75);
    top: 12%;
    left: 16%;
}

.fx-blob--b {
    background: var(--ink-800);
    bottom: 8%;
    right: 14%;
}

.fx-card:hover .fx-blob--a,
.fx-card.fx-on .fx-blob--a { transform: translate(34px, 22px) scale(1.15); }

.fx-card:hover .fx-blob--b,
.fx-card.fx-on .fx-blob--b { transform: translate(-30px, -18px) scale(1.1); }

.fx-glass {
    position: relative;
    background: rgba(255, 255, 255, 0.5);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    border: 1px solid rgba(255, 255, 255, 0.65);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
    padding: 26px 28px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    text-align: center;
}

.fx-glass-title {
    font-weight: 800;
    font-size: 1.05rem;
}

.fx-glass-sub {
    font-size: 0.8rem;
    color: var(--ink-700);
}

/* ============================================
   6. Scroll Reveal  (JS toggles .fx-play)
   ============================================ */
.fx-reveal {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    max-width: 250px;
}

.fx-reveal-item {
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-sm);
    padding: 10px 14px;
    font-size: 0.82rem;
    color: var(--text-secondary);
    opacity: 0;
    transform: translateY(14px);
}

.fx-reveal.fx-play .fx-reveal-item {
    animation: fx-rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.fx-reveal.fx-play .fx-reveal-item:nth-child(1) { animation-delay: 0.05s; }
.fx-reveal.fx-play .fx-reveal-item:nth-child(2) { animation-delay: 0.25s; }
.fx-reveal.fx-play .fx-reveal-item:nth-child(3) { animation-delay: 0.45s; }

@keyframes fx-rise {
    to { opacity: 1; transform: translateY(0); }
}

.fx-replay {
    align-self: center;
    margin-top: 4px;
    border: 1px solid var(--border-color);
    background: var(--bg-primary);
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    border-radius: 999px;
    padding: 5px 14px;
    cursor: pointer;
    transition: border-color var(--transition-fast), color var(--transition-fast);
}

.fx-replay:hover {
    border-color: var(--accent-blue);
    color: var(--accent-blue);
}

/* ============================================
   7. Scroll-Snap Gallery
   ============================================ */
.fx-snap {
    display: flex;
    width: 100%;
    height: 100%;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    cursor: grab;
}

.fx-snap.fx-dragging {
    cursor: grabbing;
    scroll-snap-type: none; /* let the drag move freely; snap back on release */
}

.fx-snap.fx-dragging .fx-snap-slide { user-select: none; }

.fx-snap::-webkit-scrollbar { display: none; }

.fx-snap-slide {
    flex: 0 0 82%;
    scroll-snap-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 18px 8px;
    border-radius: var(--radius-md);
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
}

.fx-snap-slide:first-child { margin-left: 9%; }
.fx-snap-slide:last-child { margin-right: 9%; }

.fx-snap-slide--alt {
    background: var(--ink-900);
    border-color: var(--ink-900);
}

.fx-snap-slide span {
    font-size: 1.6rem;
    font-weight: 900;
    color: var(--ink-300);
}

.fx-snap-slide--alt span { color: var(--ink-600); }

.fx-snap-slide em {
    font-style: normal;
    font-size: 0.78rem;
    color: var(--text-secondary);
    padding: 0 16px;
    text-align: center;
}

.fx-snap-slide--alt em { color: var(--ink-400); }

/* ============================================
   8. Typewriter Text
   ============================================ */
.fx-type {
    font-family: 'Consolas', 'SF Mono', 'Fira Code', monospace;
    font-size: 1.05rem;
    color: var(--ink-100);
    white-space: nowrap;
    overflow: hidden;
    border-right: 2px solid var(--accent-blue);
    padding-right: 2px;
    max-width: fit-content;
    animation: fx-caret 0.9s steps(1) infinite;
}

/* Retype on hover: the width animation only exists while hovered,
   so leaving and re-entering the card restarts it from zero. */
.fx-card:hover .fx-type,
.fx-card.fx-on .fx-type {
    animation:
        fx-typing 2s steps(23) forwards,
        fx-caret 0.9s steps(1) infinite;
}

@keyframes fx-typing {
    from { max-width: 0; }
    to   { max-width: 100%; }
}

@keyframes fx-caret {
    50% { border-right-color: transparent; }
}

/* ============================================
   9. Morphing Blob
   ============================================ */
.fx-blob-morph {
    width: 150px;
    height: 150px;
    background: linear-gradient(135deg, var(--ink-900) 30%, var(--accent-blue) 130%);
    border-radius: 42% 58% 60% 40% / 45% 40% 60% 55%;
    transition: border-radius 0.8s ease;
}

.fx-card:hover .fx-blob-morph,
.fx-card.fx-on .fx-blob-morph {
    animation: fx-morph 6s ease-in-out infinite;
}

@keyframes fx-morph {
    0%, 100% { border-radius: 42% 58% 60% 40% / 45% 40% 60% 55%; }
    25%      { border-radius: 60% 40% 35% 65% / 55% 60% 40% 45%; }
    50%      { border-radius: 38% 62% 55% 45% / 60% 35% 65% 40%; }
    75%      { border-radius: 55% 45% 42% 58% / 40% 58% 42% 60%; }
}

/* ============================================
   10. Button Micro-interactions
   ============================================ */
.fx-btn-row {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    justify-content: center;
}

.fx-btn {
    font: inherit;
    font-size: 0.88rem;
    font-weight: 600;
    border-radius: var(--radius-sm);
    padding: 11px 22px;
    cursor: pointer;
    border: 1px solid transparent;
}

/* Shine: a band of light sweeps across on hover */
.fx-btn--shine {
    position: relative;
    overflow: hidden;
    background: var(--ink-900);
    color: var(--ink-000);
}

.fx-btn--shine::after {
    content: '';
    position: absolute;
    top: 0;
    left: -80%;
    width: 45%;
    height: 100%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.35), transparent);
    transform: skewX(-20deg);
}

.fx-btn--shine:hover::after {
    animation: fx-shine 0.7s ease;
}

@keyframes fx-shine {
    to { left: 140%; }
}

/* Lift: rises with a shadow on hover, settles on press */
.fx-btn--lift {
    background: var(--bg-primary);
    color: var(--text-primary);
    border-color: var(--border-color);
    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}

.fx-btn--lift:hover {
    transform: translateY(-3px);
    box-shadow: 0 10px 22px rgba(0, 0, 0, 0.14);
    border-color: var(--ink-400);
}

.fx-btn--lift:active {
    transform: translateY(-1px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
}

/* Pulse: a ring ripples out when pressed */
.fx-btn--pulse {
    position: relative;
    background: var(--accent-blue);
    color: #fff;
    transition: background var(--transition-fast);
}

.fx-btn--pulse:hover { background: var(--accent-blue-hover); }

.fx-btn--pulse::after {
    content: '';
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    box-shadow: 0 0 0 0 rgba(var(--accent-blue-rgb), 0.55);
}

.fx-btn--pulse:active::after {
    animation: fx-pulse 0.55s ease-out;
}

@keyframes fx-pulse {
    to { box-shadow: 0 0 0 14px rgba(var(--accent-blue-rgb), 0); }
}

/* ============================================
   ★ Orbiting Words — the Riviant mark with words
   circling it on a 3D carousel ring. Still at
   rest; spins on hover like the other cards.
   ============================================ */
.fx-orbit {
    position: relative;
    width: 100%;
    height: 100%;
    perspective: 800px;
}

.fx-orbit-logo {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 112px;
    height: auto;
    /* Blends any white box in the PNG into the light stage background. */
    mix-blend-mode: multiply;
    filter: drop-shadow(0 10px 22px rgba(var(--accent-blue-rgb), 0.25));
}

.fx-orbit-tilt {
    /* The tilt makes the front of the ring dip by --fx-r × sin(22°), which
       would drop the words to the R's base. --fx-lift raises the ring so the
       front pass crosses just below the middle of the R. Lower value = words
       sit lower on the logo. */
    --fx-lift: 30px;
    position: absolute;
    left: 50%;
    top: calc(50% - var(--fx-lift));
    /* Deeper tilt = the orbit reads as a visible oval instead of a flat line. */
    transform: rotateX(-22deg);
    transform-style: preserve-3d;
}

.fx-orbit-ring {
    --fx-r: 86px;       /* ring radius — keeps the words hugging the mark */
    --fx-step: 6.6deg;  /* angle between letters — the curvature of the text */
    transform-style: preserve-3d;
    animation: fx-orbit-spin 18s linear infinite;
    animation-play-state: paused;
}

.fx-card:hover .fx-orbit-ring,
.fx-card.fx-on .fx-orbit-ring {
    animation-play-state: running;
}

.fx-orbit-word {
    /* An inline span can't carry a 3D context (preserve-3d is ignored on it,
       flattening the letters). display:contents removes the span's box entirely
       so each letter joins the ring's 3D context directly. */
    display: contents;
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--ink-600);
}

/* Each letter sits at its own angle on the ring, so the word bends
   around the curve — like text printed on a glass cylinder. */
.fx-orbit-word b {
    position: absolute;
    left: 50%;
    top: 50%;
    font-weight: inherit;
    transform: translate(-50%, -50%)
        rotateY(calc(var(--fx-a, 0deg) + var(--fx-i, 0) * var(--fx-step)))
        translateZ(var(--fx-r));
    /* Letters facing away from the viewer disappear instead of showing mirrored. */
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
}

@keyframes fx-orbit-spin {
    from { transform: rotateY(0deg); }
    to   { transform: rotateY(360deg); }
}


.fx-stage-hint {
    position: absolute;
    bottom: 10px;
    left: 12px;
    right: 12px;
    text-align: center;
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    pointer-events: none;
}

/* ============================================
   ★ Page-Flip Lookbook  (JS turns the pages)
   ============================================ */
.fx-book-scene {
    perspective: 1200px;
    width: 190px;
    height: 138px;
    position: relative;
}

.fx-book {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    transform: translateX(-25%); /* closed book: lone cover sits centered */
    transition: transform 0.7s ease;
    cursor: pointer;
    outline: none;
}

.fx-book:focus-visible { filter: drop-shadow(0 0 0 2px var(--accent-blue)); }

.fx-book--mid { transform: translateX(0); }
.fx-book--end { transform: translateX(25%); }

.fx-bsheet {
    position: absolute;
    left: 50%;
    top: 0;
    width: 50%;
    height: 100%;
    transform-origin: left center;
    transform-style: preserve-3d;
    transition: transform 0.8s cubic-bezier(0.3, 0.05, 0.2, 1);
}

.fx-bsheet.flipped { transform: rotateY(-180deg); }

.fx-bpage {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: 0 8px 8px 0;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 12px 10px;
    text-align: center;
    overflow: hidden;
}

/* Soft shadow falling out of the spine. */
.fx-bpage::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(90deg, rgba(0, 0, 0, 0.1), transparent 16%);
}

.fx-bpage--back { transform: rotateY(180deg); }

.fx-bpage--cover,
.fx-bpage--end {
    background: var(--ink-900);
    border-color: var(--ink-900);
}

.fx-bpage-orn { color: var(--accent-blue); font-size: 0.9rem; }

.fx-bpage-title {
    color: var(--ink-000);
    font-weight: 800;
    font-size: 0.92rem;
    letter-spacing: 0.01em;
}

.fx-bpage-sub {
    color: var(--ink-500);
    font-size: 0.6rem;
    letter-spacing: 0.28em;
    text-transform: uppercase;
}

.fx-bart {
    width: 72%;
    flex: 1 1 0;
    min-height: 0;
    max-height: 74px;
    border-radius: 4px;
}

.fx-bart--a { background: radial-gradient(circle at 30% 25%, rgba(var(--accent-blue-rgb), 0.65), transparent 60%), var(--ink-200); }
.fx-bart--b { background: radial-gradient(circle at 70% 70%, rgba(var(--accent-blue-rgb), 0.5), transparent 55%), var(--ink-800); }

.fx-bcap {
    font-size: 0.62rem;
    color: var(--text-muted);
    letter-spacing: 0.04em;
}

/* ============================================
   11. Glitch Text
   ============================================ */
.fx-glitch {
    position: relative;
    font-size: clamp(1.6rem, 4vw, 2.1rem);
    font-weight: 900;
    letter-spacing: 0.08em;
    color: var(--ink-000);
}

.fx-glitch::before,
.fx-glitch::after {
    content: attr(data-text);
    position: absolute;
    inset: 0;
    opacity: 0;
}

.fx-glitch::before { color: var(--accent-blue); }
.fx-glitch::after { color: var(--ink-400); }

.fx-card:hover .fx-glitch::before,
.fx-card.fx-on .fx-glitch::before {
    opacity: 0.85;
    animation: fx-glitch-a 0.8s steps(2, end) infinite;
}

.fx-card:hover .fx-glitch::after,
.fx-card.fx-on .fx-glitch::after {
    opacity: 0.85;
    animation: fx-glitch-b 0.8s steps(2, end) infinite reverse;
}

@keyframes fx-glitch-a {
    0%   { clip-path: inset(15% 0 60% 0); transform: translate(-3px, -2px); }
    25%  { clip-path: inset(60% 0 8% 0);  transform: translate(3px, 1px); }
    50%  { clip-path: inset(8% 0 72% 0);  transform: translate(-2px, 2px); }
    75%  { clip-path: inset(72% 0 4% 0);  transform: translate(2px, -1px); }
    100% { clip-path: inset(38% 0 42% 0); transform: translate(-3px, 1px); }
}

@keyframes fx-glitch-b {
    0%   { clip-path: inset(65% 0 12% 0); transform: translate(3px, 2px); }
    25%  { clip-path: inset(10% 0 65% 0); transform: translate(-3px, -1px); }
    50%  { clip-path: inset(75% 0 5% 0);  transform: translate(2px, -2px); }
    75%  { clip-path: inset(20% 0 55% 0); transform: translate(-2px, 1px); }
    100% { clip-path: inset(45% 0 35% 0); transform: translate(3px, -1px); }
}

/* --- Glitch on the hero title: short bursts every few seconds,
       full glitch while hovered. --- */
.fx-glitch-title::before,
.fx-glitch-title::after {
    content: attr(data-text);
    position: absolute;
    inset: 0;
    opacity: 0;
    pointer-events: none;
    animation: fx-title-glitch 5s steps(1, end) infinite;
}

.fx-glitch-title::before { color: var(--accent-blue); }

.fx-glitch-title::after {
    color: var(--ink-500);
    animation-delay: 2.6s; /* the grey layer bursts offbeat from the blue one */
    animation-direction: reverse;
}

/* Mostly idle — two brief bursts per 5s cycle. */
@keyframes fx-title-glitch {
    0%       { opacity: 0.75; clip-path: inset(12% 0 62% 0); transform: translate(-5px, -2px); }
    2.5%     { opacity: 0.75; clip-path: inset(58% 0 10% 0); transform: translate(5px, 2px); }
    5%       { opacity: 0.75; clip-path: inset(8% 0 74% 0);  transform: translate(-4px, 1px); }
    7.5%     { opacity: 0.75; clip-path: inset(70% 0 6% 0);  transform: translate(4px, -1px); }
    9%, 53%  { opacity: 0; clip-path: inset(50% 0 50% 0); transform: none; }
    54%      { opacity: 0.75; clip-path: inset(28% 0 48% 0); transform: translate(4px, 2px); }
    56.5%    { opacity: 0.75; clip-path: inset(66% 0 14% 0); transform: translate(-4px, -1px); }
    59%      { opacity: 0.75; clip-path: inset(10% 0 60% 0); transform: translate(3px, 1px); }
    60.5%, 100% { opacity: 0; clip-path: inset(50% 0 50% 0); transform: none; }
}

/* Hovering the title lets it glitch continuously. */
.fx-glitch-title:hover::before {
    animation: fx-glitch-a 0.8s steps(2, end) infinite;
    opacity: 0.8;
}

.fx-glitch-title:hover::after {
    animation: fx-glitch-b 0.8s steps(2, end) infinite reverse;
    opacity: 0.8;
}

/* ============================================
   12. 3D Tilt Card  (JS feeds --fx-tx/--fx-ty)
   ============================================ */
.fx-tilt {
    perspective: 700px;
    padding: 10px; /* room so the tilted card never clips */
}

.fx-tilt-card {
    width: 200px;
    padding: 34px 24px;
    background: var(--ink-900);
    color: var(--ink-000);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    text-align: center;
    transform: rotateX(var(--fx-ty, 0deg)) rotateY(var(--fx-tx, 0deg));
    transition: transform 0.15s ease-out;
    will-change: transform;
}

.fx-tilt-title { font-weight: 800; font-size: 1.05rem; }

.fx-tilt-card em {
    font-style: normal;
    font-size: 0.76rem;
    color: var(--ink-400);
}

/* ============================================
   13. Infinite Marquee
   ============================================ */
.fx-marquee {
    width: 100%;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
}

.fx-marquee-track {
    display: flex;
    align-items: center;
    width: max-content;
    animation: fx-marquee 14s linear infinite;
    animation-play-state: paused;
}

.fx-card:hover .fx-marquee-track,
.fx-card.fx-on .fx-marquee-track {
    animation-play-state: running;
}

.fx-marquee-track > * { margin-right: 30px; }

.fx-marquee-track span {
    font-size: 1.7rem;
    font-weight: 800;
    letter-spacing: 0.02em;
    white-space: nowrap;
    color: transparent;
    -webkit-text-stroke: 1px var(--ink-400);
}

.fx-marquee-track i {
    font-style: normal;
    font-size: 0.85rem;
    color: var(--accent-blue);
}

@keyframes fx-marquee {
    to { transform: translateX(-50%); }
}

/* ============================================
   14. Count-Up Numbers — a typed custom
   property transitions, a counter displays it.
   ============================================ */
@property --fx-n {
    syntax: '<integer>';
    initial-value: 0;
    inherits: false;
}

.fx-count {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}

.fx-count-num {
    --fx-n: 0;
    /* counter-reset lives here, next to the property being animated. A
       registered property with inherits:false does NOT reach a pseudo-element,
       so reading var(--fx-n) inside ::after only ever saw the initial 0 — which
       is why this sat on "0+". Counters, unlike custom properties, are in scope
       for the pseudo-element, so ::after can print it. */
    counter-reset: fxcount var(--fx-n);
    animation: fx-count-run 5s ease-out infinite;
    font-size: 3.2rem;
    font-weight: 900;
    line-height: 1.1;
    color: var(--ink-900);
    font-variant-numeric: tabular-nums;
}

.fx-count-num::after { content: counter(fxcount) '+'; }

/* Climb, hold on the finished number so it can actually be read,
   then fade out and start over. */
@keyframes fx-count-run {
    0%   { --fx-n: 0;   opacity: 1; }
    45%  { --fx-n: 128; opacity: 1; }
    88%  { --fx-n: 128; opacity: 1; }
    97%  { --fx-n: 128; opacity: 0; }
    98%  { --fx-n: 0;   opacity: 0; }
    100% { --fx-n: 0;   opacity: 1; }
}

.fx-count-label {
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ============================================
   15. Link Hovers
   ============================================ */
.fx-links {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
}

.fx-link {
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--text-primary);
    cursor: pointer;
    width: max-content;
}

/* Underline slides in from the left */
.fx-link--slide {
    background-image: linear-gradient(var(--accent-blue), var(--accent-blue));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0% 2px;
    padding-bottom: 3px;
    transition: background-size 0.3s ease;
}

.fx-link--slide:hover,
.fx-link--slide:focus-visible { background-size: 100% 2px; }

/* Highlight rises up behind the text */
.fx-link--grow {
    background-image: linear-gradient(rgba(var(--accent-blue-rgb), 0.18), rgba(var(--accent-blue-rgb), 0.18));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 0.3em;
    padding: 1px 4px;
    margin: -1px -4px;
    transition: background-size 0.25s ease;
}

.fx-link--grow:hover,
.fx-link--grow:focus-visible { background-size: 100% 100%; }

/* Arrow slides into view */
.fx-link--arrow { transition: color var(--transition-fast); }

.fx-link--arrow i {
    display: inline-block;
    font-style: normal;
    opacity: 0;
    transform: translateX(-6px);
    transition: opacity 0.25s ease, transform 0.25s ease;
}

.fx-link--arrow:hover,
.fx-link--arrow:focus-visible { color: var(--accent-blue); }

.fx-link--arrow:hover i,
.fx-link--arrow:focus-visible i {
    opacity: 1;
    transform: translateX(3px);
}

/* ============================================
   16. Aurora Background
   ============================================ */
.fx-aurora {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    overflow: hidden;
}

.fx-aurora-blob {
    position: absolute;
    border-radius: 50%;
    filter: blur(34px);
    opacity: 0.5;
    animation-duration: 8s;
    animation-timing-function: ease-in-out;
    animation-iteration-count: infinite;
    animation-direction: alternate;
    animation-play-state: paused;
}

.fx-card:hover .fx-aurora-blob,
.fx-card.fx-on .fx-aurora-blob {
    animation-play-state: running;
}

.fx-aurora-blob--a {
    width: 180px;
    height: 140px;
    left: 8%;
    top: 12%;
    background: rgba(var(--accent-blue-rgb), 0.8);
    animation-name: fx-aurora-a;
}

.fx-aurora-blob--b {
    width: 150px;
    height: 150px;
    right: 6%;
    top: 30%;
    background: #475569;
    animation-name: fx-aurora-b;
    animation-duration: 10s;
}

.fx-aurora-blob--c {
    width: 200px;
    height: 120px;
    left: 28%;
    bottom: 4%;
    background: #1e3a8a;
    animation-name: fx-aurora-c;
    animation-duration: 9s;
}

@keyframes fx-aurora-a {
    to { transform: translate(48px, 26px) scale(1.25); }
}

@keyframes fx-aurora-b {
    to { transform: translate(-42px, -20px) scale(1.15); }
}

@keyframes fx-aurora-c {
    to { transform: translate(30px, -24px) scale(0.85); }
}

.fx-aurora-label {
    position: relative;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-200);
    text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}

/* ============================================
   19. Exploded Build — real part photos rest
   apart and assemble on hover. Each part is a
   cut-out PNG; positions were laid out against
   a 1385 x 577 coordinate space (the assembled
   rifle plus room for the parts to fly out).
   ============================================ */
.fx-ar {
    /* The display box. The 1385 x 577 scene inside is scaled to fill
       this width, so every part coordinate below stays in scene units
       and only --ar-w changes between breakpoints. --ar-w is UNITLESS
       (a px count) so `--ar-w / 1385` yields a plain number for scale();
       dividing a length by 1385 would give a length and scale() would
       silently drop it, leaving the scene full-size and off-frame.
       Kept just under the card's inner stage width (~262px on a 3-up
       grid) so the muzzle never clips. */
    --ar-w: 244;
    position: relative;
    width: calc(var(--ar-w) * 1px);
    height: calc(var(--ar-w) * 1px * 577 / 1385);
}

.fx-ar-scene {
    position: absolute;
    top: 0;
    left: 0;
    width: 1385px;
    height: 577px;
    transform-origin: top left;
    transform: scale(calc(var(--ar-w) / 1385));
}

.fx-ar-part {
    position: absolute;
    display: block;
    /* The part's width is set per-rule; height follows the PNG's aspect.
       max-width:none defeats the global `img { max-width:100% }` reset. */
    height: auto;
    max-width: none;
    /* At rest each part is scattered — shifted AND rotated. Assembling
       clears both, so a part carries three numbers: offset x/y + angle. */
    transform: translate(var(--fx-dx, 0px), var(--fx-dy, 0px)) rotate(var(--fx-r, 0deg));
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.28, 1);
    transition-delay: var(--fx-d, 0s);
    filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.35));
}

.fx-card:hover .fx-ar-part,
.fx-card.fx-on .fx-ar-part {
    transform: translate(0, 0) rotate(0deg);
}

/* --- Assembled positions (local scene coords) + explode offset ---
   Assembled placement was set by hand in the drag-and-drop positioner,
   then shifted as a rigid unit (-140x, +18y) to centre the rifle in the
   1385x577 scene. Explode offsets fly each part out to its resting spot. */
.fx-ar-optic {
    left: 432px; top: 92px; width: 320px;
    --fx-dx: 448px; --fx-dy: -52px; --fx-r: 14deg; --fx-d: 0.28s;
}

.fx-ar-upper {
    left: 368px; top: 178px; width: 874px;
    --fx-dx: -8px; --fx-dy: 122px; --fx-r: -6deg; --fx-d: 0.07s;
}

.fx-ar-lower {
    left: 399px; top: 247px; width: 265px;
    --fx-dx: 361px; --fx-dy: 113px; --fx-r: 16deg; --fx-d: 0s;
}

.fx-ar-stock {
    left: 143px; top: 229px; width: 263px;
    --fx-dx: -103px; --fx-dy: -169px; --fx-r: -18deg; --fx-d: 0.14s;
}

.fx-ar-grip {
    left: 374px; top: 299px; width: 158px;
    --fx-dx: -224px; --fx-dy: 56px; --fx-r: 20deg; --fx-d: 0.2s;
}

/* ============================================
   Mobile — the grid drops to one column, so the
   demos get a little more room but the pieces
   inside them need to shrink to match.
   ============================================ */
@media (max-width: 768px) {
    .fx-grid { grid-template-columns: 1fr; gap: 22px; }

    .fx-stage { height: 210px; padding: 20px; }

    /* Orbit: tighter ring and smaller type so the words never
       run past the edges of a narrow card. */
    .fx-orbit-logo { width: 96px; }
    .fx-orbit-tilt { --fx-lift: 26px; }
    .fx-orbit-ring { --fx-r: 76px; --fx-step: 7deg; }
    .fx-orbit-word { font-size: 0.6rem; }

    .fx-book-scene { width: 170px; height: 124px; }

    .fx-flip { width: 180px; height: 138px; }

    .fx-tilt-card { width: 180px; padding: 28px 20px; }

    .fx-ring { width: 200px; }

    .fx-reveal { max-width: 230px; }

    .fx-count-num { font-size: 2.8rem; }

    .fx-marquee-track span { font-size: 1.45rem; }

    .fx-btn-row { gap: 10px; }
    .fx-btn { padding: 10px 18px; font-size: 0.84rem; }

    .fx-stage-hint { font-size: 0.62rem; }

    /* Touch: the tap-to-toggle hint matters more than the hover wording. */
    .fx-spot span { font-size: 0.82rem; }

    /* The rifle scene scales to fill --ar-w; only this number changes. */
    .fx-ar { --ar-w: 300; }
}

@media (max-width: 480px) {
    .fx-stage { height: 195px; }

    .fx-orbit-logo { width: 84px; }
    .fx-orbit-ring { --fx-r: 68px; --fx-step: 7.6deg; }
    .fx-orbit-word { font-size: 0.55rem; letter-spacing: 0.04em; }

    .fx-info { padding: 16px 18px 18px; }

    /* Let a long effect name wrap above its tag instead of squeezing. */
    .fx-title-row { flex-wrap: wrap; gap: 6px; }

    .fx-shimmer { font-size: 1.85rem; }
    .fx-glitch { font-size: 1.5rem; }

    .fx-ar { --ar-w: 264; }

    .fx-savebar {
        width: calc(100% - 24px);
        justify-content: space-between;
        padding: 10px 12px 10px 18px;
    }
}

/* Touch devices have no hover, so the pointer-driven demos need
   their instructions to match what actually works. */
@media (hover: none) {
    .fx-spot {
        cursor: default;
        animation: fx-spot-drift 11s ease-in-out infinite;
    }

    .fx-snap { cursor: default; }
}

/* ============================================
   Reduced motion — everything settles into its
   finished state, nothing moves.
   ============================================ */
@media (prefers-reduced-motion: reduce) {
    .fx-card,
    .fx-card:hover,
    .fx-card.fx-on { transform: none; transition: none; }

    .fx-flip-inner { transition-duration: 0.01s; }

    .fx-shimmer,
    .fx-ring,
    .fx-type,
    .fx-blob-morph,
    .fx-btn--shine::after,
    .fx-btn--pulse::after,
    .fx-card:hover .fx-shimmer,
    .fx-card.fx-on .fx-shimmer,
    .fx-card:hover .fx-ring,
    .fx-card.fx-on .fx-ring,
    .fx-card:hover .fx-type,
    .fx-card.fx-on .fx-type,
    .fx-card:hover .fx-blob-morph,
    .fx-card.fx-on .fx-blob-morph { animation: none; }

    .fx-blob { transition: none; }

    .fx-reveal-item,
    .fx-reveal.fx-play .fx-reveal-item {
        animation: none;
        opacity: 1;
        transform: none;
    }

    .fx-orbit-ring,
    .fx-marquee-track,
    .fx-aurora-blob,
    .fx-glitch-title::before,
    .fx-glitch-title::after,
    .fx-glitch-title:hover::before,
    .fx-glitch-title:hover::after,
    .fx-card:hover .fx-glitch::before,
    .fx-card.fx-on .fx-glitch::before,
    .fx-card:hover .fx-glitch::after,
    .fx-card.fx-on .fx-glitch::after { animation: none; }

    .fx-book,
    .fx-bsheet { transition-duration: 0.01s; }

    .fx-tilt-card { transition: none; }

    /* Show the finished stat instead of animating to it. */
    .fx-count-num { animation: none; --fx-n: 128; opacity: 1; }

    /* The spotlight stops drifting and just rests off-centre. */
    .fx-spot { animation: none; transition: none; }

    /* Show the rifle already built rather than flying together. */
    .fx-ar-part { transition: none; transform: translate(0, 0); }
}
