/* =====================================================================================================
   Motion: how the public site moves. site-v2.css says how things look (including hover and focus states, and
   the shared timing tokens --dur-*, --ease-*, --move-*); motion.js reads the data attributes and sets the
   classes and custom properties used here.

   Rules of the system
   - Scrolling stays native: nothing pins, snaps or takes over the scroll.
   - Content arrives once. A [data-enter] block plays its entrance when it first comes into view and then stays;
     scrolling back never hides it again, and text is never left half-faded while someone reads it.
   - Only what physically belongs to the scroll follows it ([data-scroll]); only what belongs to the pointer
     follows the pointer ([data-pointer]).
   - One vocabulary on shared tokens. Motion animates opacity and the individual transform properties
     (translate, scale, rotate), so it composes with hover states, which use transform.
   - One off switch: html[data-motion="reduced"], which theme.js sets for the device setting and for the
     visitor's footer switch alike; the rules that only apply with motion off are keyed on it. The device media
     query stays as the fallback for when JavaScript doesn't run, and the sections below are also gated on it.
   - Decorative loops only run where they can be seen: a [data-idle] region pauses them while it is off screen.
   - Layout-only defaults carry no specificity (:where), so a host's own layout wins; effects that must win over
     a host (a tilt over a hover lift, a spotlight over a background) match its specificity and win by loading
     after site-v2.css.

   Adding motion to new markup is an attribute, not new CSS. The vocabulary is listed at the top of motion.js.

   Sections: 1 policy · 2 enter · 3 scroll · 4 pointer · 5 idle loops · 6 keyframes
   ===================================================================================================== */

/* ---------- 1. Policy ---------- */
:root[data-motion="reduced"] { scroll-behavior: auto; }
:root[data-motion="reduced"] *, :root[data-motion="reduced"] *::before, :root[data-motion="reduced"] *::after {
    transition: none !important; animation: none !important;
}
/* Without JavaScript theme.js can't set the attribute; the device setting still stops everything, and the
   marquee still becomes a readable row (.no-js is only there when scripts didn't run). */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after { transition: none !important; animation: none !important; }
    @view-transition { navigation: none; }
    .no-js .marquee { mask-image: none; }
    .no-js .marquee-track { flex-wrap: wrap; justify-content: center; gap: 10px 28px; min-width: 0; padding: 0 16px; flex-shrink: 1; }
    .no-js .marquee-track[aria-hidden] { display: none; }
    .no-js .marquee-track li::before { margin-right: 10px; }
    .no-js .marquee-toggle { display: none; }
}
/* Off screen, a [data-idle] region's decorative loops hold still (motion.js toggles .idle-paused). Only the loops
   in section 5 read --idle, so an entrance or scroll effect in the same region is never frozen part-way. */
.idle-paused { --idle: paused; }
/* Page-to-page cross-fade; a project's cover morphs into its case study (Chromium; others just navigate;
   motion.js skips it when motion is off). */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .25s; }
/* Print: everything as it finally rests, whatever has or hasn't arrived on screen. */
@media print {
    [data-enter], [data-enter] *, [data-enter] *::before, [data-scroll] {
        opacity: 1 !important; animation: none !important; transition: none !important; clip-path: none !important;
    }
}

