/* ==========================================================================
   QDesk — sticky hero reveal + animated SVG section backgrounds
   Loaded after site.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Sticky hero, scrolled over by the content beneath it
   -------------------------------------------------------------------------- */

/* Everything after the hero rides above it and carries an opaque background,
   so it genuinely covers the hero rather than letting it show through. */
main > section:not(.hero),
main > article,
main > div:not(#main-content) {
  position: relative;
  z-index: 1;
}

main > section:not(.hero):not(.section-tint):not(.section-dark):not(.proof-bar):not(.cta-band):not(.page-hero):not(.post-hero) {
  background: var(--bg);
}

/* The first band after the hero lifts over it: rounded shoulders and a soft
   edge shadow read as one surface sliding across another. */
.hero + #main-content + section,
.hero + section {
  border-start-start-radius: clamp(18px, 3vw, 34px);
  border-start-end-radius: clamp(18px, 3vw, 34px);
  box-shadow: 0 -18px 44px rgba(9, 20, 34, .16);
}

/* The footer must also cover the pinned hero. */
.site-footer { position: relative; z-index: 1; }

/* Scroll cue at the foot of the hero. */
.hero-scroll {
  position: absolute; left: 50%; bottom: clamp(1rem, 3vh, 2rem);
  transform: translateX(-50%);
  display: grid; place-items: center;
  width: 34px; height: 54px; z-index: 2;
  color: #8fabc7;
}
.hero-scroll-mouse {
  display: block; width: 24px; height: 38px;
  border: 2px solid currentColor; border-radius: 13px; opacity: .55;
}
.hero-scroll-mouse i {
  display: block; width: 3px; height: 7px; margin: 6px auto 0;
  border-radius: 2px; background: currentColor;
  animation: scrollHint 1.9s var(--ease) infinite;
}
.hero-scroll:hover { color: #cfe0f2; }
@keyframes scrollHint {
  0%   { transform: translateY(0);    opacity: 1; }
  70%  { transform: translateY(12px); opacity: 0; }
  100% { transform: translateY(0);    opacity: 0; }
}

/* The hero must fit one screen on phones, so the copy tightens up.
   The artwork stops covering the hero and becomes a band across the middle of
   it. `slice` scales to the larger of the two ratios, so a full-height box on
   a portrait screen is scaled by height and the visible slice of a 1600-wide
   field collapses to roughly 400 units — which here would be the empty centre
   the headline sits in, and nothing else. Shrinking the box vertically lowers
   that scale and widens the slice back to about 900 units, enough to bring the
   four routes and the near edge of both clusters into frame.
   It also runs brighter than on desktop rather than dimmer, for the same
   reason: there is far less of it left to see. The copy keeps its own scrim
   (.hero-copy::before), so legibility never depended on holding the field down. */
@media (max-width: 899px) {
  .hero-art {
    opacity: .8;
    top: 50%; bottom: auto; height: 62%;
    transform: translateY(-50%);
  }

  .hero-title { font-size: clamp(1.9rem, 8.5vw, 2.9rem); }
  .hero-sub {
    font-size: 1rem; margin-bottom: 1.5rem;
    display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
  }
  .hero-trust { margin-top: 1.5rem; gap: .35rem 1rem; font-size: .78rem; }
  .hero .eyebrow { margin-bottom: .6rem; }
}

/* On a phone the band moves above the copy and gets shorter still.
   Height is what decides how much width a `slice` crop shows: at 62% of a tall
   screen the visible slice is about 670 units of a 1600-unit field — the empty
   lane the headline sits in, and nothing else. At 30% it opens to roughly 1350,
   which is the whole composition, pile and rows and all.
   It sits above the headline rather than behind it because the alternative is
   four rows of artwork running through the copy, which is the crowding this
   design exists to avoid. Small enough to read as texture rather than as a
   diagram, which on a 390px screen is the honest ambition. */
@media (max-width: 599px) {
  .hero-art {
    top: 0; bottom: auto; height: 30%;
    transform: none;
    opacity: .7;
  }
}

@media (max-width: 420px) {
  .hero-trust li:nth-child(n+3) { display: none; }
}

/* Landscape phones are too short for a full-height pinned hero. */
@media (max-height: 520px) and (max-width: 899px) {
  .hero { min-height: auto; position: relative; padding-block: 3.5rem 2.5rem; }
  .hero-scroll { display: none; }
  .hero + section, .hero + #main-content + section { box-shadow: none; }
}

/* --------------------------------------------------------------------------
   2. Animated SVG section backgrounds
   -------------------------------------------------------------------------- */
.sec-bg {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
}
.sec-bg .sb-svg { width: 100%; height: 100%; display: block; }

/* Tone controls the ink colour the SVG inherits. Kept low so the motifs stay
   texture behind the copy rather than competing with it. */
.sec-bg-light { color: var(--brand); opacity: .34; }
.sec-bg-dark  { color: #7fc4ff;      opacity: .40; }

/* Sections hosting a background need a positioning context.
   Deliberately no `overflow: hidden` here: that would turn every section into
   a scroll container and break the sticky sidebars (.split-aside, .side-sticky)
   and the sticky hero. Clipping is handled by .sec-bg itself.
   .hero is excluded so its `position: sticky` survives. */
.section, .proof-bar, .cta-band, .page-hero, .section-dark {
  position: relative;
}
.section > .wrap, .proof-bar > .wrap, .cta-band > .wrap,
.page-hero > .wrap, .site-footer > .wrap {
  position: relative; z-index: 1;
}

/* --- flow: records travelling through the pipeline ----------------------- */
.sb-lane {
  stroke-dasharray: 240 1400;
  animation: laneRun var(--dur, 12s) linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes laneRun { to { stroke-dashoffset: -1640; } }

.sb-packet {
  offset-path: none;
  animation: packetRun var(--dur, 12s) linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes packetRun {
  0%   { transform: translateX(0);      opacity: 0; }
  8%   { opacity: .55; }
  92%  { opacity: .55; }
  100% { transform: translateX(1560px); opacity: 0; }
}

/* --- records: documents rising and fading -------------------------------- */
.sb-doc {
  animation: docRise var(--dur, 20s) linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes docRise {
  0%   { transform: translateY(80px)   rotate(-4deg); opacity: 0; }
  12%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateY(-520px) rotate(5deg);  opacity: 0; }
}

/* --- scan: verification sweep -------------------------------------------- */
.sb-scan { animation: scanSweep 7s var(--ease) infinite; }
@keyframes scanSweep {
  0%   { transform: translateX(0);      opacity: 0; }
  10%  { opacity: 1; }
  90%  { opacity: 1; }
  100% { transform: translateX(1740px); opacity: 0; }
}

/* --- nodes / circuit ------------------------------------------------------ */
.sb-node {
  transform-box: fill-box; transform-origin: center;
  animation: nodePulse 4.2s var(--ease) infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes nodePulse {
  0%, 100% { transform: scale(1);    opacity: .55; }
  50%      { transform: scale(1.55); opacity: 1; }
}

.sb-trace {
  stroke-dasharray: 180 2200;
  animation: traceRun 9s linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes traceRun { to { stroke-dashoffset: -2380; } }

/* --- chart: throughput bars ---------------------------------------------- */
.sb-bar {
  transform-box: fill-box; transform-origin: bottom;
  animation: barBreathe 6s var(--ease) infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes barBreathe {
  0%, 100% { transform: scaleY(.82); }
  50%      { transform: scaleY(1); }
}

/* --------------------------------------------------------------------------
   3. Animated header strip
   -------------------------------------------------------------------------- */
/* Deliberately faint: this is texture behind the bar, not a graphic element.
   Masked at both ends so the logo and email sit on clean ground. */
.header-bg {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
  color: var(--brand); opacity: .22;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 30%, transparent 46%,
                                      transparent 58%, #000 74%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 30%, transparent 46%,
                                      transparent 58%, #000 74%, transparent);
}
.header-bg svg { width: 100%; height: 100%; display: block; }
.header-inner { position: relative; z-index: 1; }

.hdr-dash {
  stroke-dasharray: 60 300;
  animation: hdrRun 14s linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes hdrRun { to { stroke-dashoffset: -360; } }

.hdr-dot {
  transform-box: fill-box; transform-origin: center;
  animation: nodePulse 5s var(--ease) infinite;
  animation-delay: var(--delay, 0s);
}

/* --------------------------------------------------------------------------
   4. Section label — letter-by-letter focus-in
   -------------------------------------------------------------------------- */

/* Only hide the characters once JS has split them and can bring them back. */
.js .eyebrow .eb-ch {
  display: inline-block;
  white-space: pre;
  opacity: 0;
  transform: translateY(16px) scale(.86);
  filter: blur(7px);
}
.js .eyebrow.is-in .eb-ch {
  animation: ebFocusIn .62s cubic-bezier(.2, .8, .3, 1) both;
  animation-delay: calc(var(--i) * 26ms);
}
@keyframes ebFocusIn {
  0%   { opacity: 0; transform: translateY(16px) scale(.86); filter: blur(7px); }
  55%  { opacity: 1; transform: translateY(-2px) scale(1.02); filter: blur(0); }
  100% { opacity: 1; transform: none; filter: blur(0); }
}

/* A light sweeps across the finished label once. */
.js .eyebrow.is-in .eb-sheen {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg, transparent 35%,
              rgba(255, 255, 255, .85) 50%, transparent 65%);
  mix-blend-mode: overlay;
  transform: translateX(-120%);
  animation: ebSheen 1.1s var(--ease) both;
  animation-delay: calc(var(--n, 10) * 26ms + 260ms);
}
@keyframes ebSheen {
  to { transform: translateX(120%); }
}

/* --------------------------------------------------------------------------
   5. Reduced motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .sb-lane, .sb-packet, .sb-doc, .sb-scan, .sb-node,
  .sb-trace, .sb-bar,
  .hdr-dash, .hdr-dot, .hero-scroll-mouse i,
  .ha-scrap, .ha-trail, .ha-head, .ha-row, .ha-tick {
    animation: none !important;
  }
  .sb-scan { opacity: 0; }
  .sb-packet { opacity: .45; }

  /* The hero backdrop still tells its story standing still: the uneven pile,
     the four routes out of it and the ordered rows at the end are all drawn in
     place, and the travelling items simply park at the start of their route.
     Only the three whose start or end state is invisible need help — the pile
     fades in from nothing, and a row and its tick both begin transparent. */
  .ha-scrap { opacity: .34; }
  .ha-trail { stroke-opacity: .4; }
  .ha-row   { opacity: 1; transform: none; }
  .ha-tick  { stroke-dashoffset: 0; }

  /* Labels appear immediately, fully legible, with no motion. */
  .js .eyebrow .eb-ch {
    opacity: 1 !important; transform: none !important;
    filter: none !important; animation: none !important;
  }
  .js .eyebrow .eb-sheen { display: none; }
  .eyebrow::after { transition: none; }
}

/* ==========================================================================
   6. Slot artwork (partials/slot-art.php)

   One motif per empty image slot. Each is a picture of what that section
   claims, so the animations are timed to be read rather than noticed: nothing
   here runs faster than about four seconds, and every loop resolves to a
   still that still makes sense — which is what the reduced-motion block at
   the foot of this file relies on.
   ========================================================================== */
.slot-art {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--sh-2);
}
.slot-art .sa-svg { width: 100%; height: auto; display: block; }

/* A slot's artwork sits where a photograph would, so it inherits that
   figure's framing rather than adding a second one inside it. */
.side-figure .slot-art,
.oyp-figure .slot-art,
.team-photo .slot-art,
.office-photo .slot-art,
.svc-photo .slot-art,
.product-photo .slot-art { box-shadow: none; border-radius: 0; }

/* The small uppercase labels. Sized in the SVG's own user units so they scale
   with the drawing instead of drifting as the panel resizes. */
.sa-tag {
  font: 700 11px var(--font);
  fill: var(--muted);
  letter-spacing: 1.6px;
}
.sa-tag-teal { fill: var(--teal); }
.sa-coin-glyph { font: 800 34px var(--font); fill: #fff; fill-opacity: .92; }
.sa-coin-glyph-sm { font: 800 22px var(--font); fill: var(--amber-deep); fill-opacity: 1; }

/* --- shared behaviours ---------------------------------------------------- */

/* A queue that never quite settles. Deliberately small movement: this is
   meant to feel like weight, not activity. */
.sa-queue {
  transform-box: fill-box; transform-origin: left center;
  animation: saQueue 6.5s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saQueue {
  0%, 100% { transform: translateX(0)   scaleX(1);    opacity: .85; }
  50%      { transform: translateX(4px) scaleX(1.04); opacity: 1; }
}

/* Work that has been finished, arriving and then holding long enough to be
   read before the loop resets. */
.sa-cleared {
  animation: saCleared 8s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saCleared {
  0%        { opacity: 0; transform: translateX(-10px); }
  10%, 74%  { opacity: 1; transform: translateX(0); }
  90%, 100% { opacity: 0; transform: translateX(0); }
}

.sa-bar {
  transform-box: fill-box; transform-origin: bottom;
  animation: saBar 5.5s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saBar {
  0%, 100% { transform: scaleY(.7); }
  50%      { transform: scaleY(1); }
}

/* --- bandwidth ------------------------------------------------------------ */
.sa-ring {
  transform-box: fill-box; transform-origin: center;
  animation: saRing 3.4s var(--ease) infinite;
}
@keyframes saRing {
  0%        { transform: scale(1);   opacity: .9; }
  70%, 100% { transform: scale(1.5); opacity: 0; }
}

/* Capacity rising to meet the load. It stops short of the end of the track on
   purpose — a bar pegged at 100% is a bar with no headroom left, which is the
   client's problem, not the answer to it. */
.sa-meter {
  transform-box: fill-box; transform-origin: left center;
  animation: saMeter 7s var(--ease) infinite;
}
@keyframes saMeter {
  0%       { transform: scaleX(.06); }
  45%, 78% { transform: scaleX(.84); }
  100%     { transform: scaleX(.06); }
}

/* --- story ---------------------------------------------------------------- */
/* The line draws itself once, then a warm pulse runs it forever. */
.sa-spine {
  stroke-dasharray: 620;
  stroke-dashoffset: 620;
  animation: saDraw 2.6s var(--ease) .2s forwards;
}
@keyframes saDraw { to { stroke-dashoffset: 0; } }

.sa-spine-pulse {
  stroke-dasharray: 40 700;
  stroke-dashoffset: 740;
  opacity: .75;
  animation: saPulseRun 6s linear 2.4s infinite;
}
@keyframes saPulseRun { to { stroke-dashoffset: 0; } }

.sa-halo {
  transform-box: fill-box; transform-origin: center;
  animation: saHalo 6s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saHalo {
  0%, 60%, 100% { transform: scale(.7);   opacity: .35; }
  22%           { transform: scale(1.35); opacity: 1; }
}
.sa-blink {
  animation: saBlink 6s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saBlink {
  0%, 60%, 100% { opacity: .45; }
  22%           { opacity: 1; }
}

.sa-grow {
  transform-box: fill-box; transform-origin: left center;
  animation: saGrow .8s var(--ease) both;
  animation-delay: var(--d, 0s);
}
@keyframes saGrow { from { transform: scaleX(0); opacity: 0; } to { transform: none; opacity: 1; } }

/* --- team ----------------------------------------------------------------- */
.sa-person {
  transform-box: fill-box; transform-origin: center bottom;
  animation: saPop .7s var(--ease) both;
  animation-delay: var(--d, 0s);
}
@keyframes saPop {
  from { opacity: 0; transform: translateY(10px) scale(.7); }
  to   { opacity: 1; transform: none; }
}

.sa-sweep { animation: saSweep 9s var(--ease) 1.4s infinite; }
@keyframes saSweep {
  0%        { transform: translateX(0); }
  55%, 100% { transform: translateX(620px); }
}

/* --- office-head ---------------------------------------------------------- */
.sa-radar {
  transform-box: fill-box; transform-origin: center;
  animation: saRadar 4.5s cubic-bezier(.2, .6, .3, 1) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saRadar {
  0%   { transform: scale(.5);  opacity: .55; }
  100% { transform: scale(3.4); opacity: 0; }
}

/* --- office-back ---------------------------------------------------------- */
.sa-desk {
  transform-box: fill-box; transform-origin: center;
  animation: saPop .6s var(--ease) both;
  animation-delay: var(--d, 0s);
}

/* The ticket rides its route via SMIL; this only fades the ends, so it does
   not pop in and out at the edges of the floor. */
.sa-ticket { animation: saTicket 11s linear infinite; }
@keyframes saTicket {
  0%, 100% { opacity: 0; }
  6%, 92%  { opacity: 1; }
}

/* --- careers -------------------------------------------------------------- */
.sa-step {
  transform-box: fill-box; transform-origin: bottom;
  animation: saStep .85s var(--ease) both;
  animation-delay: var(--d, 0s);
}
@keyframes saStep { from { transform: scaleY(0); opacity: 0; } to { transform: none; opacity: 1; } }

.sa-badge {
  transform-box: fill-box; transform-origin: center;
  animation: saBadge .7s cubic-bezier(.3, 1.5, .5, 1) both;
  animation-delay: var(--d, 0s);
}
@keyframes saBadge {
  from { opacity: 0; transform: translateY(8px) scale(.4); }
  to   { opacity: 1; transform: none; }
}

/* The climb. Held at the top for a beat before it resets, so the loop reads
   as an arrival rather than a treadmill. */
.sa-climber {
  transform-box: fill-box; transform-origin: center;
  animation: saClimb 9s var(--ease) 1.2s infinite;
}
@keyframes saClimb {
  0%        { transform: translate(0, 0);        opacity: 0; }
  6%        { opacity: 1; }
  20%       { transform: translate(76px, -36px); }
  36%       { transform: translate(152px, -72px); }
  52%       { transform: translate(228px, -108px); }
  68%, 88%  { transform: translate(304px, -144px); opacity: 1; }
  96%, 100% { transform: translate(304px, -144px); opacity: 0; }
}

/* --- qcoin ---------------------------------------------------------------- */
.sa-coin {
  animation: saCoinDrop .8s cubic-bezier(.3, 1.3, .5, 1) both;
  animation-delay: var(--d, 0s);
}
@keyframes saCoinDrop {
  from { opacity: 0; transform: translateY(-22px); }
  to   { opacity: 1; transform: none; }
}

/* The flip. scaleX through zero is the correct way to do this here — a 3D
   rotation would need a perspective the SVG viewport does not have. */
.sa-flip { animation: saFlip 7s var(--ease) 1.2s infinite; }
@keyframes saFlip {
  0%, 34%  { transform: scaleX(1); }
  42%      { transform: scaleX(.04); }
  50%, 84% { transform: scaleX(1); }
  92%      { transform: scaleX(.04); }
  100%     { transform: scaleX(1); }
}
/* Exactly one face is ever legible; they swap while the coin is edge-on. */
.sa-face-a { animation: saFaceA 7s steps(1) 1.2s infinite; }
.sa-face-b { animation: saFaceB 7s steps(1) 1.2s infinite; }
@keyframes saFaceA { 0%, 41% { opacity: 1; } 42%, 91% { opacity: 0; } 92%, 100% { opacity: 1; } }
@keyframes saFaceB { 0%, 41% { opacity: 0; } 42%, 91% { opacity: 1; } 92%, 100% { opacity: 0; } }

.sa-peg { animation: saPegIn .9s var(--ease) 1.5s both; }
@keyframes saPegIn { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }

/* --- alias ---------------------------------------------------------------- */
.sa-card-in  { transform-box: fill-box; transform-origin: center; animation: saPop .8s var(--ease) .1s both; }
.sa-card-out { transform-box: fill-box; transform-origin: center; animation: saPop .8s var(--ease) .5s both; }

.sa-gatewash { animation: saGateWash 5s var(--ease) infinite; }
@keyframes saGateWash {
  0%, 100% { opacity: .25; transform: translateY(-14px); }
  50%      { opacity: 1;   transform: translateY(14px); }
}

/* An identity field travelling to the gate and stopping dead there. It fades
   on contact rather than continuing faintly: "never selected in the query" is
   the claim, and a field that dims but carries on would say the opposite. */
.sa-dropped {
  animation: saDropped 6.5s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes saDropped {
  0%        { transform: translateX(56px);  opacity: 0; }
  12%       { opacity: .85; }
  46%       { transform: translateX(176px); opacity: .85; }
  58%, 100% { transform: translateX(200px); opacity: 0; }
}

/* --- oyp ------------------------------------------------------------------ */
.sa-fly { transform-box: fill-box; }

/* --- blog ----------------------------------------------------------------- */
.sa-rule {
  transform-box: fill-box; transform-origin: left center;
  animation: saGrow .7s var(--ease) both;
}
.sa-type {
  transform-box: fill-box; transform-origin: left center;
  animation: saType .55s var(--ease) both;
  animation-delay: var(--d, 0s);
}
@keyframes saType { from { transform: scaleX(0); opacity: 0; } to { transform: none; opacity: 1; } }

.sa-chip-in {
  transform-box: fill-box; transform-origin: center;
  animation: saBadge .6s cubic-bezier(.3, 1.5, .5, 1) both;
  animation-delay: var(--d, 0s);
}

.sa-shimmer { animation: saShimmer 4.5s var(--ease) infinite; }
@keyframes saShimmer {
  0%        { transform: translateX(-130px); opacity: 0; }
  25%       { opacity: 1; }
  60%, 100% { transform: translateX(130px);  opacity: 0; }
}

/* ==========================================================================
   7. Headings

   The eyebrow above a heading already animated and the heading itself did
   not, which read as the label being the important part of the pair. These
   characters rise and focus in exactly as the eyebrow's do — same easing,
   same cadence — so the two land as one movement rather than two.

   The split is done in site.js, which walks text nodes and leaves element
   children (.hero-line, .hero-accent) intact. Nothing here may apply before
   that has happened, hence every rule is scoped to .js and .h-anim.
   ========================================================================== */
.js .h-anim { position: relative; }

.js .h-anim .h-ch {
  display: inline-block;
  white-space: pre;
  opacity: 0;
  transform: translateY(.36em) scale(.94);
  filter: blur(9px);
}
.js .h-anim.is-in .h-ch {
  animation: hFocusIn .68s cubic-bezier(.2, .8, .3, 1) both;
  /* --step is computed per heading, so a long headline never takes longer to
     land than a short one; --i is the character's place in the line. */
  animation-delay: calc(var(--i) * var(--step, 26ms));
}
@keyframes hFocusIn {
  0%   { opacity: 0; transform: translateY(.36em)  scale(.94);  filter: blur(9px); }
  60%  { opacity: 1; transform: translateY(-.03em) scale(1.008); filter: blur(0); }
  100% { opacity: 1; transform: none; filter: blur(0); }
}

/* A word must not break across a line mid-animation, so characters are
   wrapped one word at a time. */
.js .h-anim .h-word { display: inline-block; white-space: nowrap; }

/* A heading span that paints a gradient through its own text — the hero's
   accent line — is animated whole rather than character by character. See
   the splitter in site.js: anything composited separately inside such a span
   falls out of the background-clip and the line renders as transparent text
   on nothing. It takes the same keyframes, so it lands with the rest of the
   headline rather than looking like a separate effect. */
.js .h-anim .h-unit {
  opacity: 0;
  transform: translateY(.36em) scale(.94);
  filter: blur(9px);
}
.js .h-anim.is-in .h-unit {
  animation: hFocusIn .68s cubic-bezier(.2, .8, .3, 1) both;
  animation-delay: calc(var(--i) * var(--step, 26ms));
}

/* A soft coloured light passes behind the heading as it lands. A white sheen
   would be invisible on a pale section and a dark one invisible on a dark
   band, so this is a brand-to-amber wash at low opacity, which reads on both.
   It sits behind the text and is purely decorative. */
.js .h-anim.is-in::before {
  content: "";
  position: absolute;
  inset: -.35em -12% -.25em;
  z-index: -1;
  pointer-events: none;
  border-radius: 999px;
  background: linear-gradient(100deg,
              transparent 30%,
              color-mix(in srgb, var(--brand) 26%, transparent) 45%,
              color-mix(in srgb, var(--amber) 26%, transparent) 55%,
              transparent 70%);
  filter: blur(26px);
  opacity: 0;
  animation: hWash 1.5s var(--ease) both;
  animation-delay: calc(var(--n, 10) * var(--step, 26ms) * .45);
}
@keyframes hWash {
  0%   { opacity: 0;  transform: translateX(-32%); }
  40%  { opacity: .9; }
  100% { opacity: 0;  transform: translateX(32%); }
}

/* Without color-mix() the background is dropped and the wash becomes an
   invisible no-op — the right degradation, but the box would still be laid
   out, so take it out of the flow entirely. */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  .js .h-anim.is-in::before { display: none; }
}

/* ==========================================================================
   8. Reduced motion — everything added in sections 6 and 7
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .sa-queue, .sa-cleared, .sa-bar, .sa-ring, .sa-meter, .sa-spine, .sa-spine-pulse,
  .sa-halo, .sa-blink, .sa-grow, .sa-person, .sa-sweep, .sa-radar, .sa-desk,
  .sa-ticket, .sa-step, .sa-badge, .sa-climber, .sa-coin, .sa-flip,
  .sa-face-a, .sa-face-b, .sa-peg, .sa-card-in, .sa-card-out, .sa-gatewash,
  .sa-dropped, .sa-rule, .sa-type, .sa-chip-in, .sa-shimmer {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  /* Elements whose resting state would otherwise be wrong or invisible. Each
     motif has to still read as a picture when nothing is moving. */
  .sa-spine { stroke-dashoffset: 0; }
  .sa-spine-pulse, .sa-sweep, .sa-shimmer, .sa-ring { opacity: 0 !important; }
  .sa-meter    { transform: scaleX(.84) !important; }
  .sa-climber  { transform: translate(304px, -144px) !important; }
  .sa-face-b   { opacity: 0 !important; }
  .sa-dropped  { transform: translateX(150px) !important; opacity: .85 !important; }

  .js .h-anim .h-ch,
  .js .h-anim .h-unit {
    opacity: 1 !important; transform: none !important;
    filter: none !important; animation: none !important;
  }
  .js .h-anim.is-in::before { display: none; }
}

/* ==========================================================================
   9. Service rows and product mock-ups

   partials/service-visual.php and partials/product-mock.php. These sit beside
   copy that is being read, not glanced at, so they run slower than the slot
   artwork and none of them competes for the eye with a moving element near
   the text.
   ========================================================================== */
.mock-lbl { font: 700 8px var(--font); letter-spacing: 1.2px; }

/* --- service: staffing ---------------------------------------------------- */
/* A seat filling. One at a time, because that is how a team is actually
   built, and four people appearing at once would read as a stock photo. */
.svc-seat {
  transform-box: fill-box; transform-origin: center bottom;
  animation: saPop .8s cubic-bezier(.3, 1.35, .5, 1) both;
  animation-delay: var(--d, 0s);
}

/* --- service: customer care ----------------------------------------------- */
.svc-msg {
  transform-box: fill-box; transform-origin: left center;
  animation: svcMsg 7s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes svcMsg {
  0%       { opacity: 0; transform: translateX(-16px); }
  8%, 62%  { opacity: 1; transform: translateX(0); }
  78%,100% { opacity: .35; transform: translateX(0); }
}

/* Response time falling. The bar shrinks from the right, so the shortening is
   read as the number coming down rather than the meter emptying. */
.svc-gauge {
  transform-box: fill-box; transform-origin: left center;
  animation: svcGauge 8s var(--ease) infinite;
}
@keyframes svcGauge {
  0%       { transform: scaleX(1); }
  40%, 82% { transform: scaleX(.3); }
  100%     { transform: scaleX(1); }
}

/* --- service: development ------------------------------------------------- */
.svc-stage {
  animation: svcStage 6.5s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes svcStage {
  0%, 6%    { opacity: .25; }
  16%, 82%  { opacity: 1; }
  94%, 100% { opacity: .25; }
}

.svc-ship {
  transform-box: fill-box; transform-origin: center;
  animation: svcShip 6.5s var(--ease) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes svcShip {
  0%, 20%   { opacity: .3; transform: translateY(6px) scale(.97); }
  34%, 84%  { opacity: 1;  transform: none; }
  96%, 100% { opacity: .3; transform: translateY(6px) scale(.97); }
}

/* --- service: data management --------------------------------------------- */
/* The validation pass, running the height of the gate. */
.svc-scan { animation: svcScan 5.5s var(--ease) infinite; }
@keyframes svcScan {
  0%, 100% { transform: translateY(0);    opacity: .35; }
  50%      { transform: translateY(156px); opacity: 1; }
}

/* The duplicate being merged into the record above it. It has to disappear
   into the other row, not just fade, or the picture says "deleted". */
.svc-dupe {
  transform-box: fill-box; transform-origin: center;
  animation: svcDupe 6s var(--ease) infinite;
}
@keyframes svcDupe {
  0%, 34%   { transform: translateY(0);     opacity: .9; }
  62%, 100% { transform: translateY(18px);  opacity: 0; }
}

/* --- product: eSchoolPortal ----------------------------------------------- */
/* The roll being taken, mark by mark. */
.mock-mark {
  transform-box: fill-box; transform-origin: center;
  animation: saBadge .5s cubic-bezier(.3, 1.5, .5, 1) both;
  animation-delay: var(--d, 0s);
}

/* Fees collected. stroke-dasharray is the circumference of r=30 (188.5), so
   the offset is read directly as the fraction outstanding. */
.mock-ring {
  stroke-dasharray: 188.5;
  stroke-dashoffset: 188.5;
  animation: mockRing 2.4s var(--ease) .6s forwards;
}
@keyframes mockRing { to { stroke-dashoffset: 49; } }

/* --- product: eResBooking ------------------------------------------------- */
.mock-booking {
  transform-box: fill-box; transform-origin: left center;
  animation: mockBooking .7s var(--ease) both;
  animation-delay: var(--d, 0s);
}
@keyframes mockBooking {
  from { opacity: 0; transform: scaleX(.2) translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

.mock-occ {
  transform-box: fill-box; transform-origin: left center;
  animation: mockOcc 2.2s var(--ease) .5s both;
}
@keyframes mockOcc { from { transform: scaleX(0); } to { transform: scaleX(.78); } }

@media (prefers-reduced-motion: reduce) {
  .svc-seat, .svc-msg, .svc-gauge, .svc-stage, .svc-ship, .svc-scan, .svc-dupe,
  .mock-mark, .mock-ring, .mock-booking, .mock-occ {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* Resting states that would otherwise be wrong rather than merely still. */
  .svc-gauge  { transform: scaleX(.3) !important; }
  .svc-scan   { opacity: .35 !important; }
  .svc-dupe   { opacity: .9 !important; }
  .mock-ring  { stroke-dashoffset: 49; }
  .mock-occ   { transform: scaleX(.78) !important; }
}

/* ==========================================================================
   10. Section backdrop hues

   Every motif in partials/section-bg.php draws in currentColor, and every
   section was setting that to the brand — so eight different drawings arrived
   as the same blue haze and the eye stopped registering them as different
   sections at all.

   The hue now follows the motif's subject, using the same vocabulary as the
   rest of the site: records and documents are violet, verification is teal,
   people are orange, throughput is amber, and the pipeline stays brand blue.
   Only `color` changes — the drawings themselves are untouched.
   ========================================================================== */
.sec-bg-light.sec-bg--records { color: var(--violet); }
.sec-bg-light.sec-bg--scan    { color: var(--teal); }
.sec-bg-light.sec-bg--nodes   { color: var(--orange); }
.sec-bg-light.sec-bg--chart   { color: var(--amber); opacity: .3; }
.sec-bg-light.sec-bg--circuit { color: var(--violet); }

.sec-bg-dark.sec-bg--records  { color: var(--violet-lit); }
.sec-bg-dark.sec-bg--scan     { color: var(--teal-lit); }
.sec-bg-dark.sec-bg--nodes    { color: var(--orange-lit); }
.sec-bg-dark.sec-bg--chart    { color: var(--amber-lit); }
.sec-bg-dark.sec-bg--circuit  { color: var(--violet-lit); }

/* Amber and orange are lighter than the brand blue at the same nominal
   opacity, so they need a little more of it to carry the same weight behind
   the copy — matching the numbers, not the appearance, is what makes a
   palette look uneven. */
.sec-bg-light.sec-bg--nodes { opacity: .4; }
