/* ============================================================
   PCG — Motion layer
   Page transitions, kinetic type, magnetic CTAs, pointer
   spotlights, scroll-scrubbed depth, and the scroll-reveal
   engine (arming, instant-past, per-content motion — §11).

   Rules of the house:
   · transform + opacity only — never animate layout
   · every effect is opt-in via a data-attribute
   · everything degrades to "instantly visible" under
     prefers-reduced-motion
   ============================================================ */

/* ------------------------------------------------------------
   1. Page transitions — native, cross-document, zero JS

   WHAT WAS HERE, AND WHY IT IS GONE
   A JS curtain: three near-black panels driven by
   js/pcg-motion.js's initPageTransition(). It intercepted every
   same-origin click, called preventDefault(), ran the panels up,
   and only then set location.href — behind a hard
   setTimeout(go, 470). So the request did not even START for
   470ms after the click. On arrival it held the panels for a
   further 900ms and ran an 800ms content fade. Measured cost:
   roughly 1.4s of dead time per navigation, and because the
   panels were --onyx-deep the visitor watched a black rectangle
   for most of it. That is the "lag, black overlay, then the
   page" report, and it was self-inflicted.

   It also broke in ways a JS curtain always breaks: a slow
   network finished the animation long before the document
   arrived, a cancelled navigation left the curtain up, and the
   arrival half was gated on a sessionStorage flag, so a page
   whose JS never ran could paint stuck behind a black panel.

   WHAT REPLACES IT
   @view-transition hands the whole job to the browser. The
   navigation starts on the click — no delay, no interception, no
   preventDefault — and the browser cross-fades the old and new
   documents itself, on the compositor. Nothing is waiting on a
   timer, so a transition can never outlive or under-run the
   actual page load.

   THE FALLBACK IS A STRICT IMPROVEMENT, which is the point.
   Where @view-transition is not supported the at-rule is simply
   ignored and the visitor gets an ordinary instant navigation —
   already far faster than the curtain it replaces. So there is
   no second code path to maintain and nobody is worse off.

   Paired with the speculation rules in tools/sync-shell.js, which
   pre-fetch same-origin pages on hover, the next document is
   usually already in cache when the click lands.
   ------------------------------------------------------------ */
@view-transition{navigation:auto}

/* The cross-fade. Deliberately quick and asymmetric: the outgoing page leaves
   faster than the incoming one arrives, which reads as the new page pushing the
   old one out rather than as a dissolve. 90ms/220ms is short enough that it
   never feels like waiting — the whole point was "superfast" — and long enough
   that the swap does not flash. */
::view-transition-old(root){
  animation:vt-out 90ms var(--ease) both;
}
::view-transition-new(root){
  animation:vt-in 220ms var(--ease) both;
}
@keyframes vt-out{to{opacity:0}}
@keyframes vt-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* The shell is CONTINUOUS, not part of the cross-fade. Naming the bar and the
   brand lockup lifts them out of the root snapshot, so they hold still while the
   content swaps underneath — the single biggest reason this reads as one
   application rather than as a series of page loads.
   Names must be unique per document, which is why these are on the single nav
   and the single brand rather than on a repeated element. */
.nav{view-transition-name:pcg-nav}
.nav__brand{view-transition-name:pcg-brand}

/* .page-fade is the <main> of every page. It stays a hook, but it is now the
   content that moves while the shell above it does not. Its old
   body[data-pt] animation is gone with the curtain. */
.page-fade{view-transition-name:pcg-main}
::view-transition-old(pcg-main){animation:vt-out 90ms var(--ease) both}
::view-transition-new(pcg-main){animation:vt-in 220ms var(--ease) both}

@media (prefers-reduced-motion: reduce){
  /* Cut the movement, keep the swap instant. `navigation:auto` is left alone
     deliberately: suppressing the ANIMATION is what reduced-motion asks for,
     and disabling the transition entirely would gain nothing. */
  ::view-transition-old(root),::view-transition-new(root),
  ::view-transition-old(pcg-main),::view-transition-new(pcg-main){
    animation-duration:.01ms;
  }
  @keyframes vt-in{from{opacity:1;transform:none}to{opacity:1;transform:none}}
}

