/* ==========================================================================
   EFFECTS — motion kit
   Every entrance effect is opt-in via a data attribute set by JS, so a
   visitor with JavaScript disabled (or a crawler) sees final-state content
   rather than an empty page.
   ========================================================================== */

/* ---------- Blur-in reveal ----------------------------------------------- */
[data-reveal] {
  opacity: 0;
  filter: blur(8px);
  transform: translateY(18px);
  transition:
    opacity 560ms var(--ease-out),
    filter 560ms var(--ease-out),
    transform 560ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, filter, transform;
}
[data-reveal].is-shown {
  opacity: 1;
  filter: blur(0);
  transform: none;
}
/* Once settled we drop the filter entirely so the compositor can release
   the layer — a permanent blur(0) keeps the element on its own GPU texture. */
[data-reveal].is-settled {
  filter: none;
  will-change: auto;
}

/* Stagger children automatically. */
[data-reveal-group] > * { --reveal-delay: 0ms; }
[data-reveal-group] > *:nth-child(2) { --reveal-delay: 70ms; }
[data-reveal-group] > *:nth-child(3) { --reveal-delay: 140ms; }
[data-reveal-group] > *:nth-child(4) { --reveal-delay: 210ms; }
[data-reveal-group] > *:nth-child(5) { --reveal-delay: 280ms; }
[data-reveal-group] > *:nth-child(6) { --reveal-delay: 350ms; }

/* ---------- Scramble text ------------------------------------------------ */
[data-scramble] {
  position: relative;
  display: inline-block;
  white-space: nowrap;
}
[data-scramble] .scramble-ghost {
  visibility: hidden;
  display: inline-block;
}
[data-scramble] .scramble-live {
  position: absolute;
  inset: 0;
  display: inline-block;
}

/* ---------- Verified marker --------------------------------------------- */
.is-ready { opacity: 1; }

/* ---------- Loop scroll shell -------------------------------------------
   Applies only above 1024px and only when JS confirms the engine started.
   Without .is-loop the document scrolls natively — the effects still run. */
html.is-loop,
html.is-loop body {
  overflow: hidden;
  height: 100%;
}
.loop-root {
  position: fixed;
  inset: 0;
  overflow: hidden;
}
.loop-track {
  will-change: transform;
}
/* The duplicated half of the loop is marked inert by JS (attribute, not CSS)
   so its links never enter the tab order. */

/* ---------- Media entrance ----------------------------------------------- */
.media-in {
  animation: media-in 420ms var(--ease-out) both;
}
@keyframes media-in {
  from { opacity: 0; transform: scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* ---------- Reduced motion ---------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal] {
    opacity: 1;
    filter: none;
    transform: none;
  }
  .bg-wrap canvas { opacity: 1; }
}

/* Phones: skip entrance choreography entirely, it costs more than it gives. */
@media (max-width: 767px) {
  [data-reveal] {
    opacity: 1;
    filter: none;
    transform: none;
  }
}

/* ==========================================================================
   PATCH PASS 1 — 2026-11-10
   Fix 4  CTA labels stay optically centred once the scramble effect has run.
   Fix 5  Travelling dashes on the iso cables, glow pulse on the agent labels.
   Fix 6  Architecture stage: masked depth field + blur-in / scale-in reveal.
   ========================================================================== */

/* --- Fix 4 — the scramble spans must not shift the label ------------------ */
/* .scramble-live is absolutely positioned over the button; without a flex
   box it renders the glyphs from the top-left corner of the pill, which is
   what made "Get AI Score" look off-centre after the first hover.          */
[data-scramble] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
[data-scramble] .scramble-ghost { line-height: 1; }
[data-scramble] .scramble-live {
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* --- Fix 4b — dock pill on phones ---------------------------------------- */
/* .dock is fixed at left:50%, so a shrink-to-fit element only gets half the
   viewport to size itself in: at 390px that forced the CTA to wrap onto two
   lines and shrank the WhatsApp button. max-content frees it.              */
.dock {
  width: max-content;
  max-width: calc(100vw - 24px);
}
.dock .btn { white-space: nowrap; }

/* --- Fix 5 — travelling data on the cables ------------------------------- */
/* The cables already carry stroke-dasharray "0 10" (round-capped dots).
   Sweeping the offset by two full periods loops seamlessly.               */
@keyframes qk-flow {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -20; }
}
.iso-flow { animation: qk-flow 1.1s linear infinite; }
.iso-flow--slow { animation-duration: 1.7s; }

