/* ────────────────────────────────────────────────────────────
   Reusable section atoms - eyebrow, section heading, footnote
   markers - used by every section below the hero. Style follows
   the Dispatch hero: brass left rule, Plex Mono small caps, GT
   America for headlines, very generous left padding.
   ──────────────────────────────────────────────────────────── */

function SectionEyebrow({ index, label }) {
  return (
    <div data-role="eyebrow" style={{
      fontFamily: "var(--font-mono)",
      fontSize: 11,
      color: "#A8A8A2",
      letterSpacing: "0.20em",
      textTransform: "uppercase",
      lineHeight: 1.9,
      borderLeft: "1px solid #D4A24C",
      paddingLeft: 14,
      marginBottom: 32
    }}>
      <div>{label}</div>
    </div>);

}

/* Splits a React children prop into "lines" at <br/> boundaries.
   Each returned line is an array of nodes (text, spans, etc.). Used
   by Pattern A: hero / section headlines.

   Why we do this: <br/> alone can't host a transform, and we need
   each line to live inside its own overflow-hidden mask + inner
   span so the type can rise up from below. The user writes natural
   markup ("One system.<br/>Two frontiers.") and we transparently
   convert it into the masked structure on render. */
function __splitChildrenAtBr(children) {
  const lines = [];
  let current = [];
  React.Children.forEach(children, (child) => {
    if (
    React.isValidElement(child) && (
    child.type === "br" || child.type === "BR"))
    {
      lines.push(current);
      current = [];
    } else if (typeof child === "string" && child.includes("\n")) {
      // Allow literal newlines as line breaks too.
      const parts = child.split("\n");
      parts.forEach((p, idx) => {
        if (p) current.push(p);
        if (idx < parts.length - 1) {
          lines.push(current);
          current = [];
        }
      });
    } else {
      current.push(child);
    }
  });
  lines.push(current);
  // Drop trailing empty line if input ended with a <br/>.
  if (lines.length > 1 && lines[lines.length - 1].length === 0) lines.pop();
  return lines;
}

function SectionHeading({ children, size = "lg", color }) {
  const sizeMap = {
    lg: "clamp(2rem, 1.52rem + 2.07vw, 3.375rem)",    // 32 → 54 (default)
    md: "clamp(2rem, 1.52rem + 2.07vw, 3.375rem)",    // 32 → 54 (same as lg for now)
    sm: "clamp(1.5rem, 1.13rem + 1.50vw, 2.5rem)",    // 24 → 40 (smaller)
  };
  const fontSize = sizeMap[size] || sizeMap.lg;

  const lines = __splitChildrenAtBr(children);
  return (
    <h2 data-role="heading" data-reveal-headline style={{
      fontFamily: '"GT America", var(--font-display)',
      fontWeight: 700,
      fontSize: fontSize,
      lineHeight: 1.06,
      letterSpacing: "-0.02em",
      color: color || "#EDEAE2",
      margin: 0,
      textWrap: "balance"
    }}>
      {lines.map((line, i) =>
        <span key={i} className="ax-headline-mask">
          <span className="ax-headline-line" style={{ transitionDelay: `${i * 100}ms` }}>
            {line.length === 0 ? "\u00A0" : line}
          </span>
        </span>
      )}
    </h2>);
}

function SectionDeck({ children, max = 720 }) {
  return (
    <p data-role="body" style={{
      fontFamily: "var(--font-body)",


      color: "#A8A8A2",
      maxWidth: max,
      margin: "28px 0 0",
      textWrap: "pretty", fontSize: "16px", lineHeight: "1.5"
    }}>
      {children}
    </p>);

}