/* ------------------------------------------------------------
   2. Kinetic type — word-by-word mask reveal
   Each word carries its own mask, so the effect survives any
   line-wrap without JS having to measure lines.
   ------------------------------------------------------------ */
[data-split]{--split-step:48ms}
.sp-w{
  display:inline-block;overflow:hidden;vertical-align:bottom;
  padding-bottom:.1em;margin-bottom:-.1em;
}
.sp-word{
  display:inline-block;
  transform:translateY(105%) rotate(2deg);
  opacity:0;
  transition:transform .95s var(--ease),opacity .7s var(--ease);
  transition-delay:calc(var(--i,0) * var(--split-step));
}
[data-split].is-in .sp-word,.is-in [data-split] .sp-word{transform:none;opacity:1}

/* ------------------------------------------------------------
   3. Magnetic CTAs — the button leans toward the cursor
   ------------------------------------------------------------ */
[data-magnetic]{
  will-change:transform;
  transform:translate3d(var(--mx,0),var(--my,0),0);
  transition:transform .5s var(--ease);
}
[data-magnetic].is-pulling{transition:transform .12s linear}

/* ------------------------------------------------------------
   4. Pointer spotlight — a warm glow tracks across a surface
   ------------------------------------------------------------ */
[data-spotlight]{position:relative;isolation:isolate}
[data-spotlight]::before{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:radial-gradient(220px circle at var(--px,50%) var(--py,50%),rgba(201,161,74,.13),transparent 62%);
  opacity:0;transition:opacity .45s var(--ease);
}
.skin-creme [data-spotlight]::before,.skin-creme-deep [data-spotlight]::before{
  background:radial-gradient(220px circle at var(--px,50%) var(--py,50%),rgba(122,94,28,.10),transparent 62%);
}
[data-spotlight]:hover::before{opacity:1}
[data-spotlight]>*{position:relative;z-index:1}

/* ------------------------------------------------------------
   5. Scroll-scrubbed depth
   --p is written by JS: 0 when entering the viewport, 1 leaving
   ------------------------------------------------------------ */
[data-scrub="lift"]{transform:translate3d(0,calc((var(--p,.5) - .5) * -60px),0)}
[data-scrub="zoom"]{transform:scale(calc(1.08 - var(--p,.5) * .08))}
[data-scrub="drift"]{transform:translate3d(calc((var(--p,.5) - .5) * 40px),0,0)}
[data-scrub="fade-out"]{opacity:calc(1.15 - var(--p,0) * 1.35)}

/* ------------------------------------------------------------
   6. Process rail — the graduated spine the steps are marked on

   This is the site's SECOND .scale, and it deliberately borrows the
   first one's vocabulary rather than inventing a tick system of its
   own — see "the .scale primitive" in css/instrument-live.css. Same
   three marks, same three tokens, turned on their side:

       7px   --tick-minor   minor graduation — texture behind the majors
      13px   --tick-major   major graduation — one per real step
      17px   --needle       the step you are standing in

   The class names are not reused because there is no track element
   here to hang them on: the rail's spine, fill and item marks already
   exist as .rail::before / .rail__fill / .rail__item::before, and the
   whole point of this pass is that it adds ZERO markup. What is shared
   is the language — the same three tokens at the same three lengths,
   majors over minors, one needle at the current value.

   ############  WHAT THIS RULER MEASURES  ############
   instrument-live.css sets the condition for a graduated track to
   exist at all: a real quantity already in the DOM, its ends from the
   same source as its value, the literal always printed beside it. This
   one passes on every count. The axis is the five ordered steps, each
   already labelled 01-05 in .rail__no and named in its own <h3>. Each
   major graduation is cut at its own step's mark. The value on the axis
   is .rail__fill — scroll progress through the section, measured by
   initRail() in js/pcg-motion.js against the real geometry of the real
   items, never a constant — and .is-active says in words which step it
   has reached. Nothing here implies a quantity that is not already
   written next to it.

   The minor graduations are a fixed 11px pitch of scroll distance, so
   they encode no quantity of their own. That is exactly the status
   --tick-minor has on the price scale, and it is why the token is
   deliberately under 3:1 (rgba(201,161,74,.30) flattens to #44381F on
   --onyx = 1.73:1): it is texture, and it is never the sole carrier of
   anything. The index, the heading and the fill all say where you are.

   CONTRAST, measured on --onyx #0B0B0C:
     --needle      #E6CF8F .... 12.80:1  the active step's mark
     --tick-major  #96742C ..... 4.53:1  clears 1.4.11's 3:1, which it
                                 must: a major marks a real step
     --tick-minor  #44381F ..... 1.73:1  texture, see above
     --rule        #352C1A ..... 1.43:1  the spine itself is ornament;
                                 the marks on it are what carry meaning
   On the cream skins --tick-major and --needle both resolve to --brons
   (5.51:1 on --papier), so there the active step is told apart by
   LENGTH and WEIGHT rather than by colour — plus .rail__no and the
   <h3> both flipping to --gold-text, which they already did.
   ------------------------------------------------------------ */
