/* ==========================================================================
   adventure.css — Scroll choreography for "The System Underneath"
   Owner: pc5 (task 004, round 1). Built against the Markup API in
   specs/team-contract.md. This sheet NEVER assumes markup beyond that spec.

   Progressive-enhancement ladder (docs/ADVENTURE-DIRECTION.md):
     1. No JS, no modern CSS -> everything is visible, unstyled by this sheet's
        hidden states. Nothing here may hide content unconditionally.
     2. animation-timeline: view() supported -> native scroll-driven reveals.
     3. Not supported + JS present -> JS sets html.no-sda, IO toggles .revealed.
   Rule that enforces it: every "hidden" start state lives EITHER inside an
   @supports (animation-timeline: ...) block OR behind html.no-sda. A browser
   that satisfies neither never hides anything.

   Animation budget: opacity / translate / transform / filter only. No
   scroll-event hooks, no layout-animating properties.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Tokens + overflow guard
   -------------------------------------------------------------------------- */

:root {
    /* Reveal geometry. Kept small so inline variants never push wide elements
       far past the viewport edge (see the overflow guard directly below). */
    --rv-shift: 1.75rem;
    --rv-duration: 0.6s;
    --rv-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
    --rv-delay-step: 60ms;



    --micro-duration: 200ms; /* card lift / press feedback, 150-250ms band */
}

/* [data-reveal="start"|"end"] translates elements along the inline axis. On a
   full-bleed element that momentarily extends past the viewport edge and would
   raise a horizontal scrollbar at narrow widths.

   `clip` (not `hidden`) is load-bearing: overflow:hidden on the root creates a
   scroll container, which silently kills every position:sticky descendant --
   `clip` clips without creating a scroll container. */
html {
    overflow-x: clip;
}

/* --------------------------------------------------------------------------
   1. Reveal system
   Contract: data-reveal="fade|up|start|end" + data-reveal-delay="1".."5"
   -------------------------------------------------------------------------- */

/* Reveals animate the `translate` property, NOT `transform`. Cards already use
   `transform` for their hover lift (styles.css); keeping them on separate
   properties lets a card be revealed AND hovered without either clobbering the
   other. Both still composite on the GPU. */

@keyframes rv-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes rv-up {
    from { opacity: 0; translate: 0 var(--rv-shift); }
    to   { opacity: 1; translate: 0 0; }
}

/* Dual keyframes for the inline axis. `translate` takes no logical keyword, so
   direction is resolved by swapping animation-name under [dir="rtl"] rather
   than by flipping a value inside the keyframes (custom properties are not
   reliably interpolated inside @keyframes across engines). */
@keyframes rv-from-inline-start {
    from { opacity: 0; translate: calc(-1 * var(--rv-shift)) 0; }
    to   { opacity: 1; translate: 0 0; }
}

@keyframes rv-from-inline-end {
    from { opacity: 0; translate: var(--rv-shift) 0; }
    to   { opacity: 1; translate: 0 0; }
}

/* --- 1a. Native path: scroll-driven, no JS required ---------------------- */

@supports (animation-timeline: view()) {
    /* html:not(.no-sda) is belt-and-braces: JS only adds .no-sda when support
       is absent, so the two paths are already mutually exclusive. */
    html:not(.no-sda) [data-reveal] {
        animation-name: rv-fade;
        animation-duration: 1s; /* ignored for progress-based timelines */
        animation-timing-function: linear; /* scrubbed by scroll, not eased */
        animation-fill-mode: both; /* holds the from-state before entry,
                                      the to-state after the range ends */
        animation-timeline: view();
        animation-range: entry var(--rv-range-start, 0%) cover var(--rv-range-end, 40%);
    }

    html:not(.no-sda) [data-reveal="up"]    { animation-name: rv-up; }
    html:not(.no-sda) [data-reveal="start"] { animation-name: rv-from-inline-start; }
    html:not(.no-sda) [data-reveal="end"]   { animation-name: rv-from-inline-end; }

    [dir="rtl"] [data-reveal="start"] { animation-name: rv-from-inline-end; }
    [dir="rtl"] [data-reveal="end"]   { animation-name: rv-from-inline-start; }

    /* Stagger. A time-based animation-delay is meaningless on a scroll
       timeline, so the delay steps push the animation-range start later
       instead: a group of siblings resolves in sequence as it scrolls in. */
    [data-reveal-delay="1"] { --rv-range-start: 8%;  --rv-range-end: 46%; }
    [data-reveal-delay="2"] { --rv-range-start: 16%; --rv-range-end: 52%; }
    [data-reveal-delay="3"] { --rv-range-start: 24%; --rv-range-end: 58%; }
    [data-reveal-delay="4"] { --rv-range-start: 32%; --rv-range-end: 64%; }
    [data-reveal-delay="5"] { --rv-range-start: 40%; --rv-range-end: 70%; }
}

/* --- 1b. Fallback path: html.no-sda + IntersectionObserver -> .revealed --- */

