@charset "UTF-8";
/* ═══════════════════════════════════════════════════════════════════════
   ROAMWITHADI — effects.css
   MOTION ONLY. Owned by A12. Nothing here sets layout, and nothing here
   sets a colour except the two compass needles (assigned explicitly).

   THE MOTION CONTRACT — FOUR techniques for the whole site. There is no
   fifth. Adding one is a ban-list violation, exactly like a sixth section
   gimmick.

     1  .reveal            — the ONE entrance treatment (clip-path wipe
                             + 18px rise, stepped by --i)
     2  the hover system   — .dest-card / .video-card / .post-card ONLY
     3  the scroll-driven consumers — --dolly / --rise on #hero,
                             --tilt-x / --tilt-y on the compass
     4  the liquid seam    — the site's only divider language

   Plus the chrome that has to move to work at all: the grain overlay, the
   masthead, the nav panel, the burger, button press, form feedback, to-top.

   RULES OBEYED HERE
   · No `@media (min-width: …)` — responsive.css (A13) owns every one of
     them. The only media queries in this file are prefers-reduced-motion
     and prefers-contrast.
   · No token is declared here. Every value is a var() from style.css
     (A11). No raw hex anywhere.
   · --ease for anything that arrives by itself. --ease-out for anything
     answering a pointer or a key. NO OVERSHOOT — this is a documentary,
     not a toy.
   · transform / translate / opacity only. Never width, height, top, left.
   · .reveal is hidden ONLY under html.is-js. With JS off or the CDN
     blocked, every word of this site is on screen.

   WHY `translate:` AND `transform:` ARE USED SEPARATELY
   The entrance moves an element with the individual `translate` property;
   the hover lift moves it with `transform`. They are different properties,
   so a card can be mid-entrance and hovered at once without one rule
   stomping the other's easing — the old "reveal sets transform:none and
   kills the hover lift" defect cannot happen here.
   ══════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════
   0 · THE GRAIN — film over everything
   Drawn, never a raster. An SVG feTurbulence tile, desaturated, sitting on
   a fixed overlay that no pointer can reach. The tile is oversized and
   stepped around by 1/8 of a second-cycle so it reads as live emulsion
   rather than a static texture.

   Base opacity here is the MOBILE value (<768px is half strength, per the
   spec). responsive.css raises `.grain { opacity }` at ≥768px.
   ══════════════════════════════════════════════════════════════════════ */

.grain{
  position: fixed;
  inset: 0;
  z-index: 200;               /* over every ground; pointer-events:none */
  pointer-events: none;
  overflow: hidden;
  opacity: .045;              /* ≥768px doubles this in responsive.css */
  contain: strict;
}

.grain::before{
  content: "";
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.86' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 180px 180px;
  animation: grain-drift 8s steps(1, end) infinite;
  will-change: transform;
}