.rail{position:relative;padding-left:clamp(32px,5vw,64px)}
/* The graduated track: the spine and its minor graduations on ONE
   pseudo-element, as two background layers.

   Why not a second pseudo for the minors: ::after is generated AFTER
   the .rail__item children, so with no z-index anywhere it would paint
   its graduations over the majors and over .rail__fill. Folding both
   into ::before puts the paint order right by construction — spine and
   minors, then the fill, then the majors and the needle — with no
   z-index and no stacking context, which matters because .grid-split's
   other column is a position:sticky lede. */
.rail::before{
  content:"";position:absolute;left:0;top:6px;bottom:6px;width:7px;
  pointer-events:none;
  background:
    /* The spine, lit once along its length at 52% — the house rule for
       every hairline on the site, expressed on the axis this hairline
       actually runs on. --rake-soft is NOT usable here and the reason
       is measured, not stylistic: it is a 105deg ramp, and in a box
       1px wide by the 1486px this rail measures at 1440, its gradient
       line is only 386px long (1·sin105 + 1486·|cos105|),
       so roughly three quarters of the spine would sit flat on the
       ramp's dying end at alpha .035 and simply disappear. The stops
       below are the declared --rule / --rule-soft tokens, so no new
       pigment is introduced — only the correct axis for them. */
    linear-gradient(180deg,var(--rule-soft) 0%,var(--rule) 52%,var(--rule-soft) 100%) left top/1px 100% no-repeat,
    /* Minor graduations. 11px of scroll distance per mark. */
    repeating-linear-gradient(180deg,var(--tick-minor) 0 1px,transparent 1px 11px);
}
.rail__fill{
  position:absolute;left:0;top:6px;width:1px;height:calc(100% - 12px);
  background:var(--gold-grad);
  transform:scaleY(var(--rp,0));transform-origin:top;
  transition:transform .25s linear;
}
.rail__item{position:relative;padding-block:clamp(26px,3.4vw,46px)}
/* One major graduation per step, cut at the step's own mark and lined
   up with the midline of its .rail__no index. It is struck at the
   needle's full 17px and SHORTENED to 13px by a transform: the house
   rule at the head of this file bans animating a layout property, and
   scaleX keeps the active/inactive change on the compositor. The
   `scale` property and `transform` are separate individual transforms
   and compose (scale first, then transform), which is how the active
   mark gets 17px x 2px out of one 17px x 1px box. */
.rail__item::before{
  content:"";position:absolute;
  left:calc(clamp(32px,5vw,64px) * -1);top:calc(clamp(26px,3.4vw,46px) + 11px);
  width:17px;height:1px;
  background:var(--tick-major);
  transform-origin:left center;transform:scaleX(.765);
  transition:background-color .5s var(--ease),transform .5s var(--ease),scale .5s var(--ease);
}
.rail__item.is-active::before{background:var(--needle);transform:scaleX(1);scale:1 2}
.rail__item h3{transition:color .5s var(--ease)}
.rail__item.is-active h3{color:var(--gold-text)}
/* The .18em POSITIVE tracking stays: this is a tracked micro-label, not
   display type, so it wants the opposite treatment from the ladder. The
   weight is what separates the rail's index from .rail__body beside it,
   and with one family it has to be declared rather than inherited. */
