/* ============================================================
   PCG — Effects layer: intro, particles, sheen, reveals
   Loaded after components.css. Lean & GPU-friendly: transform/opacity
   only, no blur or pointer-tracking. Fully disabled under
   prefers-reduced-motion.
   ============================================================ */

/* ---------- Cinematic intro ----------
   THERE IS NO .pcg-intro-lock ANY MORE. It was `overflow:hidden !important;
   height:100vh` on <body> and it held the reader for about 2.8s on a site that
   paints in 310ms. js/pcg-fx.js carries the reasoning and the replacement: the
   curtain now dismisses on the first scroll, touch, key or pointer event, so it
   never has to be enforced.

   Every duration below moved with that change, and they have to stay in step with
   the 1050ms timeout in js/pcg-fx.js — the curtain used to resolve at about 1.75s,
   which is why simply shortening the timeout would have cut the bar off mid-fill.
   Roughly: mono lands by 700ms, the word completes near 830ms, the bar fills by
   920ms, then the lift. */
.pcg-preloader{
  position:fixed;inset:0;z-index:9999;background:#070707;
  display:flex;align-items:center;justify-content:center;
  overflow:hidden;
  transition:transform .8s cubic-bezier(.76,0,.24,1),opacity .45s ease .38s;
}
.pcg-preloader.lift{transform:translateY(-101%)}
.pcg-preloader::before{ /* sweeping gold seam on the curtain edge */
  content:"";position:absolute;left:0;right:0;bottom:0;height:2px;
  background:linear-gradient(90deg,transparent,#C9A14A,#E6CF8F,#C9A14A,transparent);
  opacity:0;transition:opacity .4s;
}
.pcg-preloader.lift::before{opacity:1}
.pcg-pre__inner{position:relative;display:flex;flex-direction:column;align-items:center;text-align:center}
.pcg-pre__mono{
  width:clamp(110px,16vw,168px);height:auto;
  opacity:0;transform:translateY(14px) scale(.92);
  filter:drop-shadow(0 18px 60px rgba(201,161,74,.28));
  transition:opacity .6s var(--ease),transform .7s var(--ease);
}
.pcg-preloader.in .pcg-pre__mono{opacity:.96;transform:none}
.pcg-pre__word{
  margin-top:26px;display:flex;gap:.34em;overflow:hidden;
  font-family:var(--ff-body);font-weight:500;
  font-size:clamp(9px,1vw,12px);letter-spacing:.42em;text-transform:uppercase;
  color:var(--champagne);
}
.pcg-pre__word span{display:inline-block;transform:translateY(110%);transition:transform .5s var(--ease)}
.pcg-preloader.in .pcg-pre__word span{transform:none}
/* The bar both FILLS and RAKES: `i` still scales in from the left (that is
   the progress), and ::after runs the cutter's pass over it (that is the
   waiting). 2px rather than the original 1px is a considered change and the
   only geometry this step touches — at 1px the masked plate samples a
   single row of a 14px tile and the travelling highlight simply does not
   register on a full-screen black curtain, which was verified by rendering
   it. 2px on a 160px track is still a hairline. */
.pcg-pre__bar{position:relative;margin-top:30px;width:160px;height:2px;background:rgba(201,161,74,.2);overflow:hidden}
.pcg-pre__bar i{display:block;height:100%;width:100%;background:var(--gold-grad);transform:scaleX(0);transform-origin:left;transition:transform .8s cubic-bezier(.5,0,.1,1) .12s}
.pcg-preloader.in .pcg-pre__bar i{transform:scaleX(1)}

/* ---------- The cutter's pass — ONE waiting state for the whole site -----
   Three previously unrelated "please wait" animations now run the same
   keyframe: this intro bar filling, the AI console on diensten.html
   thinking (css/assistant.css, .aia-sweep i) and the contact form
   submitting (below). The strip is 2px of --seam-ink masked by the same
   168 x 14 banknote plate every chapter-break seam is cut from, travelling
   exactly one tile per iteration so the loop has no visible join. It is a
   transform on a small pseudo-element, so it stays on the compositor;
   animating mask-position or background-position instead would repaint it
   every frame.

   WHY THE KEYFRAME LIVES HERE. css/instrument-live.css defines .is-cutting
   and declares this same keyframe, but that sheet loads on index.html
   ALONE. effects.css is in sync-shell.js's base stylesheet list (line 186)
   and therefore ships on all eleven pages, so this is the only sheet that
   can guarantee the keyframe exists wherever a waiting state does. The two
   declarations are byte-identical by design; if instrument-live.css's copy
   is ever removed, nothing breaks. Keep them identical or delete that one.

   WHY THE BODY IS NOT ALSO SHARED AS A CLASS. .is-cutting anchors its strip
   to the box's MIDLINE, which is right for a 34px graduated track and wrong
   for the three states here — a 2px intro bar, the top edge of a console
   and the foot of a form panel each need their own anchor. One keyframe and
   one recipe is the real unit of reuse; one class was not.

   CONTRAST. --seam-ink is a lit metal ramp, so the strip is deliberately
   not one value: over the intro curtain's #070707 its darkest stop --m-0
   #5A461A measures 2.23:1 and its brightest --m-4 #F0DEA9 measures 15.09:1.
   (Re-measured. The two figures previously written here, 2.11 and 15.9,
   were a stale onyx reading and a digit transposition; the curtain is
   #070707, not --onyx #0B0B0C, where the same two stops read 2.18 and
   14.73. Nothing about the judgement below changes.)
   That is the same standard the landed section seams already ship at, and
   it is correct for this construct rather than a compromise: a machined
   highlight that read as a single flat value would not be a highlight. The
   strip is never the sole carrier of the waiting state either — the intro
   bar also fills, the console also disables its input and flips
   aria-busy="true", the form also disables its submit button. */
.pcg-preloader.in .pcg-pre__bar::after,
#contactForm:has(button[type="submit"]:disabled)::after{
  content:"";position:absolute;z-index:3;pointer-events:none;
  left:0;width:calc(100% + 168px);height:2px;
  background:var(--seam-ink);
  -webkit-mask-image:var(--seam-plate);mask-image:var(--seam-plate);
  -webkit-mask-size:168px var(--seam-h);mask-size:168px var(--seam-h);
  /* -.43 of the tile lifts the plate so the strip shows its braided
     midline and the two rim rules at y=.6 / y=13.4 crop off. */
  -webkit-mask-position:0 calc(var(--seam-h) * -.43);mask-position:0 calc(var(--seam-h) * -.43);
  -webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;
  animation:pcg-cut .62s linear infinite;
}
.pcg-preloader.in .pcg-pre__bar::after{top:0}
@keyframes pcg-cut{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(-168px,0,0)}
}