/* --- Fix 5 — agent label cards breathe ----------------------------------- */
/* Offset by index (inline animation-delay set in isometric.js) so the four
   plus two labels never fire on the same beat. Drop-shadow, not box-shadow:
   these pills are SVG, and box-shadow does not apply to SVG geometry.     */
@keyframes qk-label-glow {
  0%, 62%, 100% { filter: drop-shadow(0 0 0 rgba(150, 232, 255, 0)); }
  78%           { filter: drop-shadow(0 0 6px rgba(150, 232, 255, 0.85)); }
}
[data-scene] .iso-label { animation: qk-label-glow 3s ease-in-out infinite; }

/* --- Fix 6 — the navy section gets a floor ------------------------------- */
/* The deployed index.html has no .arch__field element and this pass is not
   allowed to touch that file, so the depth field is painted here as a pseudo
   element. isometric.js sets --fx/--fy on the section, which inherits down.  */
.arch { position: relative; }
.arch::before {
  content: "";
  position: absolute;
  inset: -8%;
  pointer-events: none;
  z-index: 0;
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.46) 1.2px, transparent 1.7px);
  background-size: 26px 26px;
  -webkit-mask-image: radial-gradient(59% 55% at 50% 48%, #000 0%, rgba(0, 0, 0, 0.55) 46%, transparent 78%);
  mask-image: radial-gradient(59% 55% at 50% 48%, #000 0%, rgba(0, 0, 0, 0.55) 46%, transparent 78%);
  opacity: 0.5;
  transform: translate3d(var(--fx, 0px), var(--fy, 0px), 0);
  transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: transform;
}
.arch > .container { position: relative; z-index: 1; }
.arch__field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.22) 1px, transparent 1.4px);
  background-size: 24px 24px;
  -webkit-mask-image: radial-gradient(62% 58% at 50% 46%, #000 0%, rgba(0, 0, 0, 0.55) 46%, transparent 78%);
  mask-image: radial-gradient(62% 58% at 50% 46%, #000 0%, rgba(0, 0, 0, 0.55) 46%, transparent 78%);
  opacity: 0.32;
  transform: translate3d(var(--fx, 0px), var(--fy, 0px), 0);
  transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: transform;
}
.arch__stage {
  position: relative;
  z-index: 1;
  perspective: 1500px;
}

/* --- Fix 6 — the whole scene reveals with depth -------------------------- */
.arch .iso-frame {
  transform-origin: 50% 56%;
  opacity: 0;
  transform: scale(0.85);
  filter: blur(7px);
  transition: opacity 0.8s ease,
              transform 1s cubic-bezier(0.22, 0.61, 0.36, 1),
              filter 0.9s ease;
}
.arch .iso-frame.is-in {
  opacity: 1;
  transform: none;
  filter: none;
}

/* --- Reduced motion: every effect added above is switched off ------------ */
@media (prefers-reduced-motion: reduce) {
  .iso-flow,
  [data-scene] .iso-label { animation: none !important; }
  .arch .iso-frame {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
  .arch__field { transform: none !important; transition: none !important; }
  .arch::before { transform: none !important; transition: none !important; }
}

/* ==========================================================================
   PASS 2 — FIX 1: THE ARCHITECTURE SECTION BECOMES A LIVE AGENT NETWORK
   Everything below is scoped to .arch so the other four isometric scenes on
   the page keep their current behaviour.
   ========================================================================== */

/* 1a. Data flow on the cables ------------------------------------------------
   Each cable is a <g> holding a faint base path plus a second path.iso-flow
   that carries the travelling dash. The base path keeps its inline stroke
   colour, so the Nova branch (coral) and the output branch (teal) already read
   as two distinct streams — CSS deliberately does not touch stroke here or it
   would flatten both to one colour.
   6px dash, 4px gap => a 10px cycle; 0.1667s per cycle => 60px/s. */
.arch .iso-flow {
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-dasharray: 6 4;
  animation: qkFlow 166.7ms linear infinite;
}
@keyframes qkFlow {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -10; }
}

/* 1b. Independent node breathing --------------------------------------------
   box-shadow does nothing on SVG shapes, so the glow is a drop-shadow filter.
   0 -> 8px blur -> 0 over 2.4s, each pill offset 400ms from the last (--gi is
   set in isometric.js), so they never pulse in unison. */
@keyframes qkPillGlow {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(255, 255, 255, 0)); }
  50%      { filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.55)); }
}
.arch .iso-label rect {
  animation: qkPillGlow 2.4s ease-in-out infinite;
  animation-delay: var(--glow-delay, 0ms);
}