.rail__no{
  font-family:var(--ff-display);font-weight:600;font-size:14px;letter-spacing:.18em;
  color:var(--muted);transition:color .5s var(--ease);
}
.rail__item.is-active .rail__no{color:var(--gold-text)}
.rail__body{color:var(--muted);max-width:58ch;margin-top:.9rem}
.rail__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:1.1rem}

/* ------------------------------------------------------------
   7. Hover-reveal media (work cards)
   ------------------------------------------------------------ */
.reveal-media{position:relative;overflow:hidden}
.reveal-media__sweep{
  position:absolute;inset:0;z-index:3;pointer-events:none;
  background:linear-gradient(115deg,transparent 35%,rgba(230,207,143,.16) 50%,transparent 65%);
  transform:translateX(-120%);
}
.reveal-media:hover .reveal-media__sweep{transform:translateX(120%);transition:transform 1.1s var(--ease)}

/* ------------------------------------------------------------
   9. Count-up figures shouldn't reflow while animating
   ------------------------------------------------------------ */
[data-count]{font-variant-numeric:tabular-nums}

/* ------------------------------------------------------------
   10. Reduced motion — everything resolves, nothing moves
   ------------------------------------------------------------ */
@media (prefers-reduced-motion:reduce){
  /* Nothing for the old .pt curtain or body[data-pt] here any more — both were
     deleted with it. The reduced-motion handling for page transitions now lives
     with the transition itself, at the top of this file. */
  .sp-word{transform:none !important;opacity:1 !important;transition:none !important}
  [data-magnetic]{transform:none !important}
  [data-scrub]{transform:none !important;opacity:1 !important}
  .reveal-media__sweep{display:none}
  /* The rail's needle changes STATE instantly instead of gliding from
     the previous step's mark. Note what is NOT written here: the
     transform itself stays, because the difference between a 13px
     graduation and a 17px needle is a static fact about which step you
     are in, not a movement. Killing the transform would make every
     step look active. */
  .rail__item::before{transition:none}
}

/* ============================================================
   11. Reveal engine
   ============================================================ */

/* ------------------------------------------------------------
   11.1 The safety net — reveals only ARM themselves once JS says so

   css/pcg.css parks every reveal target at opacity:0 unconditionally,
   which means a blocked script, a JS error or a browser with scripting
   off leaves the page blank. js/pcg.js therefore stamps .pcg-armed on
   <html> as its first synchronous statement (and strips it again if
   init throws before the engine is running). Until that class exists,
   the rules below hand every target straight back to its final state.

   Specificity note: pcg.css uses bare `[data-rise]` (0,1,0); this block
   is (0,2,1) AND later in the cascade. !important is deliberate — two
   stylesheets (effects.css, pricing.css) load after this one.
   ------------------------------------------------------------ */
html:not(.pcg-armed) [data-rise],
html:not(.pcg-armed) [data-fade],
html:not(.pcg-armed) [data-clip],
html:not(.pcg-armed) [data-draw],
html:not(.pcg-armed) [data-stagger]>*,
html:not(.pcg-armed) .reveal-line>span,
html:not(.pcg-armed) .sp-w,
html:not(.pcg-armed) .sp-word{
  opacity:1 !important;
  transform:none !important;
  clip-path:none !important;
  transition:none !important;
}

/* ------------------------------------------------------------
   11.2 Content you already scrolled past is present, not performed

   On a scrollbar drag or a hard flick, elements travel from below the
   viewport to above it inside one frame. js/pcg.js reveals those with
   .is-immediate: they belong on screen, but starting a 1.1s glide for
   something the visitor has already gone past only produces a stale
   animation waiting to be scrolled back to.
   ------------------------------------------------------------ */
html.pcg-armed .is-immediate,
html.pcg-armed .is-immediate>*,
html.pcg-armed .is-immediate .sp-word,
html.pcg-armed .is-immediate .reveal-line>span{
  transition-duration:0s !important;
  transition-delay:0s !important;
}

