/* =====================================================================
   motion.css  —  the movement layer
   ---------------------------------------------------------------------
   Loaded after styles.css, so everything here is an override or an
   addition; nothing in the base sheet had to be edited.

   Four jobs:
     1. Entrances    — one shared reveal, staggered by --d from site.js
     2. Transitions  — page-to-page, and the live-content refresh
     3. Skeletons    — the beat between opening a project and its body
     4. Structure    — the corrections the new section order needs

   Everything is written so that removing this file leaves a working,
   if flatter, site. Reduced-motion is honoured at the bottom.
   ===================================================================== */

:root {
    --m-fast: .34s;
    --m-mid: .58s;
    --m-slow: .78s;
    --m-ease: cubic-bezier(.16, 1, .3, 1);
    --m-ease-soft: cubic-bezier(.33, 1, .68, 1);
}


/* =====================================================================
   1 · ENTRANCES
   The base sheet fades every revealed element over 1.2s with a 6px blur.
   That reads as sluggish once elements are staggered, because the last
   card in a row finishes almost two seconds after the first. Shorter
   travel, shorter blur, shorter duration — the stagger supplies the
   sense of sequence instead.
   ===================================================================== */

[data-reveal] {
    opacity: 0;
    transform: translate3d(0, 26px, 0);
    filter: blur(4px);
    transition:
        opacity var(--m-mid) var(--m-ease),
        transform var(--m-slow) var(--m-ease),
        filter var(--m-mid) var(--m-ease);
    transition-delay: var(--d, 0ms);
    will-change: opacity, transform;
}

[data-reveal].in {
    opacity: 1;
    transform: none;
    filter: blur(0);
}

/* Once an element has arrived it should stop advertising itself to the
   compositor — a page with forty promoted layers scrolls worse than one
   with none. */
[data-reveal].in {
    will-change: auto;
}

/* ── Scroll-linked blur ───────────────────────────────────────────────
   The rules above are the fallback: a one-shot transition fired by an
   observer. Where the browser supports scroll-driven animations, the
   entrance is driven by scroll position instead, which is a materially
   different thing — the element is genuinely out of focus as it comes up
   from the bottom of the viewport and resolves as it rises, and it tracks
   a fling, a scrub or a slow drag exactly rather than replaying a fixed
   1.2s curve whenever an observer happens to fire.

   Costs nothing per frame: no listener, no JS, and the compositor does
   the interpolation.

   The range ends at "entry 70%" rather than at full cover so an element
   is sharp well before it has finished entering. That also keeps the
   last block on the page safe — a range that needs more scroll than the
   document has left would leave it permanently soft.

   .in, added by the observer once the element is a quarter up the
   viewport, is the settled state and cancels the animation for good, so
   nothing re-blurs on the way back up. ─────────────────────────────── */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {

        [data-reveal]:not(.in) {
            opacity: 1;
            transform: none;
            filter: none;
            transition: none;
            animation: revealFocus linear both;
            animation-timeline: view();
            animation-range: entry 5% entry 70%;
        }

        @keyframes revealFocus {
            from {
                opacity: 0;
                transform: translate3d(0, 34px, 0);
                filter: blur(14px);
            }

            55% {
                opacity: 1;
            }

            to {
                opacity: 1;
                transform: none;
                filter: blur(0);
            }
        }

        /* Settled. No animation, no timeline, nothing left to recompute. */
        [data-reveal].in {
            animation: none;
            animation-timeline: auto;
            opacity: 1;
            transform: none;
            filter: none;
        }

        /* The hero is on screen at the first frame — there is no entry to
           track, so it keeps its own load animation. */
        .hero-lead .hero-grid>[data-reveal],
        .hero-intro>.shell>[data-reveal],
        .hero-intro .hero-intro-grid>[data-reveal] {
            animation-timeline: auto;
        }
    }
}

/* Section headings lead their own section by a beat. */
.sec-head[data-reveal] {
    transition-duration: var(--m-slow);
}

.sec-head .mono-label,
.sec-head .sec-title,
.sec-head .sec-sub {
    transform: translate3d(0, 14px, 0);
    opacity: 0;
    transition:
        opacity var(--m-mid) var(--m-ease),
        transform var(--m-mid) var(--m-ease);
}