/* Eight hard jumps over eight seconds — a step, never a slide. */
@keyframes grain-drift{
  0%   { transform: translate3d(0, 0, 0); }
  12.5%{ transform: translate3d(-6%, -3%, 0); }
  25%  { transform: translate3d(4%, -7%, 0); }
  37.5%{ transform: translate3d(-8%, 5%, 0); }
  50%  { transform: translate3d(6%, 6%, 0); }
  62.5%{ transform: translate3d(-3%, -8%, 0); }
  75%  { transform: translate3d(8%, -2%, 0); }
  87.5%{ transform: translate3d(-5%, 7%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}


/* ═══════════════════════════════════════════════════════════════════════
   1 · .reveal — THE ONE ENTRANCE TREATMENT
   A clip-path wipe up from the bottom edge plus an 18px rise, settling on
   --ease. `.is-in` is added by animations.js.

   ⚠ HIDDEN ONLY UNDER html.is-js. Every hidden state below is behind that
   flag; the arrived state is what a no-JS browser renders on first paint.
   A build in this repo came one class name away from erasing itself when
   a CDN failed. Not here.

   The arrived clip-path keeps NEGATIVE insets so the --shadow token, which
   is cast 26px down and 60px soft, is never sliced off at the box edge.
   ══════════════════════════════════════════════════════════════════════ */

html.is-js :is(.reveal, .reveal-stagger > *){
  opacity: 0;
  translate: 0 18px;
  clip-path: inset(0 -15% 100% -15%);
  transition:
    opacity   var(--dur) var(--ease),
    translate var(--dur) var(--ease),
    clip-path var(--dur) var(--ease);
  will-change: opacity, translate;
}

/* The per-child step. Data (--i) comes from the markup; the rhythm is ours. */
html.is-js .reveal-stagger > *{
  transition-delay: calc(var(--i, 0) * 70ms);
}

/* …but only SOME pages emit that data. home.php writes an inline --i on each
   staggered child; blog.php and about.php deliberately do not, because the
   build's hard rule bans the style attribute and only the token contract
   carves out an exception for it. With no --i those rows all resolve to
   var(--i, 0) — every child delayed by 0ms, i.e. no stagger at all, which
   reads as a single slab arriving rather than a sequence.
   So the index is ALSO derived positionally here. An inline --i is a style
   attribute and outranks any selector, so home.php's explicit values still
   win wherever they are present; everywhere else these take over. */
html.is-js .reveal-stagger > *:nth-child(1){ --i: 0; }
html.is-js .reveal-stagger > *:nth-child(2){ --i: 1; }
html.is-js .reveal-stagger > *:nth-child(3){ --i: 2; }
html.is-js .reveal-stagger > *:nth-child(4){ --i: 3; }
html.is-js .reveal-stagger > *:nth-child(5){ --i: 4; }
html.is-js .reveal-stagger > *:nth-child(6){ --i: 5; }
html.is-js .reveal-stagger > *:nth-child(7){ --i: 6; }
/* Past the eighth child the wait stops feeling like choreography and starts
   feeling like lag, so the step is capped rather than continued. */
html.is-js .reveal-stagger > *:nth-child(n+8){ --i: 7; }

/* Directional entrances — the blog's three cards come in from three sides.
   Declared before the arrived rule so the arrived rule wins the tie. */
html.is-js :is(.reveal, .reveal-stagger > *).post-card--left { translate: -32px 0; }
html.is-js :is(.reveal, .reveal-stagger > *).post-card--below{ translate: 0 32px; }
html.is-js :is(.reveal, .reveal-stagger > *).post-card--right{ translate: 32px 0; }

/* ARRIVED. Covers both authoring shapes: a child that carries .is-in
   itself, and a .reveal-stagger wrapper that carries .is-in for its row. */
html.is-js :is(.reveal.is-in, .reveal-stagger.is-in > *){
  opacity: 1;
  translate: 0 0;
  clip-path: inset(0 -15% -15% -15%);
  will-change: auto;
}

html.is-js :is(.reveal.is-in, .reveal-stagger.is-in > *):is(.post-card--left, .post-card--below, .post-card--right){
  translate: 0 0;
}


/* ═══════════════════════════════════════════════════════════════════════
   2 · THE HOVER SYSTEM
   Exactly three components carry it: .dest-card, .video-card, .post-card.
   Nothing else on this site responds to a pointer with movement.

     · the photograph pushes in to 1.05 — the .frame clips it
     · the card lifts 6px
     · a gold key-line draws left→right along the bottom edge

   --summit is used for the key-line on BOTH grounds: it is a graphic, not
   type, and the token contract permits gold graphics on ivory.
   :focus-within mirrors :hover so a keyboard gets the same answer.
   ══════════════════════════════════════════════════════════════════════ */

.dest-card,
.video-card,
.post-card{
  position: relative;                 /* the key-line's containing block */
  transition: transform var(--dur-fast) var(--ease-out);
}

/* The key-line. Scales from the left on --ease-out; retracts the same way. */
.dest-card::after,
.video-card::after,
.post-card::after{
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 2px;
  background: var(--summit);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur) var(--ease-out);
  pointer-events: none;
}

/* The photograph. Slower than the lift, so the image reads as a push-in. */
:is(.dest-card, .video-card, .post-card) img{
  transform-origin: 50% 50%;
  transition: transform var(--dur) var(--ease-out);
}

:is(.dest-card, .video-card, .post-card):is(:hover, :focus-within){
  transform: translate3d(0, -6px, 0);
}

:is(.dest-card, .video-card, .post-card):is(:hover, :focus-within)::after{
  transform: scaleX(1);
}

:is(.dest-card, .video-card, .post-card):is(:hover, :focus-within) img{
  transform: scale(1.05);
}


/* ═══════════════════════════════════════════════════════════════════════
   3 · THE SCROLL-DRIVEN CONSUMERS
   animations.js writes --dolly (0→1) and --rise (0→1) on #hero; main.js
   writes --tilt-x / --tilt-y for the compass.

   ⚠ EVERY DEFAULT BELOW IS 0, AND 0 IS THE CORRECT STATIC STATE. With no
   JS, a blocked CDN or reduced motion, the hero renders undollied, unrisen
   and fully legible. Nothing here is load-bearing.
   ══════════════════════════════════════════════════════════════════════ */

/* The dolly-in. 0 → scale(1). */
.hero-photo{
  transform-origin: 50% 50%;
  transform: scale(calc(1 + var(--dolly, 0) * .15));
  will-change: transform;
}

/* The veil deepens on its own pseudo-layer rather than by overriding the
   veil's painted opacity — at --dolly 0 this layer is fully transparent,
   so style.css keeps sole authority over how the hero is graded at rest. */
.hero-veil::after{
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink-deep);
  opacity: calc(var(--dolly, 0) * .42);
  pointer-events: none;
}

