/* =============================================================================
   enhancements.css — 10 motion / UI upgrades
   Self-contained: every rule here is additive. Delete this file, its <link>
   tags and js/enhancements.js to revert everything.
   Accent: #28e98c   Secondary: #5b6af0
============================================================================= */

/* -----------------------------------------------------------------
   01 · Contextual cursor
   One element, moved with translate3d inside a single rAF loop.
   Replaces the old canvas trail (cheaper: no per-frame repaint).
   ----------------------------------------------------------------- */
body.fx-cursor-on .cursor,
body.fx-cursor-on #trailCanvas { display: none !important; }

.fx-cursor {
    position: fixed;
    top: 0; left: 0;
    width: 26px; height: 26px;
    border: 1.5px solid rgba(40, 233, 140, 0.7);
    border-radius: 50%;
    background: rgba(40, 233, 140, 0.04);
    pointer-events: none;
    z-index: 99999;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    will-change: transform;
    transform: translate3d(-200px, -200px, 0) translate(-50%, -50%);
    transition:
        width 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
        height 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
        background 0.3s ease,
        border-color 0.3s ease,
        border-radius 0.3s ease,
        opacity 0.25s ease;
}
.fx-cursor.is-visible { opacity: 1; }

.fx-cursor-label {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: #05130c;
    opacity: 0;
    transform: scale(0.8);
    transition: opacity 0.2s ease, transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
    white-space: nowrap;
}

/* State: any link or button */
.fx-cursor[data-state="link"] {
    width: 42px; height: 42px;
    background: rgba(40, 233, 140, 0.12);
    border-color: rgba(40, 233, 140, 0.85);
}

/* State: project / blog card — becomes a solid View pill */
.fx-cursor[data-state="view"] {
    width: 74px; height: 74px;
    background: #28e98c;
    border-color: #28e98c;
}
.fx-cursor[data-state="view"] .fx-cursor-label { opacity: 1; transform: scale(1); }

/* State: over an image — a thin lens ring */
.fx-cursor[data-state="lens"] {
    width: 56px; height: 56px;
    background: rgba(255, 255, 255, 0.04);
    border-color: rgba(255, 255, 255, 0.6);
}

/* State: over body text — a slim caret */
.fx-cursor[data-state="text"] {
    width: 2px; height: 30px;
    border-radius: 2px;
    background: #28e98c;
    border-color: transparent;
}


/* -----------------------------------------------------------------
   02 · Portfolio: grid to editorial list, with hover preview
   ----------------------------------------------------------------- */
.pf-viewtabs {
    display: inline-flex;
    gap: 4px;
    padding: 4px;
    border-radius: 50px;
    border: 1px solid rgba(255, 255, 255, 0.08);
    background: rgba(255, 255, 255, 0.03);
    margin-left: 10px;
    vertical-align: middle;
}
.pf-viewtab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: 0;
    background: transparent;
    color: rgba(255, 255, 255, 0.45);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    padding: 6px 14px;
    border-radius: 50px;
    cursor: pointer;
    transition: background 0.25s ease, color 0.25s ease;
}
.pf-viewtab svg { width: 13px; height: 13px; }
.pf-viewtab:hover { color: rgba(255, 255, 255, 0.8); }
.pf-viewtab.active { background: rgba(40, 233, 140, 0.14); color: #28e98c; }

/* List mode */
.pf-grid.is-list {
    display: block;
    counter-reset: pfitem;
    border-top: 1px solid rgba(255, 255, 255, 0.07);
    max-width: 980px;
    margin: 0 auto;
}
.pf-grid.is-list .pf-card,
.pf-grid.is-list .pf-card-featured {
    grid-column: auto;
    display: flex;
    align-items: center;
    gap: 18px;
    padding: 20px 6px;
    border: 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    overflow: visible;
    transform: none;
}
.pf-grid.is-list .pf-card::before {
    counter-increment: pfitem;
    content: counter(pfitem, decimal-leading-zero);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: rgba(255, 255, 255, 0.22);
    flex-shrink: 0;
    width: 26px;
    transition: color 0.3s ease;
}
.pf-grid.is-list .pf-card-img { display: none; }
.pf-grid.is-list .pf-card-info {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex: 1;
    min-width: 0;
    padding: 0;
    background: none;
}
.pf-grid.is-list .pf-card-text {
    display: flex;
    align-items: baseline;
    gap: 14px;
    flex-wrap: wrap;
    min-width: 0;
}
.pf-grid.is-list .pf-card-title {
    font-size: clamp(19px, 2.6vw, 30px);
    font-weight: 600;
    line-height: 1.15;
    margin: 0;
    transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), color 0.3s ease;
}
.pf-grid.is-list .pf-card-tag { margin-top: 0 !important; }
.pf-grid.is-list .pf-view-pill { opacity: 0; transition: opacity 0.3s ease; }