.sec-head.in .mono-label {
    opacity: 1;
    transform: none;
    transition-delay: 40ms;
}

.sec-head.in .sec-title {
    opacity: 1;
    transform: none;
    transition-delay: 110ms;
}

.sec-head.in .sec-sub {
    opacity: 1;
    transform: none;
    transition-delay: 190ms;
}

/* The hero is the one block that must not wait for an observer — it is
   already in view on the first frame. It runs on load instead. */
.hero-lead .hero-grid>[data-reveal] {
    animation: heroRise .95s var(--m-ease) both;
    animation-delay: calc(var(--d, 0ms) + 60ms);
}

@keyframes heroRise {
    from {
        opacity: 0;
        transform: translate3d(0, 30px, 0);
        filter: blur(5px);
    }

    to {
        opacity: 1;
        transform: none;
        filter: blur(0);
    }
}

/* Cards pick up a touch of scale on the way in. Small — 0.985 to 1 — but
   it is the difference between "appeared" and "arrived". */
.proj[data-reveal],
.svc[data-reveal],
.skill-card[data-reveal],
.tst[data-reveal],
.stat[data-reveal] {
    transform: translate3d(0, 26px, 0) scale(.985);
}

.proj[data-reveal].in,
.svc[data-reveal].in,
.skill-card[data-reveal].in,
.tst[data-reveal].in,
.stat[data-reveal].in {
    transform: none;
}


/* =====================================================================
   2 · TRANSITIONS
   ===================================================================== */

/* ── Page to page ─────────────────────────────────────────────────────
   Two short halves: the outgoing page drops and dims for 170ms, the new
   one rises into place. The router times the swap to the midpoint. */

#main {
    transition:
        opacity var(--m-fast) var(--m-ease-soft),
        transform var(--m-fast) var(--m-ease-soft);
}

#main.page-leaving {
    opacity: 0;
    transform: translate3d(0, 10px, 0);
}

#main.page-entering {
    animation: pageIn var(--m-mid) var(--m-ease) both;
}