/* The copy block rises out of frame as the reel starts. */
.hero-inner{
  translate: 0 calc(var(--rise, 0) * -64px);
  opacity: calc(1 - var(--rise, 0) * .9);
  will-change: translate, opacity;
}

/* The supporting parts clear out ahead of the headline. */
.hero-eyebrow,
.hero-sub,
.hero-actions,
.hero-scroll{
  opacity: calc(1 - var(--rise, 0) * 1.7);
}

/* The compass. A few degrees at most — it is a grace note, not a toy.
   main.js writes --tilt-x / --tilt-y as UNITLESS NUMBERS OF DEGREES, so they
   are multiplied by 1deg here rather than fed to rotate3d as an axis vector.
   That distinction matters: as an axis, any non-zero pointer offset — however
   small — normalises to the same full-strength rotation, so the compass would
   snap to its limit the instant the pointer moved. Multiplied, the tilt is
   proportional to pointer distance, which is the whole point of the gesture.
   Both default to 0, and 0/0 is the correct resting frame with no JS. */
.hero-compass-svg{
  transform:
    perspective(680px)
    rotateX(calc(var(--tilt-x, 0) * 1deg))
    rotateY(calc(var(--tilt-y, 0) * 1deg));
  transition: transform var(--dur-fast) var(--ease-out);
  will-change: transform;
}

/* The inner ring turns once every 72 seconds. Slow enough that you only
   notice it if you stop and watch — which is the point. */
.cmp-ring--in{
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: cmp-drift 72s linear infinite;
}

@keyframes cmp-drift{
  from{ transform: rotate(0deg); }
  to  { transform: rotate(360deg); }
}

/* The needle reads north in signal gold, south in the ground's green. */
.cmp-needle-n{ fill: var(--summit); }
.cmp-needle-s{ fill: var(--ground-green); }


/* ═══════════════════════════════════════════════════════════════════════
   4 · THE LIQUID SEAM
   animations.js rewrites `d` per frame from scroll velocity. A short
   linear transition smooths the frame-to-frame jitter into flex without
   ever lagging behind the scroll. Nothing else on the seam moves — no
   fade, no parallax, no second divider anywhere on this site.
   Browsers that do not animate `d` simply get the rewritten path directly.
   ══════════════════════════════════════════════════════════════════════ */

.seam-path{
  transition: d 120ms linear;
  will-change: d;
}