/* ------------------------------------------------------------
   11.3 Motion that fits what it reveals

   One fade-up for every kind of content is the tell. Below: verbs that
   match their subject. Two ways in — content-aware defaults keyed to
   existing markup, and opt-in values on the attributes that already
   exist, so markup can ask for a specific verb:

     data-rise="left|right"   directional entry (side-by-side content)
     data-rise="media"        mask climb + no travel (pictures, frames)
     data-rise="settle"       short drop, tight curve (figures, numerals)
     data-rise="near"         small, quick (dense or secondary text)
     data-clip="wipe"         horizontal wipe, left → right
     data-clip="up"           mask travels bottom → top
     data-clip="center"       opens from the middle outwards
     data-draw="center|right" hairline draws from another origin
     data-draw="down"         vertical hairline draws downward
     data-stagger-mode="side|settle|wipe"   on a [data-stagger] parent

   Bare data-rise / data-fade / data-clip / data-draw / data-stagger keep
   their original behaviour untouched.
   ------------------------------------------------------------ */

/* Directional — for content that sits beside something else, so the
   entry carries the reading order instead of fighting it. */
html.pcg-armed [data-rise="left"],
html.pcg-armed [data-rise="right"],
html.pcg-armed .rail__item[data-rise]{
  transform:translate3d(var(--rise-x,-26px),6px,0);
  transition:opacity .8s var(--ease),transform 1s var(--ease);
}
html.pcg-armed [data-rise="left"]{--rise-x:-42px}
html.pcg-armed [data-rise="right"]{--rise-x:42px}
html.pcg-armed .is-in[data-rise="left"],
html.pcg-armed .is-in[data-rise="right"],
html.pcg-armed .is-in.rail__item[data-rise]{transform:none}

/* ONE COLUMN MEANS NO SIDE, so the travel goes to zero and the entry becomes the
   plain 6px vertical rise the transform above already carries.

   This is a correctness fix as much as a taste one. Below 860px every two-column
   split collapses (css/pcg.css:1069), so a [data-rise="right"] element is the full
   content width — 346px inside a 390px viewport — and translating it +42px puts its
   right edge at 410px. A transform is part of the scrollable overflow area, so the
   DOCUMENT became 410px wide and the page scrolled sideways by 20px until every
   reveal had fired. Measured on index, diensten, tarieven, werk-pierstone and
   werk-prestige-auto: all five, +20px at 390 and +4px at 768, and it was the whole
   remaining overflow on all of them.

   Setting --rise-x rather than overriding `transform` keeps the 6px lift, the
   transition, and the .is-in{transform:none} end state all in one place; and it is
   set on the same selectors so specificity cannot drift. Clipping the container was
   the alternative and was rejected: these sections hold position:sticky ledes, and an
   overflow value on an ancestor makes sticky stop working. */
@media (max-width:860px){
  html.pcg-armed [data-rise="left"],
  html.pcg-armed [data-rise="right"],
  html.pcg-armed .rail__item[data-rise]{--rise-x:0px}
}

/* Media — a picture doesn't slide onto the page, it gets uncovered.
   Using clip-path instead of transform also leaves [data-parallax]
   free to own the transform on the same element. */
html.pcg-armed [data-rise="media"],
html.pcg-armed .frame[data-rise]{
  opacity:0;
  transform:none;
  clip-path:inset(0 0 16% 0);
  transition:opacity .7s var(--ease),clip-path 1.2s var(--ease);
}
html.pcg-armed .is-in[data-rise="media"],
html.pcg-armed .is-in.frame[data-rise]{
  opacity:1;
  clip-path:inset(0 0 0 0);
}

/* Figures land. A number that glides 38px reads as decoration; a number
   that drops a few pixels and stops reads as a value being set. */
html.pcg-armed [data-rise="settle"],
html.pcg-armed [data-stagger][data-stagger-mode="settle"]>*,
html.pcg-armed .stats[data-stagger]>*{
  transform:translate3d(0,-9px,0) scale(.99);
  transition:opacity .5s var(--ease),transform .62s cubic-bezier(.2,.85,.25,1);
}
html.pcg-armed .is-in[data-rise="settle"],
html.pcg-armed .is-in[data-stagger][data-stagger-mode="settle"]>*,
html.pcg-armed .is-in.stats[data-stagger]>*{transform:none}