/* 1c. Scroll-in entry -------------------------------------------------------
   Nodes drop 24px, fade in, and settle with a slight overshoot (the spring).
   80ms apart in render order via --gi. Only runs once .is-in lands on the
   frame, which the existing IntersectionObserver already does. */
@keyframes qkNodeIn {
  from { opacity: 0; transform: translateY(-24px); }
  to   { opacity: 1; transform: translateY(0); }
}
.arch .iso-label { opacity: 0; }
.arch .iso-frame.is-in .iso-label {
  animation: qkNodeIn 220ms cubic-bezier(0.2, 0.9, 0.3, 1.28) both;
  animation-delay: calc(var(--gi, 0) * 80ms);
}

/* 1d. Cursor parallax -------------------------------------------------------
   --px/--py are set on .arch by isometric.js (+/-18px X, +/-10px Y).
   Cluster 1.0x, foreground pills 1.2x, dot field 0.3x the other way, so the
   cluster appears to float above the field rather than move with it. */
.arch [data-scene] > svg { translate: var(--px, 0px) var(--py, 0px); will-change: translate; }
/* The pill sits inside the svg, so its own offset ADDS to the cluster's 1.0x.
   +0.2 gives foreground pills a 1.2x total; -0.1 gives background 0.9x. */
.arch .iso-label[data-depth="1.2"] { translate: calc(var(--px, 0px) * 0.2) calc(var(--py, 0px) * 0.2); }
.arch .iso-label[data-depth="0.8"] { translate: calc(var(--px, 0px) * -0.1) calc(var(--py, 0px) * -0.1); }

/* 1e. Dot field: 3px dots at 0.75, parallaxed against the cluster ---------- */
.arch::before {
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.75) 1.5px, transparent 2.05px);
  background-size: 22px 22px;
  opacity: 0.75;
  translate: calc(var(--px, 0px) * -0.3) calc(var(--py, 0px) * -0.3);
}

/* ==========================================================================
   FIX 2 — scramble text can never change layout
   Both layers sit in one grid cell, so the element's width is fixed for the
   duration and the glyphs cannot spill out of the pill.
   ========================================================================== */
.scramble-wrap { display: inline-grid; align-items: center; justify-items: center; }
.scramble-wrap > * { grid-area: 1 / 1; }
.scramble-wrap .scramble-ghost { visibility: hidden; }
.scramble-wrap .scramble-live { position: relative; white-space: nowrap; }
#dock .scramble-live, .dock .scramble-live { position: static; }

/* ==========================================================================
   FIX 3 — scramble-in headings
   .decode-pending keeps the heading in flow but stops the un-decoded string
   from flashing: the glyphs are painted by JS on the first frame, so a very
   short fade is all that is needed.
   ========================================================================== */
h1.decode-pending, h2.decode-pending, h3.decode-pending { opacity: 0.999; }

/* ==========================================================================
   FIX 4 — loop band
   Scrolling container: <div class="loopband__track" data-loop-track>, inside
   <div class="loopband" data-loop>. Contents: 10 <span class="loopband__item">
   sector labels (Solar & energy -> Trades & maintenance), cloned once by
   loopscroll.js so the wrap is seamless. Speed is 28px/s in loopscroll.js —
   above the 20px/s floor — so the only change needed here is the edge fade.
   ========================================================================== */
.loopband {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .arch .iso-flow { animation: none; }
  .arch .iso-label rect { animation: none; filter: none; }
  .arch .iso-label { opacity: 1; animation: none; }
  .arch [data-scene] > svg, .arch .iso-label { translate: none; }
  .arch::before { translate: none; }
}