// Section frame - Dispatch-style, with corner register marks and
// a quiet plate caption in the bottom-right.
function SectionFrame({ index, plate, dispatch, children, bg = "#15171A", padded = true, theme = "dark", compactTop = false }) {
  const isLight = theme === "light";
  const markColor = isLight ? "rgba(26,22,18,0.20)" : "rgba(237,234,226,0.18)";
  const plateMain = isLight ? "#5A574F" : "#A8A8A2";
  const plateSub = isLight ? "#8C887E" : "#6F7178";
  return (
    <section style={{
      position: "relative",
      background: bg,
      color: isLight ? "#1A1612" : "#EDEAE2",
      padding: padded
        ? `${compactTop ? "var(--sp-fluid-base)" : "var(--sp-section-y)"} var(--sp-section-x) var(--sp-section-y)`
        : "0",
      borderTop: isLight ? "1px solid rgba(26,22,18,0.10)" : "1px solid rgba(237,234,226,0.08)",
      isolation: "isolate"
    }}>
      {/* corner register marks */}
      {[
      { top: "clamp(20px, 1.5vw, 24px)", left: "clamp(8px, 1.5vw, 20px)", s: "TL" },
      { top: "clamp(20px, 1.5vw, 24px)", right: "clamp(8px, 1.5vw, 20px)", s: "TR" },
      { bottom: "clamp(20px, 1.5vw, 24px)", left: "clamp(8px, 1.5vw, 20px)", s: "BL" },
      { bottom: "clamp(20px, 1.5vw, 24px)", right: "clamp(8px, 1.5vw, 20px)", s: "BR" }].
      map((c, i) =>
      <div key={i} style={{
        position: "absolute", ...c, width: 14, height: 14,
        borderColor: markColor,
        borderStyle: "solid",
        borderWidth: 0,
        ...(c.s.includes("T") ? { borderTopWidth: 1 } : {}),
        ...(c.s.includes("B") ? { borderBottomWidth: 1 } : {}),
        ...(c.s.includes("L") ? { borderLeftWidth: 1 } : {}),
        ...(c.s.includes("R") ? { borderRightWidth: 1 } : {})
      }} />
      )}

      {children}

      {/* plate caption - last thing to land, like a stamp.
                                   Fade-in is driven by site-motion.js's plate observer
                                   (50% viewport threshold, 600ms ease-out-expo). */}
      {plate &&
      <div className="ax-plate-caption" style={{
        position: "absolute",
        bottom: "var(--sp-fluid-loose)", right: "var(--sp-section-x)",
        textAlign: "right",
        fontFamily: "var(--font-mono)",
        fontSize: 11, color: plateSub,
        letterSpacing: "0.16em", textTransform: "uppercase",
        lineHeight: 1.7, zIndex: 4
      }}>
          <div style={{ color: plateMain }}>Plate {plate}</div>
          {dispatch && <div>{dispatch}</div>}
        </div>
      }
    </section>);

}

/* ────────────────────────────────────────────────────────────
   Image entrance - the "settle" pattern (motion brief §8).
   Every prominent image on the page should ARRIVE, not pop in.

   Initial state: opacity 0, scale 1.06, y +40px
   Final state:   opacity 1, scale 1.00, y 0
   Duration:      1200ms (motion-deliberate)
   Easing:        ease-out-expo
   Trigger:       section enters viewport at 80% threshold

   The 1.06→1.00 scale-down over 1.2s reads as "coming into focus"
   and gives the image weight - same technique used by Minta /
   Optimus / the references in the brief.

   Two surface APIs:
     • <ImageSettle>...</ImageSettle>  - wraps the visual element.
     • useImageSettle(ref)             - for cases where you can't
                                         add a wrapper element
                                         (e.g. inline backgrounds).
   ──────────────────────────────────────────────────────────── */

const __SETTLE_EASE = "cubic-bezier(0.16, 1, 0.3, 1)"; // ease-out-expo
const __SETTLE_DUR = 1200;