/* The third state: the contact form in flight. contact.html's handler
   disables the submit button for exactly the duration of the fetch and
   re-enables it on failure, so :has() reads that state with no markup, no
   extra class and no script — and the strip sits on the FORM rather than on
   the button because .btn:disabled drops to opacity:.45, which would drag
   the cue down to 2.5:1 with it. At the form's foot it renders at full
   strength, immediately under the button and its privacy note.
   overflow:hidden is scoped to the in-flight state too: the strip is one
   tile wider than its track and has to be clipped, but the form has no
   reason to be a clipping box for the other 99.9% of its life. */
#contactForm:has(button[type="submit"]:disabled){position:relative;overflow:hidden}
#contactForm:has(button[type="submit"]:disabled)::after{bottom:0}

/* gate hero reveal until the curtain lifts */
body.pcg-intro .hero [data-rise]{opacity:0 !important;transform:translateY(34px) !important}
body.pcg-intro .hero .reveal-line>span{transform:translateY(112%) !important}
body.pcg-intro .hero__mono{opacity:0 !important}
.hero__mono{transition:opacity 1.4s var(--ease)}

/* ---------- Foil sheen — removed ----------
   This animated the background-position of .gold-foil's clipped gradient
   so emphasised words shimmered. .gold-foil no longer has a background
   to move (see pcg.css: gradient text is banned here — its pale stop
   measured 1.39:1 on the cream skin), and an 8s infinite loop on every
   emphasised word on the page was a permanent repaint for no meaning.
   Gold now reads as metal through a static halo, not through motion. */