/* ===== Pass 3 — chat launcher legibility ==================================
   LIVE OBSERVED (2026-11-10): .nova-trigger computed background was
  rgb(25,25,112) — the same navy as the architecture section it floats over,
  so the button vanished into the section except for its 22px glyph.
   z-index was already 9999 and hit-testable (elementFromPoint === trigger),
   so this is a CONTRAST bug, not a stacking bug. Fixed at the colour level,
   with a 2px light ring so it also separates from white sections.
   Placed in effects.css because that file loads after sections.css, and
   `body .nova-trigger` outranks the base `.ngola`-era rule regardless.       */
body .nova-trigger {
  background: var(--qc-accent, #FF6B5A);
  color: #0b0b10;
  border: 0;
  box-shadow: 0 10px 25px rgba(10, 10, 10, .38), 0 0 0 2px rgba(255, 255, 255, .92);
}
body .nova-trigger:hover {
  background: var(--qc-teal, #19c4c4);
  color: #0b0b10;
  transform: scale(1.06);
}
body .nova-trigger:focus-visible {
  outline: 3px solid #19c4c4;
  outline-offset: 3px;
}
/* keep the launcher clear of the bottom dock on narrow screens */
@media (max-width: 640px) {
  body .nova-trigger { bottom: 88px; }
}

/* ==========================================================================
   PASS 4 — FIX A: scramble can never resize the pill
   The PASS 2 rule `.scramble-wrap .scramble-live { position: relative }` put
   the scrambling text back in flow, so wide glyphs (the ⚡ emoji) widened the
   grid cell and pushed the nav sideways. The live layer goes back to being an
   absolutely positioned overlay sized by the invisible ghost, and clips.
   ========================================================================== */
[data-scramble].scramble-wrap .scramble-live {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  white-space: nowrap;
  line-height: 1;
}

/* ==========================================================================
   PASS 4 — FIX B: glass agent console in #architecture
   Dark glass, because this section is navy (.section--ink). It overlaps the
   bottom of .arch__stage so the backdrop blur refracts the iso scene and the
   parallax dot field behind it. Entrance is CSS-only (scroll-driven where
   supported, otherwise simply visible) so no JS file is touched.
   z-index 2 sits above .arch__stage (z 1) inside .arch > .container (z 1).
   ========================================================================== */
.arch-console {
  position: relative;
  z-index: 2;
  max-width: 880px;
  margin: clamp(-120px, -8vw, -48px) auto var(--space-12);
  border-radius: var(--radius-card);
  background: rgba(255, 255, 255, 0.06);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  backdrop-filter: blur(16px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 32px 64px -24px rgba(5, 5, 40, 0.65), inset 0 1px 0 rgba(255, 255, 255, 0.14);
  overflow: hidden;
  color: rgba(255, 255, 255, 0.86);
  text-align: left;
}
.arch-console__bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  font-family: var(--font-display);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
}
.arch-console__dots { display: inline-flex; gap: 6px; }
.arch-console__dots i { width: 8px; height: 8px; border-radius: 50%; background: rgba(255, 255, 255, 0.18); }
.arch-console__title { flex: 1; }
.arch-console__status { display: inline-flex; align-items: center; gap: 8px; }

.arch-console__body { display: grid; grid-template-columns: 260px minmax(0, 1fr); }
.arch-console__agents {
  margin: 0;
  padding: 10px;
  list-style: none;
  background: rgba(0, 0, 0, 0.18);
  border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.arch-console__agents li { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 12px; }
.arch-console__agents li.is-active { background: rgba(255, 255, 255, 0.07); }
.arch-console__agents b {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 14px;
  line-height: 1.2;
  color: #fff;
}
.arch-console__agents small { display: block; font-size: 12px; line-height: 1.35; color: rgba(255, 255, 255, 0.6); }

.arch-console__pane { padding: 20px 24px 24px; }
.arch-console__eyebrow {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--coral);
}
.arch-console__task { margin: 0 0 16px; font-family: var(--font-display); font-weight: 600; font-size: 1.125rem; color: #fff; }
.arch-console__steps { margin: 0 0 20px; padding: 0; list-style: none; display: grid; gap: 10px; font-size: 0.9375rem; }
.arch-console__steps li { display: flex; align-items: center; gap: 10px; color: rgba(255, 255, 255, 0.78); }
.arch-console__steps li::before {
  content: "";
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.28);
  font-size: 10px;
  line-height: 1;
}
.arch-console__steps li.is-done::before { content: "\2713"; color: var(--ink); background: var(--teal); border-color: var(--teal); }
.arch-console__steps li.is-run::before { border-color: var(--coral); border-top-color: transparent; animation: qkSpin 0.9s linear infinite; }

.arch-console__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.arch-chip {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 600;
}
.arch-chip--teal { background: rgba(65, 179, 163, 0.14); border: 1px solid rgba(65, 179, 163, 0.35); color: #8FDCCF; }
.arch-chip--coral { background: rgba(226, 125, 96, 0.14); border: 1px solid rgba(226, 125, 96, 0.4); color: #F2B5A3; }

.arch-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--teal); }
.arch-dot--coral { background: var(--coral); box-shadow: 0 0 8px rgba(226, 125, 96, 0.6); animation: qkDotPulse 2s ease-in-out infinite; }
.arch-dot--lavender { background: var(--lavender); }
.arch-dot--live { box-shadow: 0 0 8px rgba(65, 179, 163, 0.6); animation: qkDotPulse 2s ease-in-out 1s infinite; }

.arch-shimmer {
  color: transparent;
  background: linear-gradient(90deg, var(--activity-from), var(--activity-to), var(--activity-from)) 0 0 / 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  animation: qkShimmer 2s steps(48) infinite;
}


@keyframes qkDotPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.2); opacity: 0.4; } }
@keyframes qkShimmer { from { background-position: 200% 0; } to { background-position: 0 0; } }
@keyframes qkSpin { to { transform: rotate(360deg); } }
@keyframes qkConsoleIn {
  from { opacity: 0; transform: translateY(24px); filter: blur(8px); }
  to   { opacity: 1; transform: none; filter: none; }
}
@keyframes qkRowIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* Scroll-driven entrance: progressive enhancement, no JS. */
@supports (animation-timeline: view()) {
  .arch-console { animation: qkConsoleIn linear both; animation-timeline: view(); animation-range: entry 0% entry 60%; }
  .arch-console__agents li { animation: qkRowIn linear both; animation-timeline: view(); }
  .arch-console__agents li:nth-child(1) { animation-range: entry 10% entry 60%; }
  .arch-console__agents li:nth-child(2) { animation-range: entry 20% entry 70%; }
  .arch-console__agents li:nth-child(3) { animation-range: entry 30% entry 80%; }
  .arch-console__agents li:nth-child(4) { animation-range: entry 40% entry 90%; }
  .arch-console__agents li:nth-child(5) { animation-range: entry 50% entry 100%; }
}