/* Small, secondary or dense text gets a small, quick move. */
html.pcg-armed [data-rise="near"],
html.pcg-armed .crumbs[data-rise]{
  transform:translate3d(0,10px,0);
  transition:opacity .45s var(--ease),transform .55s var(--ease);
}
html.pcg-armed .is-in[data-rise="near"],
html.pcg-armed .is-in.crumbs[data-rise]{transform:none}

/* Quotes and pull-outs breathe rather than travel — same verb, longer
   line, so the pace of the section changes without a new gesture. */
html.pcg-armed .quote[data-rise],
html.pcg-armed .pull[data-rise]{
  transform:translate3d(0,18px,0);
  transition:opacity 1.4s var(--ease),transform 1.5s var(--ease);
}
html.pcg-armed .is-in.quote[data-rise],
html.pcg-armed .is-in.pull[data-rise]{transform:none}

/* Clip variants — a mask has an axis; let the markup choose it. */
html.pcg-armed [data-clip="wipe"]{clip-path:inset(0 100% 0 0)}
html.pcg-armed [data-clip="up"]{clip-path:inset(100% 0 0 0)}
html.pcg-armed [data-clip="center"]{clip-path:inset(0 50% 0 50%)}
html.pcg-armed .is-in[data-clip="wipe"],
html.pcg-armed .is-in[data-clip="up"],
html.pcg-armed .is-in[data-clip="center"]{clip-path:inset(0 0 0 0)}

/* Hairlines — same draw, other origins (and one that runs vertically). */
html.pcg-armed [data-draw="center"]{transform:scaleX(0);transform-origin:center}
html.pcg-armed [data-draw="right"]{transform:scaleX(0);transform-origin:right}
html.pcg-armed .is-in[data-draw="center"],
html.pcg-armed .is-in[data-draw="right"]{transform:scaleX(1)}
html.pcg-armed [data-draw="down"]{
  transform:scaleY(0);transform-origin:top;
  transition:transform 1s var(--ease);
}
html.pcg-armed .is-in[data-draw="down"]{transform:scaleY(1)}

/* Stagger modes — the children of a [data-stagger] parent keep their JS
   delays but can move on a different axis. */
html.pcg-armed [data-stagger][data-stagger-mode="side"]>*{
  transform:translate3d(-24px,0,0);
  transition:opacity .8s var(--ease),transform .95s var(--ease);
}
html.pcg-armed .is-in[data-stagger][data-stagger-mode="side"]>*{transform:none}
html.pcg-armed [data-stagger][data-stagger-mode="wipe"]>*{
  transform:none;
  clip-path:inset(0 0 100% 0);
  transition:opacity .6s var(--ease),clip-path 1s var(--ease);
}
html.pcg-armed .is-in[data-stagger][data-stagger-mode="wipe"]>*{clip-path:inset(0 0 0 0)}

/* ------------------------------------------------------------
   11.4 Reduced motion — one verb left: a plain crossfade

   Nothing travels, nothing scales, no mask moves and no stagger is
   applied (js/pcg.js skips the inline delays under this preference).
   Content still arrives, it just arrives by opacity alone.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion:reduce){
  html.pcg-armed [data-rise],
  html.pcg-armed [data-fade],
  html.pcg-armed [data-clip],
  html.pcg-armed [data-draw],
  html.pcg-armed [data-stagger]>*,
  html.pcg-armed .reveal-line>span,
  html.pcg-armed [data-split] .sp-word{
    opacity:0 !important;
    transform:none !important;
    clip-path:none !important;
    transition:opacity .4s linear !important;
  }
  html.pcg-armed .is-in[data-rise],
  html.pcg-armed .is-in[data-fade],
  html.pcg-armed .is-in[data-clip],
  html.pcg-armed .is-in[data-draw],
  html.pcg-armed .is-in[data-stagger]>*,
  html.pcg-armed .is-in .reveal-line>span,
  html.pcg-armed [data-split].is-in .sp-word,
  html.pcg-armed .is-in [data-split] .sp-word{opacity:1 !important}
  html.pcg-armed .is-immediate,
  html.pcg-armed .is-immediate>*,
  html.pcg-armed .is-immediate .sp-word,
  html.pcg-armed .is-immediate .reveal-line>span{transition-duration:0s !important}
}