@keyframes pageIn {
    from {
        opacity: 0;
        transform: translate3d(0, 16px, 0);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* ── Live refresh ─────────────────────────────────────────────────────
   When the sheet returns newer projects or reviews, the section is
   replaced in place. It should read as an update, not a reload: a brief
   accent rule sweeps the block and the new content settles half a pixel.
   Anything already revealed stays revealed, so nothing re-animates. */

.is-refreshed {
    animation: sectionRefresh .9s var(--m-ease) both;
    position: relative;
}

@keyframes sectionRefresh {
    from {
        opacity: .55;
        transform: translate3d(0, 4px, 0);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

.is-refreshed::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 1px;
    background: linear-gradient(90deg, var(--bg-0), var(--accent), var(--bg-0));
    opacity: 0;
    animation: refreshSweep .9s var(--m-ease-soft) both;
    pointer-events: none;
}

@keyframes refreshSweep {
    0% {
        opacity: 0;
        transform: scaleX(.2);
    }

    35% {
        opacity: .8;
    }

    100% {
        opacity: 0;
        transform: scaleX(1);
    }
}

/* ── Hover ────────────────────────────────────────────────────────────
   The base sheet already lifts cards. This only smooths the curve and
   makes the two halves of the movement agree with each other. */

.proj,
.svc,
.skill-card,
.tst,
.stat {
    transition:
        transform var(--m-fast) var(--m-ease-soft),
        border-color var(--m-fast) var(--m-ease-soft),
        box-shadow var(--m-fast) var(--m-ease-soft),
        background var(--m-fast) var(--m-ease-soft);
}

@media (hover: hover) and (pointer: fine) {

    .btn {
        transition:
            transform var(--m-fast) var(--m-ease-soft),
            background var(--m-fast) var(--m-ease-soft),
            border-color var(--m-fast) var(--m-ease-soft),
            color var(--m-fast) var(--m-ease-soft);
    }

    .btn:active {
        transform: translateY(1px) scale(.99);
    }

    /* A single pass of light across the primary button on hover. */
    .btn-accent {
        position: relative;
        overflow: hidden;
        isolation: isolate;
    }

    .btn-accent::after {
        content: "";
        position: absolute;
        inset: 0;
        z-index: -1;
        background: linear-gradient(115deg,
                transparent 30%,
                rgba(255, 255, 255, .38) 50%,
                transparent 70%);
        transform: translateX(-120%);
        transition: transform .7s var(--m-ease-soft);
    }

    .btn-accent:hover::after {
        transform: translateX(120%);
    }
}

/* Anchor jumps inside the page shouldn't fight Lenis. */
html {
    scroll-behavior: auto;
}


/* =====================================================================
   3 · SKELETONS
   Only one place needs them: a project opened cold, where the body text
   is fetched separately from the card data.
   ===================================================================== */

.sk-block {
    display: grid;
    gap: .85rem;
}

.sk-line {
    display: block;
    height: .8rem;
    border-radius: var(--r-xs);
    background: linear-gradient(90deg,
            var(--surface) 0%,
            var(--surface-2) 45%,
            var(--surface) 90%);
    background-size: 220% 100%;
    animation: skPulse 1.5s var(--m-ease-soft) infinite;
}

.sk-line.sk-head {
    height: 1.35rem;
    margin-top: 1.4rem;
    background: linear-gradient(90deg,
            var(--surface-2) 0%,
            var(--line) 45%,
            var(--surface-2) 90%);
    background-size: 220% 100%;
}

.sk-block .sk-line:first-child {
    margin-top: 0;
}

@keyframes skPulse {
    from {
        background-position: 120% 0;
    }

    to {
        background-position: -120% 0;
    }
}


/* =====================================================================
   4 · STRUCTURE
   Corrections the new running order needs. The old layout opened with a
   portrait-led "about" hero, so the base sheet made #about behave like a
   hero: no eyebrow, display type at seven rem. About is now the seventh
   block on the page and has to sit in the same rhythm as its neighbours.
   ===================================================================== */

/* The numbered eyebrow comes back — it is how the page is counted. */
#about .sec-head .mono-label {
    display: inline-flex;
}

/* And the heading returns to section scale. */
#about .sec-title em,
[data-theme="light"] #about .sec-title em {
    font-size: inherit;
    font-weight: 700;
}

/* The terminal hero is now the only hero, so the copy-centring rules that
   were bound to #hero-2 are rebound to the class instead — the id is free
   to change again without taking the mobile layout with it. */
@media (max-width: 1020px) {

    .hero-lead .hero-grid>[data-reveal]:first-child {
        text-align: center;
    }

    .hero-lead .hero-sub {
        margin-inline: auto;
    }

    .hero-lead .hero-cta,
    .hero-lead .hero-trust {
        justify-content: center;
    }
}

/* A hairline between blocks, so eleven sections read as a sequence rather
   than one continuous field. Drawn on the section itself — not inside the
   shell, which is a grid container in several places and would gain a
   stray cell. */
main>.section+.section {
    border-top: 1px solid var(--line-soft);
}

/* Where the browser supports scroll-driven animation the line draws
   itself as the section arrives. Everywhere else it is simply there. */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        main>.section+.section {
            border-top-color: transparent;
            background-image: linear-gradient(90deg, var(--bg-0), var(--line), var(--bg-0));
            background-repeat: no-repeat;
            background-size: 100% 1px;
            background-position: 0 0;
            animation: ruleDraw 1s var(--m-ease) both;
            animation-timeline: view();
            animation-range: entry 0% cover 18%;
        }

        @keyframes ruleDraw {
            from {
                background-size: 0% 1px;
            }

            to {
                background-size: 100% 1px;
            }
        }
    }
}


/* =====================================================================
   5 · HERO (INTRO)
   The portrait-led opener. Headline across the top with the name at
   display scale, then a two-column grid: photo card, statement of
   approach, four proof badges, the two calls to action.

   Built on the existing tokens, so it repaints correctly in both themes
   and picks up any palette change made in ⚙ Config. No new webfont: the
   headline uses --ff-display, which is already loaded.
   ===================================================================== */

.hero-intro {
    min-height: auto;
    padding-block: clamp(3.5rem, 7vh, 6rem) clamp(3rem, 7vh, 5.5rem);
    display: block;
}

.hero-intro>.shell {
    position: relative;
}