/* ---------- 2. Enter ---------- */
@media (prefers-reduced-motion: no-preference) {
    /* Held back only once motion.js can reveal it (theme.js sets motion-on and removes it if motion.js never
       arrives). The hero is excluded: it plays on load from CSS alone. */
    .motion-on [data-enter]:not(.in):not([data-enter="hero"]) { opacity: 0; }

    [data-enter], [data-enter="rise"] { --enter: m-rise; --enter-dur: var(--dur-slow); }
    :where(span[data-enter]) { display: inline-block; }   /* transforms skip plain inline boxes; a host's own display wins */
    [data-enter="card"] { --enter: m-card; }
    [data-enter="deal"] { --enter: m-deal; --enter-dur: 820ms; }   /* from the container's middle (--deal, motion.js) */
    [data-enter="flip"] { --enter: m-flip; transform-origin: 50% 100%; }
    [data-enter="slide-left"] { --enter: m-slide-left; }
    [data-enter="slide-right"] { --enter: m-slide-right; }
    [data-enter="tilt-left"] { --enter: m-tilt-left; }
    [data-enter="tilt-right"] { --enter: m-tilt-right; }
    [data-enter="widen"] { --enter: m-widen; }
    [data-stagger]:has(> [data-enter="flip"]) { perspective: 1200px; }

    .motion-on [data-enter].in {
        animation: var(--enter) var(--enter-dur) var(--ease-out) both;
        animation-delay: calc(var(--i, 0) * var(--stagger));
    }

    /* heading: the label slides in and its chapter rule draws, the title rises out of a mask, the note follows. */
    .motion-on [data-enter="heading"].in { animation: none; }
    .motion-on [data-enter="heading"]:not(.in) .kicker::before { background-size: 0 1px; }
    .motion-on [data-enter="heading"].in .kicker { display: inline-block; animation: m-slide-in var(--dur-slow) var(--ease-out) both; }
    .motion-on [data-enter="heading"].in h2 { animation: m-unmask 760ms var(--ease-out) 60ms backwards; }
    .motion-on [data-enter="heading"].in :is(p, a) { animation: m-rise-s var(--dur-slow) var(--ease-out) 180ms both; }

    /* hero: on load, piece by piece in the order the markup gives (--i). Starts at .01, not 0: Chrome doesn't
       count fully transparent text as painted, and 0 would push back the Largest Contentful Paint. */
    [data-enter="hero"] { animation: m-hero var(--hero-dur) var(--ease-out) both; animation-delay: calc(var(--i, 0) * var(--hero-step) + 60ms); }

    /* Items: after their block, one after another. */
    .motion-on [data-enter].in [data-enter-item] {
        animation: var(--item) var(--dur-base) var(--ease-out) both;
        animation-delay: calc(var(--i, 0) * var(--stagger) + 200ms + var(--j, 0) * var(--stagger-item));
    }
    [data-enter-item="pop"] { --item: m-pop; }
    [data-enter-item="spin"] { --item: m-spin; }
    [data-enter-item="slide-right"] { --item: m-slide-right; }
    /* dot: the item's ::before marker (the timeline's role dots) fills in; the item itself stays put. */
    .motion-on [data-enter].in [data-enter-item="dot"] { animation: none; }
    .motion-on [data-enter].in [data-enter-item="dot"]::before {
        animation: m-dot var(--dur-base) var(--ease-out) both;
        animation-delay: calc(250ms + var(--j, 0) * 150ms);
    }

    /* The projects page's "nothing matches" note. */
    .empty:not([hidden]) { animation: m-rise-s var(--dur-base) var(--ease-out) both; }
}

/* ---------- 3. Scroll ---------- */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        [data-scroll="lift"] { animation: m-lift linear both; animation-timeline: view(); animation-range: exit 10% exit 100%; }
        [data-scroll="recede"] { animation: m-recede linear both; animation-timeline: view(); animation-range: exit 0% exit 100%; }
        [data-scroll="fade"] { animation: m-fade-down linear both; animation-timeline: scroll(root); animation-range: 0 160px; }
        [data-scroll="depth-far"] { animation: m-depth-far linear both; animation-timeline: view(); animation-range: cover 0% cover 100%; }
        [data-scroll="depth-near"] { animation: m-depth-near linear both; animation-timeline: view(); animation-range: cover 0% cover 100%; }
        [data-scroll="ambient"] { inset: -15%; animation: m-ambient linear both; animation-timeline: scroll(root); }
        /* draw: an accent line fills the element's left edge (its border is the track) as it passes through
           the viewport. */
        [data-scroll="draw"]::before {
            content: ""; position: absolute; left: -2px; top: 0; bottom: 0; width: 2px;
            background: linear-gradient(var(--accent), color-mix(in srgb, var(--accent) 35%, transparent)); transform-origin: top;
            animation: m-draw linear both; animation-timeline: view(); animation-range: entry 10% cover 50%;
        }
    }
}

/* ---------- 4. Pointer ----------
   Every effect works on its own element (data-pointer) and on each child of a list (data-pointer-each). */
/* spotlight: a soft light under the pointer (--mx/--my), painted as the top layer of the host's background;
   a host keeps its own background image in --spot-under and sets the light's size with --spot-size. */
:is([data-pointer="spotlight"], [data-pointer-each="spotlight"] > *) {
    --mx: 20%; --my: 20%;
    background-image: radial-gradient(var(--spot-size, 480px) circle at var(--mx) var(--my), var(--accent-soft), transparent 62%), var(--spot-under, none);
}
/* glow: the same light inside a card (--gx/--gy). */
:is([data-pointer="glow"], [data-pointer-each="glow"] > *) {
    --gx: 50%; --gy: 0%;
    background-image: radial-gradient(260px circle at var(--gx) var(--gy), var(--accent-soft), transparent 65%);
}
/* tilt: a few degrees toward the pointer, with a glow and, if given, a badge riding under the pointer. */
:is([data-pointer="tilt"], [data-pointer-each="tilt"] > *) { --gx: 50%; --gy: 50%; --rx: 0deg; --ry: 0deg; transform-style: preserve-3d; }
:is([data-pointer="tilt"], [data-pointer-each="tilt"] > *)::before {
    content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; border-radius: inherit; opacity: 0;
    background: radial-gradient(360px circle at var(--gx) var(--gy), color-mix(in srgb, var(--accent) 16%, transparent), transparent 60%);
    transition: opacity var(--dur-base) var(--ease-std);
}
/* Specificity matches the host's hover transform rule (e.g. .project-card:hover); the tilt wins while active
   because this file loads after site-v2.css. */