@media (max-width: 760px) {
  .arch-console { margin-top: 0; }
  .arch-console__body { grid-template-columns: minmax(0, 1fr); }
  .arch-console__agents { border-right: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
  .arch-console__agents li:nth-child(n + 4) { display: none; }
}
@media (max-width: 767px), (prefers-reduced-motion: reduce) {
  .arch-console,
  .arch-console__agents li { animation: none !important; opacity: 1; transform: none; filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .arch-dot--coral, .arch-dot--live, .arch-console__steps li.is-run::before { animation: none !important; }
  .arch-shimmer { animation: none !important; color: var(--coral); background: none; }
}


/* ==========================================================================
   PASS 4 — FIX C: a closed Nova panel must not catch clicks
   sections.css line 1171 sets .nova-panel { display: flex } which overrides
   the [hidden] attribute. This one-line override restores the expected
   behaviour. chat.js already toggles [hidden], so open/close keep working.
   ========================================================================== */
.nova-panel[hidden] { display: none; }

/* ==========================================================================
   PASS 5 — 3D arch constellation container
   ========================================================================== */
.arch-3d-stage {
  width: 100%;
  height: 420px;
  display: block;
  position: relative;
}
#arch-3d-canvas {
  display: block;
  width: 100%;
  height: 100%;
}
@media (max-width: 767px) {
  .arch-3d-stage { height: 300px; }
}
@media (prefers-reduced-motion: reduce) {
  .arch-3d-stage { opacity: 0.85; }
}