@media (hover: hover) and (pointer: fine) {
    .pf-grid.is-list .pf-card:hover { transform: none; }
    .pf-grid.is-list .pf-card:hover::before { color: #28e98c; }
    .pf-grid.is-list .pf-card:hover .pf-card-title { transform: translateX(16px); color: #28e98c; }
    .pf-grid.is-list .pf-card:hover .pf-view-pill { opacity: 1; }
}
/* Mobile never gets the list layout, so it never gets the switch either. */
@media (max-width: 768px) {
    .pf-viewtabs { display: none !important; }
}

/* Floating hover preview (list mode only, desktop only) */
.pf-preview {
    position: fixed;
    top: 0; left: 0;
    width: 300px; height: 200px;
    pointer-events: none;
    z-index: 99997;
    will-change: transform;
    transform: translate3d(-500px, -500px, 0) translate(-50%, -50%);
}
.pf-preview-inner {
    width: 100%; height: 100%;
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid rgba(40, 233, 140, 0.25);
    box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55);
    opacity: 0;
    transform: scale(0.9) rotate(-2deg);
    transition: opacity 0.28s ease, transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pf-preview.is-on .pf-preview-inner { opacity: 1; transform: scale(1) rotate(0deg); }
.pf-preview-inner img { width: 100%; height: 100%; object-fit: cover; display: block; }


/* -----------------------------------------------------------------
   03 · Trusted-by infinite marquee — CSS only, no per-frame JS
   ----------------------------------------------------------------- */
.trusted-by-inner.is-marquee { flex-wrap: nowrap; justify-content: flex-start !important; }

.tb-viewport {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.trusted-logos.tb-track {
    --tb-gap: 12px;                     /* must match .trusted-logos gap */
    flex-wrap: nowrap;
    width: max-content;
    will-change: transform;
    animation: tbScroll 34s linear infinite;
}
.tb-viewport:hover .trusted-logos.tb-track { animation-play-state: paused; }

/* The track holds two identical copies, so one loop is half its width — plus
   half a gap, because the two copies are separated by one extra flex gap.
   Without that correction the loop jumps by gap/2 on every repeat. */
@keyframes tbScroll {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(calc(-50% - var(--tb-gap) / 2), 0, 0); }
}
/* Matches the page's own 768px breakpoint, where .trusted-logos gap drops to 8px */
@media (max-width: 768px) {
    .trusted-by-inner.is-marquee { flex-direction: column; align-items: stretch; }
    .trusted-by-inner.is-marquee .trusted-by-label { text-align: center; }
    .trusted-logos.tb-track { --tb-gap: 8px; animation-duration: 24s; }
}


/* -----------------------------------------------------------------
   04 · Experience timeline draws itself on scroll
   ----------------------------------------------------------------- */
.exp-item .exp-dot { position: relative; }
.exp-item .exp-line { position: relative; overflow: hidden; }
.exp-item .exp-line::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, #28e98c 0%, rgba(40, 233, 140, 0.18) 100%);
    transform: scaleY(0);
    transform-origin: top center;
    transition: transform 0.85s cubic-bezier(0.4, 0, 0.2, 1);
}
.exp-item.is-lit .exp-line::after { transform: scaleY(1); }

.exp-item.is-lit .exp-dot {
    border-color: #28e98c;
    box-shadow: 0 0 0 4px rgba(40, 233, 140, 0.1);
}
.exp-item.is-lit .exp-dot::after {
    content: '';
    position: absolute;
    inset: -3px;
    border-radius: 50%;
    border: 1px solid rgba(40, 233, 140, 0.55);
    animation: expPing 0.9s ease-out 1 forwards;
}
@keyframes expPing {
    from { transform: scale(0.7); opacity: 0.9; }
    to   { transform: scale(2.1); opacity: 0; }
}


/* -----------------------------------------------------------------
   05 · Skill cards: ring draws in, numbers count up
   ----------------------------------------------------------------- */
.skill-card-icon-wrap { position: relative; }
.skill-ring {
    position: absolute;
    inset: -7px;
    width: calc(100% + 14px);
    height: calc(100% + 14px);
    pointer-events: none;
    overflow: visible;
}
.skill-ring rect {
    fill: none;
    stroke: #28e98c;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
    opacity: 0.5;
    transition: stroke-dashoffset 1.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease;
}
.skill-card.is-lit .skill-ring rect { stroke-dashoffset: 0; opacity: 0.65; }
@media (hover: hover) and (pointer: fine) {
    .skill-card:hover .skill-ring rect { opacity: 1; }
}

.count-up {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: #28e98c;
}


/* -----------------------------------------------------------------
   06 · Hero aurora that drifts toward the pointer
   ----------------------------------------------------------------- */
.hero-aurora {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
}
.hero-aurora::before {
    content: '';
    position: absolute;
    width: 620px;
    height: 620px;
    left: var(--ax, 50%);
    top: var(--ay, 42%);
    margin: -310px 0 0 -310px;
    border-radius: 50%;
    background: radial-gradient(circle,
        rgba(40, 233, 140, 0.17) 0%,
        rgba(40, 233, 140, 0.06) 38%,
        rgba(91, 106, 240, 0.05) 58%,
        transparent 72%);
    animation: auroraBreathe 9s ease-in-out infinite;
}
@keyframes auroraBreathe {
    0%, 100% { transform: scale(1);    opacity: 0.85; }
    50%      { transform: scale(1.12); opacity: 1; }
}
.header-crev > .container { position: relative; z-index: 2; }

/* A 620px radial gradient is expensive on phones — skip it there */
@media (max-width: 991px) { .hero-aurora { display: none; } }


/* -----------------------------------------------------------------
   09 · Project cards: 3D tilt + glare
   ----------------------------------------------------------------- */
.pf-glare {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 3;
    opacity: 0;
    transition: opacity 0.35s ease;
    background: radial-gradient(300px circle at var(--gx, 50%) var(--gy, 50%),
        rgba(255, 255, 255, 0.13), transparent 62%);
}
@media (hover: hover) and (pointer: fine) {
    .pf-grid:not(.is-list) .pf-card:hover .pf-glare { opacity: 1; }
}
.pf-grid.is-list .pf-glare { display: none; }
.pf-card.fx-tilting { transition: transform 0.12s linear, border-color 0.3s ease, box-shadow 0.35s ease; }


/* -----------------------------------------------------------------
   10 · Guided (one-question-at-a-time) contact form
   ----------------------------------------------------------------- */
.cform-guided .cform-row { display: block; }

/* Inactive steps stay in the DOM (autofill + a11y) but out of sight */
.cform-guided .cform-group[data-step]:not(.is-step-active) {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
.cform-guided .cform-group[data-step].is-step-active {
    animation: cformStepIn 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes cformStepIn {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: translateY(0); }
}

.cform-progress-wrap { margin-bottom: 22px; }
.cform-progress {
    height: 3px;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.07);
    overflow: hidden;
}
.cform-progress > span {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 3px;
    background: linear-gradient(90deg, #28e98c, #5b6af0);
    transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.cform-progress-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: 9px;
    font-size: 11px;
    color: rgba(255, 255, 255, 0.4);
}
.cform-progress-meta b { color: #28e98c; font-weight: 700; }

.cform-modeswitch {
    border: 0;
    background: none;
    padding: 0;
    font-size: 11px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.45);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
    transition: color 0.25s ease;
}
.cform-modeswitch:hover { color: #28e98c; }

/* [hidden] must beat the display rules below it */
.cform-progress-wrap[hidden],
.cform-nav[hidden],
.cform-back[hidden] { display: none !important; }

.cform-nav { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
.cform-back {
    border: 0;
    background: none;
    padding: 8px 4px;
    font-size: 12px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.45);
    cursor: pointer;
    transition: color 0.25s ease;
}
.cform-back:hover { color: #fff; }
.cform-next {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    border: 0;
    border-radius: 10px;
    padding: 15px 24px;
    font-size: 14px;
    font-weight: 700;
    color: #05130c;
    background: #28e98c;
    cursor: pointer;
    transition: background 0.25s ease, transform 0.25s ease;
}
.cform-next:hover { background: #4ff0a5; transform: translateY(-1px); }
.cform-next kbd {
    font: inherit;
    font-size: 10px;
    font-weight: 600;
    opacity: 0.6;
    border: 1px solid rgba(0, 0, 0, 0.25);
    border-radius: 4px;
    padding: 1px 5px;
}

/* Guided mode hides the real submit until the final step */
.cform-guided:not(.is-last-step) .cform-submit,
.cform-guided:not(.is-last-step) .cform-foot { display: none !important; }
.cform-guided.is-last-step .cform-next { display: none; }
.cform-guided.is-last-step .cform-nav { margin-bottom: 14px; }


/* -----------------------------------------------------------------
   Reduced motion — one switch that calms every effect above
   ----------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .fx-cursor,
    .pf-preview,
    .hero-aurora { display: none !important; }

    .trusted-logos.tb-track { animation: none; transform: none; }
    .trusted-by-inner.is-marquee { flex-wrap: wrap; }
    .tb-viewport { overflow: visible; -webkit-mask-image: none; mask-image: none; }

    .exp-item .exp-line::after { transition: none; transform: scaleY(1); }
    .exp-item.is-lit .exp-dot::after { animation: none; display: none; }

    .skill-ring rect { transition: none; stroke-dashoffset: 0; }

    .hero-aurora::before { animation: none; }

    .pf-grid.is-list .pf-card-title { transition: none; }
    .pf-grid.is-list .pf-card:hover .pf-card-title { transform: none; }
    .pf-card.fx-tilting { transition: none; }
    .pf-glare { display: none; }

    .cform-guided .cform-group[data-step].is-step-active { animation: none; }
    .cform-progress > span { transition: none; }

    .pc-curtain { display: none !important; }
}


/* -----------------------------------------------------------------
   11 · Page curtain — the transition between documents
   Two panels: an accent sheet leads, a dark sheet settles behind it, so
   the green reads as a moving band and never as a full-screen flash.

   The move is split across the navigation. The OUTGOING page plays the
   cover and draws the site mark on it, then navigates at full coverage.
   The INCOMING page starts already covered (html.pc-arriving, set by the
   inline head script before first paint), reveals where you landed, and
   holds until the document has actually loaded before uncovering.

   Timing lives in js/enhancements.js — the only numbers here are the
   ones the browser needs before that script runs.
   ----------------------------------------------------------------- */
.pc-curtain {
    position: fixed;
    inset: 0;
    z-index: 100000;          /* above the preloader (9999) and everything else */
    display: none;
    pointer-events: none;
    contain: layout paint;    /* nothing inside can invalidate the page behind it */
}
.pc-curtain.is-live { display: block; pointer-events: auto; }

.pc-lead, .pc-main {
    position: absolute;
    inset: 0;
}
/* Promoted only while the curtain is actually up — a permanent will-change on
   two viewport-sized layers is memory a phone should not be spending. */
.pc-curtain.is-live .pc-lead,
.pc-curtain.is-live .pc-main,
html.pc-arriving .pc-lead,
html.pc-arriving .pc-main { will-change: transform; }

.pc-lead { z-index: 1; background: #28e98c; box-shadow: 0 -18px 50px rgba(40, 233, 140, 0.28); }
.pc-main {
    z-index: 2;
    background: #0b1011;
    display: grid;
    place-items: center;
    box-shadow: 0 -24px 70px rgba(0, 0, 0, 0.7);
}

/* Nothing behind an opaque curtain is worth animating. */
html.pc-leaving  .trusted-logos.tb-track,
html.pc-arriving .trusted-logos.tb-track { animation-play-state: paused; }

/* Phones: no blurred shadows on viewport-sized layers. They are only visible
   for the few hundred ms of travel, and a 70px blur across a full screen is
   the single most expensive thing in this transition. The curved leading edge
   is dropped in js/enhancements.js on the same media query, for the same
   reason — animating border-radius repaints the whole layer every frame. */
@media (pointer: coarse), (max-width: 768px) {
    .pc-lead, .pc-main { box-shadow: none; }
    .pc-prog i { box-shadow: none; }
}

/* Contents are built by JS (the mark is cloned from the preloader so the
   path data lives in exactly one place), so they fade in once ready. */
.pc-inner { opacity: 0; padding: 0 26px; text-align: center; }

.pc-mark { width: min(26vw, 86px); height: auto; overflow: visible; display: block; margin: 0 auto; }
.pc-mark .bl-fill path { animation: none; opacity: 0; fill: #28e98c; }
.pc-mark .bl-line path {
    animation: none;
    stroke: #28e98c;
    stroke-width: 6;
    stroke-dashoffset: 1;
}

.pc-text { margin-top: 20px; }
.pc-kk {
    display: block;
    font: 600 9.5px/1 'Lexend', 'Poppins', sans-serif;
    letter-spacing: 3.4px;
    text-transform: uppercase;
    color: #28e98c;
    margin-bottom: 9px;
    opacity: 0;
}
.pc-mask { display: block; overflow: hidden; padding-bottom: 3px; }
.pc-tt {
    display: block;
    font-family: 'Righteous', 'Lexend', sans-serif;
    font-size: clamp(20px, 3.2vw, 32px);
    letter-spacing: 1px;
    color: #eef2f1;
    transform: translateY(115%);
}

/* Runs while the next document is genuinely still loading. */
.pc-prog { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgba(255, 255, 255, 0.07); }
.pc-prog i {
    display: block;
    height: 100%;
    background: #28e98c;
    transform: scaleX(0);
    transform-origin: left center;
    box-shadow: 0 0 12px rgba(40, 233, 140, 0.7);
}

/* Incoming page: covered from the very first paint. */
html.pc-arriving .pc-curtain { display: block; pointer-events: auto; }
html.pc-arriving body,
html.pc-leaving  body        { overflow: hidden; }

/* The preloader is suppressed for the life of an arrived-at page, not just
   while the curtain is up: it enforces its own 2.3s minimum, so releasing it
   when pc-arriving comes off would pop it open right after the curtain left.
   The curtain has already covered that load. */
html.pc-noloader #bl-loader { display: none !important; }

@media (prefers-reduced-motion: reduce) {
    .pc-curtain, html.pc-arriving .pc-curtain { display: none !important; }
    html.pc-arriving body { overflow: visible; }
}


/* ============================================================
   §12b — Off-screen animations are parked (js/enhancements.js §12)

   The class is set on the element that was observed; the decorations
   themselves are often its children or pseudo-elements, so the rule
   reaches all three. Paused, not cancelled: an animation resumes from
   where it stopped when the element scrolls back into view.
   ============================================================ */
.anim-idle,
.anim-idle::before,
.anim-idle::after,
.anim-idle *,
.anim-idle *::before,
.anim-idle *::after {
    animation-play-state: paused !important;
}


/* ============================================================
   §12 — Scroll performance (mobile)

   Decorative animations that animate background-position or box-shadow
   cannot be handled by the compositor, so each one repaints its element
   on every frame, forever, scrolling or not. They are paused only while
   a scroll is in flight and resume when it stops, so the design is
   unchanged at rest. Animations driven by transform or opacity are left
   alone: the compositor handles those for free.

   The navbar is left entirely alone here — blur, sheen and all.

   Its backdrop-filter looks like dead weight, because .nav-scroll
   .container is also painted at rgba(12,15,13,.94). But that rule sits in
   a (hover:hover) and (pointer:fine) block, so it is desktop-only: on a
   coarse pointer the container keeps its translucent gradient and the
   blur is visible design, not waste. Pausing the sheen mid-sweep is a
   visible change to the header too, so that stays put as well.
   ============================================================ */

@media (max-width: 768px) {
    html.is-scrolling .about-photo-glow,
    html.is-scrolling .avail-dot,
    html.is-scrolling .cform-pulse {
        animation-play-state: paused;
    }
}
