/* ===========================================================================
   INTERACTION GRAMMAR — loaded last, owns every hover / press / swap state.
   Behaviour ported from the quadcode.ai interaction contract
   (meta/plans/quadcode-interactions.md). Timings and curves only: colours,
   type and geometry stay ours.

   THE RULES THIS FILE ENFORCES
   L1  animate transform / opacity / filter only
   L2  never `transition: all` — every property is named
   L4  hover is gated by INPUT CAPABILITY, never by width
   L7  no layout shift on interaction: borders are always painted,
       padding is unconditional, variants swap inside one grid cell
   =========================================================================== */

/* ---------- 1. Press: the universal affordance ----------
   120ms is the window where feedback reads as "pressed": faster is a glitch,
   slower is lag. 0.97, not 0.9 — pressed, not squashed. */
:where(.press,
  .btn,
  .portfolio-tabs button,
  .project-open,
  .project-close,
  .project-gallery-nav button,
  .project-thumbnails button,
  .cine-step,
  .cine-play,
  .cine-replay,
  .partners-item,
  .hero-directions a,
  .menu-toggle) {
  transition-property: transform, background-color, border-color, color, opacity;
  transition-duration: 120ms, 180ms, 180ms, 180ms, 180ms;
  transition-timing-function: var(--ease-out-quint), ease, ease, ease, ease;
}
:where(.press, .btn, .portfolio-tabs button, .project-open, .project-close,
  .project-gallery-nav button, .project-thumbnails button, .cine-step,
  .cine-play, .cine-replay, .partners-item, .hero-directions a):active {
  transform: scale(.97);
}
/* A full-width row squashing to 97% looks broken; large surfaces barely move. */
:where(.hero-directions a, .partners-item, .project-open):active { transform: scale(.99); }

/* ---------- 2. Lift: hover travel is ONE pixel ----------
   The visitor must see that the thing reacts, not that it jumped. Gated on a
   real pointer: on touch `:hover` sticks after a tap and leaves controls
   looking permanently half-selected. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover,
  .press-lift:hover,
  .partners-item.partners-logo:hover { transform: translateY(-1px); }
  .btn:active, .press-lift:active { transform: scale(.97); }
}

/* ---------- 3. Card-like surfaces: colour, not choreography ----------
   ONE recipe for every card on the page, so "this is interactive" is learned
   once. The old version ran three animations at the same time (a 3px lift, a
   44px shadow and a 6% zoom of the swatch over 600ms) — that is what read as
   cheap: a piece of paper does not levitate when you look at it.
   The whole card is clickable through .project-open::after, so the press has
   to answer on the card, not on an invisible overlay the finger never sees. */
#portfolio .project {
  transition-property: background-color, border-color, transform;
  transition-duration: 180ms, 180ms, 120ms;
  transition-timing-function: ease, ease, var(--ease-out-quint);
}
@media (hover: hover) and (pointer: fine) {
  #portfolio .project:hover {
    background: #fff;
    border-color: #7f857b;
  }
  /* The swatch is texture, not a photo gallery: a hairline settle, no zoom. */
  #portfolio .project:hover .project-swatch::after { transform: scale(1.015); }
  #portfolio .project:hover .project-open { color: var(--ink); border-color: var(--ink); }
}
#portfolio .project:has(.project-open:active) { transform: scale(.995); }
#portfolio .project-swatch::after {
  transition-property: transform;
  transition-duration: 240ms;
  transition-timing-function: var(--expo-out);
}

/* ---------- 4. Arrow micro-motion: the entire animation budget of a link --- */
.hero-directions a > span:last-child > [aria-hidden] {
  display: inline-block;
  transition-property: transform;
  transition-duration: 200ms;
  transition-timing-function: var(--ease-out-quint);
}
@media (hover: hover) and (pointer: fine) {
  .hero-directions a:hover > span:last-child > [aria-hidden] { transform: translate3d(2px, -2px, 0); }
}

/* ---------- 5. Tab row (`pill` grammar) ----------
   The border is ALWAYS painted and padding is unconditional: selecting a tab
   can never change a box size, so the row can never reflow. Selection is the
   HIGHEST-contrast label in the row — colour lives in the fill. */
.portfolio-tabs button {
  transition-property: background-color, color, transform;
  transition-duration: 180ms, 180ms, 120ms;
  transition-timing-function: ease, ease, var(--ease-out-quint);
}
@media (hover: hover) and (pointer: fine) {
  .portfolio-tabs button:not([aria-selected="true"]):hover { background: #dfe1db; color: var(--ink); }
}

/* ---------- 6. `stack`: zero-layout-shift variant swapping ----------
   Every variant is rendered into the SAME single grid cell, so the container
   is permanently as tall as its tallest variant and switching costs no
   re-measure — three cards and four cards no longer snap the section height.
   GRID, not absolute positioning: an absolutely positioned child contributes
   nothing to the parent's size, so the parent would collapse.
   `visibility: hidden`, not `display: none`: the hidden variant must keep its
   box so the cell stays measured.
   The 2px blur on the way out stops two layers crossfading at the same
   position from reading as double-struck glyphs. */
.stack { display: grid; align-items: start; }
.stack > * { grid-area: 1 / 1; }
.stack-item {
  opacity: 0;
  visibility: hidden;
  filter: blur(2px);
  pointer-events: none;
  transition-property: opacity, filter, transform, visibility;
  transition-duration: 160ms, 160ms, 160ms, 0s;
  transition-delay: 0s, 0s, 0s, 160ms;
  transition-timing-function: ease;
}
.stack-item--in {
  opacity: 1;
  visibility: visible;
  filter: blur(0);
  pointer-events: auto;
  /* Asymmetric: the arriving variant is the one being read, so it is slower. */
  transition-duration: 220ms, 220ms, 340ms, 0s;
  transition-delay: 0s;
  transition-timing-function: var(--ease-out-quint);
}
/* Content needs a direction cue; the OUTGOING item never travels. */
.stack--travel > .stack-item { transform: translate3d(0, 8px, 0); }
.stack--travel > .stack-item--in { transform: none; }

/* ---------- 7. Reduced motion = reduce, not remove ----------
   Content still changes and still fades; travel, blur and press are dropped. */
@media (prefers-reduced-motion: reduce) {
  :where(.press, .btn, .portfolio-tabs button, .project-open, .project-close,
    .project-gallery-nav button, .project-thumbnails button, .cine-step,
    .cine-play, .cine-replay, .partners-item, .hero-directions a):active,
  .press-lift:hover, .press-lift:active, .btn:hover, .btn:active,
  .partners-item.partners-logo:hover,
  #portfolio .project:has(.project-open:active),
  #portfolio .project:hover .project-swatch::after,
  .hero-directions a:hover > span:last-child > [aria-hidden] { transform: none; }
  .stack-item, .stack-item--in { filter: none; transition-duration: 100ms, 100ms, 100ms, 0s; }
  .stack--travel > .stack-item, .stack--travel > .stack-item--in { transform: none; }
}