:is([data-pointer="tilt"], [data-pointer-each="tilt"] > *).tilting::before { opacity: 1; }
:is([data-pointer="tilt"], [data-pointer-each="tilt"] > *).tilting {
    transform: perspective(900px) rotateX(var(--rx)) rotateY(var(--ry)) translateY(-3px);
    transition: transform .08s linear, box-shadow .2s, border-color .2s;
}
[data-badge]::after {
    content: attr(data-badge); content: attr(data-badge) / "";   /* a visual hint; the card's link already names it */
    position: absolute; left: var(--gx); top: var(--gy); z-index: 2; pointer-events: none;
    translate: -50% -50%; scale: .4; opacity: 0; white-space: nowrap;
    padding: 8px 14px; border-radius: 999px; font-size: 12.5px; font-weight: 800; letter-spacing: .02em;
    color: var(--accent-ink); background: var(--accent); box-shadow: 0 10px 30px -10px rgba(0, 0, 0, .5);
    transition: opacity var(--dur-fast) var(--ease-std), scale var(--dur-base) var(--ease-out);
}
[data-badge].tilting::after { opacity: 1; scale: 1; }
/* lean: the host's [data-lean] descendants follow --px/--py (-1..1) and settle back. */
:is([data-pointer="lean"], [data-pointer-each="lean"] > *) [data-lean] {
    transform: translate(calc(var(--px, 0) * 10px), calc(var(--py, 0) * 8px)) rotate(calc(var(--px, 0) * 1.5deg));
    transition: transform var(--dur-slow) var(--ease-out);
}
/* magnet: drawn toward the pointer (--tx/--ty) through translate, so the host's hover transform still applies.
   Mouse and pen only, like the script; the transition list keeps a button's own transitions. */
:is([data-pointer="magnet"], [data-pointer-each="magnet"] > *) { translate: var(--tx, 0) var(--ty, 0); }
@media (hover: hover) and (pointer: fine) {
    :is([data-pointer="magnet"], [data-pointer-each="magnet"] > *) {
        transition: translate var(--dur-base) var(--ease-out), transform .15s, background .15s, border-color .15s, color .15s;
    }
}
/* Hover focus: in a [data-hover-focus] group, the hovered child stands forward and its siblings quiet down.
   Hover devices only; the transition list keeps the cards' own transitions. */
@media (hover: hover) {
    [data-hover-focus] > * { transition: filter var(--dur-base) var(--ease-std), border-color .2s, transform .2s, box-shadow .2s; }
    [data-hover-focus]:hover > :not(:hover) { filter: saturate(.35) brightness(.94); }
    [data-hover-focus] > :hover { border-color: var(--border-strong); }
}

/* ---------- 5. Idle loops (decorative, always optional) ---------- */
@media (prefers-reduced-motion: no-preference) {
    .status-dot::after { animation: m-pulse 2s ease-out infinite; animation-play-state: var(--idle, running); }
    .avatar-wrap::before { animation: m-ring 8s linear infinite; animation-play-state: var(--idle, running); }
    .hero-aurora span { animation: m-aurora-a 22s ease-in-out infinite alternate; animation-play-state: var(--idle, running); }
    .hero-aurora span + span { animation: m-aurora-b 28s ease-in-out infinite alternate; animation-play-state: var(--idle, running); }
    /* Surname: its hero entrance, then a light passing across it every few seconds. */
    .hero h1 .word:last-child {
        animation: m-hero var(--hero-dur) var(--ease-out) both, m-sheen 7s var(--ease-std) infinite;
        animation-delay: calc(var(--i, 0) * var(--hero-step) + 60ms), 1.6s;
        animation-play-state: running, var(--idle, running);
    }
    .marquee-track { animation: m-marquee 40s linear infinite; animation-play-state: var(--idle, running); }
    .scroll-cue-mouse span { animation: m-cue 1.8s var(--ease-out) infinite; animation-play-state: var(--idle, running); }
    .ia-wave-1 { animation: m-bob 5s ease-in-out infinite; animation-play-state: var(--idle, running); }
    .ia-wave-2 { animation: m-bob 7s ease-in-out infinite reverse; animation-play-state: var(--idle, running); }
    .ia-sun { animation: m-glow 6s ease-in-out infinite; animation-play-state: var(--idle, running); transform-origin: 248px 52px; }
    .ia-tree { transform-origin: 150px 116px; animation: m-sway 6s ease-in-out infinite; animation-play-state: var(--idle, running); }
}
/* Paused by a hover over the marquee, or by a [data-pause] button for everything in its region. */
.marquee:hover .marquee-track,
[data-pausable].paused *, [data-pausable].paused *::before, [data-pausable].paused *::after { animation-play-state: paused; }
/* Motion off: the marquee becomes one static, wrapping row. */
:root[data-motion="reduced"] .marquee { mask-image: none; }
:root[data-motion="reduced"] .marquee-track { flex-wrap: wrap; justify-content: center; gap: 10px 28px; min-width: 0; padding: 0 16px; flex-shrink: 1; }
:root[data-motion="reduced"] .marquee-track[aria-hidden] { display: none; }
:root[data-motion="reduced"] .marquee-track li::before { margin-right: 10px; }
:root[data-motion="reduced"] .marquee-toggle { display: none; }