/* ── Headline ─────────────────────────────────────────────────────────
   "I am *Shawon*, automation engineer" — the asterisks in Intro Heading
   become the <em>, which is the part that gets the size and the accent. */
.hero-intro-title {
    /* Not var(--heading): on the light palette that token is a mid lavender
       tuned for section titles over a tinted band, and at hero scale
       against white it reads as washed out. */
    color: var(--text);
    font-family: var(--ff-display);
    font-weight: 500;
    font-size: clamp(2rem, 4.4vw, 3.5rem);
    line-height: 1.14;
    letter-spacing: -.015em;
    margin: 0 0 clamp(2.2rem, 4vw, 3.4rem);
    max-width: 20ch;
    text-wrap: balance;
}

.hero-intro-title em {
    font-style: normal;
    font-weight: 800;
    color: var(--accent);
    /* Set against the line rather than in absolute terms, so the name
       scales with the headline at every breakpoint instead of needing
       its own clamp. */
    font-size: 1.55em;
    line-height: .9;
    letter-spacing: -.03em;
}

[data-theme="light"] .hero-intro-title em {
    color: #4c0585;
}

/* ── Grid ─────────────────────────────────────────────────────────── */
.hero-intro-grid {
    display: grid;
    grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr);
    gap: clamp(2rem, 4vw, 3.4rem);
    align-items: start;
}

/* ── Photo card ───────────────────────────────────────────────────── */
.photo-card {
    position: relative;
    margin: 0;
    border-radius: var(--r-lg);
    overflow: hidden;
    aspect-ratio: 3 / 3.7;
    background: var(--surface);
    border: 1px solid var(--line);
    box-shadow: var(--card-sh);
}

.photo-card img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 1.1s var(--m-ease);
}

@media (hover: hover) and (pointer: fine) {
    .photo-card:hover img {
        transform: scale(1.035);
    }
}

/* The scrim only needs to exist where the caption sits. Running it to
   the top would grey the portrait for no reason. */
.photo-card::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top,
            rgba(0, 0, 0, .74) 0%,
            rgba(0, 0, 0, .16) 38%,
            rgba(0, 0, 0, 0) 62%);
    pointer-events: none;
}

.photo-card figcaption {
    position: absolute;
    left: clamp(1rem, 2vw, 1.4rem);
    right: clamp(1rem, 2vw, 1.4rem);
    bottom: clamp(1rem, 2vw, 1.4rem);
    z-index: 2;
    display: grid;
    gap: .45rem;
}

/* The tag sits on the dark scrim at the foot of the portrait, so it needs
   a colour that works against black in BOTH palettes — the light theme's
   accent is a deep purple and disappears there. */
.photo-tag {
    font-family: var(--ff-mono);
    font-weight: 600;
    font-size: .69rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--accent);
}

[data-theme="light"] .photo-tag {
    color: #c9a6ff;
}

.photo-name {
    font-family: var(--ff-display);
    font-weight: 600;
    font-size: 1.35rem;
    line-height: 1.1;
    color: #fff;
}

.photo-void {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-family: var(--ff-display);
    font-size: 4rem;
    color: var(--accent);
    background: var(--accent-dim);
}

/* ── Approach ─────────────────────────────────────────────────────── */
.approach h2 {
    font-family: var(--ff-display);
    font-weight: 700;
    font-size: clamp(1.4rem, 2.3vw, 1.85rem);
    line-height: 1.15;
    margin: 0 0 1.1rem;
    color: var(--accent);
}

[data-theme="light"] .approach h2 {
    color: #4c0585;
}

.approach p {
    font-size: 1rem;
    line-height: 1.72;
    color: var(--muted);
    margin: 0 0 1rem;
    max-width: 62ch;
}

.approach p b,
.approach p strong {
    color: var(--text);
    font-weight: 600;
}

/* ── Badges ───────────────────────────────────────────────────────── */
.badge-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .75rem;
    margin: 1.7rem 0 2rem;
    max-width: 44rem;
}

.badge {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .85rem 1rem;
    border-radius: var(--r-sm);
    background: var(--surface);
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    transition:
        transform var(--m-fast) var(--m-ease-soft),
        border-color var(--m-fast) var(--m-ease-soft),
        background var(--m-fast) var(--m-ease-soft);
}