// Hook: applies the settle to a ref'd element when it crosses the
// 80% viewport threshold. Once-only - disconnects after firing so
// scrolling back up doesn't re-trigger.
function useImageSettle(ref, opts = {}) {
  const { delay = 0, disabled = false } = opts;
  React.useEffect(() => {
    const el = ref && ref.current;
    if (!el || disabled) return;

    // Capture the caller's intended final opacity. Some images render
    // at <1 opacity (e.g. a recessed bg layer at 0.18). We want the
    // settle to fade INTO that, not over-bright it to 1. Read it
    // from inline style first, then computed style as a fallback.
    const inlineOp = el.style.opacity;
    const computedOp = window.getComputedStyle(el).opacity;
    const targetOpacity = inlineOp !== "" ? inlineOp : computedOp || "1";

    // Honor reduced motion - skip the choreography entirely.
    const prm = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)");
    if (prm && prm.matches) {
      el.style.opacity = targetOpacity;
      el.style.transform = "none";
      return;
    }

    // Set the start state synchronously so the very first paint
    // is the pre-settle frame (no flash of the final state).
    el.style.opacity = "0";
    el.style.transform = "translate3d(0, 40px, 0) scale(1.06)";
    el.style.willChange = "opacity, transform";
    // We use transition rather than animation so the final state
    // sticks naturally - no fill-mode footguns.
    el.style.transition =
    `opacity ${__SETTLE_DUR}ms ${__SETTLE_EASE} ${delay}ms, ` +
    `transform ${__SETTLE_DUR}ms ${__SETTLE_EASE} ${delay}ms`;

    const fire = () => {
      // Two rAFs: first to apply the start state, second to flip
      // to the end state - guarantees the transition runs.
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          el.style.opacity = targetOpacity;
          el.style.transform = "translate3d(0, 0, 0) scale(1)";
        });
      });
      // Drop will-change once the transition has finished so the
      // GPU layer is released.
      const onEnd = () => {
        el.style.willChange = "";
        el.removeEventListener("transitionend", onEnd);
      };
      el.addEventListener("transitionend", onEnd);
    };

    const io = new IntersectionObserver((entries) => {
      for (const ent of entries) {
        if (ent.isIntersecting) {
          fire();
          io.disconnect();
          break;
        }
      }
    }, {
      // Brief: "fires when the section has entered 80% of the way
      // into view." Implemented as a negative bottom margin equal
      // to 20% of the viewport - the IO considers the element
      // intersecting once its top crosses the line 20% up from the
      // viewport's bottom edge (i.e. 80% down from the top).
      // Threshold 0 instead of 0.8 so this works for full-bleed
      // backgrounds taller than the viewport - the brief's intent
      // is "scroll position has reached 80%," not "80% of pixels
      // visible," which is impossible for above-fold tall layers.
      rootMargin: "0px 0px -20% 0px",
      threshold: 0
    });
    io.observe(el);

    // If the element's top is already above the 80% line on mount
    // (e.g. above-fold elements on short viewports), fire
    // immediately - IO won't always emit a callback for elements
    // that intersect before observation.
    const mountRect = el.getBoundingClientRect();
    const vh = window.innerHeight || 800;
    if (mountRect.top < vh * 0.8 && mountRect.bottom > 0) {
      fire();
      io.disconnect();
    }

    return () => io.disconnect();
  }, [ref, delay, disabled]);
}

// Component wrapper. `as` lets the caller pick the tag (default div);
// any other props pass through. Inline style is merged with the
// settle's transition/transform so callers don't have to think
// about it.
function ImageSettle({ as: Tag = "div", delay = 0, disabled = false, style, children, ...rest }) {
  const ref = React.useRef(null);
  useImageSettle(ref, { delay, disabled });
  return (
    <Tag ref={ref} style={style} {...rest}>
      {children}
    </Tag>);

}

window.SectionEyebrow = SectionEyebrow;
window.SectionHeading = SectionHeading;
window.SectionDeck = SectionDeck;
window.SectionFrame = SectionFrame;
window.useImageSettle = useImageSettle;
window.ImageSettle = ImageSettle;

/* ────────────────────────────────────────────────────────────
   Parallax - the technique that makes anchored imagery feel
   monumental (motion brief §11). The image translates vertically
   slower than the surrounding content, creating the illusion of
   depth as the camera "pulls back" from a massive object.

   API:
     useParallax(imgRef, {
       speed = 0.5,    // 0–1. Lower = slower = more monumental.
                      // 0.5 means the image moves at half the
                      // rate of natural scroll. 1.0 means no
                      // parallax (moves with the page).
       container = null, // optional ref to the trigger section.
                      // Defaults to imgRef's closest <section>.
     })

   Implementation:
     • progress = how far the section has traveled through the
       viewport. 0 when section's top hits the viewport bottom,
       1 when the section's bottom hits the viewport top.
     • maxOffset = sectionHeight * (1 - speed). At speed=0.5 the
       image traverses a distance equal to half the section's
       height over its full visibility.
     • translateY = (progress - 0.5) * 2 * (-maxOffset / 2)
       - centered around the section's midpoint, so the image is
       "neutral" when the section is centered in the viewport.

   Caller is responsible for sizing the image taller than its
   container (110–115%) so the parallax offset never reveals
   empty edges. The brief is firm on this; the hook can't enforce
   layout on the caller's behalf.

   The hook honors prefers-reduced-motion: reduce - it sets the
   image to its neutral state and skips the listener entirely.
   ──────────────────────────────────────────────────────────── */
