/* ==========================================================================
   Steel Point Engineering — keyframes + scroll-reveal classes
   Everything here animates transform/opacity/clip-path only. Nothing that
   triggers layout. Reveal 600ms, hover 200ms, nothing over 900ms.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Reveal engine (driven by one IntersectionObserver in scripts/main.js)
   -------------------------------------------------------------------------- */

/* B5 — the hide is gated on `html.js`, which an inline script in <head> sets
   before this sheet is even parsed and which main.js removes again if its IIFE
   throws. ~95 elements start hidden here — every h2, every card, the whole quote
   form — so without the gate a single uncaught error, a blocked script or a
   failed download rendered the lead-gen page blank with no degradation. The
   <noscript> block in index.html stays as belt and braces.

   The gate is wrapped in `:where()` so it adds NO specificity. Written plainly,
   `html.js [data-reveal]` scores (0,2,1) and would outrank
   `[data-reveal].is-revealed` at (0,2,0) — the hide would beat the reveal and
   the page would never appear at all. `:where(html.js) [data-reveal]` keeps the
   original (0,1,0), so the cascade below is untouched. A browser too old to
   parse `:where()` drops the rule entirely, which fails towards visible.

   The transform rules below keep their ungated selectors on purpose: a
   translate with opacity 1 is harmless, and repeating the gate five times would
   only invite one of them to drift. The mask-up clip-path IS gated, because
   `inset(100% 0 0 0)` hides content outright rather than merely offsetting it.

   M9 — `will-change: transform, opacity` used to sit here, promoting all ~95
   revealable elements to their own compositor layer at first paint. is-revealed
   released it, but only after an element revealed, so everything below the fold
   held a layer for the whole scroll — tens of MB on a mid-range Android, enough
   to push the browser back to software compositing and make the marquee and
   parallax janky, i.e. the exact opposite of the intent. Every current engine
   promotes opacity and transform transitions on demand anyway. */
:where(html.js) [data-reveal] {
  opacity: 0;
  transition:
    opacity var(--dur-reveal) var(--ease-reveal),
    transform var(--dur-reveal) var(--ease-reveal),
    clip-path var(--dur-reveal) var(--ease-reveal);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-reveal="fade-up"],
[data-reveal=""] {
  transform: translateY(24px);
}
[data-reveal="fade-left"] {
  transform: translateX(-28px);
}
[data-reveal="fade-right"] {
  transform: translateX(28px);
}
[data-reveal="scale-in"] {
  transform: scale(0.96);
}
/* Elements that own a clip-path must be exempted from every clip-path here.
   This file is UNLAYERED and main.css sits in @layer components, so ANY
   clip-path here beats the component's outright, at any specificity: before
   reveal it clipped the element to nothing, and after reveal `inset(0)`
   squared its corners off. Such elements reveal on opacity + transform only.

   The exemption list is explicit because CSS cannot select "has a --chamfer".
   .chamfer / .chamfer-sm are the shared utilities; .gcard and .inset-panel
   declare their own `clip-path: polygon(...)` without carrying either class, so
   they were being squared off after reveal — the 15 revealable .gcards (9
   service, 4 why-us, the form panel, the contact aside) and the labour-hire
   panel. Add any future clip-path-owning component here, and keep this list in
   step with the two below. */
:where(html.js) [data-reveal="mask-up"]:not(.chamfer, .chamfer-sm, .gcard, .inset-panel) {
  clip-path: inset(100% 0 0 0);
}

[data-reveal="mask-up"] {
  transform: translateY(12px);
}