@media (hover: hover) and (pointer: fine) {
    .badge:hover {
        transform: translateY(-2px);
        background: var(--surface-2);
    }
}

.badge-tick {
    flex: none;
    display: grid;
    place-items: center;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 50%;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: .6rem;
}

.badge span:last-child {
    font-size: .875rem;
    line-height: 1.35;
    color: var(--text);
}

/* ── Entrance ─────────────────────────────────────────────────────── */
/* The hero is in view on the first frame, so it runs on load rather than
   waiting for an observer — same treatment the terminal hero gets. */
.hero-intro>.shell>[data-reveal],
.hero-intro .hero-intro-grid>[data-reveal] {
    animation: heroRise .95s var(--m-ease) both;
    animation-delay: calc(var(--d, 0ms) + 60ms);
}

.hero-intro .badge {
    animation: heroRise .8s var(--m-ease) both;
    animation-delay: calc(var(--d, 0ms) + 200ms);
}

/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 1020px) {
    .hero-intro-grid {
        grid-template-columns: 1fr;
    }

    .photo-card {
        aspect-ratio: 4 / 3.4;
        max-width: 26rem;
    }
}

@media (max-width: 620px) {
    .hero-intro-title {
        max-width: none;
    }

    .hero-intro-title em {
        font-size: 1.4em;
    }

    .badge-grid {
        grid-template-columns: 1fr;
    }

    .photo-card {
        max-width: none;
        aspect-ratio: 4 / 4.2;
    }
}


/* =====================================================================
   6 · ABOUT
   The counterpart to the hero, and deliberately built from a different
   set of parts: a layered media column with glass plates reading over
   it, a story, a numbered account of the work, a closing line.

   The glass is real backdrop-filter where it exists and a solid surface
   where it does not — see the @supports block at the end of this
   section. Nothing here is load-bearing for legibility: every plate has
   its own background, so an unsupported filter costs texture, not text.
   ===================================================================== */

.about-x {
    position: relative;
    overflow: clip;
}

.about-x-grid {
    display: grid;
    grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr);
    gap: clamp(2rem, 4.5vw, 4rem);
    align-items: start;
}

/* ── Media column ─────────────────────────────────────────────────── */
.ax-media {
    position: sticky;
    top: calc(var(--hdr-h, 5rem) + 2rem);
    display: grid;
    gap: 1rem;
}

.ax-frame {
    position: relative;
    margin: 0;
    border-radius: var(--r-lg);
    overflow: hidden;
    aspect-ratio: 4 / 4.7;
    background: var(--surface);
    border: 1px solid var(--line);
    box-shadow: var(--card-sh);
    isolation: isolate;
}

/* The drift, written by axFrame(). A transition would fight the scroll,
   so the value is applied raw and the smoothing comes from the frame
   rate. */
.ax-photo {
    position: absolute;
    inset: -3.5% 0;
    width: 100%;
    height: 107%;
    object-fit: cover;
    transform: translate3d(0, var(--ax-shift, 0px), 0);
    will-change: transform;
}

.ax-void {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-family: var(--ff-display);
    font-size: 4.5rem;
    color: var(--accent);
    background: var(--accent-dim);
}

/* A soft accent bloom behind the plate, so the glass has something to
   refract rather than sitting on flat photography. */
.ax-glow {
    position: absolute;
    z-index: 1;
    left: -20%;
    right: -20%;
    bottom: -35%;
    height: 70%;
    background: radial-gradient(60% 60% at 50% 60%, var(--accent-dim), transparent 70%);
    filter: blur(28px);
    opacity: .9;
    pointer-events: none;
}

/* ── Glass plate ──────────────────────────────────────────────────── */
.ax-plate {
    position: absolute;
    z-index: 2;
    left: .8rem;
    right: .8rem;
    bottom: .8rem;
    display: grid;
    gap: .3rem;
    padding: .9rem 1rem;
    border-radius: var(--r);
    background: rgba(20, 22, 26, .52);
    border: 1px solid rgba(255, 255, 255, .16);
    backdrop-filter: blur(16px) saturate(140%);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    box-shadow: 0 10px 30px -12px rgba(0, 0, 0, .5);
    color: #fff;
}

.ax-plate b {
    font-family: var(--ff-display);
    font-size: 1.02rem;
    font-weight: 600;
    line-height: 1.2;
}