function useParallax(imgRef, opts = {}) {
  const { speed = 0.5, container = null, disabled = false } = opts;
  React.useEffect(() => {
    const img = imgRef && imgRef.current;
    if (!img || disabled) return;

    // Reduced motion: pin the image to its neutral state and bail.
    const prm = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)");
    if (prm && prm.matches) {
      img.style.transform = "translate3d(0, 0, 0)";
      return;
    }

    // Resolve trigger container. If the caller passed a ref, use
    // its current node; otherwise climb to the closest <section>
    // - that's the convention the brief assumes.
    const section = container && container.current || img.closest("section");
    if (!section) return;

    img.style.willChange = "transform";

    let ticking = false;
    const update = () => {
      ticking = false;
      const rect = section.getBoundingClientRect();
      const vh = window.innerHeight || 800;
      // Section is visible when its top is above viewport bottom
      // and its bottom is below viewport top. Outside that window
      // we don't write transforms - saves a few cycles.
      if (rect.bottom < 0 || rect.top > vh) return;

      // Progress: 0 when the section's TOP enters the viewport
      // bottom, 1 when the section's BOTTOM exits the viewport
      // top. Clamped so the transform doesn't keep growing past
      // either edge.
      const total = vh + rect.height;
      const traveled = vh - rect.top;
      let progress = traveled / total;
      if (progress < 0) progress = 0;else
      if (progress > 1) progress = 1;

      // Centered around 0.5 so the image is at neutral (y=0) when
      // the section is mid-viewport. (1 - speed) is the parallax
      // strength: at speed=0.5 the image moves 50% of the
      // section's height total; at 0.85 it moves 15% total.
      const maxOffset = rect.height * (1 - speed);
      const translateY = (progress - 0.5) * -maxOffset;

      img.style.transform = `translate3d(0, ${translateY.toFixed(2)}px, 0)`;
    };

    const onScroll = () => {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(update);
    };

    // Run once so the initial paint is correct (e.g. for sections
    // already partly in view on page load).
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      img.style.willChange = "";
    };
  }, [imgRef, container, speed, disabled]);
}

window.useParallax = useParallax;

/* ────────────────────────────────────────────────────────────
   Motion + stagger token resolver - gives JS code a single source
   of truth for the design tokens declared in colors_and_type.css.

   Tokens read from :root and parsed once on first call:
     --motion-instant      150
     --motion-fast         300
     --motion-base         600
     --motion-slow         900
     --motion-deliberate  1200

     --stagger-tight       40
     --stagger-base        80
     --stagger-loose      160
     --stagger-narrative  240

   Usage:
     ax.stagger("tight" | "base" | "loose" | "narrative")
     ax.motion("instant" | "fast" | "base" | "slow" | "deliberate")

   Returns a number (ms). Falls back to documented values if the
   token isn't present yet (during early page boot).
   ──────────────────────────────────────────────────────────── */
const __AX_TOKEN_FALLBACKS = {
  motion: { instant: 150, fast: 300, base: 600, slow: 900, deliberate: 1200 },
  stagger: { tight: 40, base: 80, loose: 160, narrative: 240 }
};
const __axTokenCache = {};
function __axResolveToken(group, key) {
  const cacheKey = group + ":" + key;
  if (cacheKey in __axTokenCache) return __axTokenCache[cacheKey];
  let value = __AX_TOKEN_FALLBACKS[group][key];
  try {
    const raw = getComputedStyle(document.documentElement).
    getPropertyValue("--" + group + "-" + key).trim();
    const parsed = parseFloat(raw);
    if (!isNaN(parsed) && parsed > 0) value = parsed;
  } catch (e) {/* SSR/early boot - use fallback */}
  __axTokenCache[cacheKey] = value;
  return value;
}
const ax = {
  motion: (key) => __axResolveToken("motion", key),
  stagger: (key) => __axResolveToken("stagger", key)
};
window.ax = ax;