/* ═══════════════════════════════════════════════════════════════════════
   5 · CHROME THAT HAS TO MOVE
   ══════════════════════════════════════════════════════════════════════ */

/* ── the masthead settling into its stuck state ───────────────────────── */
.masthead{
  transition:
    background-color   var(--dur-fast) var(--ease),
    border-color       var(--dur-fast) var(--ease),
    backdrop-filter    var(--dur-fast) var(--ease),
    -webkit-backdrop-filter var(--dur-fast) var(--ease),
    box-shadow         var(--dur-fast) var(--ease);
}

/* The blur is ours; the stuck background colour stays style.css's call and
   simply fades in through the transition above. */
.masthead.is-stuck{
  -webkit-backdrop-filter: blur(14px) saturate(1.04);
  backdrop-filter: blur(14px) saturate(1.04);
}

/* ── the nav panel ────────────────────────────────────────────────────────
   ⚠ THE NO-JS RULE. The panel is open and in flow by default. ONLY
   html.is-js closes it. Written in that direction and never the reverse.  */
html.is-js .nav-panel{
  opacity: 0;
  visibility: hidden;
  translate: 0 -10px;
  pointer-events: none;
  transition:
    opacity    var(--dur-fast) var(--ease-out),
    translate  var(--dur-fast) var(--ease-out),
    visibility 0s linear var(--dur-fast);
}

html.is-js .nav-panel.is-open{
  opacity: 1;
  visibility: visible;
  translate: 0 0;
  pointer-events: auto;
  transition:
    opacity    var(--dur-fast) var(--ease-out),
    translate  var(--dur-fast) var(--ease-out),
    visibility 0s linear 0s;
}

/* ── the burger → cross ───────────────────────────────────────────────────
   Written with :first-child / :last-child so it morphs correctly whether
   the toggle ships two bars or three, and keyed off aria-expanded as well
   as .is-open so it cannot fall out of step with main.js.               */
.nav-toggle{
  --bar-shift: 6px;
}

.nav-toggle-bar{
  transform-origin: center;
  transition:
    transform var(--dur-fast) var(--ease-out),
    opacity   var(--dur-fast) var(--ease-out);
}

:is(.nav-toggle[aria-expanded="true"], .nav-toggle.is-open) .nav-toggle-bar:first-child{
  transform: translateY(var(--bar-shift)) rotate(45deg);
}

:is(.nav-toggle[aria-expanded="true"], .nav-toggle.is-open) .nav-toggle-bar:nth-child(2):not(:last-child){
  opacity: 0;
  transform: scaleX(.2);
}

:is(.nav-toggle[aria-expanded="true"], .nav-toggle.is-open) .nav-toggle-bar:last-child{
  transform: translateY(calc(var(--bar-shift) * -1)) rotate(-45deg);
}

/* ── buttons ──────────────────────────────────────────────────────────────
   A press, not a bounce. No overshoot, no scale above 1 on a text button. */
.btn,
.btn-icon{
  transition:
    transform        var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    border-color     var(--dur-fast) var(--ease-out),
    color            var(--dur-fast) var(--ease-out),
    opacity          var(--dur-fast) var(--ease-out);
}

.btn:active        { transform: translate3d(0, 1px, 0) scale(.985); }
.btn-icon:hover,
.btn-icon:focus-visible{ transform: scale(1.05); }
.btn-icon:active   { transform: scale(.93); }

/* ── server feedback arriving ─────────────────────────────────────────────
   An animation, not a transition: the element does not exist until the
   flash does, so there is no "from" state to transition out of. No fill
   forwards is needed past the end — the resting state is simply visible. */
.form-msg{
  animation: msg-in var(--dur) var(--ease) both;
}

@keyframes msg-in{
  from{ opacity: 0; translate: 0 -8px; }
  to  { opacity: 1; translate: 0 0; }
}

/* ── back to top ──────────────────────────────────────────────────────────
   Hidden ONLY under html.is-js, and it accepts either state class main.js
   might use. With JS off the control stays visible and reachable.       */