.ax-plate small {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    font-size: .78rem;
    opacity: .78;
}

.ax-live {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    font-family: var(--ff-mono);
    font-size: .64rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--accent);
}

[data-theme="light"] .ax-live {
    color: #d7b8ff;
}

.ax-pip {
    width: .42rem;
    height: .42rem;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 0 0 currentColor;
    animation: axPip 2.4s var(--m-ease-soft) infinite;
}

@keyframes axPip {
    0% {
        box-shadow: 0 0 0 0 currentColor;
        opacity: 1;
    }

    70% {
        box-shadow: 0 0 0 .5rem transparent;
        opacity: .75;
    }

    100% {
        box-shadow: 0 0 0 0 transparent;
        opacity: 1;
    }
}

/* ── Figures ──────────────────────────────────────────────────────── */
.ax-facts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .65rem;
}

.ax-fact {
    padding: .8rem .9rem;
    border-radius: var(--r-sm);
    background: color-mix(in srgb, var(--surface) 72%, transparent);
    border: 1px solid var(--line);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    /* Each chip clears its own blur on the way in — the section resolves
       rather than simply appearing. */
    animation: axClear .8s var(--m-ease) both;
    animation-delay: var(--d, 0ms);
}

@keyframes axClear {
    from {
        opacity: 0;
        transform: translate3d(0, 14px, 0);
        filter: blur(8px);
    }

    to {
        opacity: 1;
        transform: none;
        filter: blur(0);
    }
}

.ax-fact b {
    display: block;
    font-family: var(--ff-display);
    font-size: 1.5rem;
    line-height: 1;
    color: var(--accent);
}

[data-theme="light"] .ax-fact b {
    color: #4c0585;
}

.ax-fact span {
    display: block;
    margin-top: .3rem;
    font-size: .74rem;
    line-height: 1.35;
    color: var(--muted);
}

/* ── Story ────────────────────────────────────────────────────────── */
.ax-lead {
    font-family: var(--ff-display);
    font-size: clamp(1.15rem, 1.9vw, 1.5rem);
    font-weight: 500;
    line-height: 1.45;
    letter-spacing: -.01em;
    color: var(--text);
    margin: 0 0 1.4rem;
    max-width: 40ch;
    text-wrap: pretty;
}

.ax-lead em {
    font-style: normal;
    color: var(--accent);
}

[data-theme="light"] .ax-lead em {
    color: #4c0585;
}

.ax-story p {
    font-size: .98rem;
    line-height: 1.75;
    color: var(--muted);
    margin: 0 0 .95rem;
    max-width: 64ch;
}

.ax-story strong {
    color: var(--text);
    font-weight: 600;
}

/* ── Process ──────────────────────────────────────────────────────── */
.ax-process {
    margin: 2.2rem 0 1.8rem;
    padding-top: 1.6rem;
    border-top: 1px solid var(--line-soft);
}

.ax-process h3 {
    font-family: var(--ff-mono);
    font-size: .72rem;
    font-weight: 500;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--faint);
    margin: 0 0 1.1rem;
}

.ax-process ol {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: .5rem;
}

.ax-process li {
    display: flex;
    gap: 1rem;
    padding: .85rem .95rem;
    border-radius: var(--r-sm);
    border: 1px solid transparent;
    transition:
        background var(--m-fast) var(--m-ease-soft),
        border-color var(--m-fast) var(--m-ease-soft),
        transform var(--m-fast) var(--m-ease-soft);
}

@media (hover: hover) and (pointer: fine) {
    .ax-process li:hover {
        background: color-mix(in srgb, var(--surface) 80%, transparent);
        border-color: var(--line);
        transform: translateX(3px);
    }
}

.ax-step {
    flex: none;
    font-family: var(--ff-mono);
    font-size: .75rem;
    font-weight: 700;
    line-height: 1.7;
    color: var(--accent);
    opacity: .8;
}

[data-theme="light"] .ax-step {
    color: #4c0585;
}

.ax-process li b {
    display: block;
    font-size: .95rem;
    font-weight: 600;
    color: var(--text);
    margin-bottom: .2rem;
}

.ax-process li p {
    margin: 0;
    font-size: .86rem;
    line-height: 1.6;
    color: var(--muted);
    max-width: 56ch;
}