/* ---------- 6. Keyframes ---------- */
/* entrances */
@keyframes m-rise { from { opacity: 0; translate: 0 var(--move-m); } }
@keyframes m-rise-s { from { opacity: 0; translate: 0 var(--move-s); } }
@keyframes m-card { from { opacity: 0; translate: 0 var(--move-l); scale: .96; } }
@keyframes m-deal {
    from { opacity: 0; translate: calc(var(--deal, 0) * 100%) var(--move-l); rotate: calc(var(--deal, 0) * 7deg); scale: .92; }
    40% { opacity: 1; }
}
@keyframes m-flip { from { opacity: 0; rotate: x 70deg; translate: 0 var(--move-m); } }
@keyframes m-slide-left { from { opacity: 0; translate: calc(var(--move-l) * -1) 0; } }
@keyframes m-slide-right { from { opacity: 0; translate: var(--move-l) 0; } }
@keyframes m-slide-in { from { opacity: 0; translate: calc(var(--move-m) * -1) 0; } }
@keyframes m-tilt-left { from { opacity: 0; rotate: -3deg; translate: -32px var(--move-l); } }
@keyframes m-tilt-right { from { opacity: 0; rotate: 3deg; translate: 32px var(--move-l); } }
@keyframes m-widen { from { opacity: 0; scale: .94 1; } }
@keyframes m-unmask { from { clip-path: inset(0 0 100% 0); translate: 0 .4em; } to { clip-path: inset(0 0 -15% 0); } }
@keyframes m-hero { from { opacity: .01; translate: 0 18px; } }
@keyframes m-pop { from { opacity: 0; scale: .6; } }
@keyframes m-spin { from { opacity: 0; rotate: -120deg; scale: .3; } }
/* The dot flashes full on its way in and settles back to its own look. */
@keyframes m-dot { from { scale: 0; } 60% { scale: 1.35; background: var(--accent); } to { scale: 1; } }
/* scroll */
@keyframes m-lift { to { opacity: .35; translate: 0 calc(var(--move-l) * -1); } }
@keyframes m-recede { to { scale: .9; opacity: .5; } }
@keyframes m-fade-down { to { opacity: 0; translate: 0 12px; } }
@keyframes m-depth-far { from { translate: 0 30px; } to { translate: 0 -30px; } }
@keyframes m-depth-near { from { translate: 0 14px; } to { translate: 0 -6px; } }
@keyframes m-ambient { from { translate: -6% -4%; } 50% { translate: 6% 3%; } to { translate: -3% 6%; } }
@keyframes m-draw { from { transform: scaleY(0); } to { transform: scaleY(1); } }
/* idle */
@property --ring { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@keyframes m-ring { to { --ring: 360deg; } }
@keyframes m-pulse { from { transform: scale(1); opacity: .7; } to { transform: scale(3.2); opacity: 0; } }
@keyframes m-aurora-a { to { translate: 14vmax 8vmax; scale: 1.15; } }
@keyframes m-aurora-b { to { translate: -12vmax -6vmax; scale: .9; } }
/* Only the light band (first layer) moves, from resting off one side of the name to off the other. */
@keyframes m-sheen { 0% { background-position: 100% 0, 0 0; } 25%, 100% { background-position: 0 0, 0 0; } }
@keyframes m-marquee { to { transform: translateX(-100%); } }
@keyframes m-cue { 0% { translate: 0 0; opacity: 1; } 70% { translate: 0 10px; opacity: 0; } 100% { translate: 0 0; opacity: 0; } }
@keyframes m-bob { 50% { transform: translateX(-14px) translateY(3px); } }
@keyframes m-glow { 50% { transform: scale(1.12); opacity: 1; } }
@keyframes m-sway { 50% { transform: rotate(2.5deg); } }