html.is-js .to-top{
  opacity: 0;
  visibility: hidden;
  translate: 0 12px;
  pointer-events: none;
  transition:
    opacity    var(--dur-fast) var(--ease),
    translate  var(--dur-fast) var(--ease),
    transform  var(--dur-fast) var(--ease-out),
    visibility 0s linear var(--dur-fast);
}

html.is-js .to-top:is(.is-in, .is-visible){
  opacity: 1;
  visibility: visible;
  translate: 0 0;
  pointer-events: auto;
  transition:
    opacity    var(--dur-fast) var(--ease),
    translate  var(--dur-fast) var(--ease),
    transform  var(--dur-fast) var(--ease-out),
    visibility 0s linear 0s;
}


/* ═══════════════════════════════════════════════════════════════════════
   6 · REDUCED MOTION — mandatory, and genuinely complete
   Everything below is switched OFF, not slowed down. After this block the
   page is the same page: every element visible, every control reachable,
   the seam at its resting curve, the grain still a texture.
   ══════════════════════════════════════════════════════════════════════ */

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

  /* Flatten every transition and animation on the site — including the
     ones style.css and responsive.css declare. */
  *,
  *::before,
  *::after{
    animation-duration: .01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  html{ scroll-behavior: auto !important; }

  /* 1 · .reveal renders ARRIVED, whether or not .is-in ever lands. */
  html.is-js :is(.reveal, .reveal-stagger > *),
  html.is-js :is(.reveal, .reveal-stagger > *).post-card--left,
  html.is-js :is(.reveal, .reveal-stagger > *).post-card--below,
  html.is-js :is(.reveal, .reveal-stagger > *).post-card--right{
    opacity: 1 !important;
    translate: none !important;
    clip-path: none !important;
    transition: none !important;
    transition-delay: 0ms !important;
    will-change: auto !important;
  }

  /* 2 · The hover system holds its states but stops travelling. */
  :is(.dest-card, .video-card, .post-card):is(:hover, :focus-within){
    transform: none !important;
  }
  :is(.dest-card, .video-card, .post-card):is(:hover, :focus-within) img{
    transform: none !important;
  }

  /* 3 · The hero keeps nothing scroll-driven. Every consumer is pinned to
         its rest value, so the opening shot is simply a photograph.     */
  .hero-photo   { transform: none !important; }
  .hero-veil::after{ opacity: 0 !important; }
  .hero-inner   { translate: none !important; opacity: 1 !important; }
  .hero-eyebrow,
  .hero-sub,
  .hero-actions,
  .hero-scroll  { opacity: 1 !important; }

  /* No compass tilt and no idle spin. */
  .hero-compass-svg{ transform: none !important; transition: none !important; }
  .cmp-ring--in    { animation: none !important; transform: none !important; }

  /* The pinned horizontal reel never engages — the rail stays a native
     scroll-snap row, so no card can be stranded off screen. */
  .dest-track{ transform: none !important; translate: none !important; }

  /* 4 · The seam renders the resting curve animations.js starts from. */
  .seam-path{ transition: none !important; will-change: auto !important; }

  /* Chrome: the texture stays, the shimmer goes. */
  .grain::before{ animation: none !important; transform: none !important; }

  .masthead      { transition: none !important; }
  .nav-toggle-bar{ transition: none !important; }
  .form-msg      { animation: none !important; opacity: 1 !important; translate: none !important; }
  .btn:active,
  .btn-icon:hover,
  .btn-icon:focus-visible,
  .btn-icon:active{ transform: none !important; }

  /* The panel and the to-top still open and close — instantly. */
  html.is-js .nav-panel,
  html.is-js .nav-panel.is-open,
  html.is-js .to-top,
  html.is-js .to-top:is(.is-in, .is-visible){
    transition: none !important;
    translate: none !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════
   7 · FORCED / HIGH CONTRAST
   Grain is atmosphere. When a reader has asked for maximum contrast, it is
   noise sitting on top of their type — so it goes.
   ══════════════════════════════════════════════════════════════════════ */

@media (prefers-contrast: more){
  .grain{ opacity: 0; }
}