/* ---------- Image clip reveal ---------- */
.fx-clip{clip-path:inset(0 0 0 0)}
.fx-clip .frame__media{transform:scale(1.25);transition:transform 1.6s var(--ease)}
.fx-clip.is-shown .frame__media{transform:scale(1)}

/* ---------- Work cards: gentle, no tilt ---------- */
.card__stretch{position:absolute;inset:0;z-index:4;display:block}
.card__stretch:focus-visible{outline:2px solid var(--gold-text);outline-offset:-4px}
/* The image comes off the page: a tight contact term plus a wide warm
   cast, so it reads as the picture being lifted into the light rather
   than as a generic drop-shadow. */
.work .frame{transition:box-shadow .6s var(--ease)}
.work:hover .frame,.work:focus-within .frame{
  box-shadow:var(--lift-3),var(--cast-gold);
}
/* Keep this in step with the .card rule in components.css — effects.css
   loads later, so whatever is written here is the transition that runs.
   (It used to also carry a fully transparent inset shadow on :hover,
   which silently outranked and cancelled the card's own lift.)

   ############  THE HOVER VALUE IS A PAIR, NOT A TIDY  ############
   .card now carries the milled pocket var(--lip-tl) in its BASE box-shadow
   (css/components.css). box-shadow interpolates shadow-for-shadow down a
   list, so a hover value of var(--lift-2) alone would animate a one-shadow
   list from a two-shadow one: the pocket would vanish and the lift would
   snap in from nothing, on every card on all eleven pages. The hovered
   value therefore has to be BOTH — two shadows to two shadows, pocket
   first, lift second. It is written here rather than left to
   components.css because this sheet loads later and owns the transition,
   so this is the declaration that actually runs; components.css declares
   the same value, and the two must not drift.
   Order matters: --lip-tl is inset and --lift-2 is not, so they never
   overlap, but the inset pair has to be listed first to stay paired with
   the base value's single entry during the interpolation. */
.card{transition:background-color .6s var(--ease),box-shadow .5s var(--ease)}
.card:hover,.card:focus-within{box-shadow:var(--lip-tl),var(--lift-2)}

/* contact form focus glow */
#contactForm:focus-within{box-shadow:0 0 0 1px rgba(201,161,74,.38),0 2px 8px rgba(0,0,0,.3),0 40px 120px -40px rgba(201,161,74,.28)}
#contactForm{transition:box-shadow .6s var(--ease)}

/* ---------- Auto-hide nav ----------
   `padding` is gone from this list: the scrolled state is a composited
   `translate` now (see components.css). `transform` stays reserved for
   the auto-hide alone, so the two never fight over one property. */
.nav{transition:background .6s var(--ease),translate .6s var(--ease),border-color .6s var(--ease),box-shadow .6s var(--ease),transform .55s var(--ease)}
.nav--hidden{transform:translateY(-100%)}

/* ---------- Scroll-progress dot read-out ---------- */
.progress__fill{box-shadow:0 0 18px rgba(201,161,74,.5)}

/* ---------- Reduced motion safety ---------- */
@media (prefers-reduced-motion:reduce){
  .pcg-preloader{display:none !important}
  body.pcg-intro .hero [data-rise]{opacity:1 !important;transform:none !important}
  body.pcg-intro .hero .reveal-line>span{transform:none !important}
  body.pcg-intro .hero__mono{opacity:.55 !important}
  /* Depth still reads, it just stops travelling: the shadow and colour
     states below are kept, only their duration collapses. */
  .nav,.card,.work .frame,#contactForm,.fx-clip .frame__media,.hero__mono{transition-duration:.01ms}
  .fx-clip .frame__media{transform:none}
  /* The cutter's pass STOPS but stays visible, so a persistent working
     state is still communicated without motion. (The intro curtain is
     display:none above, so only the form's strip is reachable here.) */
  #contactForm:has(button[type="submit"]:disabled)::after{animation:none}
}

/* Mask-built line-work has nothing to paint in a forced palette. The state
   is still carried by the disabled button, so nothing is lost. */
@media (forced-colors:active){
  .pcg-preloader.in .pcg-pre__bar::after,
  #contactForm:has(button[type="submit"]:disabled)::after{display:none}
}