html.no-sda [data-reveal] {
    opacity: 0;
    /* Deliberately NO transition on the hidden state. A transition here would
       also animate the 1 -> 0 hide that happens the moment JS adds .no-sda,
       so content would visibly fade OUT before fading back in. Transitions are
       read from the destination style, so declaring them on .revealed alone
       makes hiding instant while the reveal itself still animates. */
}

html.no-sda [data-reveal="up"]    { translate: 0 var(--rv-shift); }
html.no-sda [data-reveal="start"] { translate: calc(-1 * var(--rv-shift)) 0; }
html.no-sda [data-reveal="end"]   { translate: var(--rv-shift) 0; }

/* Inline variants flip with the document direction. These are plain
   declarations, so a value swap is safe here (unlike inside @keyframes).

   Both selector forms are required. The site sets direction on the root
   (document.documentElement.dir = 'rtl'), so html carries BOTH .no-sda and
   [dir] and they must be written as a compound with no descendant combinator;
   the spaced form additionally covers a dir set on a container further down. */
html.no-sda[dir="rtl"] [data-reveal="start"],
html.no-sda [dir="rtl"] [data-reveal="start"] { translate: var(--rv-shift) 0; }
html.no-sda[dir="rtl"] [data-reveal="end"],
html.no-sda [dir="rtl"] [data-reveal="end"]   { translate: calc(-1 * var(--rv-shift)) 0; }

html.no-sda [data-reveal].revealed {
    opacity: 1;
    translate: 0 0;
    transition:
        opacity var(--rv-duration) var(--rv-ease),
        translate var(--rv-duration) var(--rv-ease);
    transition-delay: calc(var(--rv-delay, 0) * var(--rv-delay-step));
}

html.no-sda [data-reveal-delay="1"] { --rv-delay: 1; }
html.no-sda [data-reveal-delay="2"] { --rv-delay: 2; }
html.no-sda [data-reveal-delay="3"] { --rv-delay: 3; }
html.no-sda [data-reveal-delay="4"] { --rv-delay: 4; }
html.no-sda [data-reveal-delay="5"] { --rv-delay: 5; }

/* --------------------------------------------------------------------------
   2. Scroll progress bar — div#scroll-progress (decorative, aria-hidden)
   -------------------------------------------------------------------------- */

/* Rendered ONLY where it can be driven natively. Under no-sda it is hidden
   outright: a scroll-event polyfill would violate the INP budget (no scroll
   handlers doing layout reads) for a purely decorative affordance. */
#scroll-progress {
    display: none;
}

@supports (animation-timeline: scroll()) {
    html:not(.no-sda) #scroll-progress {
        display: block;
        position: fixed;
        inset-block-start: 0;
        inset-inline: 0;
        height: 2px;
        z-index: 1100; /* above .site-header (1000) */
        background: var(--accent);
        transform: scaleX(0);
        transform-origin: left center;
        animation-name: sp-grow;
        animation-duration: 1s; /* ignored for progress-based timelines */
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: scroll(root);
    }

    /* transform-origin takes no logical keyword; the bar must grow from the
       inline-start edge, which is the right edge in RTL. */
    [dir="rtl"] #scroll-progress {
        transform-origin: right center;
    }
}

@keyframes sp-grow {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
}

/* styles.css draws a text-decoration underline on .learn-more:hover; that
   would double up with the animated rule above. */
.portfolio-card .learn-more:hover {
    text-decoration: none;
}

/* --- 5b. Card lift ------------------------------------------------------- */

/* styles.css animates `all 0.3s` on these cards -- `all` also transitions the
   border/background repaints and 300ms sits above the 150-250ms band that
   reads as responsive. Narrowed to the two properties that carry the lift. */
.portfolio-card,
.service-card {
    transition:
        transform var(--micro-duration) var(--rv-ease),
        box-shadow var(--micro-duration) var(--rv-ease),
        border-color var(--micro-duration) var(--rv-ease);
}

.portfolio-card:hover,
.portfolio-card:focus-within {
    transform: translateY(-2px);
    box-shadow: var(--card-shadow-hover);
}

.service-card:hover,
.service-card:focus-within {
    transform: translateY(-2px);
}

.button:active,
.submit-button:active {
    transform: translateY(1px) scale(0.985);
    box-shadow: none;
}

/* --------------------------------------------------------------------------
   6. Portfolio scale-of-outcome
   -------------------------------------------------------------------------- */

/* "Flagship" = a project with a case study behind it -- exactly the five in
   the contract's slug list. Selecting on the link rather than on .featured is
   deliberate: .featured marks three cards, and the set that owns a case study
   is the one the direction calls flagship. :has() also keeps this correct
   through pc2's markup restructure without needing a new class. */