/* ── Closing line ─────────────────────────────────────────────────── */
.ax-signature {
    margin: 0 0 1.8rem;
    padding: 1.1rem 1.3rem;
    border-radius: var(--r);
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    background: color-mix(in srgb, var(--surface) 66%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
}

.ax-signature p {
    margin: 0 0 .5rem;
    font-family: var(--ff-display);
    font-size: 1.02rem;
    font-style: italic;
    line-height: 1.55;
    color: var(--text);
}

.ax-signature em {
    font-style: normal;
    color: var(--accent);
}

[data-theme="light"] .ax-signature em {
    color: #4c0585;
}

.ax-signature cite {
    font-family: var(--ff-mono);
    font-size: .68rem;
    font-style: normal;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--faint);
}

.ax-cta {
    display: flex;
    flex-wrap: wrap;
    gap: .7rem;
}

/* ── Without backdrop-filter ──────────────────────────────────────── */
/* The plates carry text, so they get a solid surface rather than a
   translucent one that would leave the photo showing through. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {

    .ax-plate {
        background: rgba(16, 19, 23, .88);
    }

    .ax-fact,
    .ax-signature {
        background: var(--surface);
    }
}

/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 1020px) {
    .about-x-grid {
        grid-template-columns: 1fr;
    }

    .ax-media {
        position: static;
        max-width: 30rem;
    }

    .ax-frame {
        aspect-ratio: 4 / 3.6;
    }
}

@media (max-width: 620px) {
    .ax-media {
        max-width: none;
    }

    .ax-facts {
        grid-template-columns: 1fr 1fr;
    }

    .ax-process li {
        padding-inline: 0;
    }
}


/* ── Heading staging, under the scroll-linked path ────────────────────
   The three-beat entrance on a section heading keys off .in, which the
   observer may never add if the visitor jumps down the page by anchor —
   and an unset .in there leaves the heading invisible, not merely
   unanimated. Where the scroll-linked path is running, the header itself
   carries the entrance and the children start settled. Nesting a second
   blurred layer inside a blurred one would have cost twice as much to
   composite anyway.

   Declared here rather than beside the rest of the @supports block
   because it has to outrank the staging rules above it, and it has the
   same specificity as they do. */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {

        .sec-head .mono-label,
        .sec-head .sec-title,
        .sec-head .sec-sub {
            opacity: 1;
            transform: none;
        }
    }
}

/* ── Heading size is not an entrance ──────────────────────────────────
   styles.css puts the display size of a centred section title behind
   .sec-head.center.in, so a heading whose observer never fired renders a
   size smaller than the ones around it — visible if you jump down the
   page by anchor rather than scrolling. The size is unhooked from the
   class here; the entrance is still the entrance. */
.sec-head.center em {
    font-size: clamp(3.5rem, 4vw, 4.4rem);
    font-weight: 900;
}

[data-theme="light"] .sec-head.center em {
    color: #4c0585;
}


/* =====================================================================
   REDUCED MOTION
   Nothing above is load-bearing: every animated property has a resting
   state that is also its correct final state, so this can switch the
   whole layer off without leaving anything invisible.
   ===================================================================== */

@media (prefers-reduced-motion: reduce) {

    [data-reveal],
    [data-reveal].in,
    .sec-head .mono-label,
    .sec-head .sec-title,
    .sec-head .sec-sub {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
        transition: none !important;
    }

    [data-reveal]:not(.in),
    .hero-lead .hero-grid>[data-reveal],
    .hero-intro>.shell>[data-reveal],
    .hero-intro .hero-intro-grid>[data-reveal],
    .hero-intro .badge,
    .ax-fact,
    .is-refreshed,
    #main.page-entering {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }

    #main.page-leaving {
        opacity: 1;
        transform: none;
    }

    .is-refreshed::before {
        display: none;
    }

    .sk-line,
    .ax-pip {
        animation: none;
    }

    /* The drift is written as a custom property, so it is cancelled here
       rather than in JS — belt and braces with the media-query guard in
       aboutParallax(). */
    .ax-photo {
        transform: none !important;
    }

    .ax-media {
        position: static;
    }

    .btn-accent::after {
        display: none;
    }
}