/* FIXED — was the KNOWN ISSUE recorded as WORK-LOG.md §5.4 item 4.

   Everything in this file is UNLAYERED and main.css sits in `@layer components`,
   so while `data-reveal` is present these `transform`, `clip-path`, `transition`
   and `background-position` declarations outrank every component hover/focus
   rule at any specificity. `.gcard:hover`'s 6px lift was dead on every .gcard
   that was also a revealable, while the rest lifted normally.

   The fix is in main.js, not here: `settle()` moves `data-reveal` to
   `data-revealed` on `transitionend`, so these selectors simply stop matching
   once the reveal has finished and the component rules take over cleanly. Do not
   "fix" it again by weakening the selectors below — they are correct, and they
   need to keep winning for the duration of the reveal itself.

   For the record, what does NOT work here, so nobody retries it: a
   `var(--reveal-rest)` indirection. Custom properties INHERIT, so hovering a
   card also lifted any nested revealable (the tick icons inside the "Why Steel
   Point" cards moved a second 6px), and the unlayered `transition` above still
   won, so the restored lift ran on the reveal's 600ms plus up to 480ms of
   stagger delay instead of 200ms. */
[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

[data-reveal].is-revealed:not(.chamfer, .chamfer-sm, .gcard, .inset-panel) {
  clip-path: inset(0 0 0 0);
}

/* --------------------------------------------------------------------------
   Keyframes
   -------------------------------------------------------------------------- */

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

@keyframes word-up {
  from {
    opacity: 0;
    transform: translateY(22px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes rule-draw {
  to {
    transform: scaleX(1);
  }
}

@keyframes ken-burns {
  from {
    transform: scale(1.08);
  }
  to {
    transform: scale(1);
  }
}

@keyframes bob {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(8px);
  }
}

@keyframes blue-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(43, 133, 232, 0);
  }
  35% {
    box-shadow: 0 0 0 8px rgba(43, 133, 232, 0.28);
  }
  100% {
    box-shadow: 0 0 0 14px rgba(43, 133, 232, 0);
  }
}

/* Final-CTA button: very subtle breathing glow, 0.35 → 0.5 */
@keyframes glow-breathe {
  0%,
  100% {
    box-shadow: 0 0 26px -6px rgba(43, 133, 232, 0.35);
  }
  50% {
    box-shadow: 0 0 34px -4px rgba(43, 133, 232, 0.5);
  }
}

.glow-breathe {
  animation: glow-breathe 3s var(--ease-hover) infinite;
}

@keyframes marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@keyframes sheen {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -100% 0;
  }
}

/* `ring-pulse` was here. Its only consumer, `.step-node.is-lit::after`, was
   clipped away by the node's own `.chamfer-sm` clip-path and never rendered —
   see the note where that rule used to be in main.css. Removed together. */