.portfolio-card--flagship,
.portfolio-card:has(a[href^="/case-studies/"]) {
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.portfolio-card--flagship .card-visual,
.portfolio-card:has(a[href^="/case-studies/"]) .card-visual {
    height: 200px;
}

/* Two-column span only once the grid is genuinely wide enough for it. Below
   this breakpoint a spanning item would force a second 280px track into a
   viewport narrower than 560px and blow out horizontally. */
@media (min-width: 960px) {
    .portfolio-card--flagship,
    .portfolio-card:has(a[href^="/case-studies/"]) {
        grid-column: span 2;
    }

    /* dense back-fills the single-width cards into the gaps a spanning item
       leaves behind, so the grid keeps no holes. */
    .section .portfolio-grid {
        grid-auto-flow: dense;
    }
}

/* Overflow-proof track sizing. `min(280px, 100%)` keeps the track from ever
   demanding more than the container at 320px. Scoped with .section so the
   rule wins on specificity rather than on stylesheet order (pc2 owns the
   <head>, so link order is not this sheet's to guarantee). */
.section .portfolio-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
}

/* 9 tiles = 5 span-2 flagships + 4 singles — an area (14) no column count
   divides, so dense flow always strands one empty cell (the r4 CRM merge cut
   the singles from 5 to 4). The 5th flagship spanning the full row makes the
   remainder 12 + a closing banner row: hole-free at 2, 3 and 4 columns, and
   a no-op in the 1-column stack. */
.section .portfolio-grid > .portfolio-card:nth-child(5) {
    grid-column: 1 / -1;
}

/* --------------------------------------------------------------------------
   7. Count-up / stat numerals
   -------------------------------------------------------------------------- */

/* The HTML carries the real formatted value; JS only animates 0 -> final. Both
   guards below keep that morph from moving the page:
     - tabular-nums: every digit occupies one advance width, so 8 -> 1 does not
       reflow the number.
     - min-width in ch: the box is already as wide as the final value before
       the count starts, so a widening number cannot shift its neighbours. */
.stat-number,
.count-up {
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum' 1;
    display: inline-block;
    min-width: 6ch; /* widest real value is "96,000" = 6 characters */
    text-align: center;
}

/* --------------------------------------------------------------------------
   8. Reduced motion
   Convention (styles.css): collapse to opacity/instant, never hide content.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    /* Reveals resolve to their final state immediately, on BOTH paths.
       !important is required to beat the @supports/no-sda start states. */
    [data-reveal] {
        animation: none !important;
        opacity: 1 !important;
        translate: none !important;
        transition: none !important;
    }

    /* Decorative and scroll-coupled -- nothing is lost by dropping it. */
    #scroll-progress {
        display: none !important;
    }

    .portfolio-card,
    .service-card,
    .button,
    .submit-button {
        transition: none !important;
    }

    .portfolio-card:hover,
    .portfolio-card:focus-within,
    .service-card:hover,
    .service-card:focus-within,
    .button:active,
    .submit-button:active {
        transform: none;
    }
}

/* --------------------------------------------------------------------------
   9. View transitions — bind the cross-document card -> case-study morph
   Finding 14. The markup carries data-vt="vt-<part>-<slug>" on BOTH sides:
   the portfolio card icon + title, the case-study hero mark + h1, and the
   case-next teaser at the foot of each study. This turns each data-vt value
   into the matching view-transition-name, so the paired boxes morph across
   the navigation instead of hard-cutting. The navigation trigger itself
   (@view-transition { navigation: auto }) lives in fonts.css (pc2).

   Each page shows any given slug at most once per part, so the names are
   unique per document (a hard requirement — duplicate names abort the
   transition). Where the View Transition API is unsupported the property is
   simply ignored and the page navigates as before: progressive enhancement,
   no JS involved.
   -------------------------------------------------------------------------- */

[data-vt="vt-img-ralph-loops"]           { view-transition-name: vt-img-ralph-loops; }
[data-vt="vt-title-ralph-loops"]         { view-transition-name: vt-title-ralph-loops; }
[data-vt="vt-img-showpulse"]             { view-transition-name: vt-img-showpulse; }
[data-vt="vt-title-showpulse"]           { view-transition-name: vt-title-showpulse; }
[data-vt="vt-img-dg-logistic"]           { view-transition-name: vt-img-dg-logistic; }
[data-vt="vt-title-dg-logistic"]         { view-transition-name: vt-title-dg-logistic; }
[data-vt="vt-img-dg-media-search"]       { view-transition-name: vt-img-dg-media-search; }
[data-vt="vt-title-dg-media-search"]     { view-transition-name: vt-title-dg-media-search; }
[data-vt="vt-img-operations-calendar"]   { view-transition-name: vt-img-operations-calendar; }
[data-vt="vt-title-operations-calendar"] { view-transition-name: vt-title-operations-calendar; }

/* Tasteful timing. The UA interpolates each paired group's position and size
   (the morph) and cross-fades the outgoing/incoming snapshots on opacity. We
   only pull the duration into the 150-300ms band and share the sheet's ease;
   no layout-animating property is touched — opacity + transform only. */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
    animation-duration: 220ms;
    animation-timing-function: var(--rv-ease);
}

/* Inert under reduced motion: kill the group morph and the fade so the swap is
   instant. The pages still transition — they just cut, with nothing moving. */
@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}