@keyframes scale-in {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes menu-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes step-in {
  from {
    opacity: 0;
    transform: translateX(24px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes step-in-back {
  from {
    opacity: 0;
    transform: translateX(-24px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes err-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes dash-pulse {
  0%,
  100% {
    border-color: rgba(43, 133, 232, 0.55);
  }
  50% {
    border-color: rgba(43, 133, 232, 1);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Stroke draw — checkmarks, icons, the thank-you tick */
@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}

.draw-on-reveal path,
.draw-on-reveal circle,
.draw-on-reveal polyline {
  stroke-dasharray: var(--dash, 60);
  stroke-dashoffset: var(--dash, 60);
}

.draw-on-reveal.is-revealed path,
.draw-on-reveal.is-revealed circle,
.draw-on-reveal.is-revealed polyline {
  animation: draw 700ms var(--ease-reveal) forwards;
}

/* Service-card icon re-draws its stroke on hover */
@media (hover: hover) and (pointer: fine) {
  .gcard:hover .ico path,
  .gcard:hover .ico polyline,
  .gcard:hover .ico line,
  .gcard:hover .ico rect,
  .gcard:hover .ico circle {
    stroke-dasharray: 120;
    animation: redraw 700ms var(--ease-hover) forwards;
  }
}

@keyframes redraw {
  from {
    stroke-dashoffset: 120;
  }
  to {
    stroke-dashoffset: 0;
  }
}

/* Problem-section ✕ markers */
.x-marker {
  opacity: 0;
  transform: scale(0.6);
  transition:
    opacity 320ms var(--ease-reveal),
    transform 320ms var(--ease-reveal);
  transition-delay: calc(var(--reveal-delay, 0ms) + 200ms);
}

.is-revealed .x-marker,
[data-reveal].is-revealed.x-line .x-marker {
  opacity: 1;
  transform: scale(1);
}

/* Stat-bar dividers scale out from the centre */
.divider-x {
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 700ms var(--ease-reveal);
}

.is-revealed .divider-x,
.divider-x.is-revealed {
  transform: scaleX(1);
}

/* --------------------------------------------------------------------------
   MANDATORY: reduced motion kills everything
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }

  /* Same exemption list as the two reveal rules above, and it must stay in step
     with them: a blanket `clip-path: none` erases the corner cut on everything
     that owns one — the 11 gallery tiles and the positioning figure (.chamfer),
     the 15 revealable .gcards (9 service, 4 why-us, form panel, contact
     aside) and the labour-hire panel
     (.inset-panel) all squared off for reduced-motion users. Nothing that owns a
     clip-path is ever clipped to nothing, so there is nothing to reset. */
  [data-reveal]:not(.chamfer, .chamfer-sm, .gcard, .inset-panel) {
    clip-path: none !important;
  }

  .x-marker,
  .divider-x {
    opacity: 1 !important;
    transform: none !important;
  }

  .draw-on-reveal path,
  .draw-on-reveal circle,
  .draw-on-reveal polyline {
    stroke-dashoffset: 0 !important;
  }

  .marquee-track {
    transform: none !important;
  }

  .hero-media img {
    transform: none !important;
  }

  .step.is-active {
    animation: none !important;
  }

  .timeline-rail::after {
    transform: scaleY(1) !important;
  }

  /* Everything below is the same class of bug: an element whose ONLY source of
     a visible state was an animation the blanket rule above kills. The safe
     pattern for anything added later is to set the visible state in CSS and
     animate *from* a keyframe `from{}`, so the reset is never needed.

     B3 — the mobile menu links (main.css: opacity 0 + translateY, restored only
     by `menu-in`). Reduced motion is a default on many OS installs, so this left
     those users with a menu showing the logo, the close button and the CTA and
     ZERO navigation links — no working navigation on mobile at all. */
  .mobile-menu a.m-link {
    opacity: 1 !important;
    transform: none !important;
  }

  /* The hero's 84px blue rule, drawn only by `rule-draw`. */
  .rule-draw {
    transform: scaleX(1) !important;
  }

  /* The h1's per-word spans (main.css: opacity 0 + translateY, revealed only by
     `word-up`). main.js already writes these three values inline when reduced
     motion is set, but the headline is the largest text on the page and should
     not depend on a script having got that far. */
  .hero h1 .word {
    opacity: 1 !important;
    transform: none !important;
  }

  /* m2 — the scroll-linked rule between the positioning columns. Its scaleY(0)
     is inline in the HTML and main.js returns before touching it under reduced
     motion, so it never appeared. */
  #posRule {
    transform: scaleY(1) !important;
  }

  /* m3 — the submit tick is hidden by stroke-dashoffset and drawn only by
     `draw`, so the quote form's success confirmation was a blank button for the
     650ms before the redirect. */
  .btn-submit .tick path {
    stroke-dashoffset: 0 !important;
  }

  /* Not in the audit: .err is `display: none; opacity: 0` and `.field.is-error
     .err` only flips it to `display: block` plus `animation: err-in forwards`.
     With the animation killed every field-level validation message in the quote
     form was laid out and then painted at zero opacity — reduced-motion users
     were told a field was wrong (2px amber border) with no way to read why. */
  .err {
    opacity: 1 !important;
    transform: none !important;
  }

  /* A `.step-node.is-lit::after { opacity: 0 !important }` reset was here. It
     was reasoned from a real rule — kill the animation and a ring that ends at
     opacity 0 would otherwise stay painted — but the ring it protected against
     was itself clipped away by the node's `.chamfer-sm` and never rendered in
     any motion mode. Both are gone; see main.css at `.step-node.is-lit`. */
}